├── 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 | ![screenshot from 2014-06-28 14 52 34](https://cloud.githubusercontent.com/assets/3920290/3420901/c15975ce-fecb-11e3-9517-970c919815b4.png) 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 |
204 | 206 |
207 |
208 |
209 |
210 |
211 |
212 |
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 | --------------------------------------------------------------------------------