diff --git a/owrx/client.py b/owrx/client.py index 8ec7f4da..2c38b39a 100644 --- a/owrx/client.py +++ b/owrx/client.py @@ -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] diff --git a/owrx/connection.py b/owrx/connection.py index 0f7fe8bb..99934dfe 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -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 = { diff --git a/owrx/controllers/clients.py b/owrx/controllers/clients.py index 4fd2a5ce..d1c8f0cc 100644 --- a/owrx/controllers/clients.py +++ b/owrx/controllers/clients.py @@ -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): """.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) diff --git a/owrx/http.py b/owrx/http.py index aff0f8a4..2653d6f4 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -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) diff --git a/owrx/websocket.py b/owrx/websocket.py index 6708fbe0..1e7e7796 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -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