From 6a6e854caf18fc01b6f38badbffe38272c73508c Mon Sep 17 00:00:00 2001 From: Hemna Date: Wed, 19 Jul 2023 11:27:34 -0400 Subject: [PATCH] Removed flask-classful from webchat This patch removed the dependency on flask-classful. This required making all of the flask web routing non class based. This patch also changes the aprsis class to allow retries for failed connections when the aprsis servers are full and not responding to login requests. --- aprsd/client.py | 3 +- aprsd/clients/aprsis.py | 17 +-- aprsd/cmds/webchat.py | 216 ++++++++++++++++++------------------- aprsd/wsgi.py | 7 +- tests/cmds/test_webchat.py | 4 +- 5 files changed, 127 insertions(+), 120 deletions(-) diff --git a/aprsd/client.py b/aprsd/client.py index 9c9522d..08df034 100644 --- a/aprsd/client.py +++ b/aprsd/client.py @@ -152,9 +152,10 @@ class APRSISClient(Client): except LoginError as e: LOG.error(f"Failed to login to APRS-IS Server '{e}'") connected = False - raise e + time.sleep(backoff) except Exception as e: LOG.error(f"Unable to connect to APRS-IS server. '{e}' ") + connected = False time.sleep(backoff) backoff = backoff * 2 continue diff --git a/aprsd/clients/aprsis.py b/aprsd/clients/aprsis.py index 5ba53b1..aa1a9ba 100644 --- a/aprsd/clients/aprsis.py +++ b/aprsd/clients/aprsis.py @@ -112,23 +112,23 @@ class Aprsdis(aprslib.IS): self._sendall(login_str) self.sock.settimeout(5) test = self.sock.recv(len(login_str) + 100) + self.logger.debug("Server: '%s'", test) if is_py3: test = test.decode("latin-1") test = test.rstrip() - self.logger.debug("Server: %s", test) + self.logger.debug("Server: '%s'", test) - a, b, callsign, status, e = test.split(" ", 4) + if not test: + raise LoginError(f"Server Response Empty: '{test}'") + + _, _, callsign, status, e = test.split(" ", 4) s = e.split(",") if len(s): server_string = s[0].replace("server ", "") else: server_string = e.replace("server ", "") - self.logger.info(f"Connected to {server_string}") - self.server_string = server_string - stats.APRSDStats().set_aprsis_server(server_string) - if callsign == "": raise LoginError("Server responded with empty callsign???") if callsign != self.callsign: @@ -141,6 +141,10 @@ class Aprsdis(aprslib.IS): else: self.logger.info("Login successful") + self.logger.info(f"Connected to {server_string}") + self.server_string = server_string + stats.APRSDStats().set_aprsis_server(server_string) + except LoginError as e: self.logger.error(str(e)) self.close() @@ -148,6 +152,7 @@ class Aprsdis(aprslib.IS): except Exception as e: self.close() self.logger.error(f"Failed to login '{e}'") + self.logger.exception(e) raise LoginError("Failed to login") def consumer(self, callback, blocking=True, immortal=False, raw=False): diff --git a/aprsd/cmds/webchat.py b/aprsd/cmds/webchat.py index c0e719d..fbdc87d 100644 --- a/aprsd/cmds/webchat.py +++ b/aprsd/cmds/webchat.py @@ -12,7 +12,6 @@ import click import flask from flask import request from flask.logging import default_handler -import flask_classful from flask_httpauth import HTTPBasicAuth from flask_socketio import Namespace, SocketIO from oslo_config import cfg @@ -31,9 +30,16 @@ from aprsd.utils import objectstore, trace CONF = cfg.CONF LOG = logging.getLogger("APRSD") auth = HTTPBasicAuth() -users = None +users = {} socketio = None +flask_app = flask.Flask( + "aprsd", + static_url_path="/static", + static_folder="web/chat/static", + template_folder="web/chat/templates", +) + def signal_handler(sig, frame): @@ -174,106 +180,108 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread): ) -class WebChatFlask(flask_classful.FlaskView): +def set_config(): + global users - def set_config(self): - global users - self.users = {} - user = CONF.admin.user - self.users[user] = generate_password_hash(CONF.admin.password) - users = self.users - def _get_transport(self, stats): - if CONF.aprs_network.enabled: - transport = "aprs-is" - aprs_connection = ( - "APRS-IS Server: " - "{}".format(stats["stats"]["aprs-is"]["server"]) - ) - else: - # We might be connected to a KISS socket? - if client.KISSClient.is_enabled(): - transport = client.KISSClient.transport() - if transport == client.TRANSPORT_TCPKISS: - aprs_connection = ( - "TCPKISS://{}:{}".format( - CONF.kiss_tcp.host, - CONF.kiss_tcp.port, - ) - ) - elif transport == client.TRANSPORT_SERIALKISS: - # for pep8 violation - aprs_connection = ( - "SerialKISS://{}@{} baud".format( - CONF.kiss_serial.device, - CONF.kiss_serial.baudrate, - ), - ) - - return transport, aprs_connection - - @auth.login_required - def index(self): - ua_str = request.headers.get("User-Agent") - # this takes about 2 seconds :( - user_agent = ua_parse(ua_str) - LOG.debug(f"Is mobile? {user_agent.is_mobile}") - stats = self._stats() - - if user_agent.is_mobile: - html_template = "mobile.html" - else: - html_template = "index.html" - - # For development - # html_template = "mobile.html" - - LOG.debug(f"Template {html_template}") - - transport, aprs_connection = self._get_transport(stats) - LOG.debug(f"transport {transport} aprs_connection {aprs_connection}") - - stats["transport"] = transport - stats["aprs_connection"] = aprs_connection - LOG.debug(f"initial stats = {stats}") - - return flask.render_template( - html_template, - initial_stats=stats, - aprs_connection=aprs_connection, - callsign=CONF.callsign, - version=aprsd.__version__, +def _get_transport(stats): + if CONF.aprs_network.enabled: + transport = "aprs-is" + aprs_connection = ( + "APRS-IS Server: " + "{}".format(stats["stats"]["aprs-is"]["server"]) ) + else: + # We might be connected to a KISS socket? + if client.KISSClient.is_enabled(): + transport = client.KISSClient.transport() + if transport == client.TRANSPORT_TCPKISS: + aprs_connection = ( + "TCPKISS://{}:{}".format( + CONF.kiss_tcp.host, + CONF.kiss_tcp.port, + ) + ) + elif transport == client.TRANSPORT_SERIALKISS: + # for pep8 violation + aprs_connection = ( + "SerialKISS://{}@{} baud".format( + CONF.kiss_serial.device, + CONF.kiss_serial.baudrate, + ), + ) - @auth.login_required - def send_message_status(self): - LOG.debug(request) - msgs = SentMessages() - info = msgs.get_all() - return json.dumps(info) + return transport, aprs_connection - def _stats(self): - stats_obj = stats.APRSDStats() - now = datetime.datetime.now() - time_format = "%m-%d-%Y %H:%M:%S" - stats_dict = stats_obj.stats() - # Webchat doesnt need these - del stats_dict["aprsd"]["watch_list"] - del stats_dict["aprsd"]["seen_list"] - # del stats_dict["email"] - # del stats_dict["plugins"] - # del stats_dict["messages"] +@auth.login_required +@flask_app.route("/") +def index(): + ua_str = request.headers.get("User-Agent") + # this takes about 2 seconds :( + user_agent = ua_parse(ua_str) + LOG.debug(f"Is mobile? {user_agent.is_mobile}") + stats = _stats() - result = { - "time": now.strftime(time_format), - "stats": stats_dict, - } + if user_agent.is_mobile: + html_template = "mobile.html" + else: + html_template = "index.html" - return result + # For development + # html_template = "mobile.html" - def stats(self): - return json.dumps(self._stats()) + LOG.debug(f"Template {html_template}") + + transport, aprs_connection = _get_transport(stats) + LOG.debug(f"transport {transport} aprs_connection {aprs_connection}") + + stats["transport"] = transport + stats["aprs_connection"] = aprs_connection + LOG.debug(f"initial stats = {stats}") + + return flask.render_template( + html_template, + initial_stats=stats, + aprs_connection=aprs_connection, + callsign=CONF.callsign, + version=aprsd.__version__, + ) + + +@auth.login_required +@flask_app.route("//send-message-status") +def send_message_status(): + LOG.debug(request) + msgs = SentMessages() + info = msgs.get_all() + return json.dumps(info) + + +def _stats(): + stats_obj = stats.APRSDStats() + now = datetime.datetime.now() + + time_format = "%m-%d-%Y %H:%M:%S" + stats_dict = stats_obj.stats() + # Webchat doesnt need these + del stats_dict["aprsd"]["watch_list"] + del stats_dict["aprsd"]["seen_list"] + # del stats_dict["email"] + # del stats_dict["plugins"] + # del stats_dict["messages"] + + result = { + "time": now.strftime(time_format), + "stats": stats_dict, + } + + return result + + +@flask_app.route("/stats") +def get_stats(): + return json.dumps(_stats()) class SendMessageNamespace(Namespace): @@ -377,21 +385,9 @@ def setup_logging(flask_app, loglevel, quiet): @trace.trace def init_flask(loglevel, quiet): - global socketio + global socketio, flask_app - flask_app = flask.Flask( - "aprsd", - static_url_path="/static", - static_folder="web/chat/static", - template_folder="web/chat/templates", - ) setup_logging(flask_app, loglevel, quiet) - server = WebChatFlask() - server.set_config() - flask_app.route("/", methods=["GET"])(server.index) - flask_app.route("/stats", methods=["GET"])(server.stats) - # flask_app.route("/send-message", methods=["GET"])(server.send_message) - flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status) socketio = SocketIO( flask_app, logger=False, engineio_logger=False, @@ -407,7 +403,7 @@ def init_flask(loglevel, quiet): "/sendmsg", ), ) - return socketio, flask_app + return socketio # main() ### @@ -448,6 +444,8 @@ def webchat(ctx, flush, port): LOG.info(f"APRSD Started version: {aprsd.__version__}") CONF.log_opt_values(LOG, logging.DEBUG) + user = CONF.admin.user + users[user] = generate_password_hash(CONF.admin.password) # Initialize the client factory and create # The correct client object ready for use @@ -466,7 +464,7 @@ def webchat(ctx, flush, port): packets.WatchList() packets.SeenList() - (socketio, app) = init_flask(loglevel, quiet) + socketio = init_flask(loglevel, quiet) rx_thread = rx.APRSDPluginRXThread( packet_queue=threads.packet_queue, ) @@ -482,7 +480,7 @@ def webchat(ctx, flush, port): keepalive.start() LOG.info("Start socketio.run()") socketio.run( - app, + flask_app, ssl_context="adhoc", host=CONF.admin.web_ip, port=port, diff --git a/aprsd/wsgi.py b/aprsd/wsgi.py index c68511b..17ffbe3 100644 --- a/aprsd/wsgi.py +++ b/aprsd/wsgi.py @@ -187,6 +187,7 @@ def index(): plugin_count=plugin_count, ) + @auth.login_required def messages(): track = packets.PacketTrack() @@ -197,9 +198,10 @@ def messages(): return flask.render_template("messages.html", messages=json.dumps(msgs)) + @auth.login_required @app.route("/packets") -def packets(): +def get_packets(): LOG.debug("/packets called") packet_list = aprsd_rpc_client.RPCClient().get_packet_list() if packet_list: @@ -212,6 +214,7 @@ def packets(): else: return json.dumps([]) + @auth.login_required @app.route("/plugins") def plugins(): @@ -221,6 +224,7 @@ def plugins(): return "reloaded" + @auth.login_required @app.route("/save") def save(): @@ -230,7 +234,6 @@ def save(): return json.dumps({"messages": "saved"}) - class LogUpdateThread(threads.APRSDThread): def __init__(self): diff --git a/tests/cmds/test_webchat.py b/tests/cmds/test_webchat.py index 3a0103d..53deb0d 100644 --- a/tests/cmds/test_webchat.py +++ b/tests/cmds/test_webchat.py @@ -39,9 +39,9 @@ class TestSendMessageCommand(unittest.TestCase): CliRunner() self.config_and_init() - socketio, flask_app = webchat.init_flask("DEBUG", False) + socketio = webchat.init_flask("DEBUG", False) self.assertIsInstance(socketio, flask_socketio.SocketIO) - self.assertIsInstance(flask_app, flask.Flask) + self.assertIsInstance(webchat.flask_app, flask.Flask) @mock.patch("aprsd.packets.tracker.PacketTrack.remove") @mock.patch("aprsd.cmds.webchat.socketio")