1lann

Firewolf Client 3.5 (Aug 10)

Jun 20th, 2014
1,531
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 75.77 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.2"
  13. local build = 20
  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 = 1
  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.  
  934. local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
  935.  
  936.  
  937. local function sixBitToBase64(input)
  938.     return string.sub(alphabet, input+1, input+1)
  939. end
  940.  
  941.  
  942. local function base64ToSixBit(input)
  943.     for i=1, 64 do
  944.         if input == string.sub(alphabet, i, i) then
  945.             return i-1
  946.         end
  947.     end
  948. end
  949.  
  950.  
  951. local function octetToBase64(o1, o2, o3)
  952.     local shifted = bit.brshift(bit.band(o1, 0xFC), 2)
  953.     local i1 = sixBitToBase64(shifted)
  954.     local i2 = "A"
  955.     local i3 = "="
  956.     local i4 = "="
  957.     if o2 then
  958.         i2 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o1, 3), 4), bit.brshift(bit.band(o2, 0xF0), 4) ))
  959.         if not o3 then
  960.             i3 = sixBitToBase64(bit.blshift(bit.band(o2, 0x0F), 2))
  961.         else
  962.             i3 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o2, 0x0F), 2), bit.brshift(bit.band(o3, 0xC0), 6) ))
  963.         end
  964.     else
  965.         i2 = sixBitToBase64(bit.blshift(bit.band(o1, 3), 4))
  966.     end
  967.     if o3 then
  968.         i4 = sixBitToBase64(bit.band(o3, 0x3F))
  969.     end
  970.  
  971.     return i1..i2..i3..i4
  972. end
  973.  
  974.  
  975. local function base64ToThreeOctet(s1)
  976.     local c1 = base64ToSixBit(string.sub(s1, 1, 1))
  977.     local c2 = base64ToSixBit(string.sub(s1, 2, 2))
  978.     local c3 = 0
  979.     local c4 = 0
  980.     local o1 = 0
  981.     local o2 = 0
  982.     local o3 = 0
  983.     if string.sub(s1, 3, 3) == "=" then
  984.         c3 = nil
  985.         c4 = nil
  986.     elseif string.sub(s1, 4, 4) == "=" then
  987.         c3 = base64ToSixBit(string.sub(s1, 3, 3))
  988.         c4 = nil
  989.     else
  990.         c3 = base64ToSixBit(string.sub(s1, 3, 3))
  991.         c4 = base64ToSixBit(string.sub(s1, 4, 4))
  992.     end
  993.     o1 = bit.bor( bit.blshift(c1, 2), bit.brshift(bit.band( c2, 0x30 ), 4) )
  994.     if c3 then
  995.         o2 = bit.bor( bit.blshift(bit.band(c2, 0x0F), 4), bit.brshift(bit.band( c3, 0x3C ), 2) )
  996.     else
  997.         o2 = nil
  998.     end
  999.     if c4 then
  1000.         o3 = bit.bor( bit.blshift(bit.band(c3, 3), 6), c4 )
  1001.     else
  1002.         o3 = nil
  1003.     end
  1004.     return o1, o2, o3
  1005. end
  1006.  
  1007.  
  1008. local function splitIntoBlocks(bytes)
  1009.     local blockNum = 1
  1010.     local blocks = {}
  1011.     for i=1, #bytes, 3 do
  1012.         blocks[blockNum] = {bytes[i], bytes[i+1], bytes[i+2]}
  1013.         blockNum = blockNum+1
  1014.     end
  1015.     return blocks
  1016. end
  1017.  
  1018.  
  1019. function base64Encode(bytes)
  1020.     local blocks = splitIntoBlocks(bytes)
  1021.     local output = ""
  1022.     for i=1, #blocks do
  1023.         output = output..octetToBase64( unpack(blocks[i]) )
  1024.     end
  1025.     return output
  1026. end
  1027.  
  1028.  
  1029. function base64Decode(str)
  1030.     local bytes = {}
  1031.     local blocks = {}
  1032.     local blockNum = 1
  1033.  
  1034.     for i=1, #str, 4 do
  1035.         blocks[blockNum] = string.sub(str, i, i+3)
  1036.         blockNum = blockNum+1
  1037.     end
  1038.  
  1039.     for i=1, #blocks do
  1040.         local o1, o2, o3 = base64ToThreeOctet(blocks[i])
  1041.         table.insert(bytes, o1)
  1042.         table.insert(bytes, o2)
  1043.         table.insert(bytes, o3)
  1044.     end
  1045.  
  1046.     return bytes
  1047. end
  1048.  
  1049.  
  1050.  
  1051. --  SHA-256
  1052. --
  1053. --  Adaptation of the Secure Hashing Algorithm (SHA-244/256)
  1054. --  Found Here: http://lua-users.org/wiki/SecureHashAlgorithm
  1055. --
  1056. --  Using an adapted version of the bit library
  1057. --  Found Here: https://bitbucket.org/Boolsheet/bslf/src/1ee664885805/bit.lua
  1058.  
  1059.  
  1060. local MOD = 2^32
  1061. local MODM = MOD-1
  1062.  
  1063.  
  1064. local function memoize(f)
  1065.     local mt = {}
  1066.     local t = setmetatable({}, mt)
  1067.     function mt:__index(k)
  1068.         local v = f(k)
  1069.         t[k] = v
  1070.         return v
  1071.     end
  1072.     return t
  1073. end
  1074.  
  1075.  
  1076. local function make_bitop_uncached(t, m)
  1077.     local function bitop(a, b)
  1078.         local res,p = 0,1
  1079.         while a ~= 0 and b ~= 0 do
  1080.             local am, bm = a % m, b % m
  1081.             res = res + t[am][bm] * p
  1082.             a = (a - am) / m
  1083.             b = (b - bm) / m
  1084.             p = p * m
  1085.         end
  1086.         res = res + (a + b) * p
  1087.         return res
  1088.     end
  1089.  
  1090.     return bitop
  1091. end
  1092.  
  1093.  
  1094. local function make_bitop(t)
  1095.     local op1 = make_bitop_uncached(t,2^1)
  1096.     local op2 = memoize(function(a)
  1097.         return memoize(function(b)
  1098.             return op1(a, b)
  1099.         end)
  1100.     end)
  1101.     return make_bitop_uncached(op2, 2 ^ (t.n or 1))
  1102. end
  1103.  
  1104.  
  1105. local customBxor1 = make_bitop({[0] = {[0] = 0,[1] = 1}, [1] = {[0] = 1, [1] = 0}, n = 4})
  1106.  
  1107. local function customBxor(a, b, c, ...)
  1108.     local z = nil
  1109.     if b then
  1110.         a = a % MOD
  1111.         b = b % MOD
  1112.         z = customBxor1(a, b)
  1113.         if c then
  1114.             z = customBxor(z, c, ...)
  1115.         end
  1116.         return z
  1117.     elseif a then
  1118.         return a % MOD
  1119.     else
  1120.         return 0
  1121.     end
  1122. end
  1123.  
  1124.  
  1125. local function customBand(a, b, c, ...)
  1126.     local z
  1127.     if b then
  1128.         a = a % MOD
  1129.         b = b % MOD
  1130.         z = ((a + b) - customBxor1(a,b)) / 2
  1131.         if c then
  1132.             z = customBand(z, c, ...)
  1133.         end
  1134.         return z
  1135.     elseif a then
  1136.         return a % MOD
  1137.     else
  1138.         return MODM
  1139.     end
  1140. end
  1141.  
  1142.  
  1143. local function bnot(x)
  1144.     return (-1 - x) % MOD
  1145. end
  1146.  
  1147.  
  1148. local function rshift1(a, disp)
  1149.     if disp < 0 then
  1150.         return lshift(a, -disp)
  1151.     end
  1152.     return math.floor(a % 2 ^ 32 / 2 ^ disp)
  1153. end
  1154.  
  1155.  
  1156. local function rshift(x, disp)
  1157.     if disp > 31 or disp < -31 then
  1158.         return 0
  1159.     end
  1160.     return rshift1(x % MOD, disp)
  1161. end
  1162.  
  1163.  
  1164. local function lshift(a, disp)
  1165.     if disp < 0 then
  1166.         return rshift(a, -disp)
  1167.     end
  1168.     return (a * 2 ^ disp) % 2 ^ 32
  1169. end
  1170.  
  1171.  
  1172. local function rrotate(x, disp)
  1173.     x = x % MOD
  1174.     disp = disp % 32
  1175.     local low = customBand(x, 2 ^ disp - 1)
  1176.     return rshift(x, disp) + lshift(low, 32 - disp)
  1177. end
  1178.  
  1179.  
  1180. local k = {
  1181.     0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
  1182.     0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
  1183.     0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
  1184.     0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
  1185.     0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
  1186.     0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
  1187.     0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
  1188.     0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
  1189.     0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
  1190.     0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
  1191.     0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
  1192.     0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
  1193.     0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
  1194.     0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
  1195.     0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
  1196.     0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
  1197. }
  1198.  
  1199.  
  1200. local function str2hexa(s)
  1201.     return (string.gsub(s, ".", function(c)
  1202.         return string.format("%02x", string.byte(c))
  1203.     end))
  1204. end
  1205.  
  1206.  
  1207. local function num2s(l, n)
  1208.     local s = ""
  1209.     for i = 1, n do
  1210.         local rem = l % 256
  1211.         s = string.char(rem) .. s
  1212.         l = (l - rem) / 256
  1213.     end
  1214.     return s
  1215. end
  1216.  
  1217.  
  1218. local function s232num(s, i)
  1219.     local n = 0
  1220.     for i = i, i + 3 do
  1221.         n = n*256 + string.byte(s, i)
  1222.     end
  1223.     return n
  1224. end
  1225.  
  1226.  
  1227. local function preproc(msg, len)
  1228.     local extra = 64 - ((len + 9) % 64)
  1229.     len = num2s(8 * len, 8)
  1230.     msg = msg .. "\128" .. string.rep("\0", extra) .. len
  1231.     assert(#msg % 64 == 0)
  1232.     return msg
  1233. end
  1234.  
  1235.  
  1236. local function initH256(H)
  1237.     H[1] = 0x6a09e667
  1238.     H[2] = 0xbb67ae85
  1239.     H[3] = 0x3c6ef372
  1240.     H[4] = 0xa54ff53a
  1241.     H[5] = 0x510e527f
  1242.     H[6] = 0x9b05688c
  1243.     H[7] = 0x1f83d9ab
  1244.     H[8] = 0x5be0cd19
  1245.     return H
  1246. end
  1247.  
  1248.  
  1249. local function digestblock(msg, i, H)
  1250.     local w = {}
  1251.     for j = 1, 16 do
  1252.         w[j] = s232num(msg, i + (j - 1)*4)
  1253.     end
  1254.     for j = 17, 64 do
  1255.         local v = w[j - 15]
  1256.         local s0 = customBxor(rrotate(v, 7), rrotate(v, 18), rshift(v, 3))
  1257.         v = w[j - 2]
  1258.         w[j] = w[j - 16] + s0 + w[j - 7] + customBxor(rrotate(v, 17), rrotate(v, 19), rshift(v, 10))
  1259.     end
  1260.  
  1261.     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]
  1262.     for i = 1, 64 do
  1263.         local s0 = customBxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22))
  1264.         local maj = customBxor(customBand(a, b), customBand(a, c), customBand(b, c))
  1265.         local t2 = s0 + maj
  1266.         local s1 = customBxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25))
  1267.         local ch = customBxor (customBand(e, f), customBand(bnot(e), g))
  1268.         local t1 = h + s1 + ch + k[i] + w[i]
  1269.         h, g, f, e, d, c, b, a = g, f, e, d + t1, c, b, a, t1 + t2
  1270.     end
  1271.  
  1272.     H[1] = customBand(H[1] + a)
  1273.     H[2] = customBand(H[2] + b)
  1274.     H[3] = customBand(H[3] + c)
  1275.     H[4] = customBand(H[4] + d)
  1276.     H[5] = customBand(H[5] + e)
  1277.     H[6] = customBand(H[6] + f)
  1278.     H[7] = customBand(H[7] + g)
  1279.     H[8] = customBand(H[8] + h)
  1280. end
  1281.  
  1282.  
  1283. local function sha256(msg)
  1284.     msg = preproc(msg, #msg)
  1285.     local H = initH256({})
  1286.     for i = 1, #msg, 64 do
  1287.         digestblock(msg, i, H)
  1288.     end
  1289.     return str2hexa(num2s(H[1], 4) .. num2s(H[2], 4) .. num2s(H[3], 4) .. num2s(H[4], 4) ..
  1290.         num2s(H[5], 4) .. num2s(H[6], 4) .. num2s(H[7], 4) .. num2s(H[8], 4))
  1291. end
  1292.  
  1293.  
  1294. local protocolName = "Firewolf"
  1295.  
  1296.  
  1297.  
  1298. --    Cryptography
  1299.  
  1300.  
  1301. local Cryptography = {}
  1302. Cryptography.sha = {}
  1303. Cryptography.base64 = {}
  1304. Cryptography.aes = {}
  1305.  
  1306.  
  1307. function Cryptography.bytesFromMessage(msg)
  1308.     local bytes = {}
  1309.  
  1310.     for i = 1, msg:len() do
  1311.         local letter = string.byte(msg:sub(i, i))
  1312.         table.insert(bytes, letter)
  1313.     end
  1314.  
  1315.     return bytes
  1316. end
  1317.  
  1318.  
  1319. function Cryptography.messageFromBytes(bytes)
  1320.     local msg = ""
  1321.  
  1322.     for i = 1, #bytes do
  1323.         local letter = string.char(bytes[i])
  1324.         msg = msg .. letter
  1325.     end
  1326.  
  1327.     return msg
  1328. end
  1329.  
  1330.  
  1331. function Cryptography.bytesFromKey(key)
  1332.     local bytes = {}
  1333.  
  1334.     for i = 1, key:len() / 2 do
  1335.         local group = key:sub((i - 1) * 2 + 1, (i - 1) * 2 + 1)
  1336.         local num = tonumber(group, 16)
  1337.         table.insert(bytes, num)
  1338.     end
  1339.  
  1340.     return bytes
  1341. end
  1342.  
  1343.  
  1344. function Cryptography.sha.sha256(msg)
  1345.     return sha256(msg)
  1346. end
  1347.  
  1348.  
  1349. function Cryptography.aes.encrypt(msg, key)
  1350.     return base64Encode(crypt(msg, key))
  1351. end
  1352.  
  1353.  
  1354. function Cryptography.aes.decrypt(msg, key)
  1355.     return crypt(base64Decode(msg), key)
  1356. end
  1357.  
  1358.  
  1359. function Cryptography.base64.encode(msg)
  1360.     return base64Encode(Cryptography.bytesFromMessage(msg))
  1361. end
  1362.  
  1363.  
  1364. function Cryptography.base64.decode(msg)
  1365.     return Cryptography.messageFromBytes(base64Decode(msg))
  1366. end
  1367.  
  1368.  
  1369. function Cryptography.channel(text)
  1370.     local hashed = Cryptography.sha.sha256(text)
  1371.  
  1372.     local total = 0
  1373.  
  1374.     for i = 1, hashed:len() do
  1375.         total = total + string.byte(hashed:sub(i, i))
  1376.     end
  1377.  
  1378.     return (total % 55530) + 10000
  1379. end
  1380.  
  1381.  
  1382. function Cryptography.sanatize(text)
  1383.     local sanatizeChars = {"%", "(", ")", "[", "]", ".", "+", "-", "*", "?", "^", "$"}
  1384.  
  1385.     for _, char in pairs(sanatizeChars) do
  1386.         text = text:gsub("%"..char, "%%%"..char)
  1387.     end
  1388.     return text
  1389. end
  1390.  
  1391.  
  1392.  
  1393. --  Modem
  1394.  
  1395.  
  1396. local Modem = {}
  1397. Modem.modems = {}
  1398.  
  1399.  
  1400. function Modem.exists()
  1401.     Modem.exists = false
  1402.     for _, side in pairs(rs.getSides()) do
  1403.         if peripheral.isPresent(side) and peripheral.getType(side) == "modem" then
  1404.             Modem.exists = true
  1405.  
  1406.             if not Modem.modems[side] then
  1407.                 Modem.modems[side] = peripheral.wrap(side)
  1408.             end
  1409.         end
  1410.     end
  1411.  
  1412.     return Modem.exists
  1413. end
  1414.  
  1415.  
  1416. function Modem.open(channel)
  1417.     if not Modem.exists then
  1418.         return false
  1419.     end
  1420.  
  1421.     for side, modem in pairs(Modem.modems) do
  1422.         modem.open(channel)
  1423.         rednet.open(side)
  1424.     end
  1425.  
  1426.     return true
  1427. end
  1428.  
  1429.  
  1430. function Modem.close(channel)
  1431.     if not Modem.exists then
  1432.         return false
  1433.     end
  1434.  
  1435.     for side, modem in pairs(Modem.modems) do
  1436.         modem.close(channel)
  1437.     end
  1438.  
  1439.     return true
  1440. end
  1441.  
  1442.  
  1443. function Modem.closeAll()
  1444.     if not Modem.exists then
  1445.         return false
  1446.     end
  1447.  
  1448.     for side, modem in pairs(Modem.modems) do
  1449.         modem.closeAll()
  1450.     end
  1451.  
  1452.     return true
  1453. end
  1454.  
  1455.  
  1456. function Modem.isOpen(channel)
  1457.     if not Modem.exists then
  1458.         return false
  1459.     end
  1460.  
  1461.     local isOpen = false
  1462.     for side, modem in pairs(Modem.modems) do
  1463.         if modem.isOpen(channel) then
  1464.             isOpen = true
  1465.             break
  1466.         end
  1467.     end
  1468.  
  1469.     return isOpen
  1470. end
  1471.  
  1472.  
  1473. function Modem.transmit(channel, msg)
  1474.     if not Modem.exists then
  1475.         return false
  1476.     end
  1477.  
  1478.     if not Modem.isOpen(channel) then
  1479.         Modem.open(channel)
  1480.     end
  1481.  
  1482.     for side, modem in pairs(Modem.modems) do
  1483.         modem.transmit(channel, channel, msg)
  1484.     end
  1485.  
  1486.     return true
  1487. end
  1488.  
  1489.  
  1490.  
  1491. --    Handshake
  1492.  
  1493.  
  1494. local Handshake = {}
  1495.  
  1496. Handshake.prime = 625210769
  1497. Handshake.channel = 54569
  1498. Handshake.base = -1
  1499. Handshake.secret = -1
  1500. Handshake.sharedSecret = -1
  1501. Handshake.packetHeader = "["..protocolName.."-Handshake-Packet-Header]"
  1502. Handshake.packetMatch = "%["..protocolName.."%-Handshake%-Packet%-Header%](.+)"
  1503.  
  1504.  
  1505. function Handshake.exponentWithModulo(base, exponent, modulo)
  1506.     local remainder = base
  1507.  
  1508.     for i = 1, exponent-1 do
  1509.         remainder = remainder * remainder
  1510.         if remainder >= modulo then
  1511.             remainder = remainder % modulo
  1512.         end
  1513.     end
  1514.  
  1515.     return remainder
  1516. end
  1517.  
  1518.  
  1519. function Handshake.clear()
  1520.     Handshake.base = -1
  1521.     Handshake.secret = -1
  1522.     Handshake.sharedSecret = -1
  1523. end
  1524.  
  1525.  
  1526. function Handshake.generateInitiatorData()
  1527.     Handshake.base = math.random(10,99999)
  1528.     Handshake.secret = math.random(10,99999)
  1529.     return {
  1530.         type = "initiate",
  1531.         prime = Handshake.prime,
  1532.         base = Handshake.base,
  1533.         moddedSecret = Handshake.exponentWithModulo(Handshake.base, Handshake.secret, Handshake.prime)
  1534.     }
  1535. end
  1536.  
  1537.  
  1538. function Handshake.generateResponseData(initiatorData)
  1539.     local isPrimeANumber = type(initiatorData.prime) == "number"
  1540.     local isPrimeMatching = initiatorData.prime == Handshake.prime
  1541.     local isBaseANumber = type(initiatorData.base) == "number"
  1542.     local isInitiator = initiatorData.type == "initiate"
  1543.     local isModdedSecretANumber = type(initiatorData.moddedSecret) == "number"
  1544.     local areAllNumbersNumbers = isPrimeANumber and isBaseANumber and isModdedSecretANumber
  1545.  
  1546.     if areAllNumbersNumbers and isPrimeMatching then
  1547.         if isInitiator then
  1548.             Handshake.base = initiatorData.base
  1549.             Handshake.secret = math.random(10,99999)
  1550.             Handshake.sharedSecret = Handshake.exponentWithModulo(initiatorData.moddedSecret, Handshake.secret, Handshake.prime)
  1551.             return {
  1552.                 type = "response",
  1553.                 prime = Handshake.prime,
  1554.                 base = Handshake.base,
  1555.                 moddedSecret = Handshake.exponentWithModulo(Handshake.base, Handshake.secret, Handshake.prime)
  1556.             }, Handshake.sharedSecret
  1557.         elseif initiatorData.type == "response" and Handshake.base > 0 and Handshake.secret > 0 then
  1558.             Handshake.sharedSecret = Handshake.exponentWithModulo(initiatorData.moddedSecret, Handshake.secret, Handshake.prime)
  1559.             return Handshake.sharedSecret
  1560.         else
  1561.             return false
  1562.         end
  1563.     else
  1564.         return false
  1565.     end
  1566. end
  1567.  
  1568.  
  1569.  
  1570. --    Secure Connection
  1571.  
  1572.  
  1573. local SecureConnection = {}
  1574. SecureConnection.__index = SecureConnection
  1575.  
  1576.  
  1577. SecureConnection.packetHeaderA = "["..protocolName.."-"
  1578. SecureConnection.packetHeaderB = "-SecureConnection-Packet-Header]"
  1579. SecureConnection.packetMatchA = "%["..protocolName.."%-"
  1580. SecureConnection.packetMatchB = "%-SecureConnection%-Packet%-Header%](.+)"
  1581. SecureConnection.connectionTimeout = 0.1
  1582. SecureConnection.successPacketTimeout = 0.1
  1583.  
  1584.  
  1585. function SecureConnection.new(secret, key, identifier, distance, isRednet)
  1586.     local self = setmetatable({}, SecureConnection)
  1587.     self:setup(secret, key, identifier, distance, isRednet)
  1588.     return self
  1589. end
  1590.  
  1591.  
  1592. function SecureConnection:setup(secret, key, identifier, distance, isRednet)
  1593.     local rawSecret
  1594.  
  1595.     if isRednet then
  1596.         self.isRednet = true
  1597.         self.distance = -1
  1598.         self.rednet_id = distance
  1599.         rawSecret = protocolName .. "|" .. tostring(secret) .. "|" .. tostring(identifier) ..
  1600.         "|" .. tostring(key) .. "|rednet"
  1601.     else
  1602.         self.isRednet = false
  1603.         self.distance = distance
  1604.         rawSecret = protocolName .. "|" .. tostring(secret) .. "|" .. tostring(identifier) ..
  1605.         "|" .. tostring(key) .. "|" .. tostring(distance)
  1606.     end
  1607.  
  1608.     self.identifier = identifier
  1609.     self.packetMatch = SecureConnection.packetMatchA .. Cryptography.sanatize(identifier) .. SecureConnection.packetMatchB
  1610.     self.packetHeader = SecureConnection.packetHeaderA .. identifier .. SecureConnection.packetHeaderB
  1611.     self.secret = Cryptography.sha.sha256(rawSecret)
  1612.     self.channel = Cryptography.channel(self.secret)
  1613.  
  1614.     if not self.isRednet then
  1615.         Modem.open(self.channel)
  1616.     end
  1617. end
  1618.  
  1619.  
  1620. function SecureConnection:verifyHeader(msg)
  1621.     if type(msg) ~= "string" then return false end
  1622.  
  1623.     if msg:match(self.packetMatch) then
  1624.         return true
  1625.     else
  1626.         return false
  1627.     end
  1628. end
  1629.  
  1630.  
  1631. function SecureConnection:sendMessage(msg, rednetProtocol)
  1632.     local rawEncryptedMsg = Cryptography.aes.encrypt(self.packetHeader .. msg, self.secret)
  1633.     local encryptedMsg = self.packetHeader .. rawEncryptedMsg
  1634.  
  1635.     if self.isRednet then
  1636.         rednet.send(self.rednet_id, encryptedMsg, rednetProtocol)
  1637.         return true
  1638.     else
  1639.         return Modem.transmit(self.channel, encryptedMsg)
  1640.     end
  1641. end
  1642.  
  1643.  
  1644. function SecureConnection:decryptMessage(msg)
  1645.     if self:verifyHeader(msg) then
  1646.         local encrypted = msg:match(self.packetMatch)
  1647.  
  1648.         local unencryptedMsg = nil
  1649.         pcall(function() unencryptedMsg = Cryptography.aes.decrypt(encrypted, self.secret) end)
  1650.         if not unencryptedMsg then
  1651.             return false, "Could not decrypt"
  1652.         end
  1653.  
  1654.         if self:verifyHeader(unencryptedMsg) then
  1655.             return true, unencryptedMsg:match(self.packetMatch)
  1656.         else
  1657.             return false, "Could not verify"
  1658.         end
  1659.     else
  1660.         return false, "Could not stage 1 verify"
  1661.     end
  1662. end
  1663.  
  1664.  
  1665.  
  1666. --    RDNT Protocol
  1667.  
  1668.  
  1669. protocols["rdnt"] = {}
  1670.  
  1671. local header = {}
  1672. header.dnsPacket = "[Firewolf-DNS-Packet]"
  1673. header.dnsHeaderMatch = "^%[Firewolf%-DNS%-Response%](.+)$"
  1674. header.rednetHeader = "[Firewolf-Rednet-Channel-Simulation]"
  1675. header.rednetMatch = "^%[Firewolf%-Rednet%-Channel%-Simulation%](%d+)$"
  1676. header.responseMatchA = "^%[Firewolf%-"
  1677. header.responseMatchB = "%-"
  1678. header.responseMatchC = "%-Handshake%-Response%](.+)$"
  1679. header.requestHeaderA = "[Firewolf-"
  1680. header.requestHeaderB = "-Handshake-Request]"
  1681. header.pageRequestHeaderA = "[Firewolf-"
  1682. header.pageRequestHeaderB = "-Page-Request]"
  1683. header.pageResponseMatchA = "^%[Firewolf%-"
  1684. header.pageResponseMatchB = "%-Page%-Response%]%[HEADER%](.-)%[BODY%](.+)$"
  1685. header.closeHeaderA = "[Firewolf-"
  1686. header.closeHeaderB = "-Connection-Close]"
  1687.  
  1688.  
  1689. protocols["rdnt"]["setup"] = function()
  1690.     if not Modem.exists() then
  1691.         error("No modem found!")
  1692.     end
  1693. end
  1694.  
  1695.  
  1696. protocols["rdnt"]["fetchAllSearchResults"] = function()
  1697.     Modem.open(publicDNSChannel)
  1698.     Modem.open(publicResponseChannel)
  1699.     Modem.transmit(publicDNSChannel, header.dnsPacket)
  1700.     Modem.close(publicDNSChannel)
  1701.  
  1702.     rednet.broadcast(header.dnsPacket, header.rednetHeader .. publicDNSChannel)
  1703.  
  1704.     local uniqueServers = {}
  1705.     local uniqueDomains = {}
  1706.  
  1707.     local timer = os.startTimer(searchResultTimeout)
  1708.  
  1709.     while true do
  1710.         local event, id, channel, protocol, message, dist = os.pullEventRaw()
  1711.         if event == "modem_message" then
  1712.             if channel == publicResponseChannel and type(message) == "string" and message:match(header.dnsHeaderMatch) then
  1713.                 if not uniqueServers[tostring(dist)] then
  1714.                     uniqueServers[tostring(dist)] = true
  1715.                     local domain = message:match(header.dnsHeaderMatch)
  1716.                     if not uniqueDomains[domain] then
  1717.                         if not(domain:find("/") or domain:find(":") or domain:find("%?")) and #domain > 4 then
  1718.                             timer = os.startTimer(searchResultTimeout)
  1719.                             uniqueDomains[message:match(header.dnsHeaderMatch)] = tostring(dist)
  1720.                         end
  1721.                     end
  1722.                 end
  1723.             end
  1724.         elseif event == "rednet_message" and allowUnencryptedConnections then
  1725.             if protocol and tonumber(protocol:match(header.rednetMatch)) == publicResponseChannel and channel:match(header.dnsHeaderMatch) then
  1726.                 if not uniqueServers[tostring(id)] then
  1727.                     uniqueServers[tostring(id)] = true
  1728.                     local domain = channel:match(header.dnsHeaderMatch)
  1729.                     if not uniqueDomains[domain] then
  1730.                         if not(domain:find("/") or domain:find(":") or domain:find("%?")) and #domain > 4 then
  1731.                             timer = os.startTimer(searchResultTimeout)
  1732.                             uniqueDomains[domain] = tostring(id)
  1733.                         end
  1734.                     end
  1735.                 end
  1736.             end
  1737.         elseif event == "timer" and id == timer then
  1738.             local results = {}
  1739.             for k, _ in pairs(uniqueDomains) do
  1740.                 table.insert(results, k)
  1741.             end
  1742.  
  1743.             return results
  1744.         end
  1745.     end
  1746. end
  1747.  
  1748.  
  1749. protocols["rdnt"]["fetchConnectionObject"] = function(url)
  1750.     local serverChannel = Cryptography.channel(url)
  1751.     local requestHeader = header.requestHeaderA .. url .. header.requestHeaderB
  1752.     local responseMatch = header.responseMatchA .. Cryptography.sanatize(url) .. header.responseMatchB
  1753.  
  1754.     local serializedHandshake = textutils.serialize(Handshake.generateInitiatorData())
  1755.  
  1756.     local rednetResults = {}
  1757.     local directResults = {}
  1758.  
  1759.     local disconnectOthers = function(ignoreDirect)
  1760.         for k,v in pairs(rednetResults) do
  1761.             v.close()
  1762.         end
  1763.         for k,v in pairs(directResults) do
  1764.             if k ~= ignoreDirect then
  1765.                 v.close()
  1766.             end
  1767.         end
  1768.     end
  1769.  
  1770.     local timer = os.startTimer(initiationTimeout)
  1771.  
  1772.     Modem.open(serverChannel)
  1773.     Modem.transmit(serverChannel, requestHeader .. serializedHandshake)
  1774.  
  1775.     rednet.broadcast(requestHeader .. serializedHandshake, header.rednetHeader .. serverChannel)
  1776.  
  1777.     -- Extendable to have server selection
  1778.  
  1779.     while true do
  1780.         local event, id, channel, protocol, message, dist = os.pullEventRaw()
  1781.         if event == "modem_message" then
  1782.             local fullMatch = responseMatch .. tostring(dist) .. header.responseMatchC
  1783.             if channel == serverChannel and type(message) == "string" and message:match(fullMatch) and type(textutils.unserialize(message:match(fullMatch))) == "table" then
  1784.                 local key = Handshake.generateResponseData(textutils.unserialize(message:match(fullMatch)))
  1785.                 if key then
  1786.                     local connection = SecureConnection.new(key, url, url, dist)
  1787.                     table.insert(directResults, {
  1788.                         connection = connection,
  1789.                         fetchPage = function(page)
  1790.                             if not connection then
  1791.                                 return nil
  1792.                             end
  1793.  
  1794.                             local fetchTimer = os.startTimer(fetchTimeout)
  1795.  
  1796.                             local pageRequest = header.pageRequestHeaderA .. url .. header.pageRequestHeaderB .. page
  1797.                             local pageResponseMatch = header.pageResponseMatchA .. Cryptography.sanatize(url) .. header.pageResponseMatchB
  1798.  
  1799.                             connection:sendMessage(pageRequest, header.rednetHeader .. connection.channel)
  1800.  
  1801.                             while true do
  1802.                                 local event, id, channel, protocol, message, dist = os.pullEventRaw()
  1803.                                 if event == "modem_message" and channel == connection.channel and type(message) == "string" and connection:verifyHeader(message) then
  1804.                                     local resp, data = connection:decryptMessage(message)
  1805.                                     if not resp then
  1806.                                         -- Decryption error
  1807.                                     elseif data and data ~= page then
  1808.                                         if data:match(pageResponseMatch) then
  1809.                                             local head, body = data:match(pageResponseMatch)
  1810.                                             return body, textutils.unserialize(head)
  1811.                                         end
  1812.                                     end
  1813.                                 elseif event == "timer" and id == fetchTimer then
  1814.                                     return nil
  1815.                                 end
  1816.                             end
  1817.                         end,
  1818.                         close = function()
  1819.                             if connection ~= nil then
  1820.                                 connection:sendMessage(header.closeHeaderA .. url .. header.closeHeaderB, header.rednetHeader..connection.channel)
  1821.                                 Modem.close(connection.channel)
  1822.                                 connection = nil
  1823.                             end
  1824.                         end
  1825.                     })
  1826.  
  1827.                     disconnectOthers(1)
  1828.                     return directResults[1]
  1829.                 end
  1830.             end
  1831.         elseif event == "rednet_message" then
  1832.             local fullMatch = responseMatch .. os.getComputerID() .. header.responseMatchC
  1833.             if protocol and tonumber(protocol:match(header.rednetMatch)) == serverChannel and channel:match(fullMatch) and type(textutils.unserialize(channel:match(fullMatch))) == "table" then
  1834.                 local key = Handshake.generateResponseData(textutils.unserialize(channel:match(fullMatch)))
  1835.                 if key then
  1836.                     local connection = SecureConnection.new(key, url, url, id, true)
  1837.                     table.insert(rednetResults, {
  1838.                         connection = connection,
  1839.                         fetchPage = function(page)
  1840.                             if not connection then
  1841.                                 return nil
  1842.                             end
  1843.  
  1844.                             local fetchTimer = os.startTimer(fetchTimeout)
  1845.  
  1846.                             local pageRequest = header.pageRequestHeaderA .. url .. header.pageRequestHeaderB .. page
  1847.                             local pageResponseMatch = header.pageResponseMatchA .. Cryptography.sanatize(url) .. header.pageResponseMatchB
  1848.  
  1849.                             connection:sendMessage(pageRequest, header.rednetHeader .. connection.channel)
  1850.  
  1851.                             while true do
  1852.                                 local event, id, channel, protocol, message, dist = os.pullEventRaw()
  1853.                                 if event == "rednet_message" and protocol and tonumber(protocol:match(header.rednetMatch)) == connection.channel and connection:verifyHeader(channel) then
  1854.                                     local resp, data = connection:decryptMessage(channel)
  1855.                                     if not resp then
  1856.                                         -- Decryption error
  1857.                                     elseif data and data ~= page then
  1858.                                         if data:match(pageResponseMatch) then
  1859.                                             local head, body = data:match(pageResponseMatch)
  1860.                                             return body, textutils.unserialize(head)
  1861.                                         end
  1862.                                     end
  1863.                                 elseif event == "timer" and id == fetchTimer then
  1864.                                     return nil
  1865.                                 end
  1866.                             end
  1867.                         end,
  1868.                         close = function()
  1869.                             connection:sendMessage(header.closeHeaderA .. url .. header.closeHeaderB, header.rednetHeader..connection.channel)
  1870.                             Modem.close(connection.channel)
  1871.                             connection = nil
  1872.                         end
  1873.                     })
  1874.  
  1875.                     if #rednetResults == 1 then
  1876.                         timer = os.startTimer(0.2)
  1877.                     end
  1878.                 end
  1879.             end
  1880.         elseif event == "timer" and id == timer then
  1881.             -- Return
  1882.             if #directResults > 0 then
  1883.                 disconnectOthers(1)
  1884.                 return directResults[1]
  1885.             elseif #rednetResults > 0 then
  1886.                 local lowestID = math.huge
  1887.                 local lowestResult = nil
  1888.                 for k,v in pairs(rednetResults) do
  1889.                     if v.connection.rednet_id < lowestID then
  1890.                         lowestID = v.connection.rednet_id
  1891.                         lowestResult = v
  1892.                     end
  1893.                 end
  1894.  
  1895.                 for k,v in pairs(rednetResults) do
  1896.                     if v.connection.rednet_id ~= lowestID then
  1897.                         v.close()
  1898.                     end
  1899.                 end
  1900.  
  1901.                 return lowestResult
  1902.             else
  1903.                 return nil
  1904.             end
  1905.         end
  1906.     end
  1907. end
  1908.  
  1909.  
  1910.  
  1911. --    Fetching Raw Data
  1912.  
  1913.  
  1914. local fetchSearchResultsForQuery = function(query)
  1915.     local all = protocols[currentProtocol]["fetchAllSearchResults"]()
  1916.     local results = {}
  1917.     if query and query:len() > 0 then
  1918.         for _, v in pairs(all) do
  1919.             if v:find(query:lower()) then
  1920.                 table.insert(results, v)
  1921.             end
  1922.         end
  1923.     else
  1924.         results = all
  1925.     end
  1926.  
  1927.     table.sort(results)
  1928.     return results
  1929. end
  1930.  
  1931.  
  1932. local getConnectionObjectFromURL = function(url)
  1933.     local domain = url:match("^([^/]+)")
  1934.     return protocols[currentProtocol]["fetchConnectionObject"](domain)
  1935. end
  1936.  
  1937.  
  1938. local determineLanguage = function(header)
  1939.     if type(header) == "table" then
  1940.         if header.language and header.language == "Firewolf Markup" then
  1941.             return "fwml"
  1942.         else
  1943.             return "lua"
  1944.         end
  1945.     else
  1946.         return "lua"
  1947.     end
  1948. end
  1949.  
  1950.  
  1951.  
  1952. --    History
  1953.  
  1954.  
  1955. local appendToHistory = function(url)
  1956.     if history[1] ~= url then
  1957.         table.insert(history, 1, url)
  1958.     end
  1959. end
  1960.  
  1961.  
  1962.  
  1963. --    Fetch Websites
  1964.  
  1965.  
  1966. local loadingAnimation = function()
  1967.     local state = -2
  1968.  
  1969.     term.setTextColor(theme.text)
  1970.     term.setBackgroundColor(theme.accent)
  1971.  
  1972.     term.setCursorPos(w - 5, 1)
  1973.     term.write("[=  ]")
  1974.  
  1975.     local timer = os.startTimer(animationInterval)
  1976.  
  1977.     while true do
  1978.         local event, timerID = os.pullEvent()
  1979.         if event == "timer" and timerID == timer then
  1980.             term.setTextColor(theme.text)
  1981.             term.setBackgroundColor(theme.accent)
  1982.  
  1983.             state = state + 1
  1984.  
  1985.             term.setCursorPos(w - 5, 1)
  1986.             term.write("[   ]")
  1987.             term.setCursorPos(w - 2 - math.abs(state), 1)
  1988.             term.write("=")
  1989.  
  1990.             if state == 2 then
  1991.                 state = -2
  1992.             end
  1993.  
  1994.             timer = os.startTimer(animationInterval)
  1995.         end
  1996.     end
  1997. end
  1998.  
  1999.  
  2000. local normalizeURL = function(url)
  2001.     url = url:lower():gsub(" ", "")
  2002.     if url == "home" or url == "homepage" then
  2003.         url = "firewolf"
  2004.     end
  2005.  
  2006.     return url
  2007. end
  2008.  
  2009.  
  2010. local normalizePage = function(page)
  2011.     if not page then page = "" end
  2012.     page = page:lower()
  2013.     if page == "" then
  2014.         page = "/"
  2015.     end
  2016.     return page
  2017. end
  2018.  
  2019.  
  2020. local determineActionForURL = function(url)
  2021.     if url:len() > 0 and url:gsub("/", ""):len() == 0 then
  2022.         return "none"
  2023.     end
  2024.  
  2025.     if url == "exit" then
  2026.         return "exit"
  2027.     elseif builtInSites["display"][url] then
  2028.         return "internal website"
  2029.     elseif url == "" then
  2030.         local results = fetchSearchResultsForQuery()
  2031.         if #results > 0 then
  2032.             return "search", results
  2033.         else
  2034.             return "none"
  2035.         end
  2036.     else
  2037.         local connection = getConnectionObjectFromURL(url)
  2038.         if connection then
  2039.             return "external website", connection
  2040.         else
  2041.             local results = fetchSearchResultsForQuery(url)
  2042.             if #results > 0 then
  2043.                 return "search", results
  2044.             else
  2045.                 return "none"
  2046.             end
  2047.         end
  2048.     end
  2049. end
  2050.  
  2051.  
  2052. local fetchSearch = function(url, results)
  2053.     return languages["lua"]["runWithoutAntivirus"](builtInSites["search"], results)
  2054. end
  2055.  
  2056.  
  2057. local fetchInternal = function(url)
  2058.     return languages["lua"]["runWithoutAntivirus"](builtInSites["display"][url])
  2059. end
  2060.  
  2061.  
  2062. local fetchError = function(err)
  2063.     return languages["lua"]["runWithoutAntivirus"](builtInSites["error"], err)
  2064. end
  2065.  
  2066.  
  2067. local fetchExternal = function(url, connection)
  2068.     if connection.multipleServers then
  2069.         -- Please forgive me
  2070.         -- GravityScore forced me to do it like this
  2071.         -- I don't mean it, I really don't.
  2072.         connection = connection.servers[1]
  2073.     end
  2074.  
  2075.     local page = normalizePage(url:match("^[^/]+/(.+)"))
  2076.     local contents, head = connection.fetchPage(page)
  2077.     if contents then
  2078.         if type(contents) ~= "string" then
  2079.             return fetchNone()
  2080.         else
  2081.             local language = determineLanguage(head)
  2082.             return languages[language]["run"](contents, page, connection)
  2083.         end
  2084.     else
  2085.         if connection then
  2086.             connection.close()
  2087.             return "retry"
  2088.         end
  2089.         return fetchError("A connection error/timeout has occurred!")
  2090.     end
  2091. end
  2092.  
  2093.  
  2094. local fetchNone = function()
  2095.     return languages["lua"]["runWithoutAntivirus"](builtInSites["noresults"])
  2096. end
  2097.  
  2098.  
  2099. local fetchURL = function(url, inheritConnection)
  2100.     url = normalizeURL(url)
  2101.     currentWebsiteURL = url
  2102.  
  2103.     if inheritConnection then
  2104.         local resp = fetchExternal(url, inheritConnection)
  2105.         if resp ~= "retry" then
  2106.             return resp, false, inheritConnection
  2107.         end
  2108.     end
  2109.  
  2110.     local action, connection = determineActionForURL(url)
  2111.  
  2112.     if action == "search" then
  2113.         return fetchSearch(url, connection), true
  2114.     elseif action == "internal website" then
  2115.         return fetchInternal(url), true
  2116.     elseif action == "external website" then
  2117.         local resp = fetchExternal(url, connection)
  2118.         if resp == "retry" then
  2119.             return fetchError("A connection error/timeout has occurred!"), false, connection
  2120.         else
  2121.             return resp, false, connection
  2122.         end
  2123.     elseif action == "none" then
  2124.         return fetchNone(), true
  2125.     elseif action == "exit" then
  2126.         os.queueEvent("terminate")
  2127.     end
  2128.  
  2129.     return nil
  2130. end
  2131.  
  2132.  
  2133.  
  2134. --    Tabs
  2135.  
  2136.  
  2137. local switchTab = function(index, shouldntResume)
  2138.     if not tabs[index] then
  2139.         return
  2140.     end
  2141.  
  2142.     if tabs[currentTab].win then
  2143.         tabs[currentTab].win.setVisible(false)
  2144.     end
  2145.  
  2146.     currentTab = index
  2147.     isMenubarOpen = tabs[currentTab].isMenubarOpen
  2148.     currentWebsiteURL = tabs[currentTab].url
  2149.  
  2150.     term.redirect(originalTerminal)
  2151.     clear(theme.background, theme.text)
  2152.     drawMenubar()
  2153.  
  2154.     term.redirect(tabs[currentTab].win)
  2155.     term.setCursorPos(1, 1)
  2156.     tabs[currentTab].win.setVisible(true)
  2157.     tabs[currentTab].win.redraw()
  2158.  
  2159.     if not shouldntResume then
  2160.         coroutine.resume(tabs[currentTab].thread)
  2161.     end
  2162. end
  2163.  
  2164.  
  2165. local closeCurrentTab = function()
  2166.     if #tabs <= 0 then
  2167.         return
  2168.     end
  2169.  
  2170.     table.remove(tabs, currentTab)
  2171.  
  2172.     currentTab = math.max(currentTab - 1, 1)
  2173.     switchTab(currentTab, true)
  2174. end
  2175.  
  2176.  
  2177. local loadTab = function(index, url, givenFunc)
  2178.     url = normalizeURL(url)
  2179.  
  2180.     local func = nil
  2181.     local isOpen = true
  2182.     local currentConnection = false
  2183.  
  2184.     isMenubarOpen = true
  2185.     currentWebsiteURL = url
  2186.     drawMenubar()
  2187.  
  2188.     if tabs[index] and tabs[index].connection and tabs[index].url then
  2189.         if url:match("^([^/]+)") == tabs[index].url:match("^([^/]+)") then
  2190.             currentConnection = tabs[index].connection
  2191.         else
  2192.             tabs[index].connection.close()
  2193.             tabs[index].connection = nil
  2194.         end
  2195.     end
  2196.  
  2197.     if givenFunc then
  2198.         func = givenFunc
  2199.     else
  2200.         parallel.waitForAny(function()
  2201.             func, isOpen, connection = fetchURL(url, currentConnection)
  2202.         end, function()
  2203.             while true do
  2204.                 local event, key = os.pullEvent()
  2205.                 if event == "key" and (key == 29 or key == 157) then
  2206.                     break
  2207.                 end
  2208.             end
  2209.         end, loadingAnimation)
  2210.     end
  2211.  
  2212.     if func then
  2213.         appendToHistory(url)
  2214.  
  2215.         tabs[index] = {}
  2216.         tabs[index].url = url
  2217.         tabs[index].connection = connection
  2218.         tabs[index].win = window.create(originalTerminal, 1, 1, w, h, false)
  2219.  
  2220.         tabs[index].thread = coroutine.create(func)
  2221.         tabs[index].isMenubarOpen = isOpen
  2222.         tabs[index].isMenubarPermanent = isOpen
  2223.  
  2224.         tabs[index].ox = 1
  2225.         tabs[index].oy = 1
  2226.  
  2227.         term.redirect(tabs[index].win)
  2228.         clear(theme.background, theme.text)
  2229.  
  2230.         switchTab(index)
  2231.     end
  2232. end
  2233.  
  2234.  
  2235.  
  2236. --    Website Environments
  2237.  
  2238.  
  2239. local getWhitelistedEnvironment = function()
  2240.     local env = {}
  2241.  
  2242.     local function copy(source, destination, key)
  2243.         destination[key] = {}
  2244.         for k, v in pairs(source) do
  2245.             destination[key][k] = v
  2246.         end
  2247.     end
  2248.  
  2249.     copy(bit, env, "bit")
  2250.     copy(colors, env, "colors")
  2251.     copy(colours, env, "colours")
  2252.     copy(coroutine, env, "coroutine")
  2253.  
  2254.     copy(disk, env, "disk")
  2255.     env["disk"]["setLabel"] = nil
  2256.     env["disk"]["eject"] = nil
  2257.  
  2258.     copy(gps, env, "gps")
  2259.     copy(help, env, "help")
  2260.     copy(keys, env, "keys")
  2261.     copy(math, env, "math")
  2262.  
  2263.     copy(os, env, "os")
  2264.     env["os"]["run"] = nil
  2265.     env["os"]["shutdown"] = nil
  2266.     env["os"]["reboot"] = nil
  2267.     env["os"]["setComputerLabel"] = nil
  2268.     env["os"]["queueEvent"] = nil
  2269.     env["os"]["pullEvent"] = function(filter)
  2270.         while true do
  2271.             local event = {os.pullEvent(filter)}
  2272.             if not filter then
  2273.                 return unpack(event)
  2274.             elseif filter and event[1] == filter then
  2275.                 return unpack(event)
  2276.             end
  2277.         end
  2278.     end
  2279.     env["os"]["pullEventRaw"] = env["os"]["pullEvent"]
  2280.  
  2281.     copy(paintutils, env, "paintutils")
  2282.     copy(parallel, env, "parallel")
  2283.     copy(peripheral, env, "peripheral")
  2284.     copy(rednet, env, "rednet")
  2285.     copy(redstone, env, "redstone")
  2286.     copy(redstone, env, "rs")
  2287.  
  2288.     copy(shell, env, "shell")
  2289.     env["shell"]["run"] = nil
  2290.     env["shell"]["exit"] = nil
  2291.     env["shell"]["setDir"] = nil
  2292.     env["shell"]["setAlias"] = nil
  2293.     env["shell"]["clearAlias"] = nil
  2294.     env["shell"]["setPath"] = nil
  2295.  
  2296.     copy(string, env, "string")
  2297.     copy(table, env, "table")
  2298.  
  2299.     copy(term, env, "term")
  2300.     env["term"]["redirect"] = nil
  2301.     env["term"]["restore"] = nil
  2302.  
  2303.     copy(textutils, env, "textutils")
  2304.     copy(vector, env, "vector")
  2305.  
  2306.     if turtle then
  2307.         copy(turtle, env, "turtle")
  2308.     end
  2309.  
  2310.     if http then
  2311.         copy(http, env, "http")
  2312.     end
  2313.  
  2314.     env["assert"] = assert
  2315.     env["printError"] = printError
  2316.     env["tonumber"] = tonumber
  2317.     env["tostring"] = tostring
  2318.     env["type"] = type
  2319.     env["next"] = next
  2320.     env["unpack"] = unpack
  2321.     env["pcall"] = pcall
  2322.     env["xpcall"] = xpcall
  2323.     env["sleep"] = sleep
  2324.     env["pairs"] = pairs
  2325.     env["ipairs"] = ipairs
  2326.     env["read"] = read
  2327.     env["write"] = write
  2328.     env["select"] = select
  2329.     env["print"] = print
  2330.     env["setmetatable"] = setmetatable
  2331.     env["getmetatable"] = getmetatable
  2332.  
  2333.     env["_G"] = env
  2334.  
  2335.     return env
  2336. end
  2337.  
  2338.  
  2339. local overrideEnvironment = function(env)
  2340.     local localTerm = {}
  2341.     for k, v in pairs(term) do
  2342.         localTerm[k] = v
  2343.     end
  2344.  
  2345.     env["term"]["clear"] = function()
  2346.         localTerm.clear()
  2347.         drawMenubar()
  2348.     end
  2349.  
  2350.     env["term"]["scroll"] = function(n)
  2351.         localTerm.scroll(n)
  2352.         drawMenubar()
  2353.     end
  2354.  
  2355.     env["shell"]["getRunningProgram"] = function()
  2356.         return currentWebsiteURL
  2357.     end
  2358. end
  2359.  
  2360. local urlEncode = function(url)
  2361.     local result = url
  2362.  
  2363.     result = result:gsub("%%", "%%a")
  2364.     result = result:gsub(":", "%%c")
  2365.     result = result:gsub("/", "%%s")
  2366.     result = result:gsub("\n", "%%n")
  2367.     result = result:gsub(" ", "%%w")
  2368.     result = result:gsub("&", "%%m")
  2369.     result = result:gsub("%?", "%%q")
  2370.     result = result:gsub("=", "%%e")
  2371.     result = result:gsub("%.", "%%d")
  2372.  
  2373.     return result
  2374. end
  2375.  
  2376. local urlDecode = function(url)
  2377.     local result = url
  2378.  
  2379.     result = result:gsub("%%c", ":")
  2380.     result = result:gsub("%%s", "/")
  2381.     result = result:gsub("%%n", "\n")
  2382.     result = result:gsub("%%w", " ")
  2383.     result = result:gsub("%%&", "&")
  2384.     result = result:gsub("%%q", "%?")
  2385.     result = result:gsub("%%e", "=")
  2386.     result = result:gsub("%%d", "%.")
  2387.     result = result:gsub("%%m", "%%")
  2388.  
  2389.     return result
  2390. end
  2391.  
  2392. local applyAPIFunctions = function(env, connection)
  2393.     env["firewolf"] = {}
  2394.     env["firewolf"]["version"] = version
  2395.     env["firewolf"]["domain"] = currentWebsiteURL:match("^[^/]+")
  2396.  
  2397.     env["firewolf"]["redirect"] = function(url)
  2398.         if type(url) ~= "string" then
  2399.             return error("string (url) expected, got " .. type(url))
  2400.         end
  2401.  
  2402.         os.queueEvent(redirectEvent, url)
  2403.         coroutine.yield()
  2404.     end
  2405.  
  2406.     env["firewolf"]["download"] = function(page)
  2407.         if type(page) ~= "string" then
  2408.             return error("string (page) expected")
  2409.         end
  2410.         local bannedNames = {"ls", "dir", "delete", "copy", "move", "list", "rm", "cp", "mv", "clear", "cd", "lua"}
  2411.  
  2412.         local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+"))
  2413.         if startSearch == 1 then
  2414.             if page:sub(endSearch + 1, endSearch + 1) == "/" then
  2415.                 page = page:sub(endSearch + 2, -1)
  2416.             else
  2417.                 page = page:sub(endSearch + 1, -1)
  2418.             end
  2419.         end
  2420.  
  2421.         local filename = page:match("([^/]+)$")
  2422.         if not filename then
  2423.             return false, "Cannot download index"
  2424.         end
  2425.  
  2426.         for k, v in pairs(bannedNames) do
  2427.             if filename == v then
  2428.                 return false, "Filename prohibited!"
  2429.             end
  2430.         end
  2431.  
  2432.         if not fs.exists(downloadsLocation) then
  2433.             fs.makeDir(downloadsLocation)
  2434.         elseif not fs.isDir(downloadsLocation) then
  2435.             return false, "Downloads disabled!"
  2436.         end
  2437.  
  2438.         contents = connection.fetchPage(normalizePage(page))
  2439.         if type(contents) ~= "string" then
  2440.             return false, "Download error!"
  2441.         else
  2442.             local f = io.open(downloadsLocation .. "/" .. filename, "w")
  2443.             f:write(contents)
  2444.             f:close()
  2445.             return true, downloadsLocation .. "/" .. filename
  2446.         end
  2447.     end
  2448.  
  2449.     env["firewolf"]["encode"] = function(vars)
  2450.         if type(vars) ~= "table" then
  2451.             return error("table (vars) expected, got " .. type(vars))
  2452.         end
  2453.  
  2454.         local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+"))
  2455.         if startSearch == 1 then
  2456.             if page:sub(endSearch + 1, endSearch + 1) == "/" then
  2457.                 page = page:sub(endSearch + 2, -1)
  2458.             else
  2459.                 page = page:sub(endSearch + 1, -1)
  2460.             end
  2461.         end
  2462.  
  2463.         local construct = "?"
  2464.         for k,v in pairs(vars) do
  2465.             construct = construct .. urlEncode(tostring(k)) .. "=" .. urlEncode(tostring(v)) .. "&"
  2466.         end
  2467.         -- Get rid of that last ampersand
  2468.         construct = construct:sub(1, -2)
  2469.  
  2470.         return construct
  2471.     end
  2472.  
  2473.     env["firewolf"]["query"] = function(page, vars)
  2474.         if type(page) ~= "string" then
  2475.             return error("string (page) expected, got " .. type(page))
  2476.         end
  2477.         if vars and type(vars) ~= "table" then
  2478.             return error("table (vars) expected, got " .. type(vars))
  2479.         end
  2480.  
  2481.         local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+"))
  2482.         if startSearch == 1 then
  2483.             if page:sub(endSearch + 1, endSearch + 1) == "/" then
  2484.                 page = page:sub(endSearch + 2, -1)
  2485.             else
  2486.                 page = page:sub(endSearch + 1, -1)
  2487.             end
  2488.         end
  2489.  
  2490.         local construct = page .. "?"
  2491.         if vars then
  2492.             for k,v in pairs(vars) do
  2493.                 construct = construct .. urlEncode(tostring(k)) .. "=" .. urlEncode(tostring(v)) .. "&"
  2494.             end
  2495.         end
  2496.         -- Get rid of that last ampersand
  2497.         construct = construct:sub(1, -2)
  2498.  
  2499.         contents = connection.fetchPage(normalizePage(construct))
  2500.         if type(contents) == "string" then
  2501.             return contents
  2502.         else
  2503.             return false
  2504.         end
  2505.     end
  2506.  
  2507.     env["firewolf"]["loadImage"] = function(page)
  2508.         if type(page) ~= "string" then
  2509.             return error("string (page) expected, got " .. type(page))
  2510.         end
  2511.  
  2512.         local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+"))
  2513.         if startSearch == 1 then
  2514.             if page:sub(endSearch + 1, endSearch + 1) == "/" then
  2515.                 page = page:sub(endSearch + 2, -1)
  2516.             else
  2517.                 page = page:sub(endSearch + 1, -1)
  2518.             end
  2519.         end
  2520.  
  2521.         local filename = page:match("([^/]+)$")
  2522.         if not filename then
  2523.             return false, "Cannot load index as an image!"
  2524.         end
  2525.  
  2526.         contents = connection.fetchPage(normalizePage(page))
  2527.         if type(contents) ~= "string" then
  2528.             return false, "Download error!"
  2529.         else
  2530.             local colorLookup = {}
  2531.             for n = 1, 16 do
  2532.                 colorLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1)
  2533.             end
  2534.  
  2535.             local image = {}
  2536.             for line in contents:gmatch("[^\n]+") do
  2537.                 local lines = {}
  2538.                 for x = 1, line:len() do
  2539.                     lines[x] = colorLookup[string.byte(line, x, x)] or 0
  2540.                 end
  2541.                 table.insert(image, lines)
  2542.             end
  2543.  
  2544.             return image
  2545.         end
  2546.     end
  2547.  
  2548.     env["center"] = center
  2549.     env["fill"] = fill
  2550. end
  2551.  
  2552.  
  2553. local getWebsiteEnvironment = function(antivirus, connection)
  2554.     local env = {}
  2555.  
  2556.     if antivirus then
  2557.         env = getWhitelistedEnvironment()
  2558.         overrideEnvironment(env)
  2559.     else
  2560.         setmetatable(env, {__index = _G})
  2561.     end
  2562.  
  2563.     applyAPIFunctions(env, connection)
  2564.  
  2565.     return env
  2566. end
  2567.  
  2568.  
  2569.  
  2570. --    FWML Execution
  2571.  
  2572.  
  2573. local render = {}
  2574.  
  2575. render["functions"] = {}
  2576. render["functions"]["public"] = {}
  2577. render["alignations"] = {}
  2578.  
  2579. render["variables"] = {
  2580.     scroll,
  2581.     maxScroll,
  2582.     align,
  2583.     linkData = {},
  2584.     blockLength,
  2585.     link,
  2586.     linkStart,
  2587.     markers,
  2588.     currentOffset,
  2589. }
  2590.  
  2591.  
  2592. local function getLine(loc, data)
  2593.     local _, changes = data:sub(1, loc):gsub("\n", "")
  2594.     if not changes then
  2595.         return 1
  2596.     else
  2597.         return changes + 1
  2598.     end
  2599. end
  2600.  
  2601.  
  2602. local function parseData(data)
  2603.     local commands = {}
  2604.     local searchPos = 1
  2605.  
  2606.     while #data > 0 do
  2607.         local sCmd, eCmd = data:find("%[[^%]]+%]", searchPos)
  2608.         if sCmd then
  2609.             sCmd = sCmd + 1
  2610.             eCmd = eCmd - 1
  2611.  
  2612.             if (sCmd > 2) then
  2613.                 if data:sub(sCmd - 2, sCmd - 2) == "\\" then
  2614.                     local t = data:sub(searchPos, sCmd - 1):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]")
  2615.                     if #t > 0 then
  2616.                         if #commands > 0 and type(commands[#commands][1]) == "string" then
  2617.                             commands[#commands][1] = commands[#commands][1] .. t
  2618.                         else
  2619.                             table.insert(commands, {t})
  2620.                         end
  2621.                     end
  2622.                     searchPos = sCmd
  2623.                 else
  2624.                     local t = data:sub(searchPos, sCmd - 2):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]")
  2625.                     if #t > 0 then
  2626.                         if #commands > 0 and type(commands[#commands][1]) == "string" then
  2627.                             commands[#commands][1] = commands[#commands][1] .. t
  2628.                         else
  2629.                             table.insert(commands, {t})
  2630.                         end
  2631.                     end
  2632.  
  2633.                     t = data:sub(sCmd, eCmd):gsub("\n", "")
  2634.                     table.insert(commands, {getLine(sCmd, data), t})
  2635.                     searchPos = eCmd + 2
  2636.                 end
  2637.             else
  2638.                 local t = data:sub(sCmd, eCmd):gsub("\n", "")
  2639.                 table.insert(commands, {getLine(sCmd, data), t})
  2640.                 searchPos = eCmd + 2
  2641.             end
  2642.         else
  2643.             local t = data:sub(searchPos, -1):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]")
  2644.             if #t > 0 then
  2645.                 if #commands > 0 and type(commands[#commands][1]) == "string" then
  2646.                     commands[#commands][1] = commands[#commands][1] .. t
  2647.                 else
  2648.                     table.insert(commands, {t})
  2649.                 end
  2650.             end
  2651.  
  2652.             break
  2653.         end
  2654.     end
  2655.  
  2656.     return commands
  2657. end
  2658.  
  2659.  
  2660. local function proccessData(commands)
  2661.     searchIndex = 0
  2662.  
  2663.     while searchIndex < #commands do
  2664.         searchIndex = searchIndex + 1
  2665.  
  2666.         local length = 0
  2667.         local origin = searchIndex
  2668.  
  2669.         if type(commands[searchIndex][1]) == "string" then
  2670.             length = length + #commands[searchIndex][1]
  2671.             local endIndex = origin
  2672.             for i = origin + 1, #commands do
  2673.                 if commands[i][2] then
  2674.                     local command = commands[i][2]:match("^(%w+)%s-")
  2675.                     if not (command == "c" or command == "color" or command == "bg"
  2676.                             or command == "background" or command == "newlink" or command == "endlink") then
  2677.                         endIndex = i
  2678.                         break
  2679.                     end
  2680.                 elseif commands[i][2] then
  2681.  
  2682.                 else
  2683.                     length = length + #commands[i][1]
  2684.                 end
  2685.                 if i == #commands then
  2686.                     endIndex = i
  2687.                 end
  2688.             end
  2689.  
  2690.             commands[origin][2] = length
  2691.             searchIndex = endIndex
  2692.             length = 0
  2693.         end
  2694.     end
  2695.  
  2696.     return commands
  2697. end
  2698.  
  2699.  
  2700. local function parse(original)
  2701.     return proccessData(parseData(original))
  2702. end
  2703.  
  2704.  
  2705. render["functions"]["display"] = function(text, length, offset, center)
  2706.     if not offset then
  2707.         offset = 0
  2708.     end
  2709.  
  2710.     return render.variables.align(text, length, w, offset, center);
  2711. end
  2712.  
  2713.  
  2714. render["functions"]["displayText"] = function(source)
  2715.     if source[2] then
  2716.         render.variables.blockLength = source[2]
  2717.         if render.variables.link and not render.variables.linkStart then
  2718.             render.variables.linkStart = render.functions.display(
  2719.                 source[1], render.variables.blockLength, render.variables.currentOffset, w / 2)
  2720.         else
  2721.             render.functions.display(source[1], render.variables.blockLength, render.variables.currentOffset, w / 2)
  2722.         end
  2723.     else
  2724.         if render.variables.link and not render.variables.linkStart then
  2725.             render.variables.linkStart = render.functions.display(source[1], nil, render.variables.currentOffset, w / 2)
  2726.         else
  2727.             render.functions.display(source[1], nil, render.variables.currentOffset, w / 2)
  2728.         end
  2729.     end
  2730. end
  2731.  
  2732.  
  2733. render["functions"]["public"]["br"] = function(source)
  2734.     if render.variables.link then
  2735.         return "Cannot insert new line within a link on line " .. source[1]
  2736.     end
  2737.  
  2738.     render.variables.scroll = render.variables.scroll + 1
  2739.     render.variables.maxScroll = math.max(render.variables.scroll, render.variables.maxScroll)
  2740. end
  2741.  
  2742.  
  2743. render["functions"]["public"]["c "] = function(source)
  2744.     local sColor = source[2]:match("^%w+%s+(.+)$") or ""
  2745.     if colors[sColor] then
  2746.         term.setTextColor(colors[sColor])
  2747.     else
  2748.         return "Invalid color: \"" .. sColor .. "\" on line " .. source[1]
  2749.     end
  2750. end
  2751.  
  2752.  
  2753. render["functions"]["public"]["color "] = render["functions"]["public"]["c "]
  2754.  
  2755.  
  2756. render["functions"]["public"]["bg "] = function(source)
  2757.     local sColor = source[2]:match("^%w+%s+(.+)$") or ""
  2758.     if colors[sColor] then
  2759.         term.setBackgroundColor(colors[sColor])
  2760.     else
  2761.         return "Invalid color: \"" .. sColor .. "\" on line " .. source[1]
  2762.     end
  2763. end
  2764.  
  2765.  
  2766. render["functions"]["public"]["background "] = render["functions"]["public"]["bg "]
  2767.  
  2768.  
  2769. render["functions"]["public"]["newlink "] = function(source)
  2770.     if render.variables.link then
  2771.         return "Cannot nest links on line " .. source[1]
  2772.     end
  2773.  
  2774.     render.variables.link = source[2]:match("^%w+%s+(.+)$") or ""
  2775.     render.variables.linkStart = false
  2776. end
  2777.  
  2778.  
  2779. render["functions"]["public"]["endlink"] = function(source)
  2780.     if not render.variables.link then
  2781.         return "Cannot end a link without a link on line " .. source[1]
  2782.     end
  2783.  
  2784.     local linkEnd = term.getCursorPos()-1
  2785.     table.insert(render.variables.linkData, {render.variables.linkStart,
  2786.         linkEnd, render.variables.scroll, render.variables.link})
  2787.     render.variables.link = false
  2788.     render.variables.linkStart = false
  2789. end
  2790.  
  2791.  
  2792. render["functions"]["public"]["offset "] = function(source)
  2793.     local offset = tonumber((source[2]:match("^%w+%s+(.+)$") or ""))
  2794.     if offset then
  2795.         render.variables.currentOffset = offset
  2796.     else
  2797.         return "Invalid offset value: \"" .. (source[2]:match("^%w+%s+(.+)$") or "") .. "\" on line " .. source[1]
  2798.     end
  2799. end
  2800.  
  2801.  
  2802. render["functions"]["public"]["marker "] = function(source)
  2803.     render.variables.markers[(source[2]:match("^%w+%s+(.+)$") or "")] = render.variables.scroll
  2804. end
  2805.  
  2806.  
  2807. render["functions"]["public"]["goto "] = function(source)
  2808.     local location = source[2]:match("%w+%s+(.+)$")
  2809.     if render.variables.markers[location] then
  2810.         render.variables.scroll = render.variables.markers[location]
  2811.     else
  2812.         return "No such location: \"" .. (source[2]:match("%w+%s+(.+)$") or "") .. "\" on line " .. source[1]
  2813.     end
  2814. end
  2815.  
  2816.  
  2817. render["functions"]["public"]["box "] = function(source)
  2818.     local sColor, align, height, width, offset, url = source[2]:match("^box (%a+) (%a+) (%-?%d+) (%-?%d+) (%-?%d+) ?([^ ]*)")
  2819.     if not sColor then
  2820.         return "Invalid box syntax on line " .. source[1]
  2821.     end
  2822.  
  2823.     local x, y = term.getCursorPos()
  2824.     local startX
  2825.  
  2826.     if align == "center" or align == "centre" then
  2827.         startX = math.ceil((w / 2) - width / 2) + offset
  2828.     elseif align == "left" then
  2829.         startX = 1 + offset
  2830.     elseif align == "right" then
  2831.         startX = (w - width + 1) + offset
  2832.     else
  2833.         return "Invalid align option for box on line " .. source[1]
  2834.     end
  2835.  
  2836.     if not colors[sColor] then
  2837.         return "Invalid color: \"" .. sColor .. "\" for box on line " .. source[1]
  2838.     end
  2839.  
  2840.     term.setBackgroundColor(colors[sColor])
  2841.     for i = 0, height - 1 do
  2842.         term.setCursorPos(startX, render.variables.scroll + i)
  2843.         term.write(string.rep(" ", width))
  2844.         if url:len() > 3 then
  2845.             table.insert(render.variables.linkData, {startX, startX + width - 1, render.variables.scroll + i, url})
  2846.         end
  2847.     end
  2848.  
  2849.     render.variables.maxScroll = math.max(render.variables.scroll + height - 1, render.variables.maxScroll)
  2850.     term.setCursorPos(x, y)
  2851. end
  2852.  
  2853.  
  2854. render["alignations"]["left"] = function(text, length, _, offset)
  2855.     local x, y = term.getCursorPos()
  2856.     if length then
  2857.         term.setCursorPos(1 + offset, render.variables.scroll)
  2858.         term.write(text)
  2859.         return 1 + offset
  2860.     else
  2861.         term.setCursorPos(x, render.variables.scroll)
  2862.         term.write(text)
  2863.         return x
  2864.     end
  2865. end
  2866.  
  2867.  
  2868. render["alignations"]["right"] = function(text, length, width, offset)
  2869.     local x, y = term.getCursorPos()
  2870.     if length then
  2871.         term.setCursorPos((width - length + 1) + offset, render.variables.scroll)
  2872.         term.write(text)
  2873.         return (width - length + 1) + offset
  2874.     else
  2875.         term.setCursorPos(x, render.variables.scroll)
  2876.         term.write(text)
  2877.         return x
  2878.     end
  2879. end
  2880.  
  2881.  
  2882. render["alignations"]["center"] = function(text, length, _, offset, center)
  2883.     local x, y = term.getCursorPos()
  2884.     if length then
  2885.         term.setCursorPos(math.ceil(center - length / 2) + offset, render.variables.scroll)
  2886.         term.write(text)
  2887.         return math.ceil(center - length / 2) + offset
  2888.     else
  2889.         term.setCursorPos(x, render.variables.scroll)
  2890.         term.write(text)
  2891.         return x
  2892.     end
  2893. end
  2894.  
  2895.  
  2896. render["render"] = function(data, startScroll)
  2897.     if startScroll == nil then
  2898.         render.variables.startScroll = 0
  2899.     else
  2900.         render.variables.startScroll = startScroll
  2901.     end
  2902.  
  2903.     render.variables.scroll = startScroll + 1
  2904.     render.variables.maxScroll = render.variables.scroll
  2905.  
  2906.     render.variables.linkData = {}
  2907.  
  2908.     render.variables.align = render.alignations.left
  2909.  
  2910.     render.variables.blockLength = 0
  2911.     render.variables.link = false
  2912.     render.variables.linkStart = false
  2913.     render.variables.markers = {}
  2914.     render.variables.currentOffset = 0
  2915.  
  2916.     for k, v in pairs(data) do
  2917.         if type(v[2]) ~= "string" then
  2918.             render.functions.displayText(v)
  2919.         elseif v[2] == "<" or v[2] == "left" then
  2920.             render.variables.align = render.alignations.left
  2921.         elseif v[2] == ">" or v[2] == "right" then
  2922.             render.variables.align = render.alignations.right
  2923.         elseif v[2] == "=" or v[2] == "center" then
  2924.             render.variables.align = render.alignations.center
  2925.         else
  2926.             local existentFunction = false
  2927.  
  2928.             for name, func in pairs(render.functions.public) do
  2929.                 if v[2]:find(name) == 1 then
  2930.                     existentFunction = true
  2931.                     local ret = func(v)
  2932.                     if ret then
  2933.                         return ret
  2934.                     end
  2935.                 end
  2936.             end
  2937.  
  2938.             if not existentFunction then
  2939.                 return "Non-existent tag: \"" .. v[2] .. "\" on line " .. v[1]
  2940.             end
  2941.         end
  2942.     end
  2943.  
  2944.     return render.variables.linkData, render.variables.maxScroll - render.variables.startScroll
  2945. end
  2946.  
  2947.  
  2948.  
  2949. --    Lua Execution
  2950.  
  2951.  
  2952. languages["lua"] = {}
  2953. languages["fwml"] = {}
  2954.  
  2955.  
  2956. languages["lua"]["runWithErrorCatching"] = function(func, ...)
  2957.     local _, err = pcall(func, ...)
  2958.     if err then
  2959.         os.queueEvent(websiteErrorEvent, err)
  2960.     end
  2961. end
  2962.  
  2963.  
  2964. languages["lua"]["runWithoutAntivirus"] = function(func, ...)
  2965.     local args = {...}
  2966.     local env = getWebsiteEnvironment(false)
  2967.     setfenv(func, env)
  2968.     return function()
  2969.         languages["lua"]["runWithErrorCatching"](func, unpack(args))
  2970.     end
  2971. end
  2972.  
  2973.  
  2974. languages["lua"]["run"] = function(contents, page, connection, ...)
  2975.     local func, err = loadstring("sleep(0) " .. contents, page)
  2976.     if err then
  2977.         return languages["lua"]["runWithoutAntivirus"](builtInSites["crash"], err)
  2978.     else
  2979.         local args = {...}
  2980.         local env = getWebsiteEnvironment(true, connection)
  2981.         setfenv(func, env)
  2982.         return function()
  2983.             languages["lua"]["runWithErrorCatching"](func, unpack(args))
  2984.         end
  2985.     end
  2986. end
  2987.  
  2988.  
  2989. languages["fwml"]["run"] = function(contents, page, connection, ...)
  2990.     local err, data = pcall(parse, contents)
  2991.     if not err then
  2992.         return languages["lua"]["runWithoutAntivirus"](builtInSites["crash"], data)
  2993.     end
  2994.  
  2995.     return function()
  2996.         local currentScroll = 0
  2997.         local err, links, pageHeight = pcall(render.render, data, currentScroll)
  2998.         if type(links) == "string" or not err then
  2999.             term.clear()
  3000.             os.queueEvent(websiteErrorEvent, links)
  3001.         else
  3002.             while true do
  3003.                 local e, scroll, x, y = os.pullEvent()
  3004.                 if e == "mouse_click" then
  3005.                     for k, v in pairs(links) do
  3006.                         if x >= math.min(v[1], v[2]) and x <= math.max(v[1], v[2]) and y == v[3] then
  3007.                             os.queueEvent(redirectEvent, v[4])
  3008.                             coroutine.yield()
  3009.                         end
  3010.                     end
  3011.                 elseif e == "mouse_scroll" then
  3012.                     if currentScroll - scroll - h >= -pageHeight and currentScroll - scroll <= 0 then
  3013.                         currentScroll = currentScroll - scroll
  3014.                         clear(theme.background, theme.text)
  3015.                         links = render.render(data, currentScroll)
  3016.                     end
  3017.                 elseif e == "key" and scroll == keys.up or scroll == keys.down then
  3018.                     local scrollAmount
  3019.  
  3020.                     if scroll == keys.up then
  3021.                         scrollAmount = 1
  3022.                     elseif scroll == keys.down then
  3023.                         scrollAmount = -1
  3024.                     end
  3025.  
  3026.                     local scrollLessHeight = currentScroll + scrollAmount - h >= -pageHeight
  3027.                     local scrollZero = currentScroll + scrollAmount <= 0
  3028.                     if scrollLessHeight and scrollZero then
  3029.                         currentScroll = currentScroll + scrollAmount
  3030.                         clear(theme.background, theme.text)
  3031.                         links = render.render(data, currentScroll)
  3032.                     end
  3033.                 end
  3034.             end
  3035.         end
  3036.     end
  3037. end
  3038.  
  3039.  
  3040.  
  3041. --    Query Bar
  3042.  
  3043.  
  3044. local readNewWebsiteURL = function()
  3045.     local onEvent = function(text, event, key, x, y)
  3046.         if event == "mouse_click" then
  3047.             if y == 2 then
  3048.                 local index = determineClickedTab(x, y)
  3049.                 if index == "new" and #tabs < maxTabs then
  3050.                     loadTab(#tabs + 1, "firewolf")
  3051.                 elseif index == "close" then
  3052.                     closeCurrentTab()
  3053.                 elseif index then
  3054.                     switchTab(index)
  3055.                 end
  3056.  
  3057.                 return {["nullifyText"] = true, ["exit"] = true}
  3058.             elseif y > 2 then
  3059.                 return {["nullifyText"] = true, ["exit"] = true}
  3060.             end
  3061.         elseif event == "key" then
  3062.             if key == 29 or key == 157 then
  3063.                 return {["nullifyText"] = true, ["exit"] = true}
  3064.             end
  3065.         end
  3066.     end
  3067.  
  3068.     isMenubarOpen = true
  3069.     drawMenubar()
  3070.     term.setCursorPos(2, 1)
  3071.     term.setTextColor(theme.text)
  3072.     term.setBackgroundColor(theme.accent)
  3073.     term.clearLine()
  3074.     term.write(currentProtocol .. "://")
  3075.  
  3076.     local website = modifiedRead({
  3077.         ["onEvent"] = onEvent,
  3078.         ["displayLength"] = w - 9,
  3079.         ["history"] = history,
  3080.     })
  3081.  
  3082.     if not website then
  3083.         if not tabs[currentTab].isMenubarPermanent then
  3084.             isMenubarOpen = false
  3085.             menubarWindow.setVisible(false)
  3086.         else
  3087.             isMenubarOpen = true
  3088.             menubarWindow.setVisible(true)
  3089.         end
  3090.  
  3091.         term.redirect(tabs[currentTab].win)
  3092.         tabs[currentTab].win.setVisible(true)
  3093.         tabs[currentTab].win.redraw()
  3094.  
  3095.         return
  3096.     elseif website == "exit" then
  3097.         error()
  3098.     end
  3099.  
  3100.     loadTab(currentTab, website)
  3101. end
  3102.  
  3103.  
  3104.  
  3105. --    Event Management
  3106.  
  3107.  
  3108. local handleKeyDown = function(event)
  3109.     if event[2] == 29 or event[2] == 157 then
  3110.         readNewWebsiteURL()
  3111.         return true
  3112.     end
  3113.  
  3114.     return false
  3115. end
  3116.  
  3117.  
  3118. local handleMouseDown = function(event)
  3119.     if isMenubarOpen then
  3120.         if event[4] == 1 then
  3121.             readNewWebsiteURL()
  3122.             return true
  3123.         elseif event[4] == 2 then
  3124.             local index = determineClickedTab(event[3], event[4])
  3125.             if index == "new" and #tabs < maxTabs then
  3126.                 loadTab(#tabs + 1, "firewolf")
  3127.             elseif index == "close" then
  3128.                 closeCurrentTab()
  3129.             elseif index then
  3130.                 switchTab(index)
  3131.             end
  3132.  
  3133.             return true
  3134.         end
  3135.     end
  3136.  
  3137.     return false
  3138. end
  3139.  
  3140.  
  3141. local handleEvents = function()
  3142.     loadTab(1, "firewolf")
  3143.     currentTab = 1
  3144.  
  3145.     while true do
  3146.         drawMenubar()
  3147.         local event = {os.pullEventRaw()}
  3148.         drawMenubar()
  3149.  
  3150.         local cancelEvent = false
  3151.         if event[1] == "terminate" then
  3152.             break
  3153.         elseif event[1] == "key" then
  3154.             cancelEvent = handleKeyDown(event)
  3155.         elseif event[1] == "mouse_click" then
  3156.             cancelEvent = handleMouseDown(event)
  3157.         elseif event[1] == websiteErrorEvent then
  3158.             cancelEvent = true
  3159.  
  3160.             loadTab(currentTab, tabs[currentTab].url, function()
  3161.                 builtInSites["crash"](event[2])
  3162.             end)
  3163.         elseif event[1] == redirectEvent then
  3164.             cancelEvent = true
  3165.  
  3166.             if (event[2]:match("^rdnt://(.+)$")) then
  3167.                 event[2] = event[2]:match("^rdnt://(.+)$")
  3168.             end
  3169.  
  3170.             loadTab(currentTab, event[2])
  3171.         end
  3172.  
  3173.         if not cancelEvent then
  3174.             term.redirect(tabs[currentTab].win)
  3175.             term.setCursorPos(tabs[currentTab].ox, tabs[currentTab].oy)
  3176.  
  3177.             coroutine.resume(tabs[currentTab].thread, unpack(event))
  3178.  
  3179.             local ox, oy = term.getCursorPos()
  3180.             tabs[currentTab].ox = ox
  3181.             tabs[currentTab].oy = oy
  3182.         end
  3183.     end
  3184. end
  3185.  
  3186.  
  3187.  
  3188. --    Main
  3189.  
  3190.  
  3191. local main = function()
  3192.     currentProtocol = "rdnt"
  3193.     currentTab = 1
  3194.  
  3195.     if term.isColor() then
  3196.         theme = colorTheme
  3197.         enableTabBar = true
  3198.     else
  3199.         theme = grayscaleTheme
  3200.         enableTabBar = false
  3201.     end
  3202.  
  3203.     setupMenubar()
  3204.     protocols[currentProtocol]["setup"]()
  3205.  
  3206.     clear(theme.background, theme.text)
  3207.     handleEvents()
  3208. end
  3209.  
  3210.  
  3211. local handleError = function(err)
  3212.     clear(theme.background, theme.text)
  3213.  
  3214.     fill(1, 3, w, 3, theme.subtle)
  3215.     term.setCursorPos(1, 4)
  3216.     center("Firewolf has crashed!")
  3217.  
  3218.     term.setBackgroundColor(theme.background)
  3219.     term.setCursorPos(1, 8)
  3220.     centerSplit(err, w - 4)
  3221.     print("\n")
  3222.     center("Please report this error to")
  3223.     center("GravityScore or 1lann.")
  3224.     print("")
  3225.     center("Press any key to exit.")
  3226.  
  3227.     os.pullEvent("key")
  3228.     os.queueEvent("")
  3229.     os.pullEvent()
  3230. end
  3231.  
  3232. local _, err = pcall(main)
  3233. term.redirect(originalTerminal)
  3234.  
  3235. Modem.closeAll()
  3236.  
  3237. if err and not err:lower():find("terminate") then
  3238.     handleError(err)
  3239. end
  3240.  
  3241.  
  3242. clear(colors.black, colors.white)
  3243. center("Thanks for using Firewolf " .. version)
  3244. center("Made by GravityScore and 1lann")
  3245. print("")
Add Comment
Please, Sign In to add comment