78 Commits

Author SHA1 Message Date
68bb05a482 feat: move modules from judas_server/backend to judas_server/ 2026-03-12 21:55:07 +01:00
f49c33974d chore(web): remove web code 2026-03-12 21:47:23 +01:00
github-actions[bot]
2a85084c8f chore(release): 0.7.0 2026-03-12 20:26:12 +00:00
6cc6dc5b42 Merge pull request 'chore(release): 0.7.0' (#18) from release/0.7.0 into main
Reviewed-on: #18
2026-03-12 20:17:15 +00:00
a4c07d9d2d Merge pull request 'feat: improve templates' (#17) from feat/improve-templates into develop
Reviewed-on: #17
2026-03-12 20:16:05 +00:00
a697ae6661 fix(ack_handler.py): fix error by importing annotations 2026-03-12 21:14:34 +01:00
acbcb3364e feat(web_server.py): add debug parameter to JudasWebServer.run() 2026-03-12 20:43:29 +01:00
762256c3cd fix(client_details.html): fix up code for AJAX loading 2026-03-12 20:42:45 +01:00
c442dca520 refactor(panel.html): use new base template 2026-03-12 20:42:15 +01:00
12c5de9f11 refactor(index.html): use new base template 2026-03-12 20:42:03 +01:00
aa562a0eab chore(base.html): remove leftover code 2026-03-12 20:41:33 +01:00
865112c823 chore(style.css): adjust styles 2026-03-12 20:41:08 +01:00
1d764bd77d feat(header.html): add header template 2026-03-12 20:34:30 +01:00
6f4bc3aa0f feat(base.html): add base template 2026-03-12 20:34:17 +01:00
14ea136fbb feat(panel.js): move js from inline to separate script 2026-03-12 20:33:38 +01:00
78f9508753 feat(panel.html): use ?query instead of #hash for client selection 2026-03-09 21:19:05 +01:00
github-actions[bot]
11bf344cb5 chore(release): 0.7.0-dev.1 2026-03-08 19:47:31 +00:00
5ee8eca11b Merge pull request 'chore(release): 0.7.0-dev.1' (#16) from release/0.7.0-dev.1 into main
Reviewed-on: #16
2026-03-08 19:46:56 +00:00
f18935f793 Merge pull request 'feat/add-basic-telemetry' (#15) from feat/add-basic-telemetry into develop
Reviewed-on: #15
2026-03-08 19:43:13 +00:00
d7b136851b feat(client_details.html): add temporary initial telemetry display 2026-03-08 20:38:59 +01:00
7e9a9e6eed feat(backend_server.py): add initial_telemetry support 2026-03-08 20:38:32 +01:00
6ed03ab74d feat(client.py): add Client.initial_telemetry property 2026-03-08 20:38:01 +01:00
40c08d0169 feat(initial_handler.py): add handler for TELEMETRY/INTIIAL msgs 2026-03-08 20:37:26 +01:00
3d13d24116 build(uv.lock): update judas_protocol to 0.9.1 2026-03-05 22:29:48 +01:00
28b57b6964 build(uv.lock): update judas_protocol to 0.9.0 2026-03-05 21:26:34 +01:00
github-actions[bot]
6a024bbfc6 chore(release): 0.6.0 2026-03-05 19:59:49 +00:00
e5ca1066fd Merge pull request 'chore(release): 0.6.0' (#14) from release/0.6.0 into main
Reviewed-on: #14
2026-03-05 19:59:05 +00:00
61be674258 chore: remove cache/ directory 2026-03-05 20:52:41 +01:00
72d51b451f feat(backend_server.py): add notice to known_clients.yaml 2026-03-05 20:52:18 +01:00
efbf99f356 refactor(backend_server.py): if known_clients.yaml not present, call _save_known_clients() 2026-03-05 20:51:53 +01:00
f46d27164b chore(.gitignore): ignore config/known_clients.yaml as it's generated automatically 2026-03-05 20:38:26 +01:00
f13f243b5b chore(__main__.py): set logging level for werkzeug to WARNING 2026-03-05 13:14:08 +01:00
fbb75c263c fix(backend_server.py): fix double-connection handling 2026-03-05 13:05:48 +01:00
0e7c948668 feat(config/): add config/ directory 2026-03-04 09:23:54 +01:00
35899d3668 refactor(backend_server.py): move known_clients.yaml to config/ 2026-03-04 09:23:21 +01:00
88d349090e fix(backend_server.py): fix double disconnect if client.inbound empty 2026-03-03 22:20:36 +01:00
332238b403 chore(__init__.py): correct version 2026-03-03 21:55:56 +01:00
e54cc479b5 chore(handler/__init__.py): add AckHandler to __all__ 2026-03-03 20:56:18 +01:00
97fc17fbb3 feat(hello_handler.py): remove client from pending_hello if recv'd HELLO 2026-03-03 20:55:39 +01:00
f5b14fc610 feat(backend_server.py): add timeout on HELLO 2026-03-03 20:55:39 +01:00
bf1ad0ead0 feat(ack_handler.py): add handling for ACKs 2026-03-03 20:55:39 +01:00
a9bace8aca feat(backend_server.py): add ACK_TIMEOUT constant 2026-03-03 20:42:16 +01:00
c88e39c735 feat(backend_server.py): track message ACKs and resend if no ACK recv'd within 5 seconds 2026-03-03 20:39:53 +01:00
e308a07dab fix(backend_server.py): call _initialize_handlers() on init 2026-03-03 20:38:55 +01:00
dafe418916 feat(backend_server.py): add warning if received an unknown message (no handler) 2026-03-03 20:18:24 +01:00
c64a258243 build(uv.lock): update judas_protocol to 0.8.0 2026-03-03 19:34:32 +01:00
ead2224066 fix(backend_server.py): do not disconnect a client if Exception raised on msg handling 2026-03-03 18:56:34 +01:00
ee381414a9 chore(backend_server.py): remove redundant HELLO msg handling 2026-03-03 18:55:45 +01:00
0ed478a88e feat(backend_server.py): implement message handling 2026-03-03 18:55:24 +01:00
6446fe883c fix(backend_server.py): check if client to disconnect has an open socket 2026-03-03 18:54:45 +01:00
ec58a5257a chore(handler/__init__.py): add module init 2026-03-03 18:53:05 +01:00
c952413d91 feat(hello_handler.py): add HELLO message handler 2026-03-03 18:52:39 +01:00
882c8780e1 feat(base_handler.py): add BaseHandler class for message handling 2026-03-03 18:52:13 +01:00
d3f68d3baf chore(backend/__init__.py): add Client and ClientStatus to __all__ 2026-03-03 17:46:51 +01:00
62acc4b181 style(client.py): correct property typing 2026-03-03 17:46:06 +01:00
faecc38261 feat(client_status.py): move ClientStatus enum to own module 2026-03-03 17:45:01 +01:00
3eb681e233 refactor(backend_server.py): move loading known clients to its own method 2026-03-03 17:43:19 +01:00
bda10a6248 chore(cache/): add cache/ directory 2026-03-03 17:41:48 +01:00
fa2da207a9 refactor(backend_server.py): refactor calls to Message class constructors after protocol changes 2026-03-01 20:17:05 +01:00
f41a7774ec build(uv.lock): update judas_protocol to 0.7.0 2026-03-01 20:14:03 +01:00
github-actions[bot]
2a8ac307d5 chore(release): 0.5.0 2026-02-28 22:49:49 +00:00
3911afff26 Merge pull request 'chore(release): 0.5.0' (#13) from release/0.5.0 into main
Reviewed-on: #13
2026-02-28 22:48:50 +00:00
7f904fdcd5 build(uv.lock): add pyyaml to depedencies 2026-02-28 23:44:58 +01:00
956da024c3 feat(style.css): make client list elements flexboxes 2026-02-28 23:44:43 +01:00
f11b442ece chore(style.css): style no connection message correctly 2026-02-28 23:44:03 +01:00
f54d974745 style(style.css): remove commented-out code 2026-02-28 23:43:37 +01:00
54eec657a5 feat(style.css): use monospaced font only for heading 2026-02-28 23:43:15 +01:00
1900bf46cc feat(client_details.html): add basic client details 2026-02-28 23:42:12 +01:00
c5771dc371 feat(panel.html): use client_details.html after clicking on client on sidebar 2026-02-28 23:41:30 +01:00
de9240e6e0 feat(backend_server.py): set client status to STALE if offline for >24h 2026-02-28 23:40:03 +01:00
b1656cdfa9 feat(client.py): redo ClientStatuses adding PENDING and STALE 2026-02-28 23:39:07 +01:00
563de5aa19 feat(backend_server.py): add known_clients.yaml file for storing clients between server restarts 2026-02-28 23:30:20 +01:00
72a74334e7 build(pyproject.toml): add pyyaml to depedencies 2026-02-28 21:31:57 +01:00
5ca5d9fcf4 chore(.vscode/launch.json): set cwd to workspace folder 2026-02-28 21:31:37 +01:00
f01eaecba3 build(uv.lock): update judas_protocol to 0.6.0 2026-02-28 20:58:09 +01:00
a5e1ba88ae refactor: adapt for client.id 2026-02-28 20:57:59 +01:00
1e38be5ec5 refactor(backend_server.py): adapt client's id after refactor 2026-02-28 20:48:17 +01:00
b265feba7a refactor(client.py): rename Client.mac_id -> Client.id 2026-02-28 20:47:37 +01:00
33 changed files with 825 additions and 1114 deletions

3
.gitignore vendored
View File

@@ -45,3 +45,6 @@ logs/
# Sphinx # Sphinx
docs/_build/ docs/_build/
docs/ref/modules/ docs/ref/modules/
# known clients
config/known_clients.yaml

1
.vscode/launch.json vendored
View File

@@ -7,6 +7,7 @@
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/src/judas_server/__main__.py", "program": "${workspaceFolder}/src/judas_server/__main__.py",
"console": "integratedTerminal", "console": "integratedTerminal",
"cwd": "${workspaceFolder}",
"justMyCode": true "justMyCode": true
} }
] ]

View File

@@ -2,6 +2,148 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.7.0] - 2026-03-12
### Bug Fixes
- [`a697ae6`](https://github.com/pufereq/template-repo/commit/a697ae666114ed0adfa78db7accfd81d26850111) **ack_handler.py**: fix error by importing annotations
- [`762256c`](https://github.com/pufereq/template-repo/commit/762256c3cdee4bfc8dbdbfa6d2997656486f9557) **client_details.html**: fix up code for AJAX loading
### Features
- [`acbcb33`](https://github.com/pufereq/template-repo/commit/acbcb3364e7b682bb67ee329d12626e9df0d627d) **web_server.py**: add `debug` parameter to `JudasWebServer.run()`
- [`1d764bd`](https://github.com/pufereq/template-repo/commit/1d764bd77d031123054ab86f073fb27823363532) **header.html**: add header template
- [`6f4bc3a`](https://github.com/pufereq/template-repo/commit/6f4bc3aa0f7f1a4aefe8b2d8405d4fc49c6c744d) **base.html**: add base template
- [`14ea136`](https://github.com/pufereq/template-repo/commit/14ea136fbbad447d1eb668fcc09aadb75de88ee3) **panel.js**: move js from inline to separate script
- [`78f9508`](https://github.com/pufereq/template-repo/commit/78f9508753ae47c681cff115fb960bea6bb12e20) **panel.html**: use ?query instead of #hash for client selection
### Miscellaneous Tasks
- [`aa562a0`](https://github.com/pufereq/template-repo/commit/aa562a0eab3a4ce10f225d03c9927ace6ee66678) **base.html**: remove leftover code
- [`865112c`](https://github.com/pufereq/template-repo/commit/865112c823a65d761d929c69574c47b57559804d) **style.css**: adjust styles
### Refactor
- [`c442dca`](https://github.com/pufereq/template-repo/commit/c442dca520ff381b17cd49863e5189066882f73d) **panel.html**: use new base template
- [`12c5de9`](https://github.com/pufereq/template-repo/commit/12c5de9f114d955c6b309f6020829595d125bec2) **index.html**: use new base template
## [0.7.0-dev.1] - 2026-03-08
### Features
- [`d7b1368`](https://github.com/pufereq/template-repo/commit/d7b136851bafa2c20e1634bd5568f4bac839177f) **client_details.html**: add temporary initial telemetry display
- [`7e9a9e6`](https://github.com/pufereq/template-repo/commit/7e9a9e6eede6cc926fef64c626434e65984befbc) **backend_server.py**: add `initial_telemetry` support
- [`6ed03ab`](https://github.com/pufereq/template-repo/commit/6ed03ab74de8e91d13b5f1971a3f4cec890e4fef) **client.py**: add `Client.initial_telemetry` property
- [`40c08d0`](https://github.com/pufereq/template-repo/commit/40c08d01693973f29f13c133a11fc5f166891a25) **initial_handler.py**: add handler for `TELEMETRY/INTIIAL` msgs
### Build
- [`3d13d24`](https://github.com/pufereq/template-repo/commit/3d13d241168b011c0044eb64db4b0fe70878d748) **uv.lock**: update judas_protocol to 0.9.1
- [`28b57b6`](https://github.com/pufereq/template-repo/commit/28b57b6964bfcd6ce78f2a77822f7221f6e4f7e5) **uv.lock**: update judas_protocol to 0.9.0
## [0.6.0] - 2026-03-05
### Bug Fixes
- [`fbb75c2`](https://github.com/pufereq/template-repo/commit/fbb75c263c13726d7e1c3fa28fb4e8f1f8cf0a17) **backend_server.py**: fix double-connection handling
- [`88d3490`](https://github.com/pufereq/template-repo/commit/88d349090e6e3605800656cc3e85779bf4b417c3) **backend_server.py**: fix double disconnect if client.inbound empty
- [`e308a07`](https://github.com/pufereq/template-repo/commit/e308a07dabcf20d9b080ce12a8928760dcde3057) **backend_server.py**: call `_initialize_handlers()` on init
- [`ead2224`](https://github.com/pufereq/template-repo/commit/ead22240660ff9f723c36ee35fa5918c33a0c66d) **backend_server.py**: do not disconnect a client if Exception raised on msg handling
- [`6446fe8`](https://github.com/pufereq/template-repo/commit/6446fe883cea4268992efc9f97cdb493af416576) **backend_server.py**: check if client to disconnect has an open socket
### Features
- [`72d51b4`](https://github.com/pufereq/template-repo/commit/72d51b451fc11ebe2f38d5e50bf263f9f441b03c) **backend_server.py**: add notice to known_clients.yaml
- [`0e7c948`](https://github.com/pufereq/template-repo/commit/0e7c9486683be4ceb91203109986f0b9fc83eb8b) **config/**: add `config/` directory
- [`97fc17f`](https://github.com/pufereq/template-repo/commit/97fc17fbb3ed5d681a4f5a0d488b7a2b09ab35df) **hello_handler.py**: remove client from `pending_hello` if recv'd HELLO
- [`f5b14fc`](https://github.com/pufereq/template-repo/commit/f5b14fc6109e83730c4b13aaecf6f4314c0136a4) **backend_server.py**: add timeout on HELLO
- [`bf1ad0e`](https://github.com/pufereq/template-repo/commit/bf1ad0ead0e4fc35c20d0d747cca05388b223d35) **ack_handler.py**: add handling for ACKs
- [`a9bace8`](https://github.com/pufereq/template-repo/commit/a9bace8acaab07e6e5d02155a9966c6381ad107e) **backend_server.py**: add `ACK_TIMEOUT` constant
- [`c88e39c`](https://github.com/pufereq/template-repo/commit/c88e39c7355b6f8c982198e6e56da2d09d17c61f) **backend_server.py**: track message ACKs and resend if no ACK recv'd within 5 seconds
- [`dafe418`](https://github.com/pufereq/template-repo/commit/dafe418916485258694949538eda151d7da33695) **backend_server.py**: add warning if received an unknown message (no handler)
- [`0ed478a`](https://github.com/pufereq/template-repo/commit/0ed478a88efa46f2f28b24371ad68010ecbf88d0) **backend_server.py**: implement message handling
- [`c952413`](https://github.com/pufereq/template-repo/commit/c952413d912b7a5d2000bc48c11a4d9274533caf) **hello_handler.py**: add HELLO message handler
- [`882c878`](https://github.com/pufereq/template-repo/commit/882c8780e15ec827fac1854cf7419b71d9a41a4f) **base_handler.py**: add `BaseHandler` class for message handling
- [`faecc38`](https://github.com/pufereq/template-repo/commit/faecc382610daf890c90074809085560931fd178) **client_status.py**: move `ClientStatus` enum to own module
### Miscellaneous Tasks
- [`61be674`](https://github.com/pufereq/template-repo/commit/61be674258dcce8a5b54efddb7d1d856c6cba9bc) remove `cache/` directory
- [`f46d271`](https://github.com/pufereq/template-repo/commit/f46d27164b79107abe405b221c9d424add55a723) **.gitignore**: ignore `config/known_clients.yaml` as it's generated automatically
- [`f13f243`](https://github.com/pufereq/template-repo/commit/f13f243b5b389d84acaa4ab933a234a00c6a981c) **__main__.py**: set logging level for werkzeug to WARNING
- [`332238b`](https://github.com/pufereq/template-repo/commit/332238b403782a08a8974b9267071f8d0ff5f006) **__init__.py**: correct version
- [`e54cc47`](https://github.com/pufereq/template-repo/commit/e54cc479b5f6bb3fb887803122fea0c551e27ee2) **handler/__init__.py**: add `AckHandler` to `__all__`
- [`ee38141`](https://github.com/pufereq/template-repo/commit/ee381414a931ed9d7562410e08c9de5cdc7b94e7) **backend_server.py**: remove redundant HELLO msg handling
- [`ec58a52`](https://github.com/pufereq/template-repo/commit/ec58a5257a3007a6edd502eb3967d92a9b652358) **handler/__init__.py**: add module init
- [`d3f68d3`](https://github.com/pufereq/template-repo/commit/d3f68d3baff8b903be125b0da7a88b415cd56bac) **backend/__init__.py**: add `Client` and `ClientStatus` to `__all__`
- [`bda10a6`](https://github.com/pufereq/template-repo/commit/bda10a6248478e99dfb42d8c605996cafdaf1e6e) **cache/**: add cache/ directory
### Refactor
- [`efbf99f`](https://github.com/pufereq/template-repo/commit/efbf99f356c20fbd48937f1c4e8df0a2d57f0651) **backend_server.py**: if known_clients.yaml not present, call `_save_known_clients()`
- [`35899d3`](https://github.com/pufereq/template-repo/commit/35899d366823c2878330eb06dc703f2965f61c3f) **backend_server.py**: move `known_clients.yaml` to `config/`
- [`3eb681e`](https://github.com/pufereq/template-repo/commit/3eb681e233fa151c2476581854c947bdc77b4900) **backend_server.py**: move loading known clients to its own method
- [`fa2da20`](https://github.com/pufereq/template-repo/commit/fa2da207a9cf3d90cdbfb5977033fee21d449528) **backend_server.py**: refactor calls to Message class constructors after protocol changes
### Styling
- [`62acc4b`](https://github.com/pufereq/template-repo/commit/62acc4b181cd359557909fa94f0cb0e6a4109255) **client.py**: correct property typing
### Build
- [`c64a258`](https://github.com/pufereq/template-repo/commit/c64a2582439e5ef1ab68bb9c588f7c4644bb4e07) **uv.lock**: update judas_protocol to 0.8.0
- [`f41a777`](https://github.com/pufereq/template-repo/commit/f41a7774ec5cb4997005ac8e31ed7bdc3541a8d6) **uv.lock**: update judas_protocol to 0.7.0
## [0.5.0] - 2026-02-28
### Features
- [`956da02`](https://github.com/pufereq/template-repo/commit/956da024c3bbe88d83f3ef7bfa37e380c22a04d9) **style.css**: make client list elements flexboxes
- [`54eec65`](https://github.com/pufereq/template-repo/commit/54eec657a5a4822f5b1211cbc6843045165fe25d) **style.css**: use monospaced font only for heading
- [`1900bf4`](https://github.com/pufereq/template-repo/commit/1900bf46cc6f103f6e9d359b9fd6af9a74d4cf4d) **client_details.html**: add basic client details
- [`c5771dc`](https://github.com/pufereq/template-repo/commit/c5771dc3718b447878633b252bdd94b3e793aec9) **panel.html**: use client_details.html after clicking on client on sidebar
- [`de9240e`](https://github.com/pufereq/template-repo/commit/de9240e6e0c683643df5bb9495c46cae64b0bc68) **backend_server.py**: set client status to STALE if offline for >24h
- [`b1656cd`](https://github.com/pufereq/template-repo/commit/b1656cdfa95673bb19605d9358cb3c98a69f15d2) **client.py**: redo ClientStatuses adding PENDING and STALE
- [`563de5a`](https://github.com/pufereq/template-repo/commit/563de5aa199a64946d9f41b84c6e2803aff45714) **backend_server.py**: add known_clients.yaml file for storing clients between server restarts
- [`6971548`](https://github.com/pufereq/template-repo/commit/6971548589f098f850896ee49e833c7c1bce5efc) **web_server.py**: use client_details blueprint
- [`31c5157`](https://github.com/pufereq/template-repo/commit/31c51574f7a2b92a959ddf9ae4c8cbf336494725) **client_details.py**: add client_details route
- [`b652db9`](https://github.com/pufereq/template-repo/commit/b652db930f9a299c467b52acb22420e7ca280fb7) **style.css**: style no-connection-icon and client list
- [`1dfddd2`](https://github.com/pufereq/template-repo/commit/1dfddd2fc7f4436010ddad31f50355073c8e4abb) **panel.html**: add a side panel client list
- [`d20ff9b`](https://github.com/pufereq/template-repo/commit/d20ff9be6eeb9e283304ca6d60a4635a2f9b0ab6) **panel.html**: add a no connection icon to header
- [`2bbe118`](https://github.com/pufereq/template-repo/commit/2bbe118de6290b1e014aa9c1a6f5f9d5b134d2e0) rename `details.html` -> `client_details.html`
- [`29b4f3a`](https://github.com/pufereq/template-repo/commit/29b4f3a2ff34493d245eb0eb144dd52b0cc9b134) **css/style.css**: add `.button:active` color
- [`69bf4f1`](https://github.com/pufereq/template-repo/commit/69bf4f13585f06c561c0746966bf95762c188bd0) **css/style.css**: add #content styling, make `main`'s flex column to fit `aside`
- [`563dc62`](https://github.com/pufereq/template-repo/commit/563dc626246f379f1179179faa36d13b019b6a4e) **css/style.css**: include flaticon icons
- [`bb229dc`](https://github.com/pufereq/template-repo/commit/bb229dc724ef9cec4c147c6c3d9f67b28eb700ce) **css/style.css**: make UI more compact
- [`3077a98`](https://github.com/pufereq/template-repo/commit/3077a98d6fe7f34219c861120bc217929dc919d9) **backend_server.py**: add `send_close()` method
### Miscellaneous Tasks
- [`7f904fd`](https://github.com/pufereq/template-repo/commit/7f904fdcd50ac114cc584d7438675d5490b61bcd) **uv.lock**: add pyyaml to depedencies
- [`f11b442`](https://github.com/pufereq/template-repo/commit/f11b442eced4781ee3be0102bd8ea0614bc378c4) **style.css**: style no connection message correctly
- [`72a7433`](https://github.com/pufereq/template-repo/commit/72a74334e74f1a1a73d2ae11dd08e72a8a98b094) **pyproject.toml**: add pyyaml to depedencies
- [`5ca5d9f`](https://github.com/pufereq/template-repo/commit/5ca5d9fcf4e8095b93cac1d72877769064895189) **.vscode/launch.json**: set cwd to workspace folder
- [`0580a6b`](https://github.com/pufereq/template-repo/commit/0580a6be5390eb08a13eec0ce7c41219b1dca21a) **css/style.css**: remove transition from button hover bg color
- [`97221bc`](https://github.com/pufereq/template-repo/commit/97221bc1b71112ac54168138bf5adc10acb920b5) **uv.lock**: update depedencies for Python 3.14
### Refactor
- [`a5e1ba8`](https://github.com/pufereq/template-repo/commit/a5e1ba88aeb06d337587e9dfd0a46520ef6c8d95) adapt for client.id
- [`1e38be5`](https://github.com/pufereq/template-repo/commit/1e38be5ec5d55a30fd715a650ea7402b9fb4c546) **backend_server.py**: adapt client's `id` after refactor
- [`b265feb`](https://github.com/pufereq/template-repo/commit/b265feba7a79ecd418bf8849a68a0633ae7d4778) **client.py**: rename `Client.mac_id` -> `Client.id`
- [`840d9ce`](https://github.com/pufereq/template-repo/commit/840d9ce3c1838a3d88264fe895f933aea6c8cb4f) **login.html**: move elements from `main` to `#content`
- [`9971981`](https://github.com/pufereq/template-repo/commit/9971981f666d4099132235fe0ecfb8e679745d95) **index.html**: move elements from `main` to `#content`
- [`5510e9d`](https://github.com/pufereq/template-repo/commit/5510e9dd0874a1e3448a96eb8b8472b5599458a1) **backend_server.py**: rewrite `_handle_connection()` to minimize indents
- [`1e02da1`](https://github.com/pufereq/template-repo/commit/1e02da185111f0e05651df0bf5c1df735f7dc6ff) **backend_server.py**: rename `_send_ack()` -> `send_ack()`
### Styling
- [`f54d974`](https://github.com/pufereq/template-repo/commit/f54d974745c7a0f5d8db619d86d585709649b8ec) **style.css**: remove commented-out code
### Build
- [`f01eaec`](https://github.com/pufereq/template-repo/commit/f01eaecba346715dd0c50eb4b253656df5c89ad8) **uv.lock**: update judas_protocol to 0.6.0
## [0.4.0] - 2025-11-30 ## [0.4.0] - 2025-11-30
### Bug Fixes ### Bug Fixes

View File

@@ -4,7 +4,7 @@ build-backend = "uv_build"
[project] [project]
name = "judas_server" name = "judas_server"
version = "0.4.0" version = "0.7.0"
description = "The backbone of the remote PC fleet management system." description = "The backbone of the remote PC fleet management system."
readme = "README.md" readme = "README.md"
authors = [] authors = []
@@ -14,6 +14,7 @@ dependencies = [
"flask-login>=0.6.3", "flask-login>=0.6.3",
"flask-socketio>=5.5.1", "flask-socketio>=5.5.1",
"judas-protocol", "judas-protocol",
"pyyaml>=6.0.3",
] ]
license = { text = "GPL-3.0+" } license = { text = "GPL-3.0+" }

View File

@@ -1 +1,7 @@
__version__: str = "0.1.0" from .backend_server import BackendServer
__version__: str = "0.5.0"
__all__ = [
"BackendServer",
]

View File

@@ -3,15 +3,16 @@
import logging as lg import logging as lg
if __name__ == "__main__": if __name__ == "__main__":
from judas_server.backend import BackendServer from judas_server import BackendServer
from judas_server.gaga import LADY_GAGA from judas_server.gaga import LADY_GAGA
from judas_server.web.web_server import JudasWebServer
lg.basicConfig( lg.basicConfig(
level=lg.DEBUG, level=lg.DEBUG,
format="%(asctime)s : [%(levelname)s] : %(threadName)s : %(name)s :: %(message)s", format="%(asctime)s : [%(levelname)s] : %(threadName)s : %(name)s :: %(message)s",
) )
lg.getLogger("werkzeug").setLevel(lg.WARNING)
ladygaga_logger = lg.getLogger(f"{__name__}.LAGA_DYGA") ladygaga_logger = lg.getLogger(f"{__name__}.LAGA_DYGA")
ladygaga_logger.info(LADY_GAGA) ladygaga_logger.info(LADY_GAGA)
@@ -20,11 +21,3 @@ if __name__ == "__main__":
port=3692, port=3692,
) )
backend_server.run() backend_server.run()
web_server: JudasWebServer = JudasWebServer(
backend=backend_server, secret_key="dildo"
)
web_server.run(
host="0.0.0.0",
port=5000,
)

View File

@@ -1,3 +0,0 @@
from .backend_server import BackendServer
__all__ = ["BackendServer"]

View File

@@ -1,274 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging as lg
import selectors
import socket
import threading
import time
from typing import Any
from judas_protocol import Category, ControlAction, Message
from judas_server.backend.client import Client
class BackendServer:
def __init__(self, host: str = "0.0.0.0", port: int = 3692) -> None:
"""Initialize the backend server.
Args:
host (str): The host IP address to bind the server to.
port (int): The port number to bind the server to.
"""
self.logger: lg.Logger = lg.getLogger(
f"{__name__}.{self.__class__.__name__}"
)
self.logger.debug("Initializing Server...")
self.selector = selectors.DefaultSelector()
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.setsockopt(
socket.SOL_SOCKET, socket.SO_REUSEADDR, 1
)
self._bind_socket(host, port)
self.server_socket.listen()
self.server_socket.setblocking(False)
self.selector.register(
self.server_socket, selectors.EVENT_READ, data=None
)
self.clients: dict[str, Client] = {}
self.running: bool = False
def _bind_socket(self, host: str, port: int) -> None:
"""Bind the server socket to the specified host and port.
Args:
host (str): The host IP address to bind the server to.
port (int): The port number to bind the server to.
"""
self.logger.debug(f"Binding socket to {host}:{port}")
while True:
try:
self.server_socket.bind((host, port))
self.logger.debug(f"Socket bound to {host}:{port}")
break
except OSError as e:
self.logger.error(
f"Failed to bind socket to {host}:{port}, retrying...: {e}"
)
time.sleep(1)
def send_ack(self, client: Client, target_id: str) -> None:
"""Send an ACK message to a client.
Args:
client (Client): The client to send the ACK to.
target_id (str): The id of the ACK'd message.
"""
ack: bytes = Message.ack(target_id=target_id).to_bytes()
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.
Args:
sock (socket.socket): The selected socket.
"""
conn, addr = sock.accept()
self.logger.info(f"[+] Accepted connection from {addr}")
client = Client(mac_id=None, addr=addr, socket=conn)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
self.selector.register(conn, events, data=client)
self.logger.info(f"[+] Registered client {client}, HELLO pending...")
def _disconnect(self, client: Client) -> None:
"""Disconnect a client and clean up resources.
Args:
sock (socket.socket): The client socket to disconnect.
"""
self.logger.info(f"[-] Disconnecting {client}...")
try:
self.selector.unregister(client.socket)
except Exception as e:
self.logger.error(f"Error unregistering client {client}: {e}")
client.disconnect()
def _send_outbound(self, sock: socket.socket, client: Client) -> None:
"""Queue data to be sent to a client.
Args:
client (Client): The client to send data to.
"""
self.logger.debug(f"[>] Sending data to {client}: {client.outbound!r}")
sent = sock.send(client.outbound)
client.outbound = client.outbound[sent:]
def _receive_inbound(
self, sock: socket.socket, client: Client, packet_size: int = 4096
) -> None:
"""Receive data from a client socket.
Args:
sock (socket.socket): The client socket to receive data from.
client (Client): The client object.
packet_size (int): The maximum amount of data to be received at once.
Returns:
bytes: The received data.
"""
recv_data = sock.recv(packet_size)
if recv_data:
self.logger.debug(
f"[<] Received data from {client}: {recv_data!r}"
)
client.inbound += recv_data
# set last seen
client.last_seen = time.time()
else:
self._disconnect(client)
def _handle_connection(
self, key: selectors.SelectorKey, mask: int
) -> None:
"""Handle a client connection.
Args:
key (selectors.SelectorKey): The selector key for the client.
mask (int): The event mask.
"""
sock: socket.socket = key.fileobj # type: ignore
client = key.data
try:
if mask & selectors.EVENT_READ:
self._receive_inbound(sock, client)
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
and msg.payload.get("mac") is not None
):
client.mac_id = msg.payload["mac"]
if (
client.mac_id in self.clients
and self.clients[client.mac_id].status
== "connected"
):
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.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}")
else:
self.logger.error(
f"Expected HELLO message from {client}, got {msg}"
)
self._disconnect(client)
return
while b"\n" in client.inbound:
line, client.inbound = client.inbound.split(b"\n", 1)
self.logger.debug(
f"[<] Complete message from {client}: {line!r}"
)
try:
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)
except Exception as e:
self.logger.error(
f"Failed to parse message from {client}: {e}"
)
self._disconnect(client)
return
if mask & selectors.EVENT_WRITE and client.outbound:
self._send_outbound(sock, client)
except ConnectionResetError as e:
self.logger.error(f"Connection reset by {client}, disconnect: {e}")
self._disconnect(client)
except Exception as e:
self.logger.error(f"Connection error for {client}: {e}")
self._disconnect(client)
def run(self) -> None:
"""Start the backend server."""
self.running = True
threading.Thread(
name="BackendServer thread", target=self._loop, daemon=True
).start()
def _loop(self) -> None:
"""Main server loop to handle incoming connections and data."""
self.logger.info("Server is running...")
try:
while self.running:
events = self.selector.select(timeout=1)
for key, mask in events:
if key.data is None:
self._accept_connection(key.fileobj) # type: ignore
else:
self._handle_connection(key, mask)
time.sleep(0.001) # prevent 100% CPU usage
except Exception as e:
self.logger.error(f"Server error: {e}")
raise e
finally:
self.selector.close()
self.server_socket.close()
self.logger.info("Server has stopped.")
def get_client_data(self, client_id: str) -> dict[str, Any] | None:
client: Client | None = self.clients.get(client_id, None)
if client is None:
self.logger.warning(f"Client {client_id} not found")
return None
return {
"id": client.mac_id,
"addr": client.addr,
"last_seen": client.last_seen,
"status": client.status,
}

View File

@@ -0,0 +1,416 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging as lg
import selectors
import socket
import threading
import time
from typing import TYPE_CHECKING, Any, Final
from judas_protocol.types import TelemetryAction
import yaml
from judas_protocol import Category, ControlAction, Message
from judas_server.client import Client, ClientStatus
from judas_server.handler.hello_handler import HelloHandler
from judas_server.handler.telemetry.initial_handler import (
InitialTelemetryHandler,
)
if TYPE_CHECKING:
from typing import Callable
from judas_protocol import ActionType
class BackendServer:
ACK_TIMEOUT: Final[float] = 5.0 # seconds
HELLO_TIMEOUT: Final[float] = 3.0 # seconds
def __init__(self, host: str = "0.0.0.0", port: int = 3692) -> None:
"""Initialize the backend server.
Args:
host (str): The host IP address to bind the server to.
port (int): The port number to bind the server to.
"""
self.logger: lg.Logger = lg.getLogger(
f"{__name__}.{self.__class__.__name__}"
)
self.logger.debug("Initializing Server...")
self.selector = selectors.DefaultSelector()
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.setsockopt(
socket.SOL_SOCKET, socket.SO_REUSEADDR, 1
)
self._bind_socket(host, port)
self.server_socket.listen()
self.server_socket.setblocking(False)
self.selector.register(
self.server_socket, selectors.EVENT_READ, data=None
)
self.clients: dict[str, Client] = {}
self.known_clients: dict[str, dict[str, str | float]] = {}
self.known_clients = self._load_known_clients()
self.message_handlers: dict[
tuple[Category, ActionType], Callable[[Client, Message], None]
] = {}
self._initialize_handlers()
self.pending_acks: list[tuple[Client, Message, float]] = []
self.pending_hello: dict[Client, float] = {}
self.running: bool = False
def _initialize_handlers(self) -> None:
"""Initialize message handlers."""
hello_handler = HelloHandler(self)
initial_telemetry_handler = InitialTelemetryHandler(self)
self.message_handlers[(Category.CONTROL, ControlAction.HELLO)] = (
hello_handler.handle
)
self.message_handlers[
(Category.TELEMETRY, TelemetryAction.INITIAL)
] = initial_telemetry_handler.handle
def _load_known_clients(self) -> dict[str, dict[str, str | float]]:
"""Load the list of known clients from a YAML file and validate."""
known_clients: dict[str, dict[str, str | float]] = {}
try:
with open("config/known_clients.yaml", "r") as f:
data = yaml.safe_load(f)
if not isinstance(data, dict):
raise ValueError("YAML root must be a dict")
known_clients = data.get("known_clients", {}) or {}
if not isinstance(known_clients, dict):
raise ValueError("'known_clients' must be a dict")
for client_id, client_data in known_clients.items():
if not isinstance(client_data, dict):
raise ValueError(
f"Client {client_id} data must be a dict"
)
last_seen = client_data.get("last_seen", 0.0)
if not isinstance(last_seen, (float, int)):
raise ValueError(
f"Client {client_id} 'last_seen' must be a float or int"
)
self.logger.debug(f"Loaded known clients: {known_clients}")
self.logger.info(f"Loaded {len(known_clients)} known clients")
for client_id in known_clients:
client = Client(id=client_id, addr=None, socket=None)
client.status = ClientStatus.OFFLINE
client.last_seen = float(
known_clients[client_id].get("last_seen", 0.0)
)
client.initial_telemetry = known_clients[client_id].get( # type: ignore
"initial_telemetry", None
)
self.clients[client_id] = client
except FileNotFoundError:
self.logger.warning(
"known_clients.yaml not found, creating empty known clients list"
)
self._save_known_clients()
except Exception as e:
self.logger.error(f"Error loading known clients: {e}")
raise
return known_clients
def _save_known_clients(self) -> None:
"""Save the list of known clients to a YAML file."""
with open("config/known_clients.yaml", "w") as f:
f.write(
"# This file is automatically generated by BackendServer.\n"
+ "# Do not edit manually.\n"
+ f"# Generated at: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}\n\n"
)
yaml.safe_dump({"known_clients": self.known_clients}, f)
self.logger.debug("Saved known clients")
def _bind_socket(self, host: str, port: int) -> None:
"""Bind the server socket to the specified host and port.
Args:
host (str): The host IP address to bind the server to.
port (int): The port number to bind the server to.
"""
self.logger.debug(f"Binding socket to {host}:{port}")
while True:
try:
self.server_socket.bind((host, port))
self.logger.debug(f"Socket bound to {host}:{port}")
break
except OSError as e:
self.logger.error(
f"Failed to bind socket to {host}:{port}, retrying...: {e}"
)
time.sleep(1)
def send(self, client: Client, msg: Message) -> None:
"""Send a message to a client.
Args:
client (Client): The client to send the message to.
msg (Message): The message to send.
"""
msg_bytes: bytes = msg.to_bytes()
self.logger.info(
f"[>] Sending message {msg.id} to {client}, category: {msg.category}, action: {msg.action}, ack_required: {msg.ack_required}"
)
self.logger.debug(f"[>] Message bytes: {msg_bytes!r}")
if msg.ack_required:
self.pending_acks.append((client, msg, time.time()))
client.outbound += msg_bytes
def send_ack(self, client: Client, target_id: str) -> None:
"""Send an ACK message to a client.
Args:
client (Client): The client to send the ACK to.
target_id (str): The id of the ACK'd message.
"""
ack: Message = Message.Control.ack(target_id=target_id)
self.logger.info(f"[>] Sending ACK to {client}")
self.send(client, 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: Message = Message.Control.close()
self.logger.info(f"[>] Sending CLOSE to {client}")
self.send(client, close_msg)
def _accept_connection(self, sock: socket.socket) -> None:
"""Accept a new client connection.
Args:
sock (socket.socket): The selected socket.
"""
conn, addr = sock.accept()
self.logger.info(f"[+] Accepted connection from {addr}")
client = Client(id=None, addr=addr, socket=conn)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
self.selector.register(conn, events, data=client)
self.pending_hello[client] = time.time()
self.logger.info(f"[+] Registered client {client}, HELLO pending...")
def _disconnect(self, client: Client) -> None:
"""Disconnect a client and clean up resources.
Args:
sock (socket.socket): The client socket to disconnect.
"""
self.logger.info(f"[-] Disconnecting {client}...")
if client.socket is None or client.socket._closed:
self.logger.warning(
f"Client {client} has no socket, nothing to disconnect."
)
return
try:
self.selector.unregister(client.socket)
except Exception as e:
self.logger.error(f"Error unregistering client {client}: {e}")
client.disconnect()
self._save_known_clients()
def _send_outbound(self, sock: socket.socket, client: Client) -> None:
"""Queue data to be sent to a client.
Args:
client (Client): The client to send data to.
"""
self.logger.debug(f"[>] Sending data to {client}: {client.outbound!r}")
sent = sock.send(client.outbound)
client.outbound = client.outbound[sent:]
def _receive_inbound(
self, sock: socket.socket, client: Client, packet_size: int = 4096
) -> None:
"""Receive data from a client socket.
Args:
sock (socket.socket): The client socket to receive data from.
client (Client): The client object.
packet_size (int): The maximum amount of data to be received at once.
Returns:
bytes: The received data.
"""
recv_data = sock.recv(packet_size)
if recv_data:
self.logger.debug(
f"[<] Received data from {client}: {recv_data!r}"
)
client.inbound += recv_data
# set last seen
client.last_seen = time.time()
else:
self._disconnect(client)
def _handle_connection(
self, key: selectors.SelectorKey, mask: int
) -> None:
"""Handle a client connection.
Args:
key (selectors.SelectorKey): The selector key for the client.
mask (int): The event mask.
"""
sock: socket.socket = key.fileobj # type: ignore
client = key.data
try:
if mask & selectors.EVENT_READ:
self._receive_inbound(sock, client)
while b"\n" in client.inbound:
line, client.inbound = client.inbound.split(b"\n", 1)
self.logger.debug(
f"[<] Complete message from {client}: {line!r}"
)
try:
msg = Message.from_bytes(line)
self.logger.info(f"[.] Parsed message {msg.id}")
if client.id is None:
self.logger.debug(
f"Client {client} has no ID, expecting HELLO message..."
)
if (
msg.category != Category.CONTROL
or msg.action != ControlAction.HELLO
):
self.logger.warning(
f"First message from {client} must be HELLO, disconnecting..."
)
self._disconnect(client)
continue
handler: Callable[[Client, Message], None] | None = (
self.message_handlers.get(
(msg.category, msg.action), None
)
)
if handler is not None:
handler(client, msg)
else:
self.logger.warning(
f"No handler for message {msg.id} with category {msg.category} and action {msg.action}"
)
continue
if msg.ack_required:
self.send_ack(client, target_id=msg.id)
except Exception as e:
self.logger.error(
f"Failed to parse message from {client}: {e}"
)
return
if mask & selectors.EVENT_WRITE and client.outbound:
self._send_outbound(sock, client)
except ConnectionResetError as e:
self.logger.error(f"Connection reset by {client}, disconnect: {e}")
self._disconnect(client)
except Exception as e:
self.logger.error(f"Connection error for {client}: {e}")
self._disconnect(client)
def run(self) -> None:
"""Start the backend server."""
self.running = True
self._loop()
def _loop(self) -> None:
"""Main server loop to handle incoming connections and data."""
self.logger.info("Server is running...")
try:
while self.running:
events = self.selector.select(timeout=1)
for key, mask in events:
if key.data is None:
self._accept_connection(key.fileobj) # type: ignore
else:
self._handle_connection(key, mask)
# update client statuses
now = time.time()
for client in self.clients.values():
if (
client.status == ClientStatus.OFFLINE
and now - client.last_seen > 60 * 60 * 24 # 24 hours
):
self.clients[client.id].status = ClientStatus.STALE
# check pending ACKs
for client, msg, timestamp in self.pending_acks[:]:
if time.time() - timestamp > self.ACK_TIMEOUT:
self.logger.warning(
f"ACK timeout for message {msg.id} to {client}, resending..."
)
self.send(client, msg)
self.pending_acks.remove((client, msg, timestamp))
# check pending HELLOs
for client, timestamp in list(self.pending_hello.items()):
if time.time() - timestamp > self.HELLO_TIMEOUT:
self.logger.warning(
f"HELLO timeout for {client}, disconnecting..."
)
self._disconnect(client)
del self.pending_hello[client]
time.sleep(0.001) # prevent 100% CPU usage
except Exception as e:
self.logger.error(f"Server error: {e}")
raise e
except KeyboardInterrupt:
self.logger.info("Keyboard interrupt received, stopping server...")
self.running = False
finally:
self.selector.close()
self.server_socket.close()
self.logger.info("Server has stopped.")
def get_client_data(self, client_id: str) -> dict[str, Any] | None:
client: Client | None = self.clients.get(client_id, None)
if client is None:
self.logger.warning(f"Client {client_id} not found")
return None
return {
"id": client.id,
"addr": client.addr,
"last_seen": client.last_seen,
"status": client.status,
"initial_telemetry": client.initial_telemetry,
}

View File

@@ -5,24 +5,25 @@ from __future__ import annotations
import logging as lg import logging as lg
import socket import socket
from enum import Enum import time
from typing import Any
from judas_server.client_status import ClientStatus
class ClientStatus(str, Enum):
CONNECTED = "connected"
DISCONNECTED = "disconnected"
class Client: class Client:
"""Represents a client.""" """Represents a client."""
def __init__( def __init__(
self, mac_id: str | None, addr: tuple[str, int], socket: socket.socket self,
id: str | None,
addr: tuple[str, int] | None,
socket: socket.socket | None,
) -> None: ) -> None:
"""Initialize the client. """Initialize the client.
Args: Args:
mac_id (str | None): The unique identifier for the client. id (str | None): The unique identifier for the client.
Can be None if not yet assigned. Can be None if not yet assigned.
addr (tuple[str, int]): The (IP, port) address of the client. addr (tuple[str, int]): The (IP, port) address of the client.
socket (socket.socket): The socket object for communication. socket (socket.socket): The socket object for communication.
@@ -32,27 +33,38 @@ class Client:
) )
self.logger.debug(f"Initializing Client {addr}...") self.logger.debug(f"Initializing Client {addr}...")
self.mac_id: str | None = mac_id self.id: str | None = id
self.last_seen: float = 0.0 # unix timestanp of last inbound message self.last_seen: float = 0.0 # unix timestanp of last inbound message
self.status: ClientStatus = ClientStatus.CONNECTED self.status: ClientStatus = ClientStatus.PENDING
self.socket: socket.socket = socket self.socket: socket.socket | None = socket
self.addr: tuple[str, int] = addr self.addr: tuple[str, int] | None = addr
self.inbound: bytes = b"" self.inbound: bytes = b""
self.outbound: bytes = b"" self.outbound: bytes = b""
self.initial_telemetry: dict[str, Any] | None = None
def __str__(self) -> str: def __str__(self) -> str:
return f"Client({self.mac_id} ({self.addr[0]}:{self.addr[1]}))" if self.addr:
return f"Client({self.id} ({self.addr[0]}:{self.addr[1]}))"
return f"Client({self.id} (not connected))"
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Client({self.mac_id}, {self.addr})" return f"Client({self.id}, {self.addr})"
def disconnect(self) -> None: def disconnect(self) -> None:
"""Disconnect the client and close the socket.""" """Disconnect the client and close the socket."""
self.logger.debug(f"Disconnecting Client {self}...") self.logger.debug(f"Disconnecting Client {self}...")
if self.socket is None:
self.logger.warning(
f"Client {self} not connected, nothing to disconnect."
)
return
try: try:
self.socket.close() self.socket.close()
except Exception as e: except Exception as e:
self.logger.error(f"Error closing socket for Client {self}: {e}") self.logger.error(f"Error closing socket for Client {self}: {e}")
self.status = ClientStatus.DISCONNECTED self.status = ClientStatus.OFFLINE
self.last_seen = time.time()
self.logger.info(f"Client {self} disconnected.") self.logger.info(f"Client {self} disconnected.")

View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
from enum import Enum
class ClientStatus(str, Enum):
"""Enumeration of client connection statuses."""
ONLINE = "online"
PENDING = "pending"
OFFLINE = "offline"
STALE = "stale"

View File

@@ -0,0 +1,5 @@
from .base_handler import BaseHandler
from .hello_handler import HelloHandler
from .ack_handler import AckHandler
__all__ = ["BaseHandler", "HelloHandler", "AckHandler"]

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from .base_handler import BaseHandler
if TYPE_CHECKING:
from judas_protocol import Message
from judas_server import BackendServer, Client
class AckHandler(BaseHandler):
def __init__(self, backend_server: BackendServer) -> None:
super().__init__(backend_server)
def handle(self, client: Client, message: Message) -> None:
pending_acks = self.backend_server.pending_acks
if message.id in pending_acks:
del pending_acks[message.id]
self.logger.debug(
f"[*] Received ACK for message {message.id} from {client}."
)
else:
self.logger.warning(
f"[!] Received ACK for unknown (or ACK'd) message {message.id} from {client}."
)

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging as lg
from typing import TYPE_CHECKING
from judas_server.client import Client
if TYPE_CHECKING:
from judas_protocol import Message
from judas_server import BackendServer
class BaseHandler:
"""BaseHandler is the base class for all message handlers in the backend server.
It defines the interface for handling messages and provides common functionality for all handlers.
"""
def __init__(self, backend_server: BackendServer) -> None:
"""Initialize the BaseHandler with a reference to the backend server.
Args:
backend_server (BackendServer): The backend server instance that this handler belongs to.
"""
self.logger: lg.Logger = lg.getLogger(
f"{__name__}.{self.__class__.__name__}"
)
self.backend_server: BackendServer = backend_server
def handle(self, client: Client, message: Message) -> None:
"""Handle a message from a client.
This method must be implemented by subclasses to define the specific handling logic for different message types.
Args:
client (Client): The client that sent the message.
message (Message): The message to be handled.
"""
raise NotImplementedError("handle() must be implemented by subclasses")

View File

@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING, override
from judas_protocol import Category, ControlAction, Message
from judas_server.client import ClientStatus
from judas_server.handler import BaseHandler
if TYPE_CHECKING:
from judas_server.backend_server import BackendServer
from judas_server.client import Client
class HelloHandler(BaseHandler):
def __init__(self, backend_server: BackendServer) -> None:
super().__init__(backend_server)
@override
def handle(self, client: Client, message: Message) -> None:
if client.id is not None:
return
if (
message.category != Category.CONTROL
or message.action != ControlAction.HELLO
):
self.logger.error(
f"Expected HELLO message from {client}, got {message}, disconnecting client..."
)
self.backend_server._disconnect(client)
return
if message.payload.get("id") is None:
self.logger.error(
f"HELLO message from {client} missing 'id' field, disconnecting client..."
)
self.backend_server._disconnect(client)
return
client.id = message.payload["id"]
# check if client already connected, if so disconnect old client and register new one
if (
client.id in self.backend_server.clients
and self.backend_server.clients[client.id].status
== ClientStatus.ONLINE
):
old_client: Client = self.backend_server.clients[client.id]
self.backend_server.logger.warning(
f"Client {client.id} is already connected from {old_client.addr}, disconnecting old client..."
)
self.backend_server.send_close(old_client)
return
self.backend_server.clients[client.id] = client # type: ignore
self.backend_server.known_clients[client.id] = { # type: ignore
"last_seen": client.last_seen
}
del self.backend_server.pending_hello[client]
self.backend_server._save_known_clients()
client.status = ClientStatus.ONLINE
self.logger.info(f"[+] Registered new client {client}")

View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
"""Initial telemetry handler."""
from __future__ import annotations
from typing import TYPE_CHECKING
from judas_protocol import Message
from judas_server.handler.base_handler import BaseHandler
if TYPE_CHECKING:
from judas_server import BackendServer
from judas_server.client import Client
class InitialTelemetryHandler(BaseHandler):
"""Handles the initial telemetry message from a client."""
def __init__(self, backend_server: BackendServer) -> None:
"""Initialize the handler."""
super().__init__(backend_server)
def handle(self, client: Client, message: Message) -> None:
"""Handle the initial telemetry message."""
self.logger.debug(
f"Handling initial telemetry message from {client}..."
)
client.initial_telemetry = message.payload
self.backend_server.known_clients[client.id]["initial_telemetry"] = ( # type: ignore
message.payload
)

View File

@@ -1,5 +0,0 @@
from .auth import auth_bp
from .index import index_bp
from .panel import panel_bp
__all__ = ["auth_bp", "index_bp", "panel_bp"]

View File

@@ -1,61 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import threading
from typing import TYPE_CHECKING
import flask
import flask_login
if TYPE_CHECKING:
from werkzeug.wrappers import Response
bp: flask.Blueprint = flask.Blueprint("api", __name__, url_prefix="/api")
@bp.route("/client/<client_id>", methods=["GET"])
@flask_login.login_required
def get_client_data(client_id: str) -> tuple[Response, int]:
"""API endpoint to get client data by ID.
Args:
client_id (str): The ID of the client.
Returns:
Response: JSON response with client data or error message.
"""
backend = flask.current_app.config["BACKEND"]
data = backend.get_client_data(client_id)
if data is None:
return flask.jsonify({"error": "Client not found"}), 404
return flask.jsonify(data), 200
@bp.route("/clients", methods=["GET"])
@flask_login.login_required
def list_clients() -> tuple[Response, int]:
"""API endpoint to list all clients.
Returns:
Response: JSON response with list of client IDs.
"""
backend = flask.current_app.config["BACKEND"]
client_ids = list(backend.clients.keys())
return flask.jsonify({"clients": client_ids}), 200
def emit_polled_data(app, socketio):
backend = app.config["BACKEND"]
def poll_loop():
import time
while True:
data = {}
for client_id in backend.clients.keys():
data[client_id] = backend.get_client_data(client_id)
socketio.emit("update_data", data)
time.sleep(1)
threading.Thread(name="Socketio", target=poll_loop, daemon=True).start()

View File

@@ -1,43 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
import flask
import flask_login
from judas_server.web.user import User
if TYPE_CHECKING:
from werkzeug.wrappers import Response
auth_bp: flask.Blueprint = flask.Blueprint(
"auth", __name__, url_prefix="/auth"
)
@auth_bp.route("/login", methods=["GET", "POST"])
def login() -> Response | str:
"""Handles user login via password form."""
if flask.request.method == "POST":
password = flask.request.form.get("password", "")
if password == flask.current_app.config["PASSWORD"]:
user = User("admin")
flask_login.login_user(user)
next_page = flask.request.args.get("next")
return flask.redirect(next_page or flask.url_for("index.index"))
# return flask.redirect(flask.url_for("panel.panel"))
else:
return flask.render_template(
"login.html",
error="Invalid credentials.",
)
return flask.render_template("login.html")
@auth_bp.route("/logout")
@flask_login.login_required
def logout() -> Response:
"""Logs out the current user."""
flask_login.logout_user()
return flask.redirect(flask.url_for("index.index"))

View File

@@ -1,31 +0,0 @@
# -*- 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,20 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
import flask
import flask_login
if TYPE_CHECKING:
from werkzeug.wrappers import Response
index_bp: flask.Blueprint = flask.Blueprint("index", __name__)
@index_bp.route("/")
def index() -> Response | str:
"""Renders the index page."""
return flask.render_template(
"index.html", logged=flask_login.current_user.is_authenticated
)

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import flask
import flask_login
panel_bp: flask.Blueprint = flask.Blueprint(
"panel", __name__, url_prefix="/panel"
)
@panel_bp.route("")
@flask_login.login_required
def panel() -> str:
"""Renders the main panel page with PC details.
Returns:
Rendered HTML template with PC details.
"""
return flask.render_template("panel.html")

View File

@@ -1,80 +0,0 @@
:root {
--ctp-rosewater: #f5e0dc;
--ctp-rosewater-rgb: 245 224 220;
--ctp-rosewater-hsl: 9.600 55.556% 91.176%;
--ctp-flamingo: #f2cdcd;
--ctp-flamingo-rgb: 242 205 205;
--ctp-flamingo-hsl: 0.000 58.730% 87.647%;
--ctp-pink: #f5c2e7;
--ctp-pink-rgb: 245 194 231;
--ctp-pink-hsl: 316.471 71.831% 86.078%;
--ctp-mauve: #cba6f7;
--ctp-mauve-rgb: 203 166 247;
--ctp-mauve-hsl: 267.407 83.505% 80.980%;
--ctp-red: #f38ba8;
--ctp-red-rgb: 243 139 168;
--ctp-red-hsl: 343.269 81.250% 74.902%;
--ctp-maroon: #eba0ac;
--ctp-maroon-rgb: 235 160 172;
--ctp-maroon-hsl: 350.400 65.217% 77.451%;
--ctp-peach: #fab387;
--ctp-peach-rgb: 250 179 135;
--ctp-peach-hsl: 22.957 92.000% 75.490%;
--ctp-yellow: #f9e2af;
--ctp-yellow-rgb: 249 226 175;
--ctp-yellow-hsl: 41.351 86.047% 83.137%;
--ctp-green: #a6e3a1;
--ctp-green-rgb: 166 227 161;
--ctp-green-hsl: 115.455 54.098% 76.078%;
--ctp-teal: #94e2d5;
--ctp-teal-rgb: 148 226 213;
--ctp-teal-hsl: 170.000 57.353% 73.333%;
--ctp-sky: #89dceb;
--ctp-sky-rgb: 137 220 235;
--ctp-sky-hsl: 189.184 71.014% 72.941%;
--ctp-sapphire: #74c7ec;
--ctp-sapphire-rgb: 116 199 236;
--ctp-sapphire-hsl: 198.500 75.949% 69.020%;
--ctp-blue: #89b4fa;
--ctp-blue-rgb: 137 180 250;
--ctp-blue-hsl: 217.168 91.870% 75.882%;
--ctp-lavender: #b4befe;
--ctp-lavender-rgb: 180 190 254;
--ctp-lavender-hsl: 231.892 97.368% 85.098%;
--ctp-text: #cdd6f4;
--ctp-text-rgb: 205 214 244;
--ctp-text-hsl: 226.154 63.934% 88.039%;
--ctp-subtext1: #bac2de;
--ctp-subtext1-rgb: 186 194 222;
--ctp-subtext1-hsl: 226.667 35.294% 80.000%;
--ctp-subtext0: #a6adc8;
--ctp-subtext0-rgb: 166 173 200;
--ctp-subtext0-hsl: 227.647 23.611% 71.765%;
--ctp-overlay2: #9399b2;
--ctp-overlay2-rgb: 147 153 178;
--ctp-overlay2-hsl: 228.387 16.757% 63.725%;
--ctp-overlay1: #7f849c;
--ctp-overlay1-rgb: 127 132 156;
--ctp-overlay1-hsl: 229.655 12.775% 55.490%;
--ctp-overlay0: #6c7086;
--ctp-overlay0-rgb: 108 112 134;
--ctp-overlay0-hsl: 230.769 10.744% 47.451%;
--ctp-surface2: #585b70;
--ctp-surface2-rgb: 88 91 112;
--ctp-surface2-hsl: 232.500 12.000% 39.216%;
--ctp-surface1: #45475a;
--ctp-surface1-rgb: 69 71 90;
--ctp-surface1-hsl: 234.286 13.208% 31.176%;
--ctp-surface0: #313244;
--ctp-surface0-rgb: 49 50 68;
--ctp-surface0-hsl: 236.842 16.239% 22.941%;
--ctp-base: #1e1e2e;
--ctp-base-rgb: 30 30 46;
--ctp-base-hsl: 240.000 21.053% 14.902%;
--ctp-mantle: #181825;
--ctp-mantle-rgb: 24 24 37;
--ctp-mantle-hsl: 240.000 21.311% 11.961%;
--ctp-crust: #11111b;
--ctp-crust-rgb: 17 17 27;
--ctp-crust-hsl: 240.000 22.727% 8.627%;
}

View File

@@ -1,253 +0,0 @@
@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";
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--ctp-base);
font-family: monospace, sans-serif !important;
color: var(--ctp-text);
}
.fi {
vertical-align: middle;
}
input {
font-family: inherit;
color: #eceff4;
background-color: var(--ctp-crust);
border: none;
border-radius: 0.25rem;
padding: 0.25rem;
}
a {
color: var(--ctp-blue);
text-decoration: none;
}
#wrapper {
display: flex;
flex-direction: column;
min-height: 100vh;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--ctp-mantle);
/* color: var(--ctp-text); */
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;
flex-grow: 1;
padding: 1rem;
}
header a {
text-decoration: none;
/* color: var(--nord-fg0); */
}
.button {
display: inline-block;
padding: 0.25rem 0.5rem;
background-color: var(--ctp-lavender);
color: var(--ctp-base);
border: none;
border-radius: 0.25rem;
text-decoration: none;
}
/* .button a:hover { */
/* color: var(--nord-bg0); */
/* text-decoration: none; */
/* } */
.button:hover {
background-color: var(--ctp-mauve);
cursor: pointer;
}
.button:active {
background-color: var(--ctp-sapphire);
}
.center {
text-align: center;
}
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 1rem;
padding: 1rem;
background-color: var(--ctp-surface0);
color: var(--ctp-red);
border: 6px solid var(--ctp-red);
border-radius: 24px;
}
.center-table {
margin: 0 auto;
width: 100%;
}
.select-table {
border-collapse: collapse;
border: 2px solid var(--ctp-surface0);
}
.select-table thead {
position: sticky;
top: -1px;
z-index: 2;
}
.select-table th,
.select-table td {
padding: 0.5rem;
text-align: center;
border: 1px solid var(--ctp-surface0);
border-collapse: collapse;
}
.select-table th {
background-color: var(--ctp-surface1);
color: var(--nord-fg0);
}
.select-table a {
display: block;
/* color: var(--nord-acc0); */
text-decoration: none;
transition: 0.1s ease-in-out;
}
.select-table tr {
transition: 0.1s ease-in-out;
}
.select-table tr:hover {
background-color: var(--ctp-surface0);
}
.select-table tr:hover a {
color: var(--ctp-base);
}
.red-bg {
background-color: var(--ctp-red);
}
.yellow-bg {
background-color: var(--ctp-yellow);
}
.green-bg {
background-color: var(--ctp-green);
}
.button-container {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.link {
transition: 0.3s ease-in-out;
}
.link:hover {
color: var(--ctp-blue);
}
.details-table tr td:first-child {
font-weight: 900;
background-color: var(--ctp-surface0);
white-space: nowrap;
width: 1%;
}
.details-table {
border-collapse: collapse;
border: 2px solid var(--ctp-surface1);
}
.details-table th,
.details-table td {
padding: 0.5rem;
text-align: left;
border: 1px solid var(--ctp-text);
border-collapse: collapse;
}
#notify {
display: none;
position: fixed;
top: 3rem;
right: 50%;
transform: translateX(50%);
background-color: var(--ctp-surface1);
padding: 1rem 2rem;
border: 4px solid var(--ctp-text);
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

@@ -1,9 +0,0 @@
<!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,56 +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</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<div id="wrapper">
<header>
<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 %}
<p><a class="button" href="{{ url_for('auth.login') }}">Login</a></p>
{% 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: 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>
var i = 0;
var txt = document.getElementById("typing-text").innerHTML;
var minSpeed = 50;
var maxSpeed = 200;
document.getElementById("typing-text").innerHTML = "";
function typeWriter() {
if (i < txt.length) {
document.getElementById("typing-text").innerHTML += txt.charAt(i);
i++;
var randomDelay = Math.floor(Math.random() * (maxSpeed - minSpeed + 1)) + minSpeed;
setTimeout(typeWriter, randomDelay);
}
}
typeWriter();
</script>
</body>
</html>

View File

@@ -1,34 +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 - login page</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<div id="wrapper">
<header>
<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>
<input type="password" id="password" name="password" required autofocus>
<br>
<br>
<input type="submit" value="Login" class="button">
{% if error %}
<div class="error-container">
<h1>Login failure</h1>
<p>{{ error }}</p>
</div>
{% endif %}
</form>
</div>
</main>
</div>
</body>
</html>

View File

@@ -1,93 +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</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/style.css') }}"
/>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
<script>
$(document).ready(function () {
const socket = io();
const dataElement = $("#data");
const showNotify = (message) => {
$("#notify").stop().fadeIn();
$("#notify").text(message);
setTimeout(() => {
$("#notify").fadeOut();
}, 3000);
};
$("#notify").hide();
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");
});
socket.on("update_data", (data) => {
console.log("Received data:", data);
document.getElementById("data").innerHTML = JSON.stringify(
data,
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>
</head>
<body>
<div id="wrapper">
<header>
<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>
</html>

View File

@@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import flask_login
class User(flask_login.UserMixin):
"""Represents a user for authentication purposes."""
def __init__(self, id: str) -> None:
super().__init__()
self.id = id
def get_id(self) -> str:
"""Return the unique identifier for the user."""
return self.id
def __str__(self) -> str:
return f"User(id={self.id})"
def __repr__(self) -> str:
return f"User(id={self.id})"
def load_user(user_id):
if user_id == "admin":
return User("admin")
return None

View File

@@ -1,74 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging as lg
from typing import TYPE_CHECKING
from flask import Flask
from flask_login import LoginManager
from flask_socketio import SocketIO
from judas_server.web.user import load_user
if TYPE_CHECKING:
from judas_server.backend import BackendServer
class JudasWebServer:
def __init__(self, backend: BackendServer, secret_key: str) -> None:
self.logger: lg.Logger = lg.getLogger(
f"{__name__}.{self.__class__.__name__}"
)
self.logger.debug("Initializing JudasWebServer...")
self.backend: BackendServer = backend
self.app: Flask = Flask(
__name__, static_folder="static", template_folder="templates"
)
self.app.secret_key = secret_key
self.app.config["WEB_SERVER"] = self
self.app.config["BACKEND"] = self.backend
# hard-code password
self.app.config["PASSWORD"] = "123"
# extensions
self.login_manager: LoginManager = LoginManager()
self.socketio: SocketIO = SocketIO(self.app, cors_allowed_origins="*")
self.configure_extensions()
self.init_routes()
def configure_extensions(self) -> None:
self.logger.debug("Configuring extensions...")
self.login_manager.init_app(self.app)
self.login_manager.user_loader(load_user)
self.login_manager.login_view = "auth.login"
# TODO: add login page
def init_routes(self) -> None:
self.logger.debug("Initializing routes...")
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:
self.logger.info(f"Starting web server on {host}:{port}...")
self.socketio.run(app=self.app, host=host, port=port)
self.logger.info("Server stopped.")

44
uv.lock generated
View File

@@ -358,18 +358,19 @@ wheels = [
[[package]] [[package]]
name = "judas-protocol" name = "judas-protocol"
version = "0.5.0" version = "0.9.1"
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#c48b69ecee16f5824ffd8bce8921341d5fa326b7" } source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#085c34f232f95313d66db48a7d17bc25c92a35ae" }
[[package]] [[package]]
name = "judas-server" name = "judas-server"
version = "0.4.0" version = "0.7.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "flask" }, { name = "flask" },
{ name = "flask-login" }, { name = "flask-login" },
{ name = "flask-socketio" }, { name = "flask-socketio" },
{ name = "judas-protocol" }, { name = "judas-protocol" },
{ name = "pyyaml" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
@@ -394,6 +395,7 @@ requires-dist = [
{ name = "flask-login", specifier = ">=0.6.3" }, { name = "flask-login", specifier = ">=0.6.3" },
{ name = "flask-socketio", specifier = ">=5.5.1" }, { name = "flask-socketio", specifier = ">=5.5.1" },
{ name = "judas-protocol", git = "https://gitea.pufereq.pl/judas/judas_protocol.git" }, { name = "judas-protocol", git = "https://gitea.pufereq.pl/judas/judas_protocol.git" },
{ name = "pyyaml", specifier = ">=6.0.3" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
@@ -696,6 +698,42 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" }, { url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" },
] ]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.5" version = "2.32.5"