and_cesbo

xProxy Web-UI mod

Jul 4th, 2014
1,562
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 22.98 KB | None | 0 0
  1. -- xProxy. Web-UI mod
  2. -- http://cesbo.com/astra
  3. --
  4. -- Copyright (C) 2014, Andrey Dyldin <[email protected]>
  5. --
  6. -- This program is free software: you can redistribute it and/or modify
  7. -- it under the terms of the GNU General Public License as published by
  8. -- the Free Software Foundation, either version 3 of the License, or
  9. -- (at your option) any later version.
  10. --
  11. -- This program is distributed in the hope that it will be useful,
  12. -- but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  14. -- GNU General Public License for more details.
  15. --
  16. -- You should have received a copy of the GNU General Public License
  17. -- along with this program.  If not, see <http://www.gnu.org/licenses/>.
  18. --
  19. -- Usage:
  20. -- Save into the /etc/astra/xproxy-mod.lua
  21. -- Execute: astra --xproxy /etc/astra/xproxy-mod.lua
  22.  
  23. xproxy_config_path = "/etc/astra/xproxy-db.json"
  24.  
  25. function auth_request(client_id, request, auth_callback)
  26.     if not request then
  27.         return nil
  28.     end
  29.  
  30.     function check_auth()
  31.         if not request.query or not request.query.user then return false end
  32.         for _,a in ipairs(xproxy_config.accounts) do
  33.             if a.user == request.query.user then
  34.                 return (a.pass == request.query.pass)
  35.             end
  36.         end
  37.         return false
  38.     end
  39.  
  40.     if check_auth() then
  41.         auth_callback(true)
  42.     else
  43.         auth_callback(false)
  44.     end
  45. end
  46.  
  47. --
  48.  
  49. channels = {}
  50. xproxy_config = nil
  51. function xproxy_config_load()
  52.     if utils.stat(xproxy_config_path).type == "file" then
  53.         xproxy_config = json.load(xproxy_config_path)
  54.     end
  55.     if not xproxy_config then
  56.         json.save(xproxy_config_path, {
  57.             options = {
  58.                 no_udp = true,
  59.                 no_rtp = true,
  60.                 no_http = true,
  61.             },
  62.             accounts = {},
  63.             channels = {},
  64.         })
  65.         xproxy_config = json.load(xproxy_config_path)
  66.         if xproxy_config == nil then
  67.             log.error("[xProxy] failed to load " .. xproxy_config_path)
  68.             astra.exit()
  69.         end
  70.     end
  71.  
  72.     for _,c in pairs(xproxy_config.channels) do channels[c.path] = c.source end
  73.     if xproxy_config.options.auth and #xproxy_config.options.auth > 0 then
  74.         xproxy_pass = "Basic " .. base64.encode(xproxy_config.options.auth)
  75.     end
  76.     if xproxy_config.options.no_udp == true then xproxy_allow_udp = false end
  77.     if xproxy_config.options.no_rtp == true then xproxy_allow_rtp = false end
  78.     if xproxy_config.options.no_http == true then xproxy_allow_http = false end
  79. end
  80. xproxy_config_load()
  81.  
  82. function render_stat_html()
  83.     return [[<!DOCTYPE html>
  84.  
  85. <html lang="en" ng-app="App">
  86. <head>
  87.     <meta charset="utf-8" />
  88.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  89.     <title>xProxy : Web-UI</title>
  90.  
  91.     <link lazy-href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" />
  92.     <link lazy-href="//netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" />
  93.  
  94.     <style type="text/css">
  95. * { box-sizing: border-box; }
  96. html { height: 100%; }
  97. body { height: 100%; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #333333; }
  98. a:not([href]) { cursor: pointer; }
  99.  
  100. .xproxy-nav { padding: 10px 15px; }
  101. .wrap { min-height: 100%; }
  102. .content { overflow: visible; padding-top: 5px; padding-bottom: 25px; }
  103. .footer { position: relative; height: 20px; margin-top: -20px; width: 100%; padding: 0 15px; }
  104. .footer span { font-size: 0.8em; color: #bbb; display: inline-block; width: 50%; padding: 0 15px; }
  105. .footer .version { text-align: right; }
  106.  
  107. .app-status { position: absolute; z-index: 10000; top: 0; right: 0; bottom: 0; left: 0; padding: 20px; background: #ffffff; }
  108.     </style>
  109. </head>
  110.  
  111. <body>
  112. <div class="app-status" ng-hide="version">Loading...</div>
  113.  
  114. <div class="wrap">
  115. <nav class="xproxy-nav container-fluid"><div class="row">
  116.     <div class="col-sm-3">
  117.     </div>
  118.     <div class="col-sm-offset-3 col-sm-6" style="white-space:nowrap;text-align:right;">
  119.         <a href="#/stat" class="btn btn-default" style="width:120px;" ng-class="{'active':controller==='StatController'}">Connections</a>
  120.         <a href="#/options" class="btn btn-default" style="width:120px;" ng-class="{'active':controller==='OptionsController'}">Options</a>
  121.         <a href="#/accounts" class="btn btn-default" style="width:120px;" ng-class="{'active':controller==='AccountsController'}">Accounts</a>
  122.         <a href="#/channels" class="btn btn-default" style="width:120px;" ng-class="{'active':controller==='ChannelsController'}">Channels</a>
  123.     </div>
  124. </div></nav>
  125. <div class="container-fluid content" ng-view></div>
  126. </div> <!-- wrap -->
  127.  
  128. <footer class="footer">
  129.     <span class="copyright">&copy; 2014 Cesbo Ltd. All rights reserved.</span><span class="version" ng-bind="version"></span>
  130. </footer>
  131. </body>
  132.  
  133. <script type="text/ng-template" id="stat.html">
  134. <div class="row"><div class="col-sm-offset-2 col-sm-8">
  135. <table class="table"><thead><tr>
  136.     <th width="100px">Client ID</th>
  137.     <th width="150px">IP</th>
  138.     <th>Source</th>
  139.     <th width="100px">Uptime</th>
  140.     <th width="100px"></th>
  141. </tr></thead><tbody>
  142.     <tr ng-repeat="c in stat">
  143.         <td ng-bind="c.id"></td>
  144.         <td ng-bind="c.addr"></td>
  145.         <td ng-bind="c.path"></td>
  146.         <td ng-bind="c.uptime"></td>
  147.         <td align="right"><a ng-click="disconnect(c.id)">Disconnect</a></td>
  148.     </tr>
  149.     <tr><td colspan="5"><a ng-click="refresh()">Refresh Connections List</a></td></tr>
  150. </tbody></table>
  151. </div></div>
  152. </script>
  153.  
  154. <script type="text/ng-template" id="options.html">
  155. <div class="row"><div class="col-sm-offset-3 col-sm-6">
  156. <div class="form-horizontal">
  157.     <div class="form-group">
  158.         <label class="col-sm-3 control-label">Basic Authorization</label>
  159.         <div class="col-sm-6"><input type="text" class="form-control" ng-model="xproxy.options.auth" placeholder="Login:Password" /></div>
  160.     </div>
  161.  
  162.     <div class="form-group">
  163.         <label class="col-sm-3 control-label">Disable UDP proxy</label>
  164.         <div class="col-sm-6"><label class="checkbox"><input type="checkbox" ng-model="xproxy.options.no_udp" />&nbsp;<span style="color:#737373;font-weight:normal;">http://server/udp/*</span></label></div>
  165.     </div>
  166.     <div class="form-group">
  167.         <label class="col-sm-3 control-label">Disable RTP proxy</label>
  168.         <div class="col-sm-6"><label class="checkbox"><input type="checkbox" ng-model="xproxy.options.no_rtp" />&nbsp;<span style="color:#737373;font-weight:normal;">http://server/rtp/*</span></label></div>
  169.     </div>
  170.     <div class="form-group">
  171.         <label class="col-sm-3 control-label">Disable HTTP proxy</label>
  172.         <div class="col-sm-6"><label class="checkbox"><input type="checkbox" ng-model="xproxy.options.no_http" />&nbsp;<span style="color:#737373;font-weight:normal;">http://server/http/*</span></label></div>
  173.     </div>
  174.  
  175.     <hr />
  176.     <div class="form-group">
  177.         <div class="col-sm-offset-3 col-sm-6">
  178.             <button class="btn btn-primary" style="width:100px;" ng-click="options_save()">Save</button>
  179.             <button class="btn btn-danger" style="width:100px;" ng-click="restart()">Restart</button>
  180.         </div>
  181.     </div>
  182. </div>
  183. </div></div>
  184. </script>
  185.  
  186. <script type="text/ng-template" id="accounts.html">
  187. <div class="row"><div class="col-sm-offset-2 col-sm-8"><table class="table">
  188.     <thead>
  189.         <tr>
  190.             <th>User Name</th>
  191.             <th>Password</th>
  192.             <th width="120px"></th>
  193.         </tr>
  194.     </thead><tbody>
  195.         <tr>
  196.             <td><input type="text" class="form-control" ng-model="account_new.user" /></td>
  197.             <td><input type="text" class="form-control" ng-model="account_new.pass" /></td>
  198.             <td><button class="btn btn-success" ng-click="account_save(0)"><i class="fa fa-plus fa-lg fa-fw"></i></button></td>
  199.         </tr>
  200.         <tr ng-repeat="(i,a) in xproxy.accounts">
  201.             <td><input type="text" class="form-control" ng-model="a.user" /></td>
  202.             <td><input type="text" class="form-control" ng-model="a.pass" /></td>
  203.             <td>
  204.                 <button class="btn btn-success" ng-click="account_save(i + 1)"><i class="fa fa-check fa-lg fa-fw"></i></button>
  205.                 <button class="btn btn-danger" ng-click="account_delete(i + 1)"><i class="fa fa-times fa-lg fa-fw"></i></button>
  206.             </td>
  207.         </tr>
  208.     </tbody>
  209. </table></div></div>
  210. </script>
  211.  
  212. <script type="text/ng-template" id="channels.html">
  213. <div class="row"><div class="col-sm-offset-2 col-sm-8"><table class="table">
  214.     <thead>
  215.         <tr>
  216.             <th>Source</th>
  217.             <th>Channel ID</th>
  218.             <th width="120px"></th>
  219.         </tr>
  220.     </thead><tbody>
  221.         <tr>
  222.             <td><input type="text" class="form-control" ng-model="channel_new.source" /></td>
  223.             <td><input type="text" class="form-control" ng-model="channel_new.path" /></td>
  224.             <td><button class="btn btn-success" ng-click="channel_save(0)"><i class="fa fa-plus fa-lg fa-fw"></i></button></td>
  225.         </tr>
  226.         <tr ng-repeat="(i,c) in xproxy.channels">
  227.             <td><input type="text" class="form-control" ng-model="c.source" /></td>
  228.             <td><input type="text" class="form-control" ng-model="c.path" /></td>
  229.             <td>
  230.                 <button class="btn btn-success" ng-click="channel_save(i + 1)"><i class="fa fa-check fa-lg fa-fw"></i></button>
  231.                 <button class="btn btn-danger" ng-click="channel_delete(i + 1)"><i class="fa fa-times fa-lg fa-fw"></i></button>
  232.             </td>
  233.         </tr>
  234.     </tbody>
  235. </table></div></div>
  236. </script>
  237.  
  238. <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min.js"></script>
  239. <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular-route.js"></script>
  240.  
  241. <script type="text/javascript">
  242. var App = angular.module("App", ["ngRoute"]);
  243.  
  244. App.directive("lazyHref", function() {
  245.     return {
  246.         restrict: "A",
  247.         transclude: true,
  248.         link: function (scope, element, attrs) {
  249.             element.attr("type", "text/css");
  250.             element.attr("rel", "stylesheet");
  251.             element.attr("href", attrs.lazyHref);
  252.             element.removeAttr("lazy-href");
  253.         }
  254.     };
  255. });
  256.  
  257. App.config(["$routeProvider", function($routeProvider) {
  258.     $routeProvider
  259.         .when("/stat", {
  260.             templateUrl: "stat.html",
  261.             controller: "StatController"
  262.         })
  263.         .when("/options", {
  264.             templateUrl: "options.html",
  265.             controller: "OptionsController"
  266.         })
  267.         .when("/accounts", {
  268.             templateUrl: "accounts.html",
  269.             controller: "AccountsController"
  270.         })
  271.         .when("/channels", {
  272.             templateUrl: "channels.html",
  273.             controller: "ChannelsController"
  274.         })
  275.         .otherwise({
  276.             redirectTo: '/stat'
  277.         });;
  278. }]);
  279.  
  280. App.run(["$rootScope",
  281.          "$http",
  282.          function($rootScope, $http)
  283. {
  284.     angular.element(document).ready(function() {
  285.         $http
  286.             .post('/stat/', { 'cmd': 'load' })
  287.             .success(function(data) {
  288.                 $rootScope.version = "Astra v." + data.version;
  289.                 $rootScope.xproxy = data.xproxy;
  290.                 if($rootScope.xproxy.accounts === undefined) $rootScope.xproxy.accounts = [];
  291.                 if($rootScope.xproxy.channels === undefined) $rootScope.xproxy.channels = [];
  292.             });
  293.     });
  294. }]);
  295.  
  296. App.controller('AccountsController', ['$rootScope',
  297.                                       '$scope',
  298.                                       '$http',
  299.                                       function($rootScope, $scope, $http)
  300. {
  301.     $rootScope.controller = 'AccountsController';
  302.     $scope.account_new = {};
  303.  
  304.     $scope.account_save = function(i) {
  305.         var a = (i === 0) ? $scope.account_new : $rootScope.xproxy.accounts[i - 1];
  306.         var e = false;
  307.         if(a.user.length === 0) {
  308.             a.user = "ERROR";
  309.             e = true;
  310.         }
  311.         if(a.pass.length === 0) {
  312.             a.pass = "ERROR";
  313.             e = true;
  314.         }
  315.         if(e === true) return;
  316.         $http
  317.             .post('/stat/', {
  318.                 'cmd': 'save',
  319.                 'scope': 'account',
  320.                 'id': i,
  321.                 'account': a
  322.             })
  323.             .success(function(data) {
  324.                 if(i === 0 && data.success) $rootScope.xproxy.accounts.push(data.success);
  325.             });
  326.  
  327.         $scope.account_new = {};
  328.     };
  329.  
  330.     $scope.account_delete = function(i) {
  331.         $http
  332.             .post('/stat/', {
  333.                 'cmd': 'delete',
  334.                 'scope': 'account',
  335.                 'id': i
  336.             })
  337.             .success(function(data) {
  338.                 if(data.success) $rootScope.xproxy.accounts.splice(i - 1, 1);
  339.             });
  340.     };
  341. }]);
  342.  
  343. App.controller('ChannelsController', ['$rootScope',
  344.                                       '$scope',
  345.                                       '$http',
  346.                                       function($rootScope, $scope, $http)
  347. {
  348.     $rootScope.controller = 'ChannelsController';
  349.     $scope.channel_new = {};
  350.  
  351.     $scope.channel_save = function(i) {
  352.         var c = (i === 0) ? $scope.channel_new : $rootScope.xproxy.channels[i - 1];
  353.         var e = false;
  354.         if(c.source.length === 0) {
  355.             c.source = "ERROR";
  356.             e = true;
  357.         }
  358.         if(c.path.length === 0) {
  359.             c.path = "ERROR";
  360.             e = true;
  361.         }
  362.         if(e === true) return;
  363.         $http
  364.             .post('/stat/', {
  365.                 'cmd': 'save',
  366.                 'scope': 'channel',
  367.                 'id': i,
  368.                 'channel': c
  369.             })
  370.             .success(function(data) {
  371.                 if(i === 0 && data.success) $rootScope.xproxy.channels.push(data.success);
  372.             });
  373.  
  374.         $scope.channel_new = {};
  375.     };
  376.  
  377.     $scope.channel_delete = function(i) {
  378.         $http
  379.             .post('/stat/', {
  380.                 'cmd': 'delete',
  381.                 'scope': 'channel',
  382.                 'id': i
  383.             })
  384.             .success(function(data) {
  385.                 if(data.success) $rootScope.xproxy.channels.splice(i - 1, 1);
  386.             });
  387.     };
  388. }]);
  389.  
  390. App.controller('OptionsController', ['$rootScope',
  391.                                      '$scope',
  392.                                      '$http',
  393.                                      function($rootScope, $scope, $http)
  394. {
  395.     $rootScope.controller = 'OptionsController';
  396.  
  397.     $scope.options_save = function() {
  398.         $http
  399.             .post('/stat/', {
  400.                 'cmd': 'save',
  401.                 'scope': 'options',
  402.                 'options': $rootScope.xproxy.options
  403.             })
  404.             .success(function(data) {
  405.                 //
  406.             });
  407.     };
  408.  
  409.     $scope.restart = function() {
  410.         $http
  411.             .post('/stat/', {
  412.                 'cmd': 'restart',
  413.                 'scope': 'options',
  414.             })
  415.             .success(function(data) {
  416.                 //
  417.             });
  418.     };
  419. }]);
  420.  
  421. App.controller('StatController', ['$rootScope',
  422.                                   '$scope',
  423.                                   '$http',
  424.                                   function($rootScope, $scope, $http)
  425. {
  426.     $rootScope.controller = 'StatController';
  427.  
  428.     $scope.refresh = function() {
  429.         $http
  430.             .post('/stat/', { 'cmd': 'stat' })
  431.             .success(function(data) {
  432.                 $scope.stat = (data.stat === undefined) ? [] : data.stat;
  433.             });
  434.     };
  435.  
  436.     $scope.disconnect = function(id) {
  437.         $http
  438.             .post('/stat/', { 'cmd': 'disconnect', 'id': id })
  439.             .success(function(data) {
  440.                 for(var i = 0; i < $scope.stat.length; ++i) {
  441.                     if($scope.stat[i].id === id) {
  442.                         $scope.stat.splice(i, 1);
  443.                         return;
  444.                     }
  445.                 }
  446.             });
  447.     };
  448.  
  449.     $scope.refresh();
  450. }]);
  451. </script>
  452. </html>
  453. ]]
  454. end
  455.  
  456. function on_http_stat(server, client, request)
  457.     if not request then return nil end
  458.  
  459.     if not xproxy_config then
  460.         server:abort(client, 404, "Access List is not defined")
  461.         return nil
  462.     end
  463.  
  464.     if xproxy_pass then
  465.         if request.headers["authorization"] ~= xproxy_pass then
  466.             server:send(client, {
  467.                 code = 401,
  468.                 headers = {
  469.                     "WWW-Authenticate: Basic realm=\"xProxy\"",
  470.                     "Content-Length: 0",
  471.                     "Connection: close",
  472.                 }
  473.             })
  474.             return
  475.         end
  476.     end
  477.  
  478.     if request.method == "POST" then
  479.         local data = json.decode(request.content)
  480.         if not data then
  481.             server:abort(client, 400)
  482.         elseif data.cmd == "load" then
  483.             local r = { version = astra.version, xproxy = {}, }
  484.             r.xproxy.options = xproxy_config.options
  485.             if #xproxy_config.accounts > 0 then r.xproxy.accounts = xproxy_config.accounts end
  486.             if #xproxy_config.channels > 0 then r.xproxy.channels = xproxy_config.channels end
  487.             server:send(client, {
  488.                 code = 200,
  489.                 headers = { "Content-Type: application/json", "Connection: close", },
  490.                 content = json.encode(r),
  491.             })
  492.         elseif data.cmd == "stat" then
  493.             local ct = os.time()
  494.             local l = {}
  495.             for i,c in pairs(client_list) do
  496.                 local dt = ct - c.st
  497.                 local uptime = string.format("%02d:%02d", (dt / 3600), (dt / 60) % 60)
  498.                 table.insert(l, {
  499.                     id = i,
  500.                     addr = c.addr,
  501.                     path = c.path,
  502.                     uptime = uptime,
  503.                 })
  504.             end
  505.             local r = {}
  506.             if #l > 0 then r.stat = l end
  507.             server:send(client, {
  508.                 code = 200,
  509.                 headers = { "Content-Type: application/json", "Connection: close", },
  510.                 content = json.encode(r),
  511.             })
  512.         elseif data.cmd == "disconnect" then
  513.             local client_id = tonumber(data.id)
  514.             if client_list[client_id] then
  515.                 server:close(client_list[client_id].client)
  516.             end
  517.             server:send(client, {
  518.                 code = 200,
  519.                 headers = { "Content-Type: application/json", "Connection: close", },
  520.                 content = json.encode({ success = true })
  521.             })
  522.         elseif data.cmd == "restart" then
  523.             timer({
  524.                 interval = 1,
  525.                 callback = function(self)
  526.                     self:close()
  527.                     astra.reload()
  528.                 end,
  529.             })
  530.             server:send(client, {
  531.                 code = 200,
  532.                 headers = { "Content-Type: application/json", "Connection: close", },
  533.                 content = json.encode({ success = true })
  534.             })
  535.         elseif data.cmd == "save" then
  536.             if data.scope == "options" then
  537.                 xproxy_config.options = data.options
  538.                 xproxy_pass = "Basic " .. base64.encode(xproxy_config.options.auth)
  539.                 json.save(xproxy_config_path, xproxy_config)
  540.                 server:send(client, {
  541.                     code = 200,
  542.                     headers = { "Content-Type: application/json", "Connection: close", },
  543.                     content = json.encode({ success = true })
  544.                 })
  545.             elseif data.scope == "account" then
  546.                 local id = tonumber(data.id)
  547.                 if id == nil then
  548.                     server:abort(client, 400)
  549.                     return nil
  550.                 end
  551.                 if id == 0 then
  552.                     table.insert(xproxy_config.accounts, data.account)
  553.                 else
  554.                     xproxy_config.accounts[id] = data.account
  555.                 end
  556.                 json.save(xproxy_config_path, xproxy_config)
  557.                 server:send(client, {
  558.                     code = 200,
  559.                     headers = { "Content-Type: application/json", "Connection: close", },
  560.                     content = json.encode({ success = data.account })
  561.                 })
  562.             elseif data.scope == "channel" then
  563.                 local id = tonumber(data.id)
  564.                 if id == nil then
  565.                     server:abort(client, 400)
  566.                     return nil
  567.                 end
  568.                 if id == 0 then
  569.                     table.insert(xproxy_config.channels, data.channel)
  570.                     channels[data.channel.path] = data.channel.source
  571.                 else
  572.                     local c = xproxy_config.channels[id]
  573.                     channels[c.path] = nil
  574.                     channels[data.channel.path] = data.channel.source
  575.                     xproxy_config.channels[id] = data.channel
  576.                 end
  577.                 json.save(xproxy_config_path, xproxy_config)
  578.                 server:send(client, {
  579.                     code = 200,
  580.                     headers = { "Content-Type: application/json", "Connection: close", },
  581.                     content = json.encode({ success = data.channel })
  582.                 })
  583.             else
  584.                 server:abort(client, 400)
  585.             end
  586.         elseif data.cmd == "delete" then
  587.             if data.scope == "account" then
  588.                 local id = tonumber(data.id)
  589.                 if id == nil then
  590.                     server:abort(client, 400)
  591.                     return nil
  592.                 end
  593.                 table.remove(xproxy_config.accounts, id)
  594.                 json.save(xproxy_config_path, xproxy_config)
  595.                 server:send(client, {
  596.                     code = 200,
  597.                     headers = { "Content-Type: application/json", "Connection: close", },
  598.                     content = json.encode({ success = true })
  599.                 })
  600.             elseif data.scope == "channel" then
  601.                 local id = tonumber(data.id)
  602.                 if id == nil then
  603.                     server:abort(client, 400)
  604.                     return nil
  605.                 end
  606.                 local c = xproxy_config.channels[id]
  607.                 channels[c.path] = nil
  608.                 table.remove(xproxy_config.channels, id)
  609.                 json.save(xproxy_config_path, xproxy_config)
  610.                 server:send(client, {
  611.                     code = 200,
  612.                     headers = { "Content-Type: application/json", "Connection: close", },
  613.                     content = json.encode({ success = true })
  614.                 })
  615.             else
  616.                 server:abort(client, 400)
  617.             end
  618.         else
  619.             server:abort(client, 400)
  620.         end
  621.         return nil
  622.     end
  623.  
  624.     server:send(client, {
  625.         code = 200,
  626.         headers = { "Content-Type: text/html; charset=utf-8", "Connection: close", },
  627.         content = render_stat_html(),
  628.     })
  629. end
Add Comment
Please, Sign In to add comment