├── LICENSE
├── README.md
└── lovebird.lua
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017 rxi
2 |
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | this software and associated documentation files (the "Software"), to deal in
6 | the Software without restriction, including without limitation the rights to
7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8 | of the Software, and to permit persons to whom the Software is furnished to do
9 | so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # lovebird
2 | A browser-based debug console for the [LÖVE](http://love2d.org) framework.
3 |
4 | 
5 |
6 |
7 | ## Usage
8 | Drop the [lovebird.lua](lovebird.lua?raw=1) file into an existing project and
9 | place the following line at the top of your `love.update()` function:
10 | ```lua
11 | require("lovebird").update()
12 | ```
13 | The console can then be accessed by opening the following URL in a web browser:
14 | ```
15 | http://127.0.0.1:8000
16 | ```
17 | If you want to access lovebird from another computer then `127.0.0.1` should be
18 | replaced with the IP address of the computer which LÖVE is running on; the IP
19 | address of the other computer should be added to the
20 | [lovebird.whitelist](#lovebirdwhitelist) table.
21 |
22 |
23 | ## Additional Functionality
24 | To make use of additional functionality, the module can be assigned to a
25 | variable when it is required:
26 | ```lua
27 | lovebird = require "lovebird"
28 | ```
29 | Any configuration variables should be set before `lovebird.update()` is called.
30 |
31 | ### lovebird.port
32 | The port which lovebird listens for connections on. By default this is `8000`
33 |
34 | ### lovebird.whitelist
35 | A table of hosts which lovebird will accept connections from. Any connection
36 | made from a host which is not on the whitelist is logged and closed
37 | immediately. If `lovebird.whitelist` is set to nil then all connections are
38 | accepted. The default is `{ "127.0.0.1" }`. To allow *all* computers on the
39 | local network access to lovebird, `"192.168.*.*"` can be added to this table.
40 |
41 | ### lovebird.wrapprint
42 | Whether lovebird should wrap the `print()` function or not. If this is true
43 | then all the calls to print will also be output to lovebird's console. This is
44 | `true` by default.
45 |
46 | ### lovebird.echoinput
47 | Whether lovebird should display inputted commands in the console's output
48 | buffer; `true` by default.
49 |
50 | ### lovebird.maxlines
51 | The maximum number of lines lovebird should store in its console's output
52 | buffer. By default this is `200`.
53 |
54 | ### lovebird.updateinterval
55 | The interval in seconds that the page's information is updated; this is `0.5`
56 | by default.
57 |
58 | ### lovebird.allowhtml
59 | Whether prints should allow HTML. If this is true then any HTML which is
60 | printed will be rendered as HTML; if it false then all HTML is rendered as
61 | text. This is `false` by default.
62 |
63 | ### lovebird.print(...)
64 | Prints its arguments to lovebird's console. If `lovebird.wrapprint` is set to
65 | true this function is automatically called when print() is called.
66 |
67 | ### lovebird.clear()
68 | Clears the contents of the console, returning it to an empty state.
69 |
--------------------------------------------------------------------------------
/lovebird.lua:
--------------------------------------------------------------------------------
1 | --
2 | -- lovebird
3 | --
4 | -- Copyright (c) 2017 rxi
5 | --
6 | -- This library is free software; you can redistribute it and/or modify it
7 | -- under the terms of the MIT license. See LICENSE for details.
8 | --
9 |
10 | local socket = require "socket"
11 |
12 | local lovebird = { _version = "0.4.3" }
13 |
14 | lovebird.loadstring = loadstring or load
15 | lovebird.inited = false
16 | lovebird.host = "*"
17 | lovebird.buffer = ""
18 | lovebird.lines = {}
19 | lovebird.connections = {}
20 | lovebird.pages = {}
21 |
22 | lovebird.wrapprint = true
23 | lovebird.timestamp = true
24 | lovebird.allowhtml = false
25 | lovebird.echoinput = true
26 | lovebird.port = 8000
27 | lovebird.whitelist = { "127.0.0.1" }
28 | lovebird.maxlines = 200
29 | lovebird.updateinterval = .5
30 |
31 |
32 | lovebird.pages["index"] = [[
33 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | lovebird
54 |
188 |
189 |
190 |
197 |
198 |
199 |
200 |
201 |
207 |
208 |
209 |
213 |
214 |
376 |
377 |
378 | ]]
379 |
380 |
381 | lovebird.pages["buffer"] = [[ ]]
382 |
383 |
384 | lovebird.pages["env.json"] = [[
385 |
400 | {
401 | "valid": true,
402 | "path": "",
403 | "vars": [
404 |
415 | {
416 | "key": "",
417 | "value": ,
422 | "type": "",
423 | },
424 |
425 | ]
426 | }
427 | ]]
428 |
429 |
430 |
431 | function lovebird.init()
432 | -- Init server
433 | lovebird.server = assert(socket.bind(lovebird.host, lovebird.port))
434 | lovebird.addr, lovebird.port = lovebird.server:getsockname()
435 | lovebird.server:settimeout(0)
436 | -- Wrap print
437 | lovebird.origprint = print
438 | if lovebird.wrapprint then
439 | local oldprint = print
440 | print = function(...)
441 | oldprint(...)
442 | lovebird.print(...)
443 | end
444 | end
445 | -- Compile page templates
446 | for k, page in pairs(lovebird.pages) do
447 | lovebird.pages[k] = lovebird.template(page, "lovebird, req",
448 | "pages." .. k)
449 | end
450 | lovebird.inited = true
451 | end
452 |
453 |
454 | function lovebird.template(str, params, chunkname)
455 | params = params and ("," .. params) or ""
456 | local f = function(x) return string.format(" echo(%q)", x) end
457 | str = ("?>"..str.."(.-)<%?lua", f)
458 | str = "local echo " .. params .. " = ..." .. str
459 | local fn = assert(lovebird.loadstring(str, chunkname))
460 | return function(...)
461 | local output = {}
462 | local echo = function(str) table.insert(output, str) end
463 | fn(echo, ...)
464 | return table.concat(lovebird.map(output, tostring))
465 | end
466 | end
467 |
468 |
469 | function lovebird.map(t, fn)
470 | local res = {}
471 | for k, v in pairs(t) do res[k] = fn(v) end
472 | return res
473 | end
474 |
475 |
476 | function lovebird.trace(...)
477 | local str = "[lovebird] " .. table.concat(lovebird.map({...}, tostring), " ")
478 | print(str)
479 | if not lovebird.wrapprint then lovebird.print(str) end
480 | end
481 |
482 |
483 | function lovebird.unescape(str)
484 | local f = function(x) return string.char(tonumber("0x"..x)) end
485 | return (str:gsub("%+", " "):gsub("%%(..)", f))
486 | end
487 |
488 |
489 | function lovebird.parseurl(url)
490 | local res = {}
491 | res.path, res.search = url:match("/([^%?]*)%??(.*)")
492 | res.query = {}
493 | for k, v in res.search:gmatch("([^&^?]-)=([^&^#]*)") do
494 | res.query[k] = lovebird.unescape(v)
495 | end
496 | return res
497 | end
498 |
499 |
500 | local htmlescapemap = {
501 | ["<"] = "<",
502 | ["&"] = "&",
503 | ['"'] = """,
504 | ["'"] = "'",
505 | }
506 |
507 | function lovebird.htmlescape(str)
508 | return ( str:gsub("[<&\"']", htmlescapemap) )
509 | end
510 |
511 |
512 | function lovebird.truncate(str, len)
513 | if #str <= len then
514 | return str
515 | end
516 | return str:sub(1, len - 3) .. "..."
517 | end
518 |
519 |
520 | function lovebird.compare(a, b)
521 | local na, nb = tonumber(a), tonumber(b)
522 | if na then
523 | if nb then return na < nb end
524 | return false
525 | elseif nb then
526 | return true
527 | end
528 | return tostring(a) < tostring(b)
529 | end
530 |
531 |
532 | function lovebird.checkwhitelist(addr)
533 | if lovebird.whitelist == nil then return true end
534 | for _, a in pairs(lovebird.whitelist) do
535 | local ptn = "^" .. a:gsub("%.", "%%."):gsub("%*", "%%d*") .. "$"
536 | if addr:match(ptn) then return true end
537 | end
538 | return false
539 | end
540 |
541 |
542 | function lovebird.clear()
543 | lovebird.lines = {}
544 | lovebird.buffer = ""
545 | end
546 |
547 |
548 | function lovebird.pushline(line)
549 | line.time = os.time()
550 | line.count = 1
551 | table.insert(lovebird.lines, line)
552 | if #lovebird.lines > lovebird.maxlines then
553 | table.remove(lovebird.lines, 1)
554 | end
555 | lovebird.recalcbuffer()
556 | end
557 |
558 |
559 | function lovebird.recalcbuffer()
560 | local function doline(line)
561 | local str = line.str
562 | if not lovebird.allowhtml then
563 | str = lovebird.htmlescape(line.str):gsub("\n", "
")
564 | end
565 | if line.type == "input" then
566 | str = '' .. str .. ''
567 | else
568 | if line.type == "error" then
569 | str = '! ' .. str
570 | str = '' .. str .. ''
571 | end
572 | if line.count > 1 then
573 | str = '' .. line.count .. ' ' .. str
574 | end
575 | if lovebird.timestamp then
576 | str = os.date('%H:%M:%S ', line.time) ..
577 | str
578 | end
579 | end
580 | return str
581 | end
582 | lovebird.buffer = table.concat(lovebird.map(lovebird.lines, doline), "
")
583 | end
584 |
585 |
586 | function lovebird.print(...)
587 | local t = {}
588 | for i = 1, select("#", ...) do
589 | table.insert(t, tostring(select(i, ...)))
590 | end
591 | local str = table.concat(t, " ")
592 | local last = lovebird.lines[#lovebird.lines]
593 | if last and str == last.str then
594 | -- Update last line if this line is a duplicate of it
595 | last.time = os.time()
596 | last.count = last.count + 1
597 | lovebird.recalcbuffer()
598 | else
599 | -- Create new line
600 | lovebird.pushline({ type = "output", str = str })
601 | end
602 | end
603 |
604 |
605 | function lovebird.onerror(err)
606 | lovebird.pushline({ type = "error", str = err })
607 | if lovebird.wrapprint then
608 | lovebird.origprint("[lovebird] ERROR: " .. err)
609 | end
610 | end
611 |
612 |
613 | function lovebird.onrequest(req, client)
614 | local page = req.parsedurl.path
615 | page = page ~= "" and page or "index"
616 | -- Handle "page not found"
617 | if not lovebird.pages[page] then
618 | return "HTTP/1.1 404\r\nContent-Length: 8\r\n\r\nBad page"
619 | end
620 | -- Handle page
621 | local str
622 | xpcall(function()
623 | local data = lovebird.pages[page](lovebird, req)
624 | local contenttype = "text/html"
625 | if string.match(page, "%.json$") then
626 | contenttype = "application/json"
627 | end
628 | str = "HTTP/1.1 200 OK\r\n" ..
629 | "Content-Type: " .. contenttype .. "\r\n" ..
630 | "Content-Length: " .. #data .. "\r\n" ..
631 | "\r\n" .. data
632 | end, lovebird.onerror)
633 | return str
634 | end
635 |
636 |
637 | function lovebird.receive(client, pattern)
638 | while 1 do
639 | local data, msg = client:receive(pattern)
640 | if not data then
641 | if msg == "timeout" then
642 | -- Wait for more data
643 | coroutine.yield(true)
644 | else
645 | -- Disconnected -- yielding nil means we're done
646 | coroutine.yield(nil)
647 | end
648 | else
649 | return data
650 | end
651 | end
652 | end
653 |
654 |
655 | function lovebird.send(client, data)
656 | local idx = 1
657 | while idx < #data do
658 | local res, msg = client:send(data, idx)
659 | if not res and msg == "closed" then
660 | -- Handle disconnect
661 | coroutine.yield(nil)
662 | else
663 | idx = idx + res
664 | coroutine.yield(true)
665 | end
666 | end
667 | end
668 |
669 |
670 | function lovebird.onconnect(client)
671 | -- Create request table
672 | local requestptn = "(%S*)%s*(%S*)%s*(%S*)"
673 | local req = {}
674 | req.socket = client
675 | req.addr, req.port = client:getsockname()
676 | req.request = lovebird.receive(client, "*l")
677 | req.method, req.url, req.proto = req.request:match(requestptn)
678 | req.headers = {}
679 | while 1 do
680 | local line, msg = lovebird.receive(client, "*l")
681 | if not line or #line == 0 then break end
682 | local k, v = line:match("(.-):%s*(.*)$")
683 | req.headers[k] = v
684 | end
685 | if req.headers["Content-Length"] then
686 | req.body = lovebird.receive(client, req.headers["Content-Length"])
687 | end
688 | -- Parse body
689 | req.parsedbody = {}
690 | if req.body then
691 | for k, v in req.body:gmatch("([^&]-)=([^&^#]*)") do
692 | req.parsedbody[k] = lovebird.unescape(v)
693 | end
694 | end
695 | -- Parse request line's url
696 | req.parsedurl = lovebird.parseurl(req.url)
697 | -- Handle request; get data to send and send
698 | local data = lovebird.onrequest(req)
699 | lovebird.send(client, data)
700 | -- Clear up
701 | client:close()
702 | end
703 |
704 |
705 | function lovebird.update()
706 | if not lovebird.inited then lovebird.init() end
707 | -- Handle new connections
708 | while 1 do
709 | -- Accept new connections
710 | local client = lovebird.server:accept()
711 | if not client then break end
712 | client:settimeout(0)
713 | local addr = client:getsockname()
714 | if lovebird.checkwhitelist(addr) then
715 | -- Connection okay -- create and add coroutine to set
716 | local conn = coroutine.wrap(function()
717 | xpcall(function() lovebird.onconnect(client) end, function() end)
718 | end)
719 | lovebird.connections[conn] = true
720 | else
721 | -- Reject connection not on whitelist
722 | lovebird.trace("got non-whitelisted connection attempt: ", addr)
723 | client:close()
724 | end
725 | end
726 | -- Handle existing connections
727 | for conn in pairs(lovebird.connections) do
728 | -- Resume coroutine, remove if it has finished
729 | local status = conn()
730 | if status == nil then
731 | lovebird.connections[conn] = nil
732 | end
733 | end
734 | end
735 |
736 |
737 | return lovebird
738 |
--------------------------------------------------------------------------------