diff --git a/htdocs/clients.html b/htdocs/clients.html new file mode 100644 index 00000000..f828d879 --- /dev/null +++ b/htdocs/clients.html @@ -0,0 +1,21 @@ + + + + OpenWebRX+ Clients + + + + + + + +${header} +
+
+

Clients

+
+
+ ${clients} +
+
+ diff --git a/htdocs/lib/UI.js b/htdocs/lib/UI.js index 2bea4b3a..63d6ec23 100644 --- a/htdocs/lib/UI.js +++ b/htdocs/lib/UI.js @@ -171,8 +171,12 @@ UI.toggleFrame = function(on) { this.frame = on; LS.save('ui_frame', on); $('#openwebrx-frame-checkbox').attr('checked', on); - $('#openwebrx-panel-receiver').css( 'border', on ? '2px solid white' : '2px solid transparent'); - $('#openwebrx-dialog-bookmark').css('border', on ? '2px solid white' : '2px solid transparent'); + + var border = on ? '2px solid white' : '2px solid transparent'; + $('#openwebrx-panel-receiver').css( 'border', border); + $('#openwebrx-dialog-bookmark').css('border', border); +// $('#openwebrx-digimode-canvas-container').css('border', border); +// $('.openwebrx-message-panel').css('border', border); } }; diff --git a/htdocs/lib/settings/ClientList.js b/htdocs/lib/settings/ClientList.js new file mode 100644 index 00000000..e1f091f1 --- /dev/null +++ b/htdocs/lib/settings/ClientList.js @@ -0,0 +1,26 @@ +$.fn.clientList = function() { + this.each(function() { + $(this).on('click', '.client-ban', function(e) { + var mins = $('#ban-minutes').val(); + $.ajax("/ban", { + data: JSON.stringify({ ip: this.value, mins: mins }), + contentType: 'application/json', + method: 'POST' + }).done(function() { + document.location.reload(); + }); + return false; + }); + + $(this).on('click', '.client-unban', function(e) { + $.ajax("/unban", { + data: JSON.stringify({ ip: this.value }), + contentType: 'application/json', + method: 'POST' + }).done(function() { + document.location.reload(); + }); + return false; + }); + }); +} diff --git a/htdocs/settings.html b/htdocs/settings.html index 34e609bf..7effe03f 100644 --- a/htdocs/settings.html +++ b/htdocs/settings.html @@ -37,5 +37,11 @@ ${header} Feature report +
+

Clients

+
+
+ ${clients} +
diff --git a/htdocs/settings.js b/htdocs/settings.js index b1bc7361..c222bde0 100644 --- a/htdocs/settings.js +++ b/htdocs/settings.js @@ -9,4 +9,5 @@ $(function(){ $('#scheduler').schedulerInput(); $('.exponential-input').exponentialInput(); $('.device-log-messages').logMessages(); -}); \ No newline at end of file + $('.client-list').clientList(); +}); diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py index e5da4a25..37b36880 100644 --- a/owrx/controllers/assets.py +++ b/owrx/controllers/assets.py @@ -181,6 +181,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController): "lib/settings/SchedulerInput.js", "lib/settings/ExponentialInput.js", "lib/settings/LogMessages.js", + "lib/settings/ClientList.js", "settings.js", ], } diff --git a/owrx/controllers/clients.py b/owrx/controllers/clients.py new file mode 100644 index 00000000..8782e645 --- /dev/null +++ b/owrx/controllers/clients.py @@ -0,0 +1,97 @@ +from owrx.controllers.admin import AuthorizationMixin +from owrx.controllers.template import WebpageController +from owrx.breadcrumb import Breadcrumb, BreadcrumbItem, BreadcrumbMixin +from owrx.websocket import WebSocketConnection +import json +import re + +import logging + +logger = logging.getLogger(__name__) + + +class ClientController(AuthorizationMixin, WebpageController): + def indexAction(self): + self.serve_template("clients.html", **self.template_variables()) + + def template_variables(self): + variables = super().template_variables() + variables["clients"] = self.renderClients() + return variables + + @staticmethod + def renderClients(): + return """ + + + + + + + + {clients} + + + + + +
IP AddressSDR ProfileLocal TimeActions
+ ban for + +
+ """.format( + clients="".join(ClientController.renderClient(c) for c in WebSocketConnection.listAll()) + ) + + @staticmethod + def renderClient(c): + return "{0}{1}{2} {3}{4}".format( + ClientController.renderIp(c["ip"]), + "banned" if c["ban"] else c["sdr"] + " " + c["band"] if "sdr" in c else "n/a", + "until" if c["ban"] else "since", + c["ts"].strftime('%H:%M:%S'), + ClientController.renderButtons(c) + ) + + @staticmethod + def renderIp(ip): + ip = re.sub("^::ffff:", "", ip) + return """ + {1} + """.format(ip, ip) + + @staticmethod + def renderButtons(c): + action = "unban" if c["ban"] else "ban" + return """ + + """.format(action, c["ip"], action) + + def ban(self): + try: + data = json.loads(self.get_body().decode("utf-8")) + mins = int(data["mins"]) if "mins" in data else 0 + if "ip" in data and mins > 0: + logger.info("Banning {0} for {1} minutes".format(data["ip"], mins)) + WebSocketConnection.banIp(data["ip"], mins) + self.send_response("{}", content_type="application/json", code=200) + except: + self.send_response("{}", content_type="application/json", code=400) + + def unban(self): + try: + data = json.loads(self.get_body().decode("utf-8")) + if "ip" in data: + logger.info("Unbanning {0}".format(data["ip"])) + WebSocketConnection.unbanIp(data["ip"]) + self.send_response("{}", content_type="application/json", code=200) + except: + self.send_response("{}", content_type="application/json", code=400) diff --git a/owrx/controllers/settings/__init__.py b/owrx/controllers/settings/__init__.py index 8ad8a4e8..e02404cc 100644 --- a/owrx/controllers/settings/__init__.py +++ b/owrx/controllers/settings/__init__.py @@ -1,7 +1,9 @@ from owrx.config import Config from owrx.controllers.admin import AuthorizationMixin from owrx.controllers.template import WebpageController +from owrx.controllers.clients import ClientController from owrx.breadcrumb import Breadcrumb, BreadcrumbItem, BreadcrumbMixin +from owrx.websocket import WebSocketConnection from abc import ABCMeta, abstractmethod from urllib.parse import parse_qs @@ -14,6 +16,11 @@ class SettingsController(AuthorizationMixin, WebpageController): def indexAction(self): self.serve_template("settings.html", **self.template_variables()) + def template_variables(self): + variables = super().template_variables() + variables["clients"] = ClientController.renderClients() + return variables + class SettingsFormController(AuthorizationMixin, BreadcrumbMixin, WebpageController, metaclass=ABCMeta): def __init__(self, handler, request, options): diff --git a/owrx/http.py b/owrx/http.py index 12995cac..aff0f8a4 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -6,6 +6,7 @@ from owrx.controllers.assets import OwrxAssetsController, AprsSymbolsController, from owrx.controllers.websocket import WebSocketController from owrx.controllers.api import ApiController from owrx.controllers.metrics import MetricsController +from owrx.controllers.clients import ClientController from owrx.controllers.settings import SettingsController from owrx.controllers.settings.general import GeneralSettingsController from owrx.controllers.settings.sdr import ( @@ -23,12 +24,14 @@ from owrx.controllers.session import SessionController from owrx.controllers.profile import ProfileController from owrx.controllers.imageupload import ImageUploadController from owrx.controllers.robots import RobotsController +from owrx.websocket import WebSocketConnection from owrx.storage import Storage from http.server import BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs import re from abc import ABC, abstractmethod from http.cookies import SimpleCookie +from datetime import datetime import logging @@ -158,6 +161,9 @@ class Router(object): StaticRoute( "/settings/decoding", DecodingSettingsController, method="POST", options={"action": "processFormData"} ), + StaticRoute("/clients", ClientController), + StaticRoute("/ban", ClientController, method="POST", options={"action": "ban"}), + StaticRoute("/unban", ClientController, method="POST", options={"action": "unban"}), StaticRoute("/login", SessionController, options={"action": "loginAction"}), StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}), StaticRoute("/logout", SessionController, options={"action": "logoutAction"}), @@ -173,12 +179,15 @@ class Router(object): return r def route(self, handler, request): - route = self.find_route(request) - if route is not None: - controller = route.controller - controller(handler, request, route.controllerOptions).handle_request() - else: + if WebSocketConnection.isIpBanned(handler.client_address[0]): handler.send_error(404, "Not Found", "The page you requested could not be found.") + else: + route = self.find_route(request) + if route is None: + handler.send_error(404, "Not Found", "The page you requested could not be found.") + else: + controller = route.controller + controller(handler, request, route.controllerOptions).handle_request() class RequestHandler(BaseHTTPRequestHandler): diff --git a/owrx/source/__init__.py b/owrx/source/__init__.py index 5f5fe7c9..fcb566c8 100644 --- a/owrx/source/__init__.py +++ b/owrx/source/__init__.py @@ -280,6 +280,9 @@ class SdrSource(ABC): def getName(self): return self.props["name"] + def getProfileName(self): + return self.getProfiles()[self.getProfileId()]["name"] + def getProps(self): return self.props diff --git a/owrx/websocket.py b/owrx/websocket.py index 55badf63..6708fbe0 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -6,6 +6,7 @@ from multiprocessing import Pipe import select import threading from abc import ABC, abstractmethod +from datetime import datetime, timedelta import logging @@ -46,6 +47,7 @@ class Handler(ABC): class WebSocketConnection(object): connections = [] + bans = {} @staticmethod def closeAll(): @@ -55,7 +57,61 @@ class WebSocketConnection(object): except: logger.exception("exception while shutting down websocket connections") + @staticmethod + def listAll(): + result = [] + for c in WebSocketConnection.connections: + entry = { + "ts" : c.startTime, + "ip" : c.handler.client_address[0], + "ban" : False + } + rx = c.messageHandler + if hasattr(rx, "sdr"): + entry["sdr"] = rx.sdr.getName() + entry["band"] = rx.sdr.getProfileName() + result.append(entry) + WebSocketConnection.cleanBans() + for ip in WebSocketConnection.bans: + result.append({ + "ts" : WebSocketConnection.bans[ip], + "ip" : ip, + "ban" : True + }) + return result + + @staticmethod + def banIp(ip: str, minutes: int): + WebSocketConnection.cleanBans() + WebSocketConnection.bans[ip] = datetime.now() + timedelta(minutes=minutes) + banned = [] + for c in WebSocketConnection.connections: + if ip == c.handler.client_address[0]: + banned.append(c) + for c in banned: + try: + c.close() + except: + logger.exception("exception while banning %s" % ip) + + @staticmethod + def unbanIp(ip: str): + if ip in WebSocketConnection.bans: + del WebSocketConnection.bans[ip] + + @staticmethod + def isIpBanned(ip: str): + return ip in WebSocketConnection.bans and datetime.now() < WebSocketConnection.bans[ip] + + @staticmethod + def cleanBans(): + now = datetime.now() + old = [ip for ip in WebSocketConnection.bans if now >= WebSocketConnection.bans[ip]] + for ip in old: + del WebSocketConnection.bans[ip] + def __init__(self, handler, messageHandler: Handler): + self.startTime = datetime.now() self.handler = handler self.handler.connection.setblocking(0) self.messageHandler = None