McMrARM

Firewolf 4.0

May 26th, 2015
350
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 75.60 KB | None | 0 0
  1.  
  2. --
  3. --  Firewolf
  4. --  Made by GravityScore and 1lann
  5. --
  6.  
  7.  
  8.  
  9. --    Variables
  10.  
  11.  
  12. local version = "3.5"
  13. local build = 17
  14.  
  15. local w, h = term.getSize()
  16.  
  17. local isMenubarOpen = true
  18. local menubarWindow = nil
  19.  
  20. local allowUnencryptedConnections = true
  21. local enableTabBar = true
  22.  
  23. local currentWebsiteURL = ""
  24. local builtInSites = {}
  25.  
  26. local currentProtocol = ""
  27. local protocols = {}
  28.  
  29. local currentTab = 1
  30. local maxTabs = 5
  31. local maxTabNameWidth = 8
  32. local tabs = {}
  33.  
  34. local languages = {}
  35.  
  36. local history = {}
  37.  
  38. local publicDNSChannel = 9999
  39. local publicResponseChannel = 9998
  40. local responseID = 41738
  41.  
  42. local httpTimeout = 10
  43. local searchResultTimeout = 2
  44. local initiationTimeout = 2
  45. local animationInterval = 0.125
  46. local fetchTimeout = 3
  47. local serverLimitPerComputer = 1
  48.  
  49. local websiteErrorEvent = "firewolf_websiteErrorEvent"
  50. local redirectEvent = "firewolf_redirectEvent"
  51.  
  52. local baseURL = "https://raw.githubusercontent.com/1lann/Firewolf/master/src"
  53. local buildURL = baseURL .. "/build.txt"
  54. local firewolfURL = baseURL .. "/client.lua"
  55. local serverURL = baseURL .. "/server.lua"
  56.  
  57. local originalTerminal = term.current()
  58.  
  59. local firewolfLocation = "/" .. shell.getRunningProgram()
  60. local downloadsLocation = "/downloads"
  61.  
  62.  
  63. local theme = {}
  64.  
  65. local colorTheme = {
  66.     background = colors.gray,
  67.     accent = colors.red,
  68.     subtle = colors.orange,
  69.  
  70.     lightText = colors.gray,
  71.     text = colors.white,
  72.     errorText = colors.red,
  73. }
  74.  
  75. local grayscaleTheme = {
  76.     background = colors.black,
  77.     accent = colors.black,
  78.     subtle = colors.black,
  79.  
  80.     lightText = colors.white,
  81.     text = colors.white,
  82.     errorText = colors.white,
  83. }
  84.  
  85.  
  86.  
  87. --    Utilities
  88.  
  89.  
  90. local modifiedRead = function(properties)
  91.     local text = ""
  92.     local startX, startY = term.getCursorPos()
  93.     local pos = 0
  94.  
  95.     local previousText = ""
  96.     local readHistory = nil
  97.     local historyPos = 0
  98.  
  99.     if not properties then
  100.         properties = {}
  101.     end
  102.  
  103.     if properties.displayLength then
  104.         properties.displayLength = math.min(properties.displayLength, w - 2)
  105.     else
  106.         properties.displayLength = w - startX - 1
  107.     end
  108.  
  109.     if properties.startingText then
  110.         text = properties.startingText
  111.         pos = text:len()
  112.     end
  113.  
  114.     if properties.history then
  115.         readHistory = {}
  116.         for k, v in pairs(properties.history) do
  117.             readHistory[k] = v
  118.         end
  119.     end
  120.  
  121.     if readHistory[1] == text then
  122.         table.remove(readHistory, 1)
  123.     end
  124.  
  125.     local draw = function(replaceCharacter)
  126.         local scroll = 0
  127.         if properties.displayLength and pos > properties.displayLength then
  128.             scroll = pos - properties.displayLength
  129.         end
  130.  
  131.         local repl = replaceCharacter or properties.replaceCharacter
  132.         term.setTextColor(theme.text)
  133.         term.setCursorPos(startX, startY)
  134.         if repl then
  135.             term.write(string.rep(repl:sub(1, 1), text:len() - scroll))
  136.         else
  137.             term.write(text:sub(scroll + 1))
  138.         end
  139.  
  140.         term.setCursorPos(startX + pos - scroll, startY)
  141.     end
  142.  
  143.     term.setCursorBlink(true)
  144.     draw()
  145.     while true do
  146.         local event, key, x, y, param4, param5 = os.pullEvent()
  147.  
  148.         if properties.onEvent then
  149.             -- Actions:
  150.             -- - exit (bool)
  151.             -- - text
  152.             -- - nullifyText
  153.  
  154.             term.setCursorBlink(false)
  155.             local action = properties.onEvent(text, event, key, x, y, param4, param5)
  156.             if action then
  157.                 if action.text then
  158.                     draw(" ")
  159.                     text = action.text
  160.                     pos = text:len()
  161.                 end if action.nullifyText then
  162.                     text = nil
  163.                     action.exit = true
  164.                 end if action.exit then
  165.                     break
  166.                 end
  167.             end
  168.             draw()
  169.         end
  170.  
  171.         term.setCursorBlink(true)
  172.         if event == "char" then
  173.             local canType = true
  174.             if properties.maxLength and text:len() >= properties.maxLength then
  175.                 canType = false
  176.             end
  177.  
  178.             if canType then
  179.                 text = text:sub(1, pos) .. key .. text:sub(pos + 1, -1)
  180.                 pos = pos + 1
  181.                 draw()
  182.             end
  183.         elseif event == "key" then
  184.             if key == keys.enter then
  185.                 break
  186.             elseif key == keys.left and pos > 0 then
  187.                 pos = pos - 1
  188.                 draw()
  189.             elseif key == keys.right and pos < text:len() then
  190.                 pos = pos + 1
  191.                 draw()
  192.             elseif key == keys.backspace and pos > 0 then
  193.                 draw(" ")
  194.                 text = text:sub(1, pos - 1) .. text:sub(pos + 1, -1)
  195.                 pos = pos - 1
  196.                 draw()
  197.             elseif key == keys.delete and pos < text:len() then
  198.                 draw(" ")
  199.                 text = text:sub(1, pos) .. text:sub(pos + 2, -1)
  200.                 draw()
  201.             elseif key == keys.home then
  202.                 pos = 0
  203.                 draw()
  204.             elseif key == keys["end"] then
  205.                 pos = text:len()
  206.                 draw()
  207.             elseif (key == keys.up or key == keys.down) and readHistory then
  208.                 local shouldDraw = false
  209.                 if historyPos == 0 then
  210.                     previousText = text
  211.                 elseif historyPos > 0 then
  212.                     readHistory[historyPos] = text
  213.                 end
  214.  
  215.                 if key == keys.up then
  216.                     if historyPos < #readHistory then
  217.                         historyPos = historyPos + 1
  218.                         shouldDraw = true
  219.                     end
  220.                 else
  221.                     if historyPos > 0 then
  222.                         historyPos = historyPos - 1
  223.                         shouldDraw = true
  224.                     end
  225.                 end
  226.  
  227.                 if shouldDraw then
  228.                     draw(" ")
  229.                     if historyPos > 0 then
  230.                         text = readHistory[historyPos]
  231.                     else
  232.                         text = previousText
  233.                     end
  234.                     pos = text:len()
  235.                     draw()
  236.                 end
  237.             end
  238.         elseif event == "mouse_click" then
  239.             local scroll = 0
  240.             if properties.displayLength and pos > properties.displayLength then
  241.                 scroll = pos - properties.displayLength
  242.             end
  243.  
  244.             if y == startY and x >= startX and x <= math.min(startX + text:len(), startX + (properties.displayLength or 10000)) then
  245.                 pos = x - startX + scroll
  246.                 draw()
  247.             elseif y == startY then
  248.                 if x < startX then
  249.                     pos = scroll
  250.                     draw()
  251.                 elseif x > math.min(startX + text:len(), startX + (properties.displayLength or 10000)) then
  252.                     pos = text:len()
  253.                     draw()
  254.                 end
  255.             end
  256.         end
  257.     end
  258.  
  259.     term.setCursorBlink(false)
  260.     print("")
  261.     return text
  262. end
  263.  
  264.  
  265. local prompt = function(items, x, y, w, h)
  266.     local selected = 1
  267.     local scroll = 0
  268.  
  269.     local draw = function()
  270.         for i = scroll + 1, scroll + h do
  271.             local item = items[i]
  272.             if item then
  273.                 term.setCursorPos(x, y + i - 1)
  274.                 term.setBackgroundColor(theme.background)
  275.                 term.setTextColor(theme.lightText)
  276.  
  277.                 if scroll + selected == i then
  278.                     term.setTextColor(theme.text)
  279.                     term.write(" > ")
  280.                 else
  281.                     term.write(" - ")
  282.                 end
  283.  
  284.                 term.write(item)
  285.             end
  286.         end
  287.     end
  288.  
  289.     draw()
  290.     while true do
  291.         local event, key, x, y = os.pullEvent()
  292.  
  293.         if event == "key" then
  294.             if key == keys.up and selected > 1 then
  295.                 selected = selected - 1
  296.  
  297.                 if selected - scroll == 0 then
  298.                     scroll = scroll - 1
  299.                 end
  300.             elseif key == keys.down and selected < #items then
  301.                 selected = selected + 1
  302.             end
  303.  
  304.             draw()
  305.         elseif event == "mouse_click" then
  306.  
  307.         elseif event == "mouse_scroll" then
  308.             if key > 0 then
  309.                 os.queueEvent("key", keys.down)
  310.             else
  311.                 os.queueEvent("key", keys.up)
  312.             end
  313.         end
  314.     end
  315. end
  316.  
  317.  
  318.  
  319. --    GUI
  320.  
  321.  
  322. local clear = function(bg, fg)
  323.     term.setTextColor(fg)
  324.     term.setBackgroundColor(bg)
  325.     term.clear()
  326.     term.setCursorPos(1, 1)
  327. end
  328.  
  329.  
  330. local fill = function(x, y, width, height, bg)
  331.     term.setBackgroundColor(bg)
  332.     for i = y, y + height - 1 do
  333.         term.setCursorPos(x, i)
  334.         term.write(string.rep(" ", width))
  335.     end
  336. end
  337.  
  338.  
  339. local center = function(text)
  340.     local x, y = term.getCursorPos()
  341.     term.setCursorPos(math.floor(w / 2 - text:len() / 2) + (text:len() % 2 == 0 and 1 or 0), y)
  342.     term.write(text)
  343.     term.setCursorPos(1, y + 1)
  344. end
  345.  
  346.  
  347. local centerSplit = function(text, width)
  348.     local words = {}
  349.     for word in text:gmatch("[^ \t]+") do
  350.         table.insert(words, word)
  351.     end
  352.  
  353.     local lines = {""}
  354.     while lines[#lines]:len() < width do
  355.         lines[#lines] = lines[#lines] .. words[1] .. " "
  356.         table.remove(words, 1)
  357.  
  358.         if #words == 0 then
  359.             break
  360.         end
  361.  
  362.         if lines[#lines]:len() + words[1]:len() >= width then
  363.             table.insert(lines, "")
  364.         end
  365.     end
  366.  
  367.     for _, line in pairs(lines) do
  368.         center(line)
  369.     end
  370. end
  371.  
  372.  
  373.  
  374. --    Updating
  375.  
  376.  
  377. local download = function(url)
  378.     http.request(url)
  379.     local timeoutID = os.startTimer(httpTimeout)
  380.     while true do
  381.         local event, fetchedURL, response = os.pullEvent()
  382.         if (event == "timer" and fetchedURL == timeoutID) or event == "http_failure" then
  383.             return false
  384.         elseif event == "http_success" and fetchedURL == url then
  385.             local contents = response.readAll()
  386.             response.close()
  387.             return contents
  388.         end
  389.     end
  390. end
  391.  
  392.  
  393. local downloadAndSave = function(url, path)
  394.     local contents = download(url)
  395.     if contents and not fs.isReadOnly(path) and not fs.isDir(path) then
  396.         local f = io.open(path, "w")
  397.         f:write(contents)
  398.         f:close()
  399.         return false
  400.     end
  401.     return true
  402. end
  403.  
  404.  
  405. local updateAvailable = function()
  406.     local number = download(buildURL)
  407.     if not number then
  408.         return false, true
  409.     end
  410.  
  411.     if number and tonumber(number) and tonumber(number) > build then
  412.         return true, false
  413.     end
  414.  
  415.     return false, false
  416. end
  417.  
  418.  
  419. local redownloadBrowser = function()
  420.     return downloadAndSave(firewolfURL, firewolfLocation)
  421. end
  422.  
  423.  
  424.  
  425. --    Display Websites
  426.  
  427.  
  428. builtInSites["display"] = {}
  429.  
  430.  
  431. builtInSites["display"]["firewolf"] = function()
  432.     local logo = {
  433.         "______                         _  __ ",
  434.         "|  ___|                       | |/ _|",
  435.         "| |_  _ ____ _____      _____ | | |_ ",
  436.         "|  _|| |  __/ _ \\ \\ /\\ / / _ \\| |  _|",
  437.         "| |  | | | |  __/\\ V  V / <_> | | |  ",
  438.         "\\_|  |_|_|  \\___| \\_/\\_/ \\___/|_|_|  ",
  439.     }
  440.  
  441.     clear(theme.background, theme.text)
  442.     fill(1, 3, w, 9, theme.subtle)
  443.  
  444.     term.setCursorPos(1, 3)
  445.     for _, line in pairs(logo) do
  446.         center(line)
  447.     end
  448.  
  449.     term.setCursorPos(1, 10)
  450.     center(version)
  451.  
  452.     term.setBackgroundColor(theme.background)
  453.     term.setTextColor(theme.text)
  454.     term.setCursorPos(1, 14)
  455.     center("Search using the Query Box above")
  456.     center("Visit rdnt://help for help using Firewolf.")
  457.  
  458.     term.setCursorPos(1, h - 2)
  459.     center("Made by GravityScore and 1lann")
  460. end
  461.  
  462.  
  463. builtInSites["display"]["credits"] = function()
  464.     clear(theme.background, theme.text)
  465.  
  466.     fill(1, 6, w, 3, theme.subtle)
  467.     term.setCursorPos(1, 7)
  468.     center("Credits")
  469.  
  470.     term.setBackgroundColor(theme.background)
  471.     term.setCursorPos(1, 11)
  472.     center("Written by GravityScore and 1lann")
  473.     print("")
  474.     center("RC4 Implementation by AgentE382")
  475. end
  476.  
  477.  
  478. builtInSites["display"]["help"] = function()
  479.     clear(theme.background, theme.text)
  480.  
  481.     fill(1, 3, w, 3, theme.subtle)
  482.     term.setCursorPos(1, 4)
  483.     center("Help")
  484.  
  485.     term.setBackgroundColor(theme.background)
  486.     term.setCursorPos(1, 7)
  487.     center("Click on the URL bar or press control to")
  488.     center("open the query box")
  489.     print("")
  490.     center("Type in a search query or website URL")
  491.     center("into the query box.")
  492.     print("")
  493.     center("Search for nothing to see all available")
  494.     center("websites.")
  495.     print("")
  496.     center("Visit rdnt://server to setup a server.")
  497.     center("Visit rdnt://update to update Firewolf.")
  498. end
  499.  
  500.  
  501. builtInSites["display"]["server"] = function()
  502.     clear(theme.background, theme.text)
  503.  
  504.     fill(1, 6, w, 3, theme.subtle)
  505.     term.setCursorPos(1, 7)
  506.     center("Server Software")
  507.  
  508.     term.setBackgroundColor(theme.background)
  509.     term.setCursorPos(1, 11)
  510.     if not http then
  511.         center("HTTP is not enabled!")
  512.         print("")
  513.         center("Please enable it in your config file")
  514.         center("to download Firewolf Server.")
  515.     else
  516.         center("Press space to download")
  517.         center("Firewolf Server to:")
  518.         print("")
  519.         center("/fwserver")
  520.  
  521.         while true do
  522.             local event, key = os.pullEvent()
  523.             if event == "key" and key == 57 then
  524.                 fill(1, 11, w, 4, theme.background)
  525.                 term.setCursorPos(1, 11)
  526.                 center("Downloading...")
  527.  
  528.                 local err = downloadAndSave(serverURL, "/fwserver")
  529.  
  530.                 fill(1, 11, w, 4, theme.background)
  531.                 term.setCursorPos(1, 11)
  532.                 center(err and "Download failed!" or "Download successful!")
  533.             end
  534.         end
  535.     end
  536. end
  537.  
  538.  
  539. builtInSites["display"]["update"] = function()
  540.     clear(theme.background, theme.text)
  541.  
  542.     fill(1, 3, w, 3, theme.subtle)
  543.     term.setCursorPos(1, 4)
  544.     center("Update")
  545.  
  546.     term.setBackgroundColor(theme.background)
  547.     if not http then
  548.         term.setCursorPos(1, 9)
  549.         center("HTTP is not enabled!")
  550.         print("")
  551.         center("Please enable it in your config")
  552.         center("file to download Firewolf updates.")
  553.     else
  554.         term.setCursorPos(1, 10)
  555.         center("Checking for updates...")
  556.  
  557.         local available, err = updateAvailable()
  558.  
  559.         term.setCursorPos(1, 10)
  560.         if available then
  561.             term.clearLine()
  562.             center("Update found!")
  563.             center("Press enter to download.")
  564.  
  565.             while true do
  566.                 local event, key = os.pullEvent()
  567.                 if event == "key" and key == keys.enter then
  568.                     break
  569.                 end
  570.             end
  571.  
  572.             fill(1, 10, w, 2, theme.background)
  573.             term.setCursorPos(1, 10)
  574.             center("Downloading...")
  575.  
  576.             local err = redownloadBrowser()
  577.  
  578.             term.setCursorPos(1, 10)
  579.             term.clearLine()
  580.             if err then
  581.                 center("Download failed!")
  582.             else
  583.                 center("Download succeeded!")
  584.                 center("Please restart Firewolf...")
  585.             end
  586.         elseif err then
  587.             term.clearLine()
  588.             center("Checking failed!")
  589.         else
  590.             term.clearLine()
  591.             center("No updates found.")
  592.         end
  593.     end
  594. end
  595.  
  596.  
  597.  
  598. --    Built In Websites
  599.  
  600.  
  601. builtInSites["error"] = function(err)
  602.     fill(1, 3, w, 3, theme.subtle)
  603.     term.setCursorPos(1, 4)
  604.     center("Failed to load page!")
  605.  
  606.     term.setBackgroundColor(theme.background)
  607.     term.setCursorPos(1, 9)
  608.     center(err)
  609.     print("")
  610.     center("Please try again.")
  611. end
  612.  
  613.  
  614. builtInSites["noresults"] = function()
  615.     fill(1, 3, w, 3, theme.subtle)
  616.     term.setCursorPos(1, 4)
  617.     center("No results!")
  618.  
  619.     term.setBackgroundColor(theme.background)
  620.     term.setCursorPos(1, 9)
  621.     center("Your search didn't return")
  622.     center("any results!")
  623.  
  624.     os.pullEvent("key")
  625.     os.queueEvent("")
  626.     os.pullEvent()
  627. end
  628.  
  629.  
  630. builtInSites["search advanced"] = function(results)
  631.     local startY = 6
  632.     local height = h - startY - 1
  633.     local scroll = 0
  634.  
  635.     local draw = function()
  636.         fill(1, startY, w, height + 1, theme.background)
  637.  
  638.         for i = scroll + 1, scroll + height do
  639.             if results[i] then
  640.                 term.setCursorPos(5, (i - scroll) + startY)
  641.                 term.write(currentProtocol .. "://" .. results[i])
  642.             end
  643.         end
  644.     end
  645.  
  646.     draw()
  647.     while true do
  648.         local event, but, x, y = os.pullEvent()
  649.  
  650.         if event == "mouse_click" and y >= startY and y <= startY + height then
  651.             local item = results[y - startY + scroll]
  652.             if item then
  653.                 os.queueEvent(redirectEvent, item)
  654.                 coroutine.yield()
  655.             end
  656.         elseif event == "key" then
  657.             if but == keys.up then
  658.                 scroll = math.max(0, scroll - 1)
  659.             elseif but == keys.down and #results > height then
  660.                 scroll = math.min(scroll + 1, #results - height)
  661.             end
  662.  
  663.             draw()
  664.         elseif event == "mouse_scroll" then
  665.             if but > 0 then
  666.                 os.queueEvent("key", keys.down)
  667.             else
  668.                 os.queueEvent("key", keys.up)
  669.             end
  670.         end
  671.     end
  672. end
  673.  
  674.  
  675. builtInSites["search basic"] = function(results)
  676.     local startY = 6
  677.     local height = h - startY - 1
  678.     local scroll = 0
  679.     local selected = 1
  680.  
  681.     local draw = function()
  682.         fill(1, startY, w, height + 1, theme.background)
  683.  
  684.         for i = scroll + 1, scroll + height do
  685.             if results[i] then
  686.                 if i == selected + scroll then
  687.                     term.setCursorPos(3, (i - scroll) + startY)
  688.                     term.write("> " .. currentProtocol .. "://" .. results[i])
  689.                 else
  690.                     term.setCursorPos(5, (i - scroll) + startY)
  691.                     term.write(currentProtocol .. "://" .. results[i])
  692.                 end
  693.             end
  694.         end
  695.     end
  696.  
  697.     draw()
  698.     while true do
  699.         local event, but, x, y = os.pullEvent()
  700.  
  701.         if event == "key" then
  702.             if but == keys.up and selected + scroll > 1 then
  703.                 if selected > 1 then
  704.                     selected = selected - 1
  705.                 else
  706.                     scroll = math.max(0, scroll - 1)
  707.                 end
  708.             elseif but == keys.down and selected + scroll < #results then
  709.                 if selected < height then
  710.                     selected = selected + 1
  711.                 else
  712.                     scroll = math.min(scroll + 1, #results - height)
  713.                 end
  714.             elseif but == keys.enter then
  715.                 local item = results[scroll + selected]
  716.                 if item then
  717.                     os.queueEvent(redirectEvent, item)
  718.                     coroutine.yield()
  719.                 end
  720.             end
  721.  
  722.             draw()
  723.         elseif event == "mouse_scroll" then
  724.             if but > 0 then
  725.                 os.queueEvent("key", keys.down)
  726.             else
  727.                 os.queueEvent("key", keys.up)
  728.             end
  729.         end
  730.     end
  731. end
  732.  
  733.  
  734. builtInSites["search"] = function(results)
  735.     clear(theme.background, theme.text)
  736.  
  737.     fill(1, 3, w, 3, theme.subtle)
  738.     term.setCursorPos(1, 4)
  739.     center(#results .. " Search " .. (#results == 1 and "Result" or "Results"))
  740.  
  741.     term.setBackgroundColor(theme.background)
  742.  
  743.     if term.isColor() then
  744.         builtInSites["search advanced"](results)
  745.     else
  746.         builtInSites["search basic"](results)
  747.     end
  748. end
  749.  
  750.  
  751. builtInSites["crash"] = function(err)
  752.     fill(1, 3, w, 3, theme.subtle)
  753.     term.setCursorPos(1, 4)
  754.     center("The website crashed!")
  755.  
  756.     term.setBackgroundColor(theme.background)
  757.     term.setCursorPos(1, 8)
  758.     centerSplit(err, w - 4)
  759.     print("\n")
  760.     center("Please report this error to")
  761.     center("the website creator.")
  762. end
  763.  
  764.  
  765.  
  766. --    Menubar
  767.  
  768.  
  769. local getTabName = function(url)
  770.     local name = url:match("^[^/]+")
  771.  
  772.     if not name then
  773.         name = "Search"
  774.     end
  775.  
  776.     if name:sub(1, 3) == "www" then
  777.         name = name:sub(5):gsub("^%s*(.-)%s*$", "%1")
  778.     end
  779.  
  780.     if name:len() > maxTabNameWidth then
  781.         name = name:sub(1, maxTabNameWidth):gsub("^%s*(.-)%s*$", "%1")
  782.     end
  783.  
  784.     if name:sub(-1, -1) == "." then
  785.         name = name:sub(1, -2):gsub("^%s*(.-)%s*$", "%1")
  786.     end
  787.  
  788.     return name:gsub("^%s*(.-)%s*$", "%1")
  789. end
  790.  
  791.  
  792. local determineClickedTab = function(x, y)
  793.     if y == 2 then
  794.         local minx = 2
  795.         for i, tab in pairs(tabs) do
  796.             local name = getTabName(tab.url)
  797.  
  798.             if x >= minx and x <= minx + name:len() - 1 then
  799.                 return i
  800.             elseif x == minx + name:len() and i == currentTab and #tabs > 1 then
  801.                 return "close"
  802.             else
  803.                 minx = minx + name:len() + 2
  804.             end
  805.         end
  806.  
  807.         if x == minx and #tabs < maxTabs then
  808.             return "new"
  809.         end
  810.     end
  811.  
  812.     return nil
  813. end
  814.  
  815.  
  816. local setupMenubar = function()
  817.     if enableTabBar then
  818.         menubarWindow = window.create(originalTerminal, 1, 1, w, 2, false)
  819.     else
  820.         menubarWindow = window.create(originalTerminal, 1, 1, w, 1, false)
  821.     end
  822. end
  823.  
  824.  
  825. local drawMenubar = function()
  826.     if isMenubarOpen then
  827.         term.redirect(menubarWindow)
  828.         menubarWindow.setVisible(true)
  829.  
  830.         fill(1, 1, w, 1, theme.accent)
  831.         term.setTextColor(theme.text)
  832.  
  833.         term.setBackgroundColor(theme.accent)
  834.         term.setCursorPos(2, 1)
  835.         if currentWebsiteURL:match("^[^%?]+") then
  836.             term.write(currentProtocol .. "://" .. currentWebsiteURL:match("^[^%?]+"))
  837.         else
  838.             term.write(currentProtocol .. "://" ..currentWebsiteURL)
  839.         end
  840.  
  841.         term.setCursorPos(w - 5, 1)
  842.         term.write("[===]")
  843.  
  844.         if enableTabBar then
  845.             fill(1, 2, w, 1, theme.subtle)
  846.  
  847.             term.setCursorPos(1, 2)
  848.             for i, tab in pairs(tabs) do
  849.                 term.setBackgroundColor(theme.subtle)
  850.                 term.setTextColor(theme.lightText)
  851.                 if i == currentTab then
  852.                     term.setTextColor(theme.text)
  853.                 end
  854.  
  855.                 local tabName = getTabName(tab.url)
  856.                 term.write(" " .. tabName)
  857.  
  858.                 if i == currentTab and #tabs > 1 then
  859.                     term.setTextColor(theme.errorText)
  860.                     term.write("x")
  861.                 else
  862.                     term.write(" ")
  863.                 end
  864.             end
  865.  
  866.             if #tabs < maxTabs then
  867.                 term.setTextColor(theme.lightText)
  868.                 term.setBackgroundColor(theme.subtle)
  869.                 term.write(" + ")
  870.             end
  871.         end
  872.     else
  873.         menubarWindow.setVisible(false)
  874.     end
  875. end
  876.  
  877.  
  878.  
  879. --    RC4
  880. --    Implementation by AgentE382
  881.  
  882.  
  883. local cryptWrapper = function(plaintext, salt)
  884.     local key = type(salt) == "table" and {unpack(salt)} or {string.byte(salt, 1, #salt)}
  885.     local S = {}
  886.     for i = 0, 255 do
  887.         S[i] = i
  888.     end
  889.  
  890.     local j, keylength = 0, #key
  891.     for i = 0, 255 do
  892.         j = (j + S[i] + key[i % keylength + 1]) % 256
  893.         S[i], S[j] = S[j], S[i]
  894.     end
  895.  
  896.     local i = 0
  897.     j = 0
  898.     local chars, astable = type(plaintext) == "table" and {unpack(plaintext)} or {string.byte(plaintext, 1, #plaintext)}, false
  899.  
  900.     for n = 1, #chars do
  901.         i = (i + 1) % 256
  902.         j = (j + S[i]) % 256
  903.         S[i], S[j] = S[j], S[i]
  904.         chars[n] = bit.bxor(S[(S[i] + S[j]) % 256], chars[n])
  905.         if chars[n] > 127 or chars[n] == 13 then
  906.             astable = true
  907.         end
  908.     end
  909.  
  910.     return astable and chars or string.char(unpack(chars))
  911. end
  912.  
  913.  
  914. local crypt = function(text, key)
  915.     local resp, msg = pcall(cryptWrapper, text, key)
  916.     if resp then
  917.         return msg
  918.     else
  919.         return nil
  920.     end
  921. end
  922.  
  923.  
  924.  
  925. --    Base64
  926. --
  927. --    Base64 Encryption/Decryption
  928. --    By KillaVanilla
  929. --    http://www.computercraft.info/forums2/index.php?/topic/12450-killavanillas-various-apis/
  930. --    http://pastebin.com/rCYDnCxn
  931.  
  932.  
  933. local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
  934.  
  935.  
  936. local function sixBitToBase64(input)
  937.     return string.sub(alphabet, input+1, input+1)
  938. end
  939.  
  940.  
  941. local function base64ToSixBit(input)
  942.     for i=1, 64 do
  943.         if input == string.sub(alphabet, i, i) then
  944.             return i-1
  945.         end
  946.     end
  947. end
  948.  
  949.  
  950. local function octetToBase64(o1, o2, o3)
  951.     local shifted = bit.brshift(bit.band(o1, 0xFC), 2)
  952.     local i1 = sixBitToBase64(shifted)
  953.     local i2 = "A"
  954.     local i3 = "="
  955.     local i4 = "="
  956.     if o2 then
  957.         i2 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o1, 3), 4), bit.brshift(bit.band(o2, 0xF0), 4) ))
  958.         if not o3 then
  959.             i3 = sixBitToBase64(bit.blshift(bit.band(o2, 0x0F), 2))
  960.         else
  961.             i3 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o2, 0x0F), 2), bit.brshift(bit.band(o3, 0xC0), 6) ))
  962.         end
  963.     else
  964.         i2 = sixBitToBase64(bit.blshift(bit.band(o1, 3), 4))
  965.     end
  966.     if o3 then
  967.         i4 = sixBitToBase64(bit.band(o3, 0x3F))
  968.     end
  969.  
  970.     return i1..i2..i3..i4
  971. end
  972.  
  973.  
  974. local function base64ToThreeOctet(s1)
  975.     local c1 = base64ToSixBit(string.sub(s1, 1, 1))
  976.     local c2 = base64ToSixBit(string.sub(s1, 2, 2))
  977.     local c3 = 0
  978.     local c4 = 0
  979.     local o1 = 0
  980.     local o2 = 0
  981.     local o3 = 0
  982.     if string.sub(s1, 3, 3) == "=" then
  983.         c3 = nil
  984.         c4 = nil
  985.     elseif string.sub(s1, 4, 4) == "=" then
  986.         c3 = base64ToSixBit(string.sub(s1, 3, 3))
  987.         c4 = nil
  988.     else
  989.         c3 = base64ToSixBit(string.sub(s1, 3, 3))
  990.         c4 = base64ToSixBit(string.sub(s1, 4, 4))
  991.     end
  992.     o1 = bit.bor( bit.blshift(c1, 2), bit.brshift(bit.band( c2, 0x30 ), 4) )
  993.     if c3 then
  994.         o2 = bit.bor( bit.blshift(bit.band(c2, 0x0F), 4), bit.brshift(bit.band( c3, 0x3C ), 2) )
  995.     else
  996.         o2 = nil
  997.     end
  998.     if c4 then
  999.         o3 = bit.bor( bit.blshift(bit.band(c3, 3), 6), c4 )
  1000.     else
  1001.         o3 = nil
  1002.     end
  1003.     return o1, o2, o3
  1004. end
  1005.  
  1006.  
  1007. local function splitIntoBlocks(bytes)
  1008.     local blockNum = 1
  1009.     local blocks = {}
  1010.     for i=1, #bytes, 3 do
  1011.         blocks[blockNum] = {bytes[i], bytes[i+1], bytes[i+2]}
  1012.         blockNum = blockNum+1
  1013.     end
  1014.     return blocks
  1015. end
  1016.  
  1017.  
  1018. function base64Encode(bytes)
  1019.     local blocks = splitIntoBlocks(bytes)
  1020.     local output = ""
  1021.     for i=1, #blocks do
  1022.         output = output..octetToBase64( unpack(blocks[i]) )
  1023.     end
  1024.     return output
  1025. end
  1026.  
  1027.  
  1028. function base64Decode(str)
  1029.     local bytes = {}
  1030.     local blocks = {}
  1031.     local blockNum = 1
  1032.  
  1033.     for i=1, #str, 4 do
  1034.         blocks[blockNum] = string.sub(str, i, i+3)
  1035.         blockNum = blockNum+1
  1036.     end
  1037.  
  1038.     for i=1, #blocks do
  1039.         local o1, o2, o3 = base64ToThreeOctet(blocks[i])
  1040.         table.insert(bytes, o1)
  1041.         table.insert(bytes, o2)
  1042.         table.insert(bytes, o3)
  1043.     end
  1044.  
  1045.     return bytes
  1046. end
  1047.  
  1048.  
  1049.  
  1050. --    SHA-256
  1051. --
  1052. --    Adaptation of the Secure Hashing Algorithm (SHA-244/256)
  1053. --    Found Here: http://lua-users.org/wiki/SecureHashAlgorithm
  1054. --
  1055. --    Using an adapted version of the bit library
  1056. --    Found Here: https://bitbucket.org/Boolsheet/bslf/src/1ee664885805/bit.lua
  1057.  
  1058.  
  1059. local MOD = 2^32
  1060. local MODM = MOD-1
  1061.  
  1062.  
  1063. local function memoize(f)
  1064.     local mt = {}
  1065.     local t = setmetatable({}, mt)
  1066.     function mt:__index(k)
  1067.         local v = f(k)
  1068.         t[k] = v
  1069.         return v
  1070.     end
  1071.     return t
  1072. end
  1073.  
  1074.  
  1075. local function make_bitop_uncached(t, m)
  1076.     local function bitop(a, b)
  1077.         local res,p = 0,1
  1078.         while a ~= 0 and b ~= 0 do
  1079.             local am, bm = a % m, b % m
  1080.             res = res + t[am][bm] * p
  1081.             a = (a - am) / m
  1082.             b = (b - bm) / m
  1083.             p = p * m
  1084.         end
  1085.         res = res + (a + b) * p
  1086.         return res
  1087.     end
  1088.  
  1089.     return bitop
  1090. end
  1091.  
  1092.  
  1093. local function make_bitop(t)
  1094.     local op1 = make_bitop_uncached(t,2^1)
  1095.     local op2 = memoize(function(a)
  1096.         return memoize(function(b)
  1097.             return op1(a, b)
  1098.         end)
  1099.     end)
  1100.     return make_bitop_uncached(op2, 2 ^ (t.n or 1))
  1101. end
  1102.  
  1103.  
  1104. local customBxor1 = make_bitop({[0] = {[0] = 0,[1] = 1}, [1] = {[0] = 1, [1] = 0}, n = 4})
  1105.  
  1106. local function customBxor(a, b, c, ...)
  1107.     local z = nil
  1108.     if b then
  1109.         a = a % MOD
  1110.         b = b % MOD
  1111.         z = customBxor1(a, b)
  1112.         if c then
  1113.             z = customBxor(z, c, ...)
  1114.         end
  1115.         return z
  1116.     elseif a then
  1117.         return a % MOD
  1118.     else
  1119.         return 0
  1120.     end
  1121. end
  1122.  
  1123.  
  1124. local function customBand(a, b, c, ...)
  1125.     local z
  1126.     if b then
  1127.         a = a % MOD
  1128.         b = b % MOD
  1129.         z = ((a + b) - customBxor1(a,b)) / 2
  1130.         if c then
  1131.             z = customBand(z, c, ...)
  1132.         end
  1133.         return z
  1134.     elseif a then
  1135.         return a % MOD
  1136.     else
  1137.         return MODM
  1138.     end
  1139. end
  1140.  
  1141.  
  1142. local function bnot(x)
  1143.     return (-1 - x) % MOD
  1144. end
  1145.  
  1146.  
  1147. local function rshift1(a, disp)
  1148.     if disp < 0 then
  1149.         return lshift(a, -disp)
  1150.     end
  1151.     return math.floor(a % 2 ^ 32 / 2 ^ disp)
  1152. end
  1153.  
  1154.  
  1155. local function rshift(x, disp)
  1156.     if disp > 31 or disp < -31 then
  1157.         return 0
  1158.     end
  1159.     return rshift1(x % MOD, disp)
  1160. end
  1161.  
  1162.  
  1163. local function lshift(a, disp)
  1164.     if disp < 0 then
  1165.         return rshift(a, -disp)
  1166.     end
  1167.     return (a * 2 ^ disp) % 2 ^ 32
  1168. end
  1169.  
  1170.  
  1171. local function rrotate(x, disp)
  1172.     x = x % MOD
  1173.     disp = disp % 32
  1174.     local low = customBand(x, 2 ^ disp - 1)
  1175.     return rshift(x, disp) + lshift(low, 32 - disp)
  1176. end
  1177.  
  1178.  
  1179. local k = {
  1180.     0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
  1181.     0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
  1182.     0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
  1183.     0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
  1184.     0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
  1185.     0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
  1186.     0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
  1187.     0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
  1188.     0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
  1189.     0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
  1190.     0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
  1191.     0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
  1192.     0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
  1193.     0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
  1194.     0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
  1195.     0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
  1196. }
  1197.  
  1198.  
  1199. local function str2hexa(s)
  1200.     return (string.gsub(s, ".", function(c)
  1201.         return string.format("%02x", string.byte(c))
  1202.     end))
  1203. end
  1204.  
  1205.  
  1206. local function num2s(l, n)
  1207.     local s = ""
  1208.     for i = 1, n do
  1209.         local rem = l % 256
  1210.         s = string.char(rem) .. s
  1211.         l = (l - rem) / 256
  1212.     end
  1213.     return s
  1214. end
  1215.  
  1216.  
  1217. local function s232num(s, i)
  1218.     local n = 0
  1219.     for i = i, i + 3 do
  1220.         n = n*256 + string.byte(s, i)
  1221.     end
  1222.     return n
  1223. end
  1224.  
  1225.  
  1226. local function preproc(msg, len)
  1227.     local extra = 64 - ((len + 9) % 64)
  1228.     len = num2s(8 * len, 8)
  1229.     msg = msg .. "\128" .. string.rep("\0", extra) .. len
  1230.     assert(#msg % 64 == 0)
  1231.     return msg
  1232. end
  1233.  
  1234.  
  1235. local function initH256(H)
  1236.     H[1] = 0x6a09e667
  1237.     H[2] = 0xbb67ae85
  1238.     H[3] = 0x3c6ef372
  1239.     H[4] = 0xa54ff53a
  1240.     H[5] = 0x510e527f
  1241.     H[6] = 0x9b05688c
  1242.     H[7] = 0x1f83d9ab
  1243.     H[8] = 0x5be0cd19
  1244.     return H
  1245. end
  1246.  
  1247.  
  1248. local function digestblock(msg, i, H)
  1249.     local w = {}
  1250.     for j = 1, 16 do
  1251.         w[j] = s232num(msg, i + (j - 1)*4)
  1252.     end
  1253.     for j = 17, 64 do
  1254.         local v = w[j - 15]
  1255.         local s0 = customBxor(rrotate(v, 7), rrotate(v, 18), rshift(v, 3))
  1256.         v = w[j - 2]
  1257.         w[j] = w[j - 16] + s0 + w[j - 7] + customBxor(rrotate(v, 17), rrotate(v, 19), rshift(v, 10))
  1258.     end
  1259.  
  1260.     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]
  1261.     for i = 1, 64 do
  1262.         local s0 = customBxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22))
  1263.         local maj = customBxor(customBand(a, b), customBand(a, c), customBand(b, c))
  1264.         local t2 = s0 + maj
  1265.         local s1 = customBxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25))
  1266.         local ch = customBxor (customBand(e, f), customBand(bnot(e), g))
  1267.         local t1 = h + s1 + ch + k[i] + w[i]
  1268.         h, g, f, e, d, c, b, a = g, f, e, d + t1, c, b, a, t1 + t2
  1269.     end
  1270.  
  1271.     H[1] = customBand(H[1] + a)
  1272.     H[2] = customBand(H[2] + b)
  1273.     H[3] = customBand(H[3] + c)
  1274.     H[4] = customBand(H[4] + d)
  1275.     H[5] = customBand(H[5] + e)
  1276.     H[6] = customBand(H[6] + f)
  1277.     H[7] = customBand(H[7] + g)
  1278.     H[8] = customBand(H[8] + h)
  1279. end
  1280.  
  1281.  
  1282. local function sha256(msg)
  1283.     msg = preproc(msg, #msg)
  1284.     local H = initH256({})
  1285.     for i = 1, #msg, 64 do
  1286.         digestblock(msg, i, H)
  1287.     end
  1288.     return str2hexa(num2s(H[1], 4) .. num2s(H[2], 4) .. num2s(H[3], 4) .. num2s(H[4], 4) ..
  1289.         num2s(H[5], 4) .. num2s(H[6], 4) .. num2s(H[7], 4) .. num2s(H[8], 4))
  1290. end
  1291.  
  1292.  
  1293. local protocolName = "Firewolf"
  1294.  
  1295.  
  1296.  
  1297. --    Cryptography
  1298.  
  1299.  
  1300. local Cryptography = {}
  1301. Cryptography.sha = {}
  1302. Cryptography.base64 = {}
  1303. Cryptography.aes = {}
  1304.  
  1305.  
  1306. function Cryptography.bytesFromMessage(msg)
  1307.     local bytes = {}
  1308.  
  1309.     for i = 1, msg:len() do
  1310.         local letter = string.byte(msg:sub(i, i))
  1311.         table.insert(bytes, letter)
  1312.     end
  1313.  
  1314.     return bytes
  1315. end
  1316.  
  1317.  
  1318. function Cryptography.messageFromBytes(bytes)
  1319.     local msg = ""
  1320.  
  1321.     for i = 1, #bytes do
  1322.         local letter = string.char(bytes[i])
  1323.         msg = msg .. letter
  1324.     end
  1325.  
  1326.     return msg
  1327. end
  1328.  
  1329.  
  1330. function Cryptography.bytesFromKey(key)
  1331.     local bytes = {}
  1332.  
  1333.     for i = 1, key:len() / 2 do
  1334.         local group = key:sub((i - 1) * 2 + 1, (i - 1) * 2 + 1)
  1335.         local num = tonumber(group, 16)
  1336.         table.insert(bytes, num)
  1337.     end
  1338.  
  1339.     return bytes
  1340. end
  1341.  
  1342.  
  1343. function Cryptography.sha.sha256(msg)
  1344.     return sha256(msg)
  1345. end
  1346.  
  1347.  
  1348. function Cryptography.aes.encrypt(msg, key)
  1349.     return base64Encode(crypt(msg, key))
  1350. end
  1351.  
  1352.  
  1353. function Cryptography.aes.decrypt(msg, key)
  1354.     return crypt(base64Decode(msg), key)
  1355. end
  1356.  
  1357.  
  1358. function Cryptography.base64.encode(msg)
  1359.     return base64Encode(Cryptography.bytesFromMessage(msg))
  1360. end
  1361.  
  1362.  
  1363. function Cryptography.base64.decode(msg)
  1364.     return Cryptography.messageFromBytes(base64Decode(msg))
  1365. end
  1366.  
  1367.  
  1368. function Cryptography.channel(text)
  1369.     local hashed = Cryptography.sha.sha256(text)
  1370.  
  1371.     local total = 0
  1372.  
  1373.     for i = 1, hashed:len() do
  1374.         total = total + string.byte(hashed:sub(i, i))
  1375.     end
  1376.  
  1377.     return (total % 55530) + 10000
  1378. end
  1379.  
  1380.  
  1381. function Cryptography.sanatize(text)
  1382.     local sanatizeChars = {"%", "(", ")", "[", "]", ".", "+", "-", "*", "?", "^", "$"}
  1383.  
  1384.     for _, char in pairs(sanatizeChars) do
  1385.         text = text:gsub("%"..char, "%%%"..char)
  1386.     end
  1387.     return text
  1388. end
  1389.  
  1390.  
  1391.  
  1392. --    Modem
  1393.  
  1394.  
  1395. local Modem = {}
  1396. Modem.modems = {}
  1397.  
  1398.  
  1399. function Modem.exists()
  1400.     Modem.exists = false
  1401.     for _, side in pairs(rs.getSides()) do
  1402.         if peripheral.isPresent(side) and peripheral.getType(side) == "modem" then
  1403.             Modem.exists = true
  1404.  
  1405.             if not Modem.modems[side] then
  1406.                 Modem.modems[side] = peripheral.wrap(side)
  1407.             end
  1408.         end
  1409.     end
  1410.  
  1411.     return Modem.exists
  1412. end
  1413.  
  1414.  
  1415. function Modem.open(channel)
  1416.     if not Modem.exists then
  1417.         return false
  1418.     end
  1419.  
  1420.     for side, modem in pairs(Modem.modems) do
  1421.         modem.open(channel)
  1422.         rednet.open(side)
  1423.     end
  1424.  
  1425.     return true
  1426. end
  1427.  
  1428.  
  1429. function Modem.close(channel)
  1430.     if not Modem.exists then
  1431.         return false
  1432.     end
  1433.  
  1434.     for side, modem in pairs(Modem.modems) do
  1435.         modem.close(channel)
  1436.     end
  1437.  
  1438.     return true
  1439. end
  1440.  
  1441.  
  1442. function Modem.closeAll()
  1443.     if not Modem.exists then
  1444.         return false
  1445.     end
  1446.  
  1447.     for side, modem in pairs(Modem.modems) do
  1448.         modem.closeAll()
  1449.     end
  1450.  
  1451.     return true
  1452. end
  1453.  
  1454.  
  1455. function Modem.isOpen(channel)
  1456.     if not Modem.exists then
  1457.         return false
  1458.     end
  1459.  
  1460.     local isOpen = false
  1461.     for side, modem in pairs(Modem.modems) do
  1462.         if modem.isOpen(channel) then
  1463.             isOpen = true
  1464.             break
  1465.         end
  1466.     end
  1467.  
  1468.     return isOpen
  1469. end
  1470.  
  1471.  
  1472. function Modem.transmit(channel, msg)
  1473.     if not Modem.exists then
  1474.         return false
  1475.     end
  1476.  
  1477.     if not Modem.isOpen(channel) then
  1478.         Modem.open(channel)
  1479.     end
  1480.  
  1481.     for side, modem in pairs(Modem.modems) do
  1482.         modem.transmit(channel, channel, msg)
  1483.     end
  1484.  
  1485.     return true
  1486. end
  1487.  
  1488.  
  1489.  
  1490. --    Handshake
  1491.  
  1492.  
  1493. local Handshake = {}
  1494.  
  1495. Handshake.prime = 625210769
  1496. Handshake.channel = 54569
  1497. Handshake.base = -1
  1498. Handshake.secret = -1
  1499. Handshake.sharedSecret = -1
  1500. Handshake.packetHeader = "["..protocolName.."-Handshake-Packet-Header]"
  1501. Handshake.packetMatch = "%["..protocolName.."%-Handshake%-Packet%-Header%](.+)"
  1502.  
  1503.  
  1504. function Handshake.exponentWithModulo(base, exponent, modulo)
  1505.     local remainder = base
  1506.  
  1507.     for i = 1, exponent-1 do
  1508.         remainder = remainder * remainder
  1509.         if remainder >= modulo then
  1510.             remainder = remainder % modulo
  1511.         end
  1512.     end
  1513.  
  1514.     return remainder
  1515. end
  1516.  
  1517.  
  1518. function Handshake.clear()
  1519.     Handshake.base = -1
  1520.     Handshake.secret = -1
  1521.     Handshake.sharedSecret = -1
  1522. end
  1523.  
  1524.  
  1525. function Handshake.generateInitiatorData()
  1526.     Handshake.base = math.random(10,99999)
  1527.     Handshake.secret = math.random(10,99999)
  1528.     return {
  1529.         type = "initiate",
  1530.         prime = Handshake.prime,
  1531.         base = Handshake.base,
  1532.         moddedSecret = Handshake.exponentWithModulo(Handshake.base, Handshake.secret, Handshake.prime)
  1533.     }
  1534. end
  1535.  
  1536.  
  1537. function Handshake.generateResponseData(initiatorData)
  1538.     local isPrimeANumber = type(initiatorData.prime) == "number"
  1539.     local isPrimeMatching = initiatorData.prime == Handshake.prime
  1540.     local isBaseANumber = type(initiatorData.base) == "number"
  1541.     local isInitiator = initiatorData.type == "initiate"
  1542.     local isModdedSecretANumber = type(initiatorData.moddedSecret) == "number"
  1543.     local areAllNumbersNumbers = isPrimeANumber and isBaseANumber and isModdedSecretANumber
  1544.  
  1545.     if areAllNumbersNumbers and isPrimeMatching then
  1546.         if isInitiator then
  1547.             Handshake.base = initiatorData.base
  1548.             Handshake.secret = math.random(10,99999)
  1549.             Handshake.sharedSecret = Handshake.exponentWithModulo(initiatorData.moddedSecret, Handshake.secret, Handshake.prime)
  1550.             return {
  1551.                 type = "response",
  1552.                 prime = Handshake.prime,
  1553.                 base = Handshake.base,
  1554.                 moddedSecret = Handshake.exponentWithModulo(Handshake.base, Handshake.secret, Handshake.prime)
  1555.             }, Handshake.sharedSecret
  1556.         elseif initiatorData.type == "response" and Handshake.base > 0 and Handshake.secret > 0 then
  1557.             Handshake.sharedSecret = Handshake.exponentWithModulo(initiatorData.moddedSecret, Handshake.secret, Handshake.prime)
  1558.             return Handshake.sharedSecret
  1559.         else
  1560.             return false
  1561.         end
  1562.     else
  1563.         return false
  1564.     end
  1565. end
  1566.  
  1567.  
  1568.  
  1569. --    Secure Connection
  1570.  
  1571.  
  1572. local SecureConnection = {}
  1573. SecureConnection.__index = SecureConnection
  1574.  
  1575.  
  1576. SecureConnection.packetHeaderA = "[" .. protocolName .. "-"
  1577. SecureConnection.packetHeaderB = "-SecureConnection-Packet-Header]"
  1578. SecureConnection.packetMatchA = "%[" .. protocolName .. "%-"
  1579. SecureConnection.packetMatchB = "%-SecureConnection%-Packet%-Header%](.+)"
  1580. SecureConnection.connectionTimeout = 0.1
  1581. SecureConnection.successPacketTimeout = 0.1
  1582.  
  1583.  
  1584. function SecureConnection.new(secret, key, identifier, distance, isRednet)
  1585.     local self = setmetatable({}, SecureConnection)
  1586.     self:setup(secret, key, identifier, distance, isRednet)
  1587.     return self
  1588. end
  1589.  
  1590.  
  1591. function SecureConnection:setup(secret, key, identifier, distance, isRednet)
  1592.     local rawSecret
  1593.  
  1594.     if isRednet then
  1595.         self.isRednet = true
  1596.         self.distance = -1
  1597.         self.rednet_id = distance
  1598.         rawSecret = protocolName .. "|" .. tostring(secret) .. "|" .. tostring(identifier) ..
  1599.         "|" .. tostring(key) .. "|rednet"
  1600.     else
  1601.         self.isRednet = false
  1602.         self.distance = distance
  1603.         rawSecret = protocolName .. "|" .. tostring(secret) .. "|" .. tostring(identifier) ..
  1604.         "|" .. tostring(key) .. "|" .. tostring(distance)
  1605.     end
  1606.  
  1607.     self.identifier = identifier
  1608.     self.packetMatch = SecureConnection.packetMatchA .. Cryptography.sanatize(identifier) .. SecureConnection.packetMatchB
  1609.     self.packetHeader = SecureConnection.packetHeaderA .. identifier .. SecureConnection.packetHeaderB
  1610.     self.secret = Cryptography.sha.sha256(rawSecret)
  1611.     self.channel = Cryptography.channel(self.secret)
  1612.  
  1613.     if not self.isRednet then
  1614.         Modem.open(self.channel)
  1615.     end
  1616. end
  1617.  
  1618.  
  1619. function SecureConnection:verifyHeader(msg)
  1620.     if msg:match(self.packetMatch) then
  1621.         return true
  1622.     else
  1623.         return false
  1624.     end
  1625. end
  1626.  
  1627.  
  1628. function SecureConnection:sendMessage(msg, rednetProtocol)
  1629.     local rawEncryptedMsg = Cryptography.aes.encrypt(self.packetHeader .. msg, self.secret)
  1630.     local encryptedMsg = self.packetHeader .. rawEncryptedMsg
  1631.  
  1632.     if self.isRednet then
  1633.         rednet.send(self.rednet_id, encryptedMsg, rednetProtocol)
  1634.         return true
  1635.     else
  1636.         return Modem.transmit(self.channel, encryptedMsg)
  1637.     end
  1638. end
  1639.  
  1640.  
  1641. function SecureConnection:decryptMessage(msg)
  1642.     if self:verifyHeader(msg) then
  1643.         local encrypted = msg:match(self.packetMatch)
  1644.  
  1645.         local unencryptedMsg = nil
  1646.         pcall(function() unencryptedMsg = Cryptography.aes.decrypt(encrypted, self.secret) end)
  1647.         if not unencryptedMsg then
  1648.             return false, "Could not decrypt"
  1649.         end
  1650.  
  1651.         if self:verifyHeader(unencryptedMsg) then
  1652.             return true, unencryptedMsg:match(self.packetMatch)
  1653.         else
  1654.             return false, "Could not verify"
  1655.         end
  1656.     else
  1657.         return false, "Could not stage 1 verify"
  1658.     end
  1659. end
  1660.  
  1661.  
  1662.  
  1663. --    RDNT Protocol
  1664.  
  1665.  
  1666. protocols["rdnt"] = {}
  1667.  
  1668. local header = {}
  1669. header.dnsPacket = "[Firewolf-DNS-Packet]"
  1670. header.dnsHeaderMatch = "^%[Firewolf%-DNS%-Response%](.+)$"
  1671. header.rednetHeader = "[Firewolf-Rednet-Channel-Simulation]"
  1672. header.rednetMatch = "^%[Firewolf%-Rednet%-Channel%-Simulation%](%d+)$"
  1673. header.responseMatchA = "^%[Firewolf%-"
  1674. header.responseMatchB = "%-"
  1675. header.responseMatchC = "%-Handshake%-Response%](.+)$"
  1676. header.requestHeaderA = "[Firewolf-"
  1677. header.requestHeaderB = "-Handshake-Request]"
  1678. header.pageRequestHeaderA = "[Firewolf-"
  1679. header.pageRequestHeaderB = "-Page-Request]"
  1680. header.pageResponseMatchA = "^%[Firewolf%-"
  1681. header.pageResponseMatchB = "%-Page%-Response%]%[HEADER%](.-)%[BODY%](.+)$"
  1682. header.closeHeaderA = "[Firewolf-"
  1683. header.closeHeaderB = "-Connection-Close]"
  1684.  
  1685.  
  1686. protocols["rdnt"]["setup"] = function()
  1687.     if not Modem.exists() then
  1688.         error("No modem found!")
  1689.     end
  1690. end
  1691.  
  1692.  
  1693. protocols["rdnt"]["fetchAllSearchResults"] = function()
  1694.     Modem.open(publicDNSChannel)
  1695.     Modem.open(publicResponseChannel)
  1696.     Modem.transmit(publicDNSChannel, header.dnsPacket)
  1697.     Modem.close(publicDNSChannel)
  1698.  
  1699.     rednet.broadcast(header.dnsPacket, header.rednetHeader .. publicDNSChannel)
  1700.  
  1701.     local uniqueServers = {}
  1702.     local uniqueDomains = {}
  1703.  
  1704.     local timer = os.startTimer(searchResultTimeout)
  1705.  
  1706.     while true do
  1707.         local event, id, channel, protocol, message, dist = os.pullEventRaw()
  1708.         if event == "modem_message" then
  1709.             if channel == publicResponseChannel and message:match(header.dnsHeaderMatch) then
  1710.                 if not uniqueServers[tostring(dist)] then
  1711.                     uniqueServers[tostring(dist)] = true
  1712.                     local domain = message:match(header.dnsHeaderMatch)
  1713.                     if not uniqueDomains[domain] then
  1714.                         if not(domain:find("/") or domain:find(":") or domain:find("%?")) and #domain > 4 then
  1715.                             timer = os.startTimer(searchResultTimeout)
  1716.                             uniqueDomains[message:match(header.dnsHeaderMatch)] = tostring(dist)
  1717.                         end
  1718.                     end
  1719.                 end
  1720.             end
  1721.         elseif event == "rednet_message" and allowUnencryptedConnections then
  1722.             if protocol and tonumber(protocol:match(header.rednetMatch)) == publicResponseChannel and channel:match(header.dnsHeaderMatch) then
  1723.                 if not uniqueServers[tostring(id)] then
  1724.                     uniqueServers[tostring(id)] = true
  1725.                     local domain = channel:match(header.dnsHeaderMatch)
  1726.                     if not uniqueDomains[domain] then
  1727.                         if not(domain:find("/") or domain:find(":") or domain:find("%?")) and #domain > 4 then
  1728.                             timer = os.startTimer(searchResultTimeout)
  1729.                             uniqueDomains[domain] = tostring(id)
  1730.                         end
  1731.                     end
  1732.                 end
  1733.             end
  1734.         elseif event == "timer" and id == timer then
  1735.             local results = {}
  1736.             for k, _ in pairs(uniqueDomains) do
  1737.                 table.insert(results, k)
  1738.             end
  1739.  
  1740.             return results
  1741.         end
  1742.     end
  1743. end
  1744.  
  1745.  
  1746. protocols["rdnt"]["fetchConnectionObject"] = function(url)
  1747.     local serverChannel = Cryptography.channel(url)
  1748.     local requestHeader = header.requestHeaderA .. url .. header.requestHeaderB
  1749.     local responseMatch = header.responseMatchA .. Cryptography.sanatize(url) .. header.responseMatchB
  1750.  
  1751.     local serializedHandshake = textutils.serialize(Handshake.generateInitiatorData())
  1752.  
  1753.     local rednetResults = {}
  1754.     local directResults = {}
  1755.  
  1756.     local disconnectOthers = function(ignoreDirect)
  1757.         for k,v in pairs(rednetResults) do
  1758.             v.close()
  1759.         end
  1760.         for k,v in pairs(directResults) do
  1761.             if k ~= ignoreDirect then
  1762.                 v.close()
  1763.             end
  1764.         end
  1765.     end
  1766.  
  1767.     local timer = os.startTimer(initiationTimeout)
  1768.  
  1769.     Modem.open(serverChannel)
  1770.     Modem.transmit(serverChannel, requestHeader .. serializedHandshake)
  1771.  
  1772.     rednet.broadcast(requestHeader .. serializedHandshake, header.rednetHeader .. serverChannel)
  1773.  
  1774.     -- Extendable to have server selection
  1775.  
  1776.     while true do
  1777.         local event, id, channel, protocol, message, dist = os.pullEventRaw()
  1778.         if event == "modem_message" then
  1779.             local fullMatch = responseMatch .. tostring(dist) .. header.responseMatchC
  1780.             if channel == serverChannel and message:match(fullMatch) and type(textutils.unserialize(message:match(fullMatch))) == "table" then
  1781.                 local key = Handshake.generateResponseData(textutils.unserialize(message:match(fullMatch)))
  1782.                 if key then
  1783.                     local connection = SecureConnection.new(key, url, url, dist)
  1784.                     table.insert(directResults, {
  1785.                         connection = connection,
  1786.                         fetchPage = function(page)
  1787.                             if not connection then
  1788.                                 return nil
  1789.                             end
  1790.  
  1791.                             local fetchTimer = os.startTimer(fetchTimeout)
  1792.  
  1793.                             local pageRequest = header.pageRequestHeaderA .. url .. header.pageRequestHeaderB .. page
  1794.                             local pageResponseMatch = header.pageResponseMatchA .. Cryptography.sanatize(url) .. header.pageResponseMatchB
  1795.  
  1796.                             connection:sendMessage(pageRequest, header.rednetHeader .. connection.channel)
  1797.  
  1798.                             while true do
  1799.                                 local event, id, channel, protocol, message, dist = os.pullEventRaw()
  1800.                                 if event == "modem_message" and channel == connection.channel and connection:verifyHeader(message) then
  1801.                                     local resp, data = connection:decryptMessage(message)
  1802.                                     if not resp then
  1803.                                         -- Decryption error
  1804.                                     elseif data and data ~= page then
  1805.                                         if data:match(pageResponseMatch) then
  1806.                                             local head, body = data:match(pageResponseMatch)
  1807.                                             return body, textutils.unserialize(head)
  1808.                                         end
  1809.                                     end
  1810.                                 elseif event == "timer" and id == fetchTimer then
  1811.                                     return nil
  1812.                                 end
  1813.                             end
  1814.                         end,
  1815.                         close = function()
  1816.                             connection:sendMessage(header.closeHeaderA .. url .. header.closeHeaderB, header.rednetHeader..connection.channel)
  1817.                             Modem.close(connection.channel)
  1818.                             connection = nil
  1819.                         end
  1820.                     })
  1821.  
  1822.                     disconnectOthers(1)
  1823.                     return directResults[1]
  1824.                 end
  1825.             end
  1826.         elseif event == "rednet_message" then
  1827.             local fullMatch = responseMatch .. os.getComputerID() .. header.responseMatchC
  1828.             if protocol and tonumber(protocol:match(header.rednetMatch)) == serverChannel and channel:match(fullMatch) and type(textutils.unserialize(channel:match(fullMatch))) == "table" then
  1829.                 local key = Handshake.generateResponseData(textutils.unserialize(channel:match(fullMatch)))
  1830.                 if key then
  1831.                     local connection = SecureConnection.new(key, url, url, id, true)
  1832.                     table.insert(rednetResults, {
  1833.                         connection = connection,
  1834.                         fetchPage = function(page)
  1835.                             if not connection then
  1836.                                 return nil
  1837.                             end
  1838.  
  1839.                             local fetchTimer = os.startTimer(fetchTimeout)
  1840.  
  1841.                             local pageRequest = header.pageRequestHeaderA .. url .. header.pageRequestHeaderB .. page
  1842.                             local pageResponseMatch = header.pageResponseMatchA .. Cryptography.sanatize(url) .. header.pageResponseMatchB
  1843.  
  1844.                             connection:sendMessage(pageRequest, header.rednetHeader .. connection.channel)
  1845.  
  1846.                             while true do
  1847.                                 local event, id, channel, protocol, message, dist = os.pullEventRaw()
  1848.                                 if event == "rednet_message" and protocol and tonumber(protocol:match(header.rednetMatch)) == connection.channel and connection:verifyHeader(channel) then
  1849.                                     local resp, data = connection:decryptMessage(channel)
  1850.                                     if not resp then
  1851.                                         -- Decryption error
  1852.                                     elseif data and data ~= page then
  1853.                                         if data:match(pageResponseMatch) then
  1854.                                             local head, body = data:match(pageResponseMatch)
  1855.                                             return body, textutils.unserialize(head)
  1856.                                         end
  1857.                                     end
  1858.                                 elseif event == "timer" and id == fetchTimer then
  1859.                                     return nil
  1860.                                 end
  1861.                             end
  1862.                         end,
  1863.                         close = function()
  1864.                             connection:sendMessage(header.closeHeaderA .. url .. header.closeHeaderB, header.rednetHeader..connection.channel)
  1865.                             Modem.close(connection.channel)
  1866.                             connection = nil
  1867.                         end
  1868.                     })
  1869.  
  1870.                     if #rednetResults == 1 then
  1871.                         timer = os.startTimer(0.2)
  1872.                     end
  1873.                 end
  1874.             end
  1875.         elseif event == "timer" and id == timer then
  1876.             if #directResults > 0 then
  1877.                 disconnectOthers(1)
  1878.                 return directResults[1]
  1879.             elseif #rednetResults > 0 then
  1880.                 local lowestID = math.huge
  1881.                 local lowestResult = nil
  1882.                 for k,v in pairs(rednetResults) do
  1883.                     if v.connection.rednet_id < lowestID then
  1884.                         lowestID = v.connection.rednet_id
  1885.                         lowestResult = v
  1886.                     end
  1887.                 end
  1888.  
  1889.                 for k,v in pairs(rednetResults) do
  1890.                     if v.connection.rednet_id ~= lowestID then
  1891.                         v.close()
  1892.                     end
  1893.                 end
  1894.  
  1895.                 return lowestResult
  1896.             else
  1897.                 return nil
  1898.             end
  1899.         end
  1900.     end
  1901. end
  1902.  
  1903.  
  1904.  
  1905. --    Fetching Raw Data
  1906.  
  1907.  
  1908. local fetchSearchResultsForQuery = function(query)
  1909.     local all = protocols[currentProtocol]["fetchAllSearchResults"]()
  1910.     local results = {}
  1911.     if query and query:len() > 0 then
  1912.         for _, v in pairs(all) do
  1913.             if v:find(query:lower()) then
  1914.                 table.insert(results, v)
  1915.             end
  1916.         end
  1917.     else
  1918.         results = all
  1919.     end
  1920.  
  1921.     table.sort(results)
  1922.     return results
  1923. end
  1924.  
  1925.  
  1926. local getConnectionObjectFromURL = function(url)
  1927.     local domain = url:match("^([^/]+)")
  1928.     return protocols[currentProtocol]["fetchConnectionObject"](domain)
  1929. end
  1930.  
  1931.  
  1932. local determineLanguage = function(header)
  1933.     if type(header) == "table" then
  1934.         if header.language and header.language == "Firewolf Markup" then
  1935.             return "fwml"
  1936.         else
  1937.             return "lua"
  1938.         end
  1939.     else
  1940.         return "lua"
  1941.     end
  1942. end
  1943.  
  1944.  
  1945.  
  1946. --    History
  1947.  
  1948.  
  1949. local appendToHistory = function(url)
  1950.     if history[1] ~= url then
  1951.         table.insert(history, 1, url)
  1952.     end
  1953. end
  1954.  
  1955.  
  1956.  
  1957. --    Fetch Websites
  1958.  
  1959.  
  1960. local loadingAnimation = function()
  1961.     local state = -2
  1962.  
  1963.     term.setTextColor(theme.text)
  1964.     term.setBackgroundColor(theme.accent)
  1965.  
  1966.     term.setCursorPos(w - 5, 1)
  1967.     term.write("[=  ]")
  1968.  
  1969.     local timer = os.startTimer(animationInterval)
  1970.  
  1971.     while true do
  1972.         local event, timerID = os.pullEvent()
  1973.         if event == "timer" and timerID == timer then
  1974.             term.setTextColor(theme.text)
  1975.             term.setBackgroundColor(theme.accent)
  1976.  
  1977.             state = state + 1
  1978.  
  1979.             term.setCursorPos(w - 5, 1)
  1980.             term.write("[   ]")
  1981.             term.setCursorPos(w - 2 - math.abs(state), 1)
  1982.             term.write("=")
  1983.  
  1984.             if state == 2 then
  1985.                 state = -2
  1986.             end
  1987.  
  1988.             timer = os.startTimer(animationInterval)
  1989.         end
  1990.     end
  1991. end
  1992.  
  1993.  
  1994. local normalizeURL = function(url)
  1995.     url = url:lower():gsub(" ", "")
  1996.     if url == "home" or url == "homepage" then
  1997.         url = "firewolf"
  1998.     end
  1999.  
  2000.     return url
  2001. end
  2002.  
  2003.  
  2004. local normalizePage = function(page)
  2005.     if not page then page = "" end
  2006.     page = page:lower()
  2007.     if page == "" then
  2008.         page = "/"
  2009.     end
  2010.     return page
  2011. end
  2012.  
  2013.  
  2014. local determineActionForURL = function(url)
  2015.     if url:len() > 0 and url:gsub("/", ""):len() == 0 then
  2016.         return "none"
  2017.     end
  2018.  
  2019.     if url == "exit" then
  2020.         return "exit"
  2021.     elseif builtInSites["display"][url] then
  2022.         return "internal website"
  2023.     elseif url == "" then
  2024.         local results = fetchSearchResultsForQuery()
  2025.         if #results > 0 then
  2026.             return "search", results
  2027.         else
  2028.             return "none"
  2029.         end
  2030.     else
  2031.         local connection = getConnectionObjectFromURL(url)
  2032.         if connection then
  2033.             return "external website", connection
  2034.         else
  2035.             local results = fetchSearchResultsForQuery(url)
  2036.             if #results > 0 then
  2037.                 return "search", results
  2038.             else
  2039.                 return "none"
  2040.             end
  2041.         end
  2042.     end
  2043. end
  2044.  
  2045.  
  2046. local fetchSearch = function(url, results)
  2047.     return languages["lua"]["runWithoutAntivirus"](builtInSites["search"], results)
  2048. end
  2049.  
  2050.  
  2051. local fetchInternal = function(url)
  2052.     return languages["lua"]["runWithoutAntivirus"](builtInSites["display"][url])
  2053. end
  2054.  
  2055.  
  2056. local fetchError = function(err)
  2057.     return languages["lua"]["runWithoutAntivirus"](builtInSites["error"], err)
  2058. end
  2059.  
  2060.  
  2061. local fetchExternal = function(url, connection)
  2062.     if connection.multipleServers then
  2063.         -- Please forgive me
  2064.         -- GravityScore forced me to do it like this
  2065.         -- I don't mean it, I really don't.
  2066.         connection = connection.servers[1]
  2067.     end
  2068.  
  2069.     local page = normalizePage(url:match("^[^/]+/(.+)"))
  2070.     local contents, head = connection.fetchPage(page)
  2071.     if contents then
  2072.         if type(contents) ~= "string" then
  2073.             return fetchNone()
  2074.         else
  2075.             local language = determineLanguage(head)
  2076.             return languages[language]["run"](contents, page, connection)
  2077.         end
  2078.     else
  2079.         if connection then
  2080.             connection.close()
  2081.             return "retry"
  2082.         end
  2083.         return fetchError("A connection error/timeout has occurred!")
  2084.     end
  2085. end
  2086.  
  2087.  
  2088. local fetchNone = function()
  2089.     return languages["lua"]["runWithoutAntivirus"](builtInSites["noresults"])
  2090. end
  2091.  
  2092.  
  2093. local fetchURL = function(url, inheritConnection)
  2094.     url = normalizeURL(url)
  2095.     currentWebsiteURL = url
  2096.  
  2097.     if inheritConnection then
  2098.         local resp = fetchExternal(url, inheritConnection)
  2099.         if resp ~= "retry" then
  2100.             return resp, false, inheritConnection
  2101.         end
  2102.     end
  2103.  
  2104.     local action, connection = determineActionForURL(url)
  2105.  
  2106.     if action == "search" then
  2107.         return fetchSearch(url, connection), true
  2108.     elseif action == "internal website" then
  2109.         return fetchInternal(url), true
  2110.     elseif action == "external website" then
  2111.         local resp = fetchExternal(url, connection)
  2112.         if resp == "retry" then
  2113.             return fetchError("A connection error/timeout has occurred!"), false, connection
  2114.         else
  2115.             return resp, false, connection
  2116.         end
  2117.     elseif action == "none" then
  2118.         return fetchNone(), true
  2119.     elseif action == "exit" then
  2120.         os.queueEvent("terminate")
  2121.     end
  2122.  
  2123.     return nil
  2124. end
  2125.  
  2126.  
  2127.  
  2128. --    Tabs
  2129.  
  2130.  
  2131. local switchTab = function(index, shouldntResume)
  2132.     if not tabs[index] then
  2133.         return
  2134.     end
  2135.  
  2136.     if tabs[currentTab].win then
  2137.         tabs[currentTab].win.setVisible(false)
  2138.     end
  2139.  
  2140.     currentTab = index
  2141.     isMenubarOpen = tabs[currentTab].isMenubarOpen
  2142.     currentWebsiteURL = tabs[currentTab].url
  2143.  
  2144.     term.redirect(originalTerminal)
  2145.     clear(theme.background, theme.text)
  2146.     drawMenubar()
  2147.  
  2148.     term.redirect(tabs[currentTab].win)
  2149.     term.setCursorPos(1, 1)
  2150.     tabs[currentTab].win.setVisible(true)
  2151.     tabs[currentTab].win.redraw()
  2152.  
  2153.     if not shouldntResume then
  2154.         coroutine.resume(tabs[currentTab].thread)
  2155.     end
  2156. end
  2157.  
  2158.  
  2159. local closeCurrentTab = function()
  2160.     if #tabs <= 0 then
  2161.         return
  2162.     end
  2163.  
  2164.     table.remove(tabs, currentTab)
  2165.  
  2166.     currentTab = math.max(currentTab - 1, 1)
  2167.     switchTab(currentTab, true)
  2168. end
  2169.  
  2170.  
  2171. local loadTab = function(index, url, givenFunc)
  2172.     url = normalizeURL(url)
  2173.  
  2174.     local func = nil
  2175.     local isOpen = true
  2176.     local currentConnection = false
  2177.  
  2178.     isMenubarOpen = true
  2179.     currentWebsiteURL = url
  2180.     drawMenubar()
  2181.  
  2182.     if tabs[index] and tabs[index].connection and tabs[index].url then
  2183.         if url:match("^([^/]+)") == tabs[index].url:match("^([^/]+)") then
  2184.             currentConnection = tabs[index].connection
  2185.         else
  2186.             tabs[index].connection.close()
  2187.             tabs[index].connection = nil
  2188.         end
  2189.     end
  2190.  
  2191.     if givenFunc then
  2192.         func = givenFunc
  2193.     else
  2194.         parallel.waitForAny(function()
  2195.             func, isOpen, connection = fetchURL(url, currentConnection)
  2196.         end, function()
  2197.             while true do
  2198.                 local event, key = os.pullEvent()
  2199.                 if event == "key" and (key == 29 or key == 157) then
  2200.                     break
  2201.                 end
  2202.             end
  2203.         end, loadingAnimation)
  2204.     end
  2205.  
  2206.     if func then
  2207.         appendToHistory(url)
  2208.  
  2209.         tabs[index] = {}
  2210.         tabs[index].url = url
  2211.         tabs[index].connection = connection
  2212.         tabs[index].win = window.create(originalTerminal, 1, 1, w, h, false)
  2213.  
  2214.         tabs[index].thread = coroutine.create(func)
  2215.         tabs[index].isMenubarOpen = isOpen
  2216.         tabs[index].isMenubarPermanent = isOpen
  2217.  
  2218.         tabs[index].ox = 1
  2219.         tabs[index].oy = 1
  2220.  
  2221.         term.redirect(tabs[index].win)
  2222.         clear(theme.background, theme.text)
  2223.  
  2224.         switchTab(index)
  2225.     end
  2226. end
  2227.  
  2228.  
  2229.  
  2230. --    Website Environments
  2231.  
  2232.  
  2233. local getWhitelistedEnvironment = function()
  2234.     local env = {}
  2235.  
  2236.     local function copy(source, destination, key)
  2237.         destination[key] = {}
  2238.         for k, v in pairs(source) do
  2239.             destination[key][k] = v
  2240.         end
  2241.     end
  2242.  
  2243.     copy(bit, env, "bit")
  2244.     copy(colors, env, "colors")
  2245.     copy(colours, env, "colours")
  2246.     copy(coroutine, env, "coroutine")
  2247.  
  2248.     copy(disk, env, "disk")
  2249.     env["disk"]["setLabel"] = nil
  2250.     env["disk"]["eject"] = nil
  2251.  
  2252.     copy(gps, env, "gps")
  2253.     copy(help, env, "help")
  2254.     copy(keys, env, "keys")
  2255.     copy(math, env, "math")
  2256.  
  2257.     copy(os, env, "os")
  2258.     env["os"]["run"] = nil
  2259.     env["os"]["shutdown"] = nil
  2260.     env["os"]["reboot"] = nil
  2261.     env["os"]["setComputerLabel"] = nil
  2262.     env["os"]["queueEvent"] = nil
  2263.     env["os"]["pullEvent"] = function(filter)
  2264.         while true do
  2265.             local event = {os.pullEvent(filter)}
  2266.             if not filter then
  2267.                 return unpack(event)
  2268.             elseif filter and event[1] == filter then
  2269.                 return unpack(event)
  2270.             end
  2271.         end
  2272.     end
  2273.     env["os"]["pullEventRaw"] = env["os"]["pullEvent"]
  2274.  
  2275.     copy(paintutils, env, "paintutils")
  2276.     copy(parallel, env, "parallel")
  2277.     copy(peripheral, env, "peripheral")
  2278.     copy(rednet, env, "rednet")
  2279.     copy(redstone, env, "redstone")
  2280.     copy(redstone, env, "rs")
  2281.  
  2282.     copy(shell, env, "shell")
  2283.     env["shell"]["run"] = nil
  2284.     env["shell"]["exit"] = nil
  2285.     env["shell"]["setDir"] = nil
  2286.     env["shell"]["setAlias"] = nil
  2287.     env["shell"]["clearAlias"] = nil
  2288.     env["shell"]["setPath"] = nil
  2289.  
  2290.     copy(string, env, "string")
  2291.     copy(table, env, "table")
  2292.  
  2293.     copy(term, env, "term")
  2294.     env["term"]["redirect"] = nil
  2295.     env["term"]["restore"] = nil
  2296.  
  2297.     copy(textutils, env, "textutils")
  2298.     copy(vector, env, "vector")
  2299.  
  2300.     if turtle then
  2301.         copy(turtle, env, "turtle")
  2302.     end
  2303.  
  2304.     if http then
  2305.         copy(http, env, "http")
  2306.     end
  2307.  
  2308.     env["assert"] = assert
  2309.     env["printError"] = printError
  2310.     env["tonumber"] = tonumber
  2311.     env["tostring"] = tostring
  2312.     env["type"] = type
  2313.     env["next"] = next
  2314.     env["unpack"] = unpack
  2315.     env["pcall"] = pcall
  2316.     env["xpcall"] = xpcall
  2317.     env["sleep"] = sleep
  2318.     env["pairs"] = pairs
  2319.     env["ipairs"] = ipairs
  2320.     env["read"] = read
  2321.     env["write"] = write
  2322.     env["select"] = select
  2323.     env["print"] = print
  2324.     env["setmetatable"] = setmetatable
  2325.     env["getmetatable"] = getmetatable
  2326.  
  2327.     env["_G"] = env
  2328.  
  2329.     return env
  2330. end
  2331.  
  2332.  
  2333. local overrideEnvironment = function(env)
  2334.     local localTerm = {}
  2335.     for k, v in pairs(term) do
  2336.         localTerm[k] = v
  2337.     end
  2338.  
  2339.     env["term"]["clear"] = function()
  2340.         localTerm.clear()
  2341.         drawMenubar()
  2342.     end
  2343.  
  2344.     env["term"]["scroll"] = function(n)
  2345.         localTerm.scroll(n)
  2346.         drawMenubar()
  2347.     end
  2348.  
  2349.     env["shell"]["getRunningProgram"] = function()
  2350.         return currentWebsiteURL
  2351.     end
  2352. end
  2353.  
  2354. local urlEncode = function(url)
  2355.     local result = url
  2356.  
  2357.     result = result:gsub("%%", "%%a")
  2358.     result = result:gsub(":", "%%c")
  2359.     result = result:gsub("/", "%%s")
  2360.     result = result:gsub("\n", "%%n")
  2361.     result = result:gsub(" ", "%%w")
  2362.     result = result:gsub("&", "%%m")
  2363.     result = result:gsub("%?", "%%q")
  2364.     result = result:gsub("=", "%%e")
  2365.     result = result:gsub("%.", "%%d")
  2366.  
  2367.     return result
  2368. end
  2369.  
  2370. local urlDecode = function(url)
  2371.     local result = url
  2372.  
  2373.     result = result:gsub("%%c", ":")
  2374.     result = result:gsub("%%s", "/")
  2375.     result = result:gsub("%%n", "\n")
  2376.     result = result:gsub("%%w", " ")
  2377.     result = result:gsub("%%&", "&")
  2378.     result = result:gsub("%%q", "%?")
  2379.     result = result:gsub("%%e", "=")
  2380.     result = result:gsub("%%d", "%.")
  2381.     result = result:gsub("%%m", "%%")
  2382.  
  2383.     return result
  2384. end
  2385.  
  2386. local applyAPIFunctions = function(env, connection)
  2387.     env["firewolf"] = {}
  2388.     env["firewolf"]["version"] = version
  2389.     env["firewolf"]["domain"] = currentWebsiteURL:match("^[^/]+")
  2390.  
  2391.     env["firewolf"]["redirect"] = function(url)
  2392.         if type(url) ~= "string" then
  2393.             return error("string (url) expected, got " .. type(url))
  2394.         end
  2395.  
  2396.         os.queueEvent(redirectEvent, url)
  2397.         coroutine.yield()
  2398.     end
  2399.  
  2400.     env["firewolf"]["download"] = function(page)
  2401.         if type(page) ~= "string" then
  2402.             return error("string (page) expected")
  2403.         end
  2404.         local bannedNames = {"ls", "dir", "delete", "copy", "move", "list", "rm", "cp", "mv", "clear", "cd", "lua"}
  2405.  
  2406.         local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+"))
  2407.         if startSearch == 1 then
  2408.             if page:sub(endSearch + 1, endSearch + 1) == "/" then
  2409.                 page = page:sub(endSearch + 2, -1)
  2410.             else
  2411.                 page = page:sub(endSearch + 1, -1)
  2412.             end
  2413.         end
  2414.  
  2415.         local filename = page:match("([^/]+)$")
  2416.         if not filename then
  2417.             return false, "Cannot download index"
  2418.         end
  2419.  
  2420.         for k, v in pairs(bannedNames) do
  2421.             if filename == v then
  2422.                 return false, "Filename prohibited!"
  2423.             end
  2424.         end
  2425.  
  2426.         if not fs.exists(downloadsLocation) then
  2427.             fs.makeDir(downloadsLocation)
  2428.         elseif not fs.isDir(downloadsLocation) then
  2429.             return false, "Downloads disabled!"
  2430.         end
  2431.  
  2432.         contents = connection.fetchPage(normalizePage(page))
  2433.         if type(contents) ~= "string" then
  2434.             return false, "Download error!"
  2435.         else
  2436.             local f = io.open(downloadsLocation .. "/" .. filename, "w")
  2437.             f:write(contents)
  2438.             f:close()
  2439.             return true, downloadsLocation .. "/" .. filename
  2440.         end
  2441.     end
  2442.  
  2443.     env["firewolf"]["encode"] = function(vars)
  2444.         if type(vars) ~= "table" then
  2445.             return error("table (vars) expected, got " .. type(vars))
  2446.         end
  2447.  
  2448.         local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+"))
  2449.         if startSearch == 1 then
  2450.             if page:sub(endSearch + 1, endSearch + 1) == "/" then
  2451.                 page = page:sub(endSearch + 2, -1)
  2452.             else
  2453.                 page = page:sub(endSearch + 1, -1)
  2454.             end
  2455.         end
  2456.  
  2457.         local construct = "?"
  2458.         for k,v in pairs(vars) do
  2459.             construct = construct .. urlEncode(tostring(k)) .. "=" .. urlEncode(tostring(v)) .. "&"
  2460.         end
  2461.         -- Get rid of that last ampersand
  2462.         construct = construct:sub(1, -2)
  2463.  
  2464.         return construct
  2465.     end
  2466.  
  2467.     env["firewolf"]["query"] = function(page, vars)
  2468.         if type(page) ~= "string" then
  2469.             return error("string (page) expected, got " .. type(page))
  2470.         end
  2471.         if vars and type(vars) ~= "table" then
  2472.             return error("table (vars) expected, got " .. type(vars))
  2473.         end
  2474.  
  2475.         local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+"))
  2476.         if startSearch == 1 then
  2477.             if page:sub(endSearch + 1, endSearch + 1) == "/" then
  2478.                 page = page:sub(endSearch + 2, -1)
  2479.             else
  2480.                 page = page:sub(endSearch + 1, -1)
  2481.             end
  2482.         end
  2483.  
  2484.         local construct = page .. "?"
  2485.         if vars then
  2486.             for k,v in pairs(vars) do
  2487.                 construct = construct .. urlEncode(tostring(k)) .. "=" .. urlEncode(tostring(v)) .. "&"
  2488.             end
  2489.         end
  2490.         -- Get rid of that last ampersand
  2491.         construct = construct:sub(1, -2)
  2492.  
  2493.         contents = connection.fetchPage(normalizePage(construct))
  2494.         if type(contents) == "string" then
  2495.             return contents
  2496.         else
  2497.             return false
  2498.         end
  2499.     end
  2500.  
  2501.     env["firewolf"]["loadImage"] = function(page)
  2502.         if type(page) ~= "string" then
  2503.             return error("string (page) expected, got " .. type(page))
  2504.         end
  2505.  
  2506.         local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+"))
  2507.         if startSearch == 1 then
  2508.             if page:sub(endSearch + 1, endSearch + 1) == "/" then
  2509.                 page = page:sub(endSearch + 2, -1)
  2510.             else
  2511.                 page = page:sub(endSearch + 1, -1)
  2512.             end
  2513.         end
  2514.  
  2515.         local filename = page:match("([^/]+)$")
  2516.         if not filename then
  2517.             return false, "Cannot load index as an image!"
  2518.         end
  2519.  
  2520.         contents = connection.fetchPage(normalizePage(page))
  2521.         if type(contents) ~= "string" then
  2522.             return false, "Download error!"
  2523.         else
  2524.             local colorLookup = {}
  2525.             for n = 1, 16 do
  2526.                 colorLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1)
  2527.             end
  2528.  
  2529.             local image = {}
  2530.             for line in contents:gmatch("[^\n]+") do
  2531.                 local lines = {}
  2532.                 for x = 1, line:len() do
  2533.                     lines[x] = colorLookup[string.byte(line, x, x)] or 0
  2534.                 end
  2535.                 table.insert(image, lines)
  2536.             end
  2537.  
  2538.             return image
  2539.         end
  2540.     end
  2541.  
  2542.     env["center"] = center
  2543.     env["fill"] = fill
  2544. end
  2545.  
  2546.  
  2547. local getWebsiteEnvironment = function(antivirus, connection)
  2548.     local env = {}
  2549.  
  2550.     if antivirus then
  2551.         env = getWhitelistedEnvironment()
  2552.         overrideEnvironment(env)
  2553.     else
  2554.         setmetatable(env, {__index = _G})
  2555.     end
  2556.  
  2557.     applyAPIFunctions(env, connection)
  2558.  
  2559.     return env
  2560. end
  2561.  
  2562.  
  2563.  
  2564. --    FWML Execution
  2565.  
  2566.  
  2567. local render = {}
  2568.  
  2569. render["functions"] = {}
  2570. render["functions"]["public"] = {}
  2571. render["alignations"] = {}
  2572.  
  2573. render["variables"] = {
  2574.     scroll,
  2575.     maxScroll,
  2576.     align,
  2577.     linkData = {},
  2578.     blockLength,
  2579.     link,
  2580.     linkStart,
  2581.     markers,
  2582.     currentOffset,
  2583. }
  2584.  
  2585.  
  2586. local function getLine(loc, data)
  2587.     local _, changes = data:sub(1, loc):gsub("\n", "")
  2588.     if not changes then
  2589.         return 1
  2590.     else
  2591.         return changes + 1
  2592.     end
  2593. end
  2594.  
  2595.  
  2596. local function parseData(data)
  2597.     local commands = {}
  2598.     local searchPos = 1
  2599.  
  2600.     while #data > 0 do
  2601.         local sCmd, eCmd = data:find("%[[^%]]+%]", searchPos)
  2602.         if sCmd then
  2603.             sCmd = sCmd + 1
  2604.             eCmd = eCmd - 1
  2605.  
  2606.             if (sCmd > 2) then
  2607.                 if data:sub(sCmd - 2, sCmd - 2) == "\\" then
  2608.                     local t = data:sub(searchPos, sCmd - 1):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]")
  2609.                     if #t > 0 then
  2610.                         if #commands > 0 and type(commands[#commands][1]) == "string" then
  2611.                             commands[#commands][1] = commands[#commands][1] .. t
  2612.                         else
  2613.                             table.insert(commands, {t})
  2614.                         end
  2615.                     end
  2616.                     searchPos = sCmd
  2617.                 else
  2618.                     local t = data:sub(searchPos, sCmd - 2):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]")
  2619.                     if #t > 0 then
  2620.                         if #commands > 0 and type(commands[#commands][1]) == "string" then
  2621.                             commands[#commands][1] = commands[#commands][1] .. t
  2622.                         else
  2623.                             table.insert(commands, {t})
  2624.                         end
  2625.                     end
  2626.  
  2627.                     t = data:sub(sCmd, eCmd):gsub("\n", "")
  2628.                     table.insert(commands, {getLine(sCmd, data), t})
  2629.                     searchPos = eCmd + 2
  2630.                 end
  2631.             else
  2632.                 local t = data:sub(sCmd, eCmd):gsub("\n", "")
  2633.                 table.insert(commands, {getLine(sCmd, data), t})
  2634.                 searchPos = eCmd + 2
  2635.             end
  2636.         else
  2637.             local t = data:sub(searchPos, -1):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]")
  2638.             if #t > 0 then
  2639.                 if #commands > 0 and type(commands[#commands][1]) == "string" then
  2640.                     commands[#commands][1] = commands[#commands][1] .. t
  2641.                 else
  2642.                     table.insert(commands, {t})
  2643.                 end
  2644.             end
  2645.  
  2646.             break
  2647.         end
  2648.     end
  2649.  
  2650.     return commands
  2651. end
  2652.  
  2653.  
  2654. local function proccessData(commands)
  2655.     searchIndex = 0
  2656.  
  2657.     while searchIndex < #commands do
  2658.         searchIndex = searchIndex + 1
  2659.  
  2660.         local length = 0
  2661.         local origin = searchIndex
  2662.  
  2663.         if type(commands[searchIndex][1]) == "string" then
  2664.             length = length + #commands[searchIndex][1]
  2665.             local endIndex = origin
  2666.             for i = origin + 1, #commands do
  2667.                 if commands[i][2] then
  2668.                     local command = commands[i][2]:match("^(%w+)%s-")
  2669.                     if not (command == "c" or command == "color" or command == "bg"
  2670.                             or command == "background" or command == "newlink" or command == "endlink") then
  2671.                         endIndex = i
  2672.                         break
  2673.                     end
  2674.                 elseif commands[i][2] then
  2675.  
  2676.                 else
  2677.                     length = length + #commands[i][1]
  2678.                 end
  2679.                 if i == #commands then
  2680.                     endIndex = i
  2681.                 end
  2682.             end
  2683.  
  2684.             commands[origin][2] = length
  2685.             searchIndex = endIndex
  2686.             length = 0
  2687.         end
  2688.     end
  2689.  
  2690.     return commands
  2691. end
  2692.  
  2693.  
  2694. local function parse(original)
  2695.     return proccessData(parseData(original))
  2696. end
  2697.  
  2698.  
  2699. render["functions"]["display"] = function(text, length, offset, center)
  2700.     if not offset then
  2701.         offset = 0
  2702.     end
  2703.  
  2704.     return render.variables.align(text, length, w, offset, center);
  2705. end
  2706.  
  2707.  
  2708. render["functions"]["displayText"] = function(source)
  2709.     if source[2] then
  2710.         render.variables.blockLength = source[2]
  2711.         if render.variables.link and not render.variables.linkStart then
  2712.             render.variables.linkStart = render.functions.display(
  2713.                 source[1], render.variables.blockLength, render.variables.currentOffset, w / 2)
  2714.         else
  2715.             render.functions.display(source[1], render.variables.blockLength, render.variables.currentOffset, w / 2)
  2716.         end
  2717.     else
  2718.         if render.variables.link and not render.variables.linkStart then
  2719.             render.variables.linkStart = render.functions.display(source[1], nil, render.variables.currentOffset, w / 2)
  2720.         else
  2721.             render.functions.display(source[1], nil, render.variables.currentOffset, w / 2)
  2722.         end
  2723.     end
  2724. end
  2725.  
  2726.  
  2727. render["functions"]["public"]["br"] = function(source)
  2728.     if render.variables.link then
  2729.         return "Cannot insert new line within a link on line " .. source[1]
  2730.     end
  2731.  
  2732.     render.variables.scroll = render.variables.scroll + 1
  2733.     render.variables.maxScroll = math.max(render.variables.scroll, render.variables.maxScroll)
  2734. end
  2735.  
  2736.  
  2737. render["functions"]["public"]["c "] = function(source)
  2738.     local sColor = source[2]:match("^%w+%s+(.+)$") or ""
  2739.     if colors[sColor] then
  2740.         term.setTextColor(colors[sColor])
  2741.     else
  2742.         return "Invalid color: \"" .. sColor .. "\" on line " .. source[1]
  2743.     end
  2744. end
  2745.  
  2746.  
  2747. render["functions"]["public"]["color "] = render["functions"]["public"]["c "]
  2748.  
  2749.  
  2750. render["functions"]["public"]["bg "] = function(source)
  2751.     local sColor = source[2]:match("^%w+%s+(.+)$") or ""
  2752.     if colors[sColor] then
  2753.         term.setBackgroundColor(colors[sColor])
  2754.     else
  2755.         return "Invalid color: \"" .. sColor .. "\" on line " .. source[1]
  2756.     end
  2757. end
  2758.  
  2759.  
  2760. render["functions"]["public"]["background "] = render["functions"]["public"]["bg "]
  2761.  
  2762.  
  2763. render["functions"]["public"]["newlink "] = function(source)
  2764.     if render.variables.link then
  2765.         return "Cannot nest links on line " .. source[1]
  2766.     end
  2767.  
  2768.     render.variables.link = source[2]:match("^%w+%s+(.+)$") or ""
  2769.     render.variables.linkStart = false
  2770. end
  2771.  
  2772.  
  2773. render["functions"]["public"]["endlink"] = function(source)
  2774.     if not render.variables.link then
  2775.         return "Cannot end a link without a link on line " .. source[1]
  2776.     end
  2777.  
  2778.     local linkEnd = term.getCursorPos()-1
  2779.     table.insert(render.variables.linkData, {render.variables.linkStart,
  2780.         linkEnd, render.variables.scroll, render.variables.link})
  2781.     render.variables.link = false
  2782.     render.variables.linkStart = false
  2783. end
  2784.  
  2785.  
  2786. render["functions"]["public"]["offset "] = function(source)
  2787.     local offset = tonumber((source[2]:match("^%w+%s+(.+)$") or ""))
  2788.     if offset then
  2789.         render.variables.currentOffset = offset
  2790.     else
  2791.         return "Invalid offset value: \"" .. (source[2]:match("^%w+%s+(.+)$") or "") .. "\" on line " .. source[1]
  2792.     end
  2793. end
  2794.  
  2795.  
  2796. render["functions"]["public"]["marker "] = function(source)
  2797.     render.variables.markers[(source[2]:match("^%w+%s+(.+)$") or "")] = render.variables.scroll
  2798. end
  2799.  
  2800.  
  2801. render["functions"]["public"]["goto "] = function(source)
  2802.     local location = source[2]:match("%w+%s+(.+)$")
  2803.     if render.variables.markers[location] then
  2804.         render.variables.scroll = render.variables.markers[location]
  2805.     else
  2806.         return "No such location: \"" .. (source[2]:match("%w+%s+(.+)$") or "") .. "\" on line " .. source[1]
  2807.     end
  2808. end
  2809.  
  2810.  
  2811. render["functions"]["public"]["box "] = function(source)
  2812.     local sColor, align, height, width, offset, url = source[2]:match("^box (%a+) (%a+) (%-?%d+) (%-?%d+) (%-?%d+) ?([^ ]*)")
  2813.     if not sColor then
  2814.         return "Invalid box syntax on line " .. source[1]
  2815.     end
  2816.  
  2817.     local x, y = term.getCursorPos()
  2818.     local startX
  2819.  
  2820.     if align == "center" or align == "centre" then
  2821.         startX = math.ceil((w / 2) - width / 2) + offset
  2822.     elseif align == "left" then
  2823.         startX = 1 + offset
  2824.     elseif align == "right" then
  2825.         startX = (w - width + 1) + offset
  2826.     else
  2827.         return "Invalid align option for box on line " .. source[1]
  2828.     end
  2829.  
  2830.     if not colors[sColor] then
  2831.         return "Invalid color: \"" .. sColor .. "\" for box on line " .. source[1]
  2832.     end
  2833.  
  2834.     term.setBackgroundColor(colors[sColor])
  2835.     for i = 0, height - 1 do
  2836.         term.setCursorPos(startX, render.variables.scroll + i)
  2837.         term.write(string.rep(" ", width))
  2838.         if url:len() > 3 then
  2839.             table.insert(render.variables.linkData, {startX, startX + width - 1, render.variables.scroll + i, url})
  2840.         end
  2841.     end
  2842.  
  2843.     render.variables.maxScroll = math.max(render.variables.scroll + height - 1, render.variables.maxScroll)
  2844.     term.setCursorPos(x, y)
  2845. end
  2846.  
  2847.  
  2848. render["alignations"]["left"] = function(text, length, _, offset)
  2849.     local x, y = term.getCursorPos()
  2850.     if length then
  2851.         term.setCursorPos(1 + offset, render.variables.scroll)
  2852.         term.write(text)
  2853.         return 1 + offset
  2854.     else
  2855.         term.setCursorPos(x, render.variables.scroll)
  2856.         term.write(text)
  2857.         return x
  2858.     end
  2859. end
  2860.  
  2861.  
  2862. render["alignations"]["right"] = function(text, length, width, offset)
  2863.     local x, y = term.getCursorPos()
  2864.     if length then
  2865.         term.setCursorPos((width - length + 1) + offset, render.variables.scroll)
  2866.         term.write(text)
  2867.         return (width - length + 1) + offset
  2868.     else
  2869.         term.setCursorPos(x, render.variables.scroll)
  2870.         term.write(text)
  2871.         return x
  2872.     end
  2873. end
  2874.  
  2875.  
  2876. render["alignations"]["center"] = function(text, length, _, offset, center)
  2877.     local x, y = term.getCursorPos()
  2878.     if length then
  2879.         term.setCursorPos(math.ceil(center - length / 2) + offset, render.variables.scroll)
  2880.         term.write(text)
  2881.         return math.ceil(center - length / 2) + offset
  2882.     else
  2883.         term.setCursorPos(x, render.variables.scroll)
  2884.         term.write(text)
  2885.         return x
  2886.     end
  2887. end
  2888.  
  2889.  
  2890. render["render"] = function(data, startScroll)
  2891.     if startScroll == nil then
  2892.         render.variables.startScroll = 0
  2893.     else
  2894.         render.variables.startScroll = startScroll
  2895.     end
  2896.  
  2897.     render.variables.scroll = startScroll + 1
  2898.     render.variables.maxScroll = render.variables.scroll
  2899.  
  2900.     render.variables.linkData = {}
  2901.  
  2902.     render.variables.align = render.alignations.left
  2903.  
  2904.     render.variables.blockLength = 0
  2905.     render.variables.link = false
  2906.     render.variables.linkStart = false
  2907.     render.variables.markers = {}
  2908.     render.variables.currentOffset = 0
  2909.  
  2910.     for k, v in pairs(data) do
  2911.         if type(v[2]) ~= "string" then
  2912.             render.functions.displayText(v)
  2913.         elseif v[2] == "<" or v[2] == "left" then
  2914.             render.variables.align = render.alignations.left
  2915.         elseif v[2] == ">" or v[2] == "right" then
  2916.             render.variables.align = render.alignations.right
  2917.         elseif v[2] == "=" or v[2] == "center" then
  2918.             render.variables.align = render.alignations.center
  2919.         else
  2920.             local existentFunction = false
  2921.  
  2922.             for name, func in pairs(render.functions.public) do
  2923.                 if v[2]:find(name) == 1 then
  2924.                     existentFunction = true
  2925.                     local ret = func(v)
  2926.                     if ret then
  2927.                         return ret
  2928.                     end
  2929.                 end
  2930.             end
  2931.  
  2932.             if not existentFunction then
  2933.                 return "Non-existent tag: \"" .. v[2] .. "\" on line " .. v[1]
  2934.             end
  2935.         end
  2936.     end
  2937.  
  2938.     return render.variables.linkData, render.variables.maxScroll - render.variables.startScroll
  2939. end
  2940.  
  2941.  
  2942.  
  2943. --    Lua Execution
  2944.  
  2945.  
  2946. languages["lua"] = {}
  2947. languages["fwml"] = {}
  2948.  
  2949.  
  2950. languages["lua"]["runWithErrorCatching"] = function(func, ...)
  2951.     local _, err = pcall(func, ...)
  2952.     if err then
  2953.         os.queueEvent(websiteErrorEvent, err)
  2954.     end
  2955. end
  2956.  
  2957.  
  2958. languages["lua"]["runWithoutAntivirus"] = function(func, ...)
  2959.     local args = {...}
  2960.     local env = getWebsiteEnvironment(false)
  2961.     setfenv(func, env)
  2962.     return function()
  2963.         languages["lua"]["runWithErrorCatching"](func, unpack(args))
  2964.     end
  2965. end
  2966.  
  2967.  
  2968. languages["lua"]["run"] = function(contents, page, connection, ...)
  2969.     local func, err = loadstring("sleep(0) " .. contents, page)
  2970.     if err then
  2971.         return languages["lua"]["runWithoutAntivirus"](builtInSites["crash"], err)
  2972.     else
  2973.         local args = {...}
  2974.         local env = getWebsiteEnvironment(true, connection)
  2975.         setfenv(func, env)
  2976.         return function()
  2977.             languages["lua"]["runWithErrorCatching"](func, unpack(args))
  2978.         end
  2979.     end
  2980. end
  2981.  
  2982.  
  2983. languages["fwml"]["run"] = function(contents, page, connection, ...)
  2984.     local err, data = pcall(parse, contents)
  2985.     if not err then
  2986.         return languages["lua"]["runWithoutAntivirus"](builtInSites["crash"], data)
  2987.     end
  2988.  
  2989.     return function()
  2990.         local currentScroll = 0
  2991.         local err, links, pageHeight = pcall(render.render, data, currentScroll)
  2992.         if type(links) == "string" or not err then
  2993.             term.clear()
  2994.             os.queueEvent(websiteErrorEvent, links)
  2995.         else
  2996.             while true do
  2997.                 local e, scroll, x, y = os.pullEvent()
  2998.                 if e == "mouse_click" then
  2999.                     for k, v in pairs(links) do
  3000.                         if x >= math.min(v[1], v[2]) and x <= math.max(v[1], v[2]) and y == v[3] then
  3001.                             os.queueEvent(redirectEvent, v[4])
  3002.                             coroutine.yield()
  3003.                         end
  3004.                     end
  3005.                 elseif e == "mouse_scroll" then
  3006.                     if currentScroll - scroll - h >= -pageHeight and currentScroll - scroll <= 0 then
  3007.                         currentScroll = currentScroll - scroll
  3008.                         clear(theme.background, theme.text)
  3009.                         links = render.render(data, currentScroll)
  3010.                     end
  3011.                 elseif e == "key" and scroll == keys.up or scroll == keys.down then
  3012.                     local scrollAmount
  3013.  
  3014.                     if scroll == keys.up then
  3015.                         scrollAmount = 1
  3016.                     elseif scroll == keys.down then
  3017.                         scrollAmount = -1
  3018.                     end
  3019.  
  3020.                     local scrollLessHeight = currentScroll + scrollAmount - h >= -pageHeight
  3021.                     local scrollZero = currentScroll + scrollAmount <= 0
  3022.                     if scrollLessHeight and scrollZero then
  3023.                         currentScroll = currentScroll + scrollAmount
  3024.                         clear(theme.background, theme.text)
  3025.                         links = render.render(data, currentScroll)
  3026.                     end
  3027.                 end
  3028.             end
  3029.         end
  3030.     end
  3031. end
  3032.  
  3033.  
  3034.  
  3035. --    Query Bar
  3036.  
  3037.  
  3038. local readNewWebsiteURL = function()
  3039.     local onEvent = function(text, event, key, x, y)
  3040.         if event == "mouse_click" then
  3041.             if y == 2 then
  3042.                 local index = determineClickedTab(x, y)
  3043.                 if index == "new" and #tabs < maxTabs then
  3044.                     loadTab(#tabs + 1, "firewolf")
  3045.                 elseif index == "close" then
  3046.                     closeCurrentTab()
  3047.                 elseif index then
  3048.                     switchTab(index)
  3049.                 end
  3050.  
  3051.                 return {["nullifyText"] = true, ["exit"] = true}
  3052.             elseif y > 2 then
  3053.                 return {["nullifyText"] = true, ["exit"] = true}
  3054.             end
  3055.         elseif event == "key" then
  3056.             if key == 29 or key == 157 then
  3057.                 return {["nullifyText"] = true, ["exit"] = true}
  3058.             end
  3059.         end
  3060.     end
  3061.  
  3062.     isMenubarOpen = true
  3063.     drawMenubar()
  3064.     term.setCursorPos(2, 1)
  3065.     term.setTextColor(theme.text)
  3066.     term.setBackgroundColor(theme.accent)
  3067.     term.clearLine()
  3068.     term.write(currentProtocol .. "://")
  3069.  
  3070.     local website = modifiedRead({
  3071.         ["onEvent"] = onEvent,
  3072.         ["displayLength"] = w - 9,
  3073.         ["history"] = history,
  3074.     })
  3075.  
  3076.     if not website then
  3077.         if not tabs[currentTab].isMenubarPermanent then
  3078.             isMenubarOpen = false
  3079.             menubarWindow.setVisible(false)
  3080.         else
  3081.             isMenubarOpen = true
  3082.             menubarWindow.setVisible(true)
  3083.         end
  3084.  
  3085.         term.redirect(tabs[currentTab].win)
  3086.         tabs[currentTab].win.setVisible(true)
  3087.         tabs[currentTab].win.redraw()
  3088.  
  3089.         return
  3090.     elseif website == "exit" then
  3091.         error()
  3092.     end
  3093.  
  3094.     loadTab(currentTab, website)
  3095. end
  3096.  
  3097.  
  3098.  
  3099. --    Event Management
  3100.  
  3101.  
  3102. local handleKeyDown = function(event)
  3103.     if event[2] == 29 or event[2] == 157 then
  3104.         readNewWebsiteURL()
  3105.         return true
  3106.     end
  3107.  
  3108.     return false
  3109. end
  3110.  
  3111.  
  3112. local handleMouseDown = function(event)
  3113.     if isMenubarOpen then
  3114.         if event[4] == 1 then
  3115.             readNewWebsiteURL()
  3116.             return true
  3117.         elseif event[4] == 2 then
  3118.             local index = determineClickedTab(event[3], event[4])
  3119.             if index == "new" and #tabs < maxTabs then
  3120.                 loadTab(#tabs + 1, "firewolf")
  3121.             elseif index == "close" then
  3122.                 closeCurrentTab()
  3123.             elseif index then
  3124.                 switchTab(index)
  3125.             end
  3126.  
  3127.             return true
  3128.         end
  3129.     end
  3130.  
  3131.     return false
  3132. end
  3133.  
  3134.  
  3135. local handleEvents = function()
  3136.     loadTab(1, "firewolf")
  3137.     currentTab = 1
  3138.  
  3139.     while true do
  3140.         drawMenubar()
  3141.         local event = {os.pullEventRaw()}
  3142.         drawMenubar()
  3143.  
  3144.         local cancelEvent = false
  3145.         if event[1] == "terminate" then
  3146.             break
  3147.         elseif event[1] == "key" then
  3148.             cancelEvent = handleKeyDown(event)
  3149.         elseif event[1] == "mouse_click" then
  3150.             cancelEvent = handleMouseDown(event)
  3151.         elseif event[1] == websiteErrorEvent then
  3152.             cancelEvent = true
  3153.  
  3154.             loadTab(currentTab, tabs[currentTab].url, function()
  3155.                 builtInSites["crash"](event[2])
  3156.             end)
  3157.         elseif event[1] == redirectEvent then
  3158.             cancelEvent = true
  3159.  
  3160.             if (event[2]:match("^rdnt://(.+)$")) then
  3161.                 event[2] = event[2]:match("^rdnt://(.+)$")
  3162.             end
  3163.  
  3164.             loadTab(currentTab, event[2])
  3165.         end
  3166.  
  3167.         if not cancelEvent then
  3168.             term.redirect(tabs[currentTab].win)
  3169.             term.setCursorPos(tabs[currentTab].ox, tabs[currentTab].oy)
  3170.  
  3171.             coroutine.resume(tabs[currentTab].thread, unpack(event))
  3172.  
  3173.             local ox, oy = term.getCursorPos()
  3174.             tabs[currentTab].ox = ox
  3175.             tabs[currentTab].oy = oy
  3176.         end
  3177.     end
  3178. end
  3179.  
  3180.  
  3181.  
  3182. --    Main
  3183.  
  3184.  
  3185. local main = function()
  3186.     currentProtocol = "rdnt"
  3187.     currentTab = 1
  3188.  
  3189.     if term.isColor() then
  3190.         theme = colorTheme
  3191.         enableTabBar = true
  3192.     else
  3193.         theme = grayscaleTheme
  3194.         enableTabBar = false
  3195.     end
  3196.  
  3197.     setupMenubar()
  3198.     protocols[currentProtocol]["setup"]()
  3199.  
  3200.     clear(theme.background, theme.text)
  3201.     handleEvents()
  3202. end
  3203.  
  3204.  
  3205. local handleError = function(err)
  3206.     clear(theme.background, theme.text)
  3207.  
  3208.     fill(1, 3, w, 3, theme.subtle)
  3209.     term.setCursorPos(1, 4)
  3210.     center("Firewolf has crashed!")
  3211.  
  3212.     term.setBackgroundColor(theme.background)
  3213.     term.setCursorPos(1, 8)
  3214.     centerSplit(err, w - 4)
  3215.     print("\n")
  3216.     center("Please report this error to")
  3217.     center("GravityScore or 1lann.")
  3218.     print("")
  3219.     center("Press any key to exit.")
  3220.  
  3221.     os.pullEvent("key")
  3222.     os.queueEvent("")
  3223.     os.pullEvent()
  3224. end
  3225.  
  3226. local _, err = pcall(main)
  3227. term.redirect(originalTerminal)
  3228.  
  3229. Modem.closeAll()
  3230.  
  3231. if err and not err:lower():find("terminate") then
  3232.     handleError(err)
  3233. end
  3234.  
  3235.  
  3236. clear(colors.black, colors.white)
  3237. center("Thanks for using Firewolf " .. version)
  3238. center("Made by GravityScore and 1lann")
  3239. print("")
Add Comment
Please, Sign In to add comment