123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737 |
- --
- -- lovebird
- --
- -- Copyright (c) 2017 rxi
- --
- -- This library is free software; you can redistribute it and/or modify it
- -- under the terms of the MIT license. See LICENSE for details.
- --
-
- local socket = require "socket"
-
- local lovebird = { _version = "0.4.3" }
-
- lovebird.loadstring = loadstring or load
- lovebird.inited = false
- lovebird.host = "*"
- lovebird.buffer = ""
- lovebird.lines = {}
- lovebird.connections = {}
- lovebird.pages = {}
-
- lovebird.wrapprint = true
- lovebird.timestamp = true
- lovebird.allowhtml = false
- lovebird.echoinput = true
- lovebird.port = 8000
- lovebird.whitelist = { "127.0.0.1" }
- lovebird.maxlines = 200
- lovebird.updateinterval = .5
-
-
- lovebird.pages["index"] = [[
- <?lua
- -- Handle console input
- if req.parsedbody.input then
- local str = req.parsedbody.input
- if lovebird.echoinput then
- lovebird.pushline({ type = 'input', str = str })
- end
- if str:find("^=") then
- str = "print(" .. str:sub(2) .. ")"
- end
- xpcall(function() assert(lovebird.loadstring(str, "input"))() end,
- lovebird.onerror)
- end
- ?>
-
- <!doctype html>
- <html>
- <head>
- <meta http-equiv="x-ua-compatible" content="IE=Edge"/>
- <meta charset="utf-8">
- <title>lovebird</title>
- <style>
- body {
- margin: 0px;
- font-size: 14px;
- font-family: helvetica, verdana, sans;
- background: #FFFFFF;
- }
- form {
- margin-bottom: 0px;
- }
- .timestamp {
- color: #909090;
- padding-right: 4px;
- }
- .repeatcount {
- color: #F0F0F0;
- background: #505050;
- font-size: 11px;
- font-weight: bold;
- text-align: center;
- padding-left: 4px;
- padding-right: 4px;
- padding-top: 0px;
- padding-bottom: 0px;
- border-radius: 7px;
- display: inline-block;
- }
- .errormarker {
- color: #F0F0F0;
- background: #8E0000;
- font-size: 11px;
- font-weight: bold;
- text-align: center;
- border-radius: 8px;
- width: 17px;
- padding-top: 0px;
- padding-bottom: 0px;
- display: inline-block;
- }
- .greybordered {
- margin: 12px;
- background: #F0F0F0;
- border: 1px solid #E0E0E0;
- border-radius: 3px;
- }
- .inputline {
- font-family: mono, courier;
- font-size: 13px;
- color: #606060;
- }
- .inputline:before {
- content: '\00B7\00B7\00B7';
- padding-right: 5px;
- }
- .errorline {
- color: #8E0000;
- }
- #header {
- background: #101010;
- height: 25px;
- color: #F0F0F0;
- padding: 9px
- }
- #title {
- float: left;
- font-size: 20px;
- }
- #title a {
- color: #F0F0F0;
- text-decoration: none;
- }
- #title a:hover {
- color: #FFFFFF;
- }
- #version {
- font-size: 10px;
- }
- #status {
- float: right;
- font-size: 14px;
- padding-top: 4px;
- }
- #main a {
- color: #000000;
- text-decoration: none;
- background: #E0E0E0;
- border: 1px solid #D0D0D0;
- border-radius: 3px;
- padding-left: 2px;
- padding-right: 2px;
- display: inline-block;
- }
- #main a:hover {
- background: #D0D0D0;
- border: 1px solid #C0C0C0;
- }
- #console {
- position: absolute;
- top: 40px; bottom: 0px; left: 0px; right: 312px;
- }
- #input {
- position: absolute;
- margin: 10px;
- bottom: 0px; left: 0px; right: 0px;
- }
- #inputbox {
- width: 100%;
- font-family: mono, courier;
- font-size: 13px;
- }
- #output {
- overflow-y: scroll;
- position: absolute;
- margin: 10px;
- line-height: 17px;
- top: 0px; bottom: 36px; left: 0px; right: 0px;
- }
- #env {
- position: absolute;
- top: 40px; bottom: 0px; right: 0px;
- width: 300px;
- }
- #envheader {
- padding: 5px;
- background: #E0E0E0;
- }
- #envvars {
- position: absolute;
- left: 0px; right: 0px; top: 25px; bottom: 0px;
- margin: 10px;
- overflow-y: scroll;
- font-size: 12px;
- }
- </style>
- </head>
- <body>
- <div id="header">
- <div id="title">
- <a href="https://github.com/rxi/lovebird">lovebird</a>
- <span id="version"><?lua echo(lovebird._version) ?></span>
- </div>
- <div id="status"></div>
- </div>
- <div id="main">
- <div id="console" class="greybordered">
- <div id="output"> <?lua echo(lovebird.buffer) ?> </div>
- <div id="input">
- <form method="post"
- onkeydown="return onInputKeyDown(event);"
- onsubmit="onInputSubmit(); return false;">
- <input id="inputbox" name="input" type="text"
- autocomplete="off"></input>
- </form>
- </div>
- </div>
- <div id="env" class="greybordered">
- <div id="envheader"></div>
- <div id="envvars"></div>
- </div>
- </div>
- <script>
- document.getElementById("inputbox").focus();
-
- var changeFavicon = function(href) {
- var old = document.getElementById("favicon");
- if (old) document.head.removeChild(old);
- var link = document.createElement("link");
- link.id = "favicon";
- link.rel = "shortcut icon";
- link.href = href;
- document.head.appendChild(link);
- }
-
- var truncate = function(str, len) {
- if (str.length <= len) return str;
- return str.substring(0, len - 3) + "...";
- }
-
- var geturl = function(url, onComplete, onFail) {
- var req = new XMLHttpRequest();
- req.onreadystatechange = function() {
- if (req.readyState != 4) return;
- if (req.status == 200) {
- if (onComplete) onComplete(req.responseText);
- } else {
- if (onFail) onFail(req.responseText);
- }
- }
- url += (url.indexOf("?") > -1 ? "&_=" : "?_=") + Math.random();
- req.open("GET", url, true);
- req.send();
- }
-
- var divContentCache = {}
- var updateDivContent = function(id, content) {
- if (divContentCache[id] != content) {
- document.getElementById(id).innerHTML = content;
- divContentCache[id] = content
- return true;
- }
- return false;
- }
-
- var onInputSubmit = function() {
- var b = document.getElementById("inputbox");
- var req = new XMLHttpRequest();
- req.open("POST", "/", true);
- req.send("input=" + encodeURIComponent(b.value));
- /* Do input history */
- if (b.value && inputHistory[0] != b.value) {
- inputHistory.unshift(b.value);
- }
- inputHistory.index = -1;
- /* Reset */
- b.value = "";
- refreshOutput();
- }
-
- /* Input box history */
- var inputHistory = [];
- inputHistory.index = 0;
- var onInputKeyDown = function(e) {
- var key = e.which || e.keyCode;
- if (key != 38 && key != 40) return true;
- var b = document.getElementById("inputbox");
- if (key == 38 && inputHistory.index < inputHistory.length - 1) {
- /* Up key */
- inputHistory.index++;
- }
- if (key == 40 && inputHistory.index >= 0) {
- /* Down key */
- inputHistory.index--;
- }
- b.value = inputHistory[inputHistory.index] || "";
- b.selectionStart = b.value.length;
- return false;
- }
-
- /* Output buffer and status */
- var refreshOutput = function() {
- geturl("/buffer", function(text) {
- updateDivContent("status", "connected ●");
- if (updateDivContent("output", text)) {
- var div = document.getElementById("output");
- div.scrollTop = div.scrollHeight;
- }
- /* Update favicon */
- changeFavicon("data:image/png;base64," +
- "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAP1BMVEUAAAAAAAAAAAD////19fUO"+
- "Dg7v7+/h4eGzs7MlJSUeHh7n5+fY2NjJycnGxsa3t7eioqKfn5+QkJCHh4d+fn7zU+b5AAAAAnRS"+
- "TlPlAFWaypEAAABRSURBVBjTfc9HDoAwDERRQ+w0ern/WQkZaUBC4e/mrWzppH9VJjbjZg1Ii2rM"+
- "DyR1JZ8J0dVWggIGggcEwgbYCRbuPRqgyjHNpzUP+39GPu9fgloC5L9DO0sAAAAASUVORK5CYII="
- );
- },
- function(text) {
- updateDivContent("status", "disconnected ○");
- /* Update favicon */
- changeFavicon("data:image/png;base64," +
- "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAAAAAAAADZ2dm4uLgM"+
- "DAz29vbz8/Pv7+/h4eHIyMiwsLBtbW0lJSUeHh4QEBDn5+fS0tLDw8O0tLSioqKfn5+QkJCHh4d+"+
- "fn5ycnJmZmZgYGBXV1dLS0tFRUUGBgZ0He44AAAAAnRSTlPlAFWaypEAAABeSURBVBjTfY9HDoAw"+
- "DAQD6Z3ey/9/iXMxkVDYw0g7F3tJReosUKHnwY4pCM+EtOEVXrb7wVRA0dMbaAcUwiVeDQq1Jp4a"+
- "xUg5kE0ooqZu68Di2Tgbs/DiY/9jyGf+AyFKBAK7KD2TAAAAAElFTkSuQmCC"
- );
- });
- }
- setInterval(refreshOutput,
- <?lua echo(lovebird.updateinterval) ?> * 1000);
-
- /* Environment variable view */
- var envPath = "";
- var refreshEnv = function() {
- geturl("/env.json?p=" + envPath, function(text) {
- var json = eval("(" + text + ")");
-
- /* Header */
- var html = "<a href='#' onclick=\"setEnvPath('')\">env</a>";
- var acc = "";
- var p = json.path != "" ? json.path.split(".") : [];
- for (var i = 0; i < p.length; i++) {
- acc += "." + p[i];
- html += " <a href='#' onclick=\"setEnvPath('" + acc + "')\">" +
- truncate(p[i], 10) + "</a>";
- }
- updateDivContent("envheader", html);
-
- /* Handle invalid table path */
- if (!json.valid) {
- updateDivContent("envvars", "Bad path");
- return;
- }
-
- /* Variables */
- var html = "<table>";
- for (var i = 0; json.vars[i]; i++) {
- var x = json.vars[i];
- var fullpath = (json.path + "." + x.key).replace(/^\./, "");
- var k = truncate(x.key, 15);
- if (x.type == "table") {
- k = "<a href='#' onclick=\"setEnvPath('" + fullpath + "')\">" +
- k + "</a>";
- }
- var v = "<a href='#' onclick=\"insertVar('" +
- fullpath.replace(/\.(-?[0-9]+)/g, "[$1]") +
- "');\">" + x.value + "</a>"
- html += "<tr><td>" + k + "</td><td>" + v + "</td></tr>";
- }
- html += "</table>";
- updateDivContent("envvars", html);
- });
- }
- var setEnvPath = function(p) {
- envPath = p;
- refreshEnv();
- }
- var insertVar = function(p) {
- var b = document.getElementById("inputbox");
- b.value += p;
- b.focus();
- }
- setInterval(refreshEnv, <?lua echo(lovebird.updateinterval) ?> * 1000);
- </script>
- </body>
- </html>
- ]]
-
-
- lovebird.pages["buffer"] = [[ <?lua echo(lovebird.buffer) ?> ]]
-
-
- lovebird.pages["env.json"] = [[
- <?lua
- local t = _G
- local p = req.parsedurl.query.p or ""
- p = p:gsub("%.+", "."):match("^[%.]*(.*)[%.]*$")
- if p ~= "" then
- for x in p:gmatch("[^%.]+") do
- t = t[x] or t[tonumber(x)]
- -- Return early if path does not exist
- if type(t) ~= "table" then
- echo('{ "valid": false, "path": ' .. string.format("%q", p) .. ' }')
- return
- end
- end
- end
- ?>
- {
- "valid": true,
- "path": "<?lua echo(p) ?>",
- "vars": [
- <?lua
- local keys = {}
- for k in pairs(t) do
- if type(k) == "number" or type(k) == "string" then
- table.insert(keys, k)
- end
- end
- table.sort(keys, lovebird.compare)
- for _, k in pairs(keys) do
- local v = t[k]
- ?>
- {
- "key": "<?lua echo(k) ?>",
- "value": <?lua echo(
- string.format("%q",
- lovebird.truncate(
- lovebird.htmlescape(
- tostring(v)), 26))) ?>,
- "type": "<?lua echo(type(v)) ?>",
- },
- <?lua end ?>
- ]
- }
- ]]
-
-
-
- function lovebird.init()
- -- Init server
- lovebird.server = assert(socket.bind(lovebird.host, lovebird.port))
- lovebird.addr, lovebird.port = lovebird.server:getsockname()
- lovebird.server:settimeout(0)
- -- Wrap print
- lovebird.origprint = print
- if lovebird.wrapprint then
- local oldprint = print
- print = function(...)
- oldprint(...)
- lovebird.print(...)
- end
- end
- -- Compile page templates
- for k, page in pairs(lovebird.pages) do
- lovebird.pages[k] = lovebird.template(page, "lovebird, req",
- "pages." .. k)
- end
- lovebird.inited = true
- end
-
-
- function lovebird.template(str, params, chunkname)
- params = params and ("," .. params) or ""
- local f = function(x) return string.format(" echo(%q)", x) end
- str = ("?>"..str.."<?lua"):gsub("%?>(.-)<%?lua", f)
- str = "local echo " .. params .. " = ..." .. str
- local fn = assert(lovebird.loadstring(str, chunkname))
- return function(...)
- local output = {}
- local echo = function(str) table.insert(output, str) end
- fn(echo, ...)
- return table.concat(lovebird.map(output, tostring))
- end
- end
-
-
- function lovebird.map(t, fn)
- local res = {}
- for k, v in pairs(t) do res[k] = fn(v) end
- return res
- end
-
-
- function lovebird.trace(...)
- local str = "[lovebird] " .. table.concat(lovebird.map({...}, tostring), " ")
- print(str)
- if not lovebird.wrapprint then lovebird.print(str) end
- end
-
-
- function lovebird.unescape(str)
- local f = function(x) return string.char(tonumber("0x"..x)) end
- return (str:gsub("%+", " "):gsub("%%(..)", f))
- end
-
-
- function lovebird.parseurl(url)
- local res = {}
- res.path, res.search = url:match("/([^%?]*)%??(.*)")
- res.query = {}
- for k, v in res.search:gmatch("([^&^?]-)=([^&^#]*)") do
- res.query[k] = lovebird.unescape(v)
- end
- return res
- end
-
-
- local htmlescapemap = {
- ["<"] = "<",
- ["&"] = "&",
- ['"'] = """,
- ["'"] = "'",
- }
-
- function lovebird.htmlescape(str)
- return ( str:gsub("[<&\"']", htmlescapemap) )
- end
-
-
- function lovebird.truncate(str, len)
- if #str <= len then
- return str
- end
- return str:sub(1, len - 3) .. "..."
- end
-
-
- function lovebird.compare(a, b)
- local na, nb = tonumber(a), tonumber(b)
- if na then
- if nb then return na < nb end
- return false
- elseif nb then
- return true
- end
- return tostring(a) < tostring(b)
- end
-
-
- function lovebird.checkwhitelist(addr)
- if lovebird.whitelist == nil then return true end
- for _, a in pairs(lovebird.whitelist) do
- local ptn = "^" .. a:gsub("%.", "%%."):gsub("%*", "%%d*") .. "$"
- if addr:match(ptn) then return true end
- end
- return false
- end
-
-
- function lovebird.clear()
- lovebird.lines = {}
- lovebird.buffer = ""
- end
-
-
- function lovebird.pushline(line)
- line.time = os.time()
- line.count = 1
- table.insert(lovebird.lines, line)
- if #lovebird.lines > lovebird.maxlines then
- table.remove(lovebird.lines, 1)
- end
- lovebird.recalcbuffer()
- end
-
-
- function lovebird.recalcbuffer()
- local function doline(line)
- local str = line.str
- if not lovebird.allowhtml then
- str = lovebird.htmlescape(line.str):gsub("\n", "<br>")
- end
- if line.type == "input" then
- str = '<span class="inputline">' .. str .. '</span>'
- else
- if line.type == "error" then
- str = '<span class="errormarker">!</span> ' .. str
- str = '<span class="errorline">' .. str .. '</span>'
- end
- if line.count > 1 then
- str = '<span class="repeatcount">' .. line.count .. '</span> ' .. str
- end
- if lovebird.timestamp then
- str = os.date('<span class="timestamp">%H:%M:%S</span> ', line.time) ..
- str
- end
- end
- return str
- end
- lovebird.buffer = table.concat(lovebird.map(lovebird.lines, doline), "<br>")
- end
-
-
- function lovebird.print(...)
- local t = {}
- for i = 1, select("#", ...) do
- table.insert(t, tostring(select(i, ...)))
- end
- local str = table.concat(t, " ")
- local last = lovebird.lines[#lovebird.lines]
- if last and str == last.str then
- -- Update last line if this line is a duplicate of it
- last.time = os.time()
- last.count = last.count + 1
- lovebird.recalcbuffer()
- else
- -- Create new line
- lovebird.pushline({ type = "output", str = str })
- end
- end
-
-
- function lovebird.onerror(err)
- lovebird.pushline({ type = "error", str = err })
- if lovebird.wrapprint then
- lovebird.origprint("[lovebird] ERROR: " .. err)
- end
- end
-
-
- function lovebird.onrequest(req, client)
- local page = req.parsedurl.path
- page = page ~= "" and page or "index"
- -- Handle "page not found"
- if not lovebird.pages[page] then
- return "HTTP/1.1 404\r\nContent-Length: 8\r\n\r\nBad page"
- end
- -- Handle page
- local str
- xpcall(function()
- local data = lovebird.pages[page](lovebird, req)
- local contenttype = "text/html"
- if string.match(page, "%.json$") then
- contenttype = "application/json"
- end
- str = "HTTP/1.1 200 OK\r\n" ..
- "Content-Type: " .. contenttype .. "\r\n" ..
- "Content-Length: " .. #data .. "\r\n" ..
- "\r\n" .. data
- end, lovebird.onerror)
- return str
- end
-
-
- function lovebird.receive(client, pattern)
- while 1 do
- local data, msg = client:receive(pattern)
- if not data then
- if msg == "timeout" then
- -- Wait for more data
- coroutine.yield(true)
- else
- -- Disconnected -- yielding nil means we're done
- coroutine.yield(nil)
- end
- else
- return data
- end
- end
- end
-
-
- function lovebird.send(client, data)
- local idx = 1
- while idx < #data do
- local res, msg = client:send(data, idx)
- if not res and msg == "closed" then
- -- Handle disconnect
- coroutine.yield(nil)
- else
- idx = idx + res
- coroutine.yield(true)
- end
- end
- end
-
-
- function lovebird.onconnect(client)
- -- Create request table
- local requestptn = "(%S*)%s*(%S*)%s*(%S*)"
- local req = {}
- req.socket = client
- req.addr, req.port = client:getsockname()
- req.request = lovebird.receive(client, "*l")
- req.method, req.url, req.proto = req.request:match(requestptn)
- req.headers = {}
- while 1 do
- local line, msg = lovebird.receive(client, "*l")
- if not line or #line == 0 then break end
- local k, v = line:match("(.-):%s*(.*)$")
- req.headers[k] = v
- end
- if req.headers["Content-Length"] then
- req.body = lovebird.receive(client, req.headers["Content-Length"])
- end
- -- Parse body
- req.parsedbody = {}
- if req.body then
- for k, v in req.body:gmatch("([^&]-)=([^&^#]*)") do
- req.parsedbody[k] = lovebird.unescape(v)
- end
- end
- -- Parse request line's url
- req.parsedurl = lovebird.parseurl(req.url)
- -- Handle request; get data to send and send
- local data = lovebird.onrequest(req)
- lovebird.send(client, data)
- -- Clear up
- client:close()
- end
-
-
- function lovebird.update()
- if not lovebird.inited then lovebird.init() end
- -- Handle new connections
- while 1 do
- -- Accept new connections
- local client = lovebird.server:accept()
- if not client then break end
- client:settimeout(0)
- local addr = client:getsockname()
- if lovebird.checkwhitelist(addr) then
- -- Connection okay -- create and add coroutine to set
- local conn = coroutine.wrap(function()
- xpcall(function() lovebird.onconnect(client) end, function() end)
- end)
- lovebird.connections[conn] = true
- else
- -- Reject connection not on whitelist
- lovebird.trace("got non-whitelisted connection attempt: ", addr)
- client:close()
- end
- end
- -- Handle existing connections
- for conn in pairs(lovebird.connections) do
- -- Resume coroutine, remove if it has finished
- local status = conn()
- if status == nil then
- lovebird.connections[conn] = nil
- end
- end
- end
-
-
- return lovebird
|