Source code for server.events.client

from flask import Blueprint, request, current_app

from .. import socketio, connections_users, connections_devices, start_time
from ..protections import (
    user_required_sio, admin_required_sio, user_authorized_for_device)
from ..proxy_helpers import real_remote_addr
from .update_frontend import emit_full_update

from flask_jwt_extended import get_jwt_identity
from flask_socketio import emit
import pathlib


bp = Blueprint("client_events", __name__)


# TODO: SECURITY: This completely refuses any unauthenticated connection on all
#                 channels. Otherwise they still remain connected to "/". Should
#                 we use it, or is it unnecessary?
# @socketio.on("connect")  # namespace="/" (default, allways triggered)
# def on_default_connect():
#     def check_auth(decorator):
#         try:
#             decorator(lambda: None)()
#         except ConnectionRefusedError:
#             return False
#         else:
#             return True

#     return any(map(check_auth, (
#         user_required_sio,
#         device_required_sio,
#     )))


# SECURITY: All "/client" events must be protected by @user_required_sio!
#
#           The user sends JWT token only once in the HTTP call that creates
#           the SocketIO connection. Why check it each time?
#               1. All subsequent events access the same data. The user cannot
#                  send a refereshed token. (TODO: Maybe can - via special event?)
#               2. At some point the token expires / is blacklisted. Then the
#                  connection must be terminated.
#               3. The user then reconnects with a refreshed token.
[docs] @socketio.on("connect", namespace="/client") @user_required_sio def on_connect(): ip = real_remote_addr() connections_users.add(get_jwt_identity(), request.sid, ip) current_app.logger.info("client %s connected as %s from %s", get_jwt_identity(), request.sid, ip) emit_full_update(request.sid)
# NOTE: Disconnect is the only exception for authentication. Reason: may need to # execute some code even when client is disconnected because of no auth.
[docs] @socketio.on("disconnect", namespace="/client") def on_disconnect(): if request.sid in connections_users.by_sid: ip = real_remote_addr() current_app.logger.info("client %s disconnected as %s from %s", connections_users.by_sid[request.sid], request.sid, ip) connections_users.remove(sid=request.sid)
[docs] @socketio.on("command", namespace="/client") @user_required_sio def on_command(command): """Forward command to device.""" device_id, _, cmd_id = command["command"].rpartition(".") sid_device = connections_devices.by_pubid.get(device_id, [None])[0] if not sid_device: return False, "Device is not connected." elif not user_authorized_for_device(device_id): return False, "User is not authorized for this device." else: socketio.emit( cmd_id, command["params"], namespace="/device", room=sid_device ) return True, "Ok"
[docs] @socketio.on("stats", namespace="/client") @user_required_sio def on_stats(client_stats): """Client sends its most recent stats and requests update on server stats.""" # TODO Do something? # client_stats emit("stats", { "startTime": int(start_time * 1e3), "restartPending": not pathlib.Path("logs/.server.updated").exists(), }) return True, "Ok"
[docs] @socketio.on("check_devices", namespace="/client") @admin_required_sio def on_check_devices(_): """Client (admin) triggers status check on all devices.""" socketio.emit("check_status", namespace="/device") return True, "Ok"