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 """
+
+
+ | IP Address |
+ SDR Profile |
+ Local Time |
+ Actions |
+
+ {clients}
+
+ |
+ |
+
+ 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