Merge branch 'clientinfo'

This commit is contained in:
Marat Fayzullin 2023-11-16 21:26:06 -05:00
commit 646364dac6
11 changed files with 239 additions and 8 deletions

21
htdocs/clients.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX+ Clients</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
<script src="compiled/settings.js"></script>
<meta charset="utf-8">
</head>
<body>
${header}
<div class="container">
<div class="row">
<h1 class="col-12">Clients</h1>
</div>
<div class="row client-list">
${clients}
</div>
</div>
</body>

View File

@ -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);
}
};

View File

@ -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;
});
});
}

View File

@ -37,5 +37,11 @@ ${header}
<a class="btn btn-secondary" href="features">Feature report</a>
</div>
</div>
<div class="row">
<h1 class="col-12">Clients</h1>
</div>
<div class="row client-list">
${clients}
</div>
</div>
</body>

View File

@ -9,4 +9,5 @@ $(function(){
$('#scheduler').schedulerInput();
$('.exponential-input').exponentialInput();
$('.device-log-messages').logMessages();
});
$('.client-list').clientList();
});

View File

@ -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",
],
}

View File

@ -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 """
<table class='table'>
<tr>
<th>IP Address</th>
<th>SDR Profile</th>
<th>Local Time</th>
<th>Actions</th>
</tr>
{clients}
<tr>
<td></td>
<td></td>
<td colspan="2">
ban for
<select id="ban-minutes">
<option value="15">15 minutes</option>
<option value="30">30 minutes</option>
<option value="60">1 hour</option>
<option value="180">3 hours</option>
<option value="360">6 hours</option>
<option value="720">12 hours</option>
<option value="1440">1 day</option>
</select>
</td>
</tr>
</table>
""".format(
clients="".join(ClientController.renderClient(c) for c in WebSocketConnection.listAll())
)
@staticmethod
def renderClient(c):
return "<tr><td>{0}</td><td>{1}</td><td>{2} {3}</td><td>{4}</td></tr>".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 """
<a href="https://www.geolocation.com/en_us?ip={0}#ipresult" target="_blank">{1}</a>
""".format(ip, ip)
@staticmethod
def renderButtons(c):
action = "unban" if c["ban"] else "ban"
return """
<button type="button" class="btn btn-sm btn-danger client-{0}" value="{1}">{2}</button>
""".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)

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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