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"