16 Commits

Author SHA1 Message Date
6971548589 feat(web_server.py): use client_details blueprint 2026-02-28 20:44:14 +01:00
31c51574f7 feat(client_details.py): add client_details route 2026-02-28 20:43:57 +01:00
b652db930f feat(style.css): style no-connection-icon and client list 2026-02-28 20:42:29 +01:00
1dfddd2fc7 feat(panel.html): add a side panel client list 2026-02-28 20:42:05 +01:00
d20ff9be6e feat(panel.html): add a no connection icon to header 2026-02-28 20:41:35 +01:00
2bbe118de6 feat: rename details.html -> client_details.html 2026-02-28 20:40:57 +01:00
840d9ce3c1 refactor(login.html): move elements from main to #content 2026-02-28 20:40:16 +01:00
9971981f66 refactor(index.html): move elements from main to #content 2026-02-28 20:40:05 +01:00
29b4f3a2ff feat(css/style.css): add .button:active color 2026-02-28 20:39:29 +01:00
69bf4f1358 feat(css/style.css): add #content styling, make main's flex column to fit aside 2026-02-28 20:39:06 +01:00
0580a6be53 chore(css/style.css): remove transition from button hover bg color 2026-02-28 20:37:29 +01:00
563dc62624 feat(css/style.css): include flaticon icons 2026-02-28 20:36:29 +01:00
bb229dc724 feat(css/style.css): make UI more compact 2026-02-28 20:36:10 +01:00
5510e9dd08 refactor(backend_server.py): rewrite _handle_connection() to minimize indents 2026-02-28 20:35:09 +01:00
3077a98d6f feat(backend_server.py): add send_close() method 2026-02-28 20:34:16 +01:00
1e02da1851 refactor(backend_server.py): rename _send_ack() -> send_ack() 2026-02-28 19:07:11 +01:00
9 changed files with 239 additions and 144 deletions

View File

@@ -62,7 +62,7 @@ class BackendServer:
)
time.sleep(1)
def _send_ack(self, client: Client, target_id: str) -> None:
def send_ack(self, client: Client, target_id: str) -> None:
"""Send an ACK message to a client.
Args:
@@ -73,6 +73,16 @@ class BackendServer:
self.logger.info(f"[>] Sending ACK to {client}")
client.outbound += ack
def send_close(self, client: Client) -> None:
"""Send a CLOSE message to a client.
Args:
client (Client): The client to send the CLOSE message to.
"""
close_msg: bytes = Message.close().to_bytes()
self.logger.info(f"[>] Sending CLOSE to {client}")
client.outbound += close_msg
def _accept_connection(self, sock: socket.socket) -> None:
"""Accept a new client connection.
@@ -96,7 +106,6 @@ class BackendServer:
sock (socket.socket): The client socket to disconnect.
"""
self.logger.info(f"[-] Disconnecting {client}...")
self.logger.debug("[*] Sending DNR message...")
try:
self.selector.unregister(client.socket)
@@ -155,11 +164,21 @@ class BackendServer:
try:
if mask & selectors.EVENT_READ:
self._receive_inbound(sock, client)
if client.inbound:
if not client.inbound:
self._disconnect(client)
return
if client.mac_id is None:
# expect HELLO message
try:
msg = Message.from_bytes(client.inbound)
except Exception as e:
self.logger.error(
f"Failed to parse HELLO message from {client}: {e}"
)
self._disconnect(client)
return
if (
msg.category == Category.CONTROL
and msg.action == ControlAction.HELLO
@@ -171,30 +190,21 @@ class BackendServer:
and self.clients[client.mac_id].status
== "connected"
):
old_client: Client = self.clients[
client.mac_id
]
old_client: Client = self.clients[client.mac_id]
self.logger.warning(
f"Client {client.mac_id} is already connected from {old_client.addr}, disconnecting old client..."
)
self._disconnect(old_client)
self.send_close(old_client)
# self._disconnect(old_client)
# TODO: tell client not to reconnect
self.clients[client.mac_id] = client
self.logger.info(
f"[+] Registered new client {client}"
)
self.logger.info(f"[+] Registered new client {client}")
else:
self.logger.error(
f"Expected HELLO message from {client}, got {msg}"
)
self._disconnect(client)
return
except Exception as e:
self.logger.error(
f"Failed to parse HELLO message from {client}: {e}"
)
self._disconnect(client)
return
while b"\n" in client.inbound:
line, client.inbound = client.inbound.split(b"\n", 1)
@@ -205,8 +215,7 @@ class BackendServer:
msg = Message.from_bytes(line)
self.logger.info(f"[.] Parsed message {msg.id}")
if msg.ack_required:
self._send_ack(client, target_id=msg.id)
self.send_ack(client, target_id=msg.id)
except Exception as e:
self.logger.error(
f"Failed to parse message from {client}: {e}"
@@ -214,11 +223,7 @@ class BackendServer:
self._disconnect(client)
return
else:
self._disconnect(client)
if mask & selectors.EVENT_WRITE:
if client.outbound:
if mask & selectors.EVENT_WRITE and client.outbound:
self._send_outbound(sock, client)
except ConnectionResetError as e:

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
import flask
import flask_login
if TYPE_CHECKING:
from judas_server.backend import BackendServer
from judas_server.backend.client import Client
bp: flask.Blueprint = flask.Blueprint(
"client_details", __name__, url_prefix="/client"
)
@bp.route("/<client_id>")
@flask_login.login_required
def client_details(client_id: str) -> str:
"""Renders the client details page for a specific client.
Args:
client_id: The ID of the client to display details for.
"""
backend: BackendServer = flask.current_app.config["BACKEND"]
client: Client | None = backend.clients.get(client_id)
if not client:
flask.abort(404, description="Client not found")
return flask.render_template("client_details.html", client=client)

View File

@@ -1,3 +1,9 @@
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-regular-rounded/css/uicons-regular-rounded.css");
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-regular-straight/css/uicons-regular-straight.css");
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-solid-rounded/css/uicons-solid-rounded.css");
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-solid-straight/css/uicons-solid-straight.css");
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-bold-rounded/css/uicons-bold-rounded.css");
@import "palette.css";
* {
@@ -12,6 +18,10 @@ body {
color: var(--ctp-text);
}
.fi {
vertical-align: middle;
}
input {
font-family: inherit;
color: #eceff4;
@@ -38,16 +48,26 @@ header {
align-items: center;
background-color: var(--ctp-mantle);
/* color: var(--ctp-text); */
padding: 1rem;
padding: 0.5rem 1rem;
}
main {
display: flex;
flex-grow: 1;
}
aside {
width: 20rem;
background-color: var(--ctp-base);
border-right: 2px solid var(--ctp-mantle);
}
#content {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
/* background-color: var(--ctp-base); */
flex-grow: 1;
padding: 1rem;
}
header a {
@@ -57,13 +77,12 @@ header a {
.button {
display: inline-block;
padding: 0.5rem 1rem;
padding: 0.25rem 0.5rem;
background-color: var(--ctp-lavender);
color: var(--ctp-base);
border: none;
border-radius: 0.25rem;
text-decoration: none;
transition: 0.3s ease-in-out;
}
/* .button a:hover { */
@@ -76,6 +95,10 @@ header a {
cursor: pointer;
}
.button:active {
background-color: var(--ctp-sapphire);
}
.center {
text-align: center;
}
@@ -200,3 +223,31 @@ header a {
border-radius: 1rem;
opacity: 0.8;
}
#no-connection-icon {
font-size: 1.5rem;
vertical-align: middle;
color: var(--ctp-red);
}
ul#client-list {
list-style: none;
display: flex;
flex-direction: column;
}
ul#client-list li {
padding: 0 0.5rem;
cursor: default;
}
ul#client-list li:hover {
background-color: var(--ctp-surface0);
}
ul#client-list li a {
display: block;
color: var(--ctp-text);
text-decoration: none;
transition: 0.1s ease-in-out;
}

View File

@@ -0,0 +1,9 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Details for client {{ client.id }}</title>
</head>
<body></body>
</html>

View File

@@ -1,44 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>judas panel - details</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/style.css') }}"
/>
</head>
<body>
<div id="wrapper">
<header>
<h1><a href="{{ url_for('index.index') }}">judas</a></h1>
<p><a class="button" href="{{ url_for('logout') }}">Logout</a></p>
</header>
<main>
<div class="button-container">
<p><a href="{{ url_for('panel') }}" class="link">Back to Panel</a></p>
<h2>{{ pc.id }}</h2>
<p>
<a href="{{ url_for('stream', pc_id=pc.id) }}" class="link"
>View Stream</a
>
</p>
</div>
<table class="center-table details-table">
<tbody>
<tr>
<td>Status</td>
<td>{{ pc.status }}</td>
</tr>
<tr>
<td>Last Seen</td>
<td>{{ pc.last_seen }}</td>
</tr>
</tbody>
</table>
</main>
</div>
</body>
</html>

View File

@@ -9,7 +9,7 @@
<body>
<div id="wrapper">
<header>
<h1><a href="{{ url_for('index.index') }}">judas</a></h1>
<h2><a href="{{ url_for('index.index') }}">judas</a></h2>
{% if logged %}
<p><a class="button" href="{{ url_for('auth.logout') }}">Logout</a></p>
{% else %}
@@ -17,17 +17,19 @@
{% endif %}
</header>
<main class="center">
<div id="content">
<div>
<p>Welcome to</p>
<h2 id="typing-text" style="font-size: 3rem;">judas</h2>
<p>a remote PC fleet management system</p>
</div>
<p style="color: #bf616a;"><strong>Notice:</strong> Please use this system responsibly and in accordance with all applicable laws and organizational policies.</p>
<p style="color: var(--ctp-red);"><strong>Notice:</strong> Please use this system responsibly and in accordance with all applicable laws and organizational policies.</p>
{% if logged %}
<p><a class="button" href="{{ url_for('panel.panel') }}">Go to panel</a></p>
{% else %}
<p>Please <a href="{{ url_for('auth.login')}}" class="link">log in</a> to manage your remote PCs.</p>
{% endif %}
</div>
</main>
</div>
<script>

View File

@@ -9,9 +9,10 @@
<body>
<div id="wrapper">
<header>
<h1><a href="{{ url_for('index.index') }}">judas</a></h1>
<h2><a href="{{ url_for('index.index') }}">judas</a></h2>
</header>
<main>
<div id="content">
<h1>Login</h1>
<form method="post" class="center">
<label for="password">Password:</label>
@@ -26,6 +27,7 @@
</div>
{% endif %}
</form>
</div>
</main>
</div>
</body>

View File

@@ -27,11 +27,13 @@
socket.on("connect", () => {
console.log("Connected to server");
$("#no-connection-icon").hide();
showNotify("Connected to server");
});
socket.on("disconnect", () => {
console.log("Disconnected from server");
$("#no-connection-icon").show();
showNotify("Disconnected from server");
});
@@ -42,6 +44,26 @@
null,
2,
);
// Update client list
const clientList = $("#client-list");
clientList.empty();
Object.entries(data).forEach(([clientId, client]) => {
const listItem = document.createElement("li");
const iconElement = document.createElement("i");
iconElement.classList.add("fi", "fi-sr-play");
iconElement.style.color = "var(--ctp-green)";
listItem.appendChild(iconElement);
const spanElement = document.createElement("a");
spanElement.appendChild(document.createTextNode(clientId));
spanElement.href = `#${clientId}`;
listItem.appendChild(spanElement);
clientList.append(listItem);
});
});
});
</script>
@@ -49,12 +71,22 @@
<body>
<div id="wrapper">
<header>
<h1><a href="{{ url_for('index.index') }}">judas</a></h1>
<p><a class="button" href="{{ url_for('auth.logout') }}">Logout</a></p>
<h2><a href="{{ url_for('index.index') }}">judas</a></h2>
<p>
<span id="no-connection-icon" style="display: none">
<i class="fi fi-rr-link-slash"></i>
</span>
<a class="button" href="{{ url_for('auth.logout') }}">Logout</a>
</p>
</header>
<div id="notify"></div>
<main>
<aside>
<ul id="client-list"></ul>
</aside>
<div id="content">
<pre id="data"></pre>
</div>
</main>
</div>
</body>

View File

@@ -53,12 +53,19 @@ class JudasWebServer:
def init_routes(self) -> None:
self.logger.debug("Initializing routes...")
from judas_server.web.routes import api, auth_bp, index_bp, panel_bp
from judas_server.web.routes import (
api,
auth_bp,
index_bp,
panel_bp,
client_details,
)
self.app.register_blueprint(index_bp)
self.app.register_blueprint(auth_bp)
self.app.register_blueprint(panel_bp)
self.app.register_blueprint(api.bp)
self.app.register_blueprint(client_details.bp)
api.emit_polled_data(self.app, self.socketio)
def run(self, host: str = "0.0.0.0", port: int = 5000) -> None: