Compare commits
16 Commits
97221bc1b7
...
6971548589
| Author | SHA1 | Date | |
|---|---|---|---|
|
6971548589
|
|||
|
31c51574f7
|
|||
|
b652db930f
|
|||
|
1dfddd2fc7
|
|||
|
d20ff9be6e
|
|||
|
2bbe118de6
|
|||
|
840d9ce3c1
|
|||
|
9971981f66
|
|||
|
29b4f3a2ff
|
|||
|
69bf4f1358
|
|||
|
0580a6be53
|
|||
|
563dc62624
|
|||
|
bb229dc724
|
|||
|
5510e9dd08
|
|||
|
3077a98d6f
|
|||
|
1e02da1851
|
@@ -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:
|
||||
|
||||
31
src/judas_server/web/routes/client_details.py
Normal file
31
src/judas_server/web/routes/client_details.py
Normal 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)
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
9
src/judas_server/web/templates/client_details.html
Normal file
9
src/judas_server/web/templates/client_details.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user