Moved client operations to ClientRegistry, added rudimentary chat.

This commit is contained in:
Marat Fayzullin 2023-11-18 23:12:41 -05:00
parent d3e40fb76c
commit e8b9976208
5 changed files with 92 additions and 65 deletions

View File

@ -1,4 +1,5 @@
from owrx.config import Config
from datetime import datetime, timedelta
import threading
import logging
@ -10,6 +11,10 @@ class TooManyClientsException(Exception):
pass
class BannedClientException(Exception):
pass
class ClientRegistry(object):
sharedInstance = None
creationLock = threading.Lock()
@ -23,6 +28,7 @@ class ClientRegistry(object):
def __init__(self):
self.clients = []
self.bans = {}
Config.get().wireProperty("max_clients", self._checkClientCount)
super().__init__()
@ -33,7 +39,9 @@ class ClientRegistry(object):
def addClient(self, client):
pm = Config.get()
if len(self.clients) >= pm["max_clients"]:
if self.isIpBanned(client.conn.getIp()):
raise BannedClientException()
elif len(self.clients) >= pm["max_clients"]:
raise TooManyClientsException()
self.clients.append(client)
self.broadcast()
@ -52,3 +60,58 @@ class ClientRegistry(object):
for client in self.clients[new_count:]:
logger.debug("closing one connection...")
client.close()
# Broadcast chat message to all connected clients.
def broadcastChatMessage(self, sender: str, text: str):
for c in self.clients:
c.write_chat_message(sender, text)
# List all active and banned clients.
def listAll(self):
result = []
for c in self.clients:
result.append({
"ts" : c.conn.getStartTime(),
"ip" : c.conn.getIp(),
"sdr" : c.sdr.getName(),
"band" : c.sdr.getProfileName(),
"ban" : False
})
self.expireBans()
for ip in self.bans:
result.append({
"ts" : self.bans[ip],
"ip" : ip,
"ban" : True
})
return result
# Ban a client, by IP, for given number of minutes.
def banIp(self, ip: str, minutes: int):
self.expireBans()
self.bans[ip] = datetime.now() + timedelta(minutes=minutes)
banned = []
for c in self.clients:
if ip == c.conn.getIp():
banned.append(c)
for c in banned:
try:
c.close()
except:
logger.exception("exception while banning %s" % ip)
# Unban a client, by IP.
def unbanIp(self, ip: str):
if ip in self.bans:
del self.bans[ip]
# Check if given IP is banned at the moment.
def isIpBanned(self, ip: str):
return ip in self.bans and datetime.now() < self.bans[ip]
# Delete all expired bans.
def expireBans(self):
now = datetime.now()
old = [ip for ip in self.bans if now >= self.bans[ip]]
for ip in old:
del self.bans[ip]

View File

@ -3,7 +3,7 @@ from owrx.dsp import DspManager
from owrx.cpu import CpuUsageThread
from owrx.sdr import SdrService
from owrx.source import SdrSourceState, SdrClientClass, SdrSourceEventClient
from owrx.client import ClientRegistry, TooManyClientsException
from owrx.client import ClientRegistry, TooManyClientsException, BannedClientException
from owrx.feature import FeatureDetector
from owrx.version import openwebrx_version
from owrx.bands import Bandplan
@ -166,6 +166,10 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.write_backoff_message("Too many clients")
self.close()
raise
except BannedClientException:
self.write_backoff_message("Client IP banned")
self.close()
raise
self.setupGlobalConfig()
self.stack = self.setupStack()
@ -480,6 +484,10 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def write_backoff_message(self, reason):
self.send({"type": "backoff", "reason": reason})
def write_chat_message(self, sender, text):
logger.debug("Sending {0}".format({"type": "chat_message", "sender": sender, "text": text}))
self.send({"type": "chat_message", "sender": sender, "text": text})
def write_modes(self, modes):
def to_json(m):
res = {

View File

@ -1,7 +1,7 @@
from owrx.controllers.admin import AuthorizationMixin
from owrx.controllers.template import WebpageController
from owrx.breadcrumb import Breadcrumb, BreadcrumbItem, BreadcrumbMixin
from owrx.websocket import WebSocketConnection
from owrx.client import ClientRegistry
import json
import re
@ -48,7 +48,7 @@ class ClientController(AuthorizationMixin, WebpageController):
</tr>
</table>
""".format(
clients="".join(ClientController.renderClient(c) for c in WebSocketConnection.listAll())
clients="".join(ClientController.renderClient(c) for c in ClientRegistry.getSharedInstance().listAll())
)
@staticmethod
@ -81,9 +81,10 @@ class ClientController(AuthorizationMixin, WebpageController):
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)
ClientRegistry.getSharedInstance().banIp(data["ip"], mins)
self.send_response("{}", content_type="application/json", code=200)
except:
except Exception as e:
logger.debug("ban(): " + str(e))
self.send_response("{}", content_type="application/json", code=400)
def unban(self):
@ -91,7 +92,8 @@ class ClientController(AuthorizationMixin, WebpageController):
data = json.loads(self.get_body().decode("utf-8"))
if "ip" in data:
logger.info("Unbanning {0}".format(data["ip"]))
WebSocketConnection.unbanIp(data["ip"])
ClientRegistry.getSharedInstance().unbanIp(data["ip"])
self.send_response("{}", content_type="application/json", code=200)
except:
except Exception as e:
logger.debug("unban(): " + str(e))
self.send_response("{}", content_type="application/json", code=400)

View File

@ -24,7 +24,8 @@ 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.controllers.chat import ChatController
from owrx.client import ClientRegistry
from owrx.storage import Storage
from http.server import BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
@ -171,6 +172,7 @@ class Router(object):
StaticRoute("/pwchange", ProfileController, method="POST", options={"action": "processPwChange"}),
StaticRoute("/imageupload", ImageUploadController),
StaticRoute("/imageupload", ImageUploadController, method="POST", options={"action": "processImage"}),
StaticRoute("/msgsend", ChatController, method="POST", options={"action": "send"}),
]
def find_route(self, request):
@ -179,7 +181,7 @@ class Router(object):
return r
def route(self, handler, request):
if WebSocketConnection.isIpBanned(handler.client_address[0]):
if ClientRegistry.getSharedInstance().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)

View File

@ -6,7 +6,7 @@ from multiprocessing import Pipe
import select
import threading
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from datetime import datetime
import logging
@ -47,7 +47,6 @@ class Handler(ABC):
class WebSocketConnection(object):
connections = []
bans = {}
@staticmethod
def closeAll():
@ -57,59 +56,6 @@ 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
@ -352,3 +298,9 @@ class WebSocketConnection(object):
def sendPong(self):
header = self.get_header(0, OPCODE_PONG)
self._sendBytes(header)
def getIp(self):
return self.handler.client_address[0]
def getStartTime(self):
return self.startTime