lanscape 2.3.0b1__tar.gz → 2.4.0b1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lanscape-2.3.0b1/lanscape.egg-info → lanscape-2.4.0b1}/PKG-INFO +3 -1
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/net_tools.py +4 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/runtime_args.py +6 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/main.py +22 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/css/style.css +23 -8
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/js/core.js +14 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/js/quietReload.js +3 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/main.html +30 -32
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/config.html +1 -1
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/ip-table-row.html +6 -6
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/ip-table.html +4 -4
- lanscape-2.4.0b1/lanscape/ui/ws/__init__.py +31 -0
- lanscape-2.4.0b1/lanscape/ui/ws/delta.py +170 -0
- lanscape-2.4.0b1/lanscape/ui/ws/handlers/__init__.py +20 -0
- lanscape-2.4.0b1/lanscape/ui/ws/handlers/base.py +145 -0
- lanscape-2.4.0b1/lanscape/ui/ws/handlers/port.py +184 -0
- lanscape-2.4.0b1/lanscape/ui/ws/handlers/scan.py +352 -0
- lanscape-2.4.0b1/lanscape/ui/ws/handlers/tools.py +145 -0
- lanscape-2.4.0b1/lanscape/ui/ws/protocol.py +86 -0
- lanscape-2.4.0b1/lanscape/ui/ws/server.py +375 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1/lanscape.egg-info}/PKG-INFO +3 -1
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape.egg-info/SOURCES.txt +11 -1
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape.egg-info/requires.txt +2 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/pyproject.toml +4 -2
- lanscape-2.4.0b1/tests/test_websocket.py +940 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/LICENSE +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/MANIFEST.in +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/README.md +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/__init__.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/__main__.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/__init__.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/app_scope.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/decorators.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/device_alive.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/errors.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/ip_parser.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/logger.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/mac_lookup.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/port_manager.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/scan_config.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/service_scan.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/subnet_scan.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/core/version_manager.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/resources/mac_addresses/convert_csv.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/resources/mac_addresses/mac_db.json +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/resources/ports/convert_csv.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/resources/ports/full.json +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/resources/ports/large.json +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/resources/ports/medium.json +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/resources/ports/small.json +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/resources/ports/test_port_list_scan.json +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/resources/services/definitions.jsonc +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/__init__.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/app.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/__init__.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/api/__init__.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/api/port.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/api/scan.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/api/tools.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/web/__init__.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/web/routes.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/shutdown_handler.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/favicon.ico +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/js/layout-sizing.js +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/js/main.js +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/js/on-tab-close.js +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/js/scan-config.js +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/js/shutdown-server.js +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/js/subnet-info.js +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/js/subnet-selector.js +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/static/lanscape.webmanifest +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/base.html +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/core/head.html +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/core/scripts.html +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/error.html +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/info.html +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/device-detail.html +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/export.html +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/overview.html +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/scan-error.html +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan.html +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape/ui/templates/shutdown.html +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape.egg-info/dependency_links.txt +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape.egg-info/entry_points.txt +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/lanscape.egg-info/top_level.txt +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/setup.cfg +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/tests/test_api.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/tests/test_decorators.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/tests/test_env.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/tests/test_globals.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/tests/test_library.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/tests/test_logging.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/tests/test_port_scan.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/tests/test_port_scan_linux_fd_exhaustion.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/tests/test_service_scan.py +0 -0
- {lanscape-2.3.0b1 → lanscape-2.4.0b1}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lanscape
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0b1
|
|
4
4
|
Summary: A python based local network scanner
|
|
5
5
|
Author-email: Michael Dennis <michael@dipduo.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,10 +26,12 @@ Requires-Dist: tabulate==0.9.0
|
|
|
26
26
|
Requires-Dist: pydantic
|
|
27
27
|
Requires-Dist: icmplib
|
|
28
28
|
Requires-Dist: pwa-launcher>=1.1.0
|
|
29
|
+
Requires-Dist: websockets<14.0,>=12.0
|
|
29
30
|
Provides-Extra: dev
|
|
30
31
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
31
32
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
32
33
|
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
33
35
|
Requires-Dist: openai>=1.0.0; extra == "dev"
|
|
34
36
|
Requires-Dist: pylint>=3.0; extra == "dev"
|
|
35
37
|
Requires-Dist: autopep8>=2.0; extra == "dev"
|
|
@@ -98,6 +98,10 @@ class Device(BaseModel):
|
|
|
98
98
|
if self.alive:
|
|
99
99
|
self.hostname = self._get_hostname()
|
|
100
100
|
self._get_mac_addresses()
|
|
101
|
+
if not self.manufacturer:
|
|
102
|
+
self.manufacturer = self._get_manufacturer(
|
|
103
|
+
self.get_mac()
|
|
104
|
+
)
|
|
101
105
|
|
|
102
106
|
# Fallback for pydantic v1: use dict() and enrich output
|
|
103
107
|
if not _PYD_V2:
|
|
@@ -14,6 +14,8 @@ class RuntimeArgs:
|
|
|
14
14
|
loglevel: str = 'INFO'
|
|
15
15
|
flask_logging: bool = False
|
|
16
16
|
persistent: bool = False
|
|
17
|
+
ws_server: bool = False
|
|
18
|
+
ws_port: int = 8766
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
def parse_args() -> RuntimeArgs:
|
|
@@ -35,6 +37,10 @@ def parse_args() -> RuntimeArgs:
|
|
|
35
37
|
help='Don\'t exit after browser is closed')
|
|
36
38
|
parser.add_argument('--debug', action='store_true',
|
|
37
39
|
help='Shorthand debug mode (equivalent to "--loglevel DEBUG --reloader")')
|
|
40
|
+
parser.add_argument('--ws-server', action='store_true',
|
|
41
|
+
help='Start WebSocket server instead of Flask UI')
|
|
42
|
+
parser.add_argument('--ws-port', type=int, default=8766,
|
|
43
|
+
help='Port for WebSocket server (default: 8766)')
|
|
38
44
|
|
|
39
45
|
# Parse the arguments
|
|
40
46
|
args = parser.parse_args()
|
|
@@ -16,6 +16,7 @@ from lanscape.core.logger import configure_logging
|
|
|
16
16
|
from lanscape.core.runtime_args import parse_args
|
|
17
17
|
from lanscape.core.version_manager import get_installed_version, is_update_available
|
|
18
18
|
from lanscape.ui.app import start_webserver_daemon, start_webserver
|
|
19
|
+
from lanscape.ui.ws.server import run_server
|
|
19
20
|
# do this so any logs generated on import are displayed
|
|
20
21
|
args = parse_args()
|
|
21
22
|
configure_logging(args.loglevel, args.logfile, args.flask_logging)
|
|
@@ -48,6 +49,11 @@ def _main():
|
|
|
48
49
|
else:
|
|
49
50
|
log.info('Flask reloaded app.')
|
|
50
51
|
|
|
52
|
+
# Check if WebSocket server mode is requested
|
|
53
|
+
if args.ws_server:
|
|
54
|
+
start_websocket_server()
|
|
55
|
+
return
|
|
56
|
+
|
|
51
57
|
args.port = get_valid_port(args.port)
|
|
52
58
|
|
|
53
59
|
try:
|
|
@@ -72,6 +78,22 @@ def try_check_update():
|
|
|
72
78
|
log.warning('Unable to check for updates.')
|
|
73
79
|
|
|
74
80
|
|
|
81
|
+
def start_websocket_server():
|
|
82
|
+
"""Start the WebSocket server."""
|
|
83
|
+
args.ws_port = get_valid_port(args.ws_port)
|
|
84
|
+
log.info(f'Starting WebSocket server on port {args.ws_port}')
|
|
85
|
+
log.info(f'React UI should connect to ws://localhost:{args.ws_port}')
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
run_server(host='0.0.0.0', port=args.ws_port)
|
|
89
|
+
except KeyboardInterrupt:
|
|
90
|
+
log.info('WebSocket server stopped by user')
|
|
91
|
+
except Exception as e:
|
|
92
|
+
log.critical(f'WebSocket server failed: {e}')
|
|
93
|
+
log.debug(traceback.format_exc())
|
|
94
|
+
raise
|
|
95
|
+
|
|
96
|
+
|
|
75
97
|
def open_browser(url: str, wait=2) -> Popen | None:
|
|
76
98
|
"""
|
|
77
99
|
Open a browser window to the specified
|
|
@@ -66,13 +66,16 @@ body:has(.submodule) footer {
|
|
|
66
66
|
|
|
67
67
|
#header {
|
|
68
68
|
background-color: var(--primary-bg);
|
|
69
|
-
padding: 8px
|
|
69
|
+
padding: 8px 2%;
|
|
70
70
|
margin: 0;
|
|
71
71
|
display: block;
|
|
72
72
|
position: relative;
|
|
73
73
|
box-shadow: 0 0 10px var(--box-shadow);
|
|
74
74
|
width: 100vw;
|
|
75
75
|
}
|
|
76
|
+
#header .title {
|
|
77
|
+
font-size: 36px;
|
|
78
|
+
}
|
|
76
79
|
|
|
77
80
|
footer {
|
|
78
81
|
position: sticky;
|
|
@@ -163,7 +166,12 @@ details {
|
|
|
163
166
|
|
|
164
167
|
|
|
165
168
|
#scan-form {
|
|
166
|
-
width:
|
|
169
|
+
width: 60vw;
|
|
170
|
+
margin: 0;
|
|
171
|
+
min-width: 300px;
|
|
172
|
+
max-width: 700px;
|
|
173
|
+
}
|
|
174
|
+
#scan-form .form-group {
|
|
167
175
|
margin: 0;
|
|
168
176
|
}
|
|
169
177
|
#scan-form label {
|
|
@@ -304,6 +312,9 @@ details {
|
|
|
304
312
|
}
|
|
305
313
|
#advanced-modal label {
|
|
306
314
|
font-size: 12px;
|
|
315
|
+
text-overflow: ellipsis;
|
|
316
|
+
overflow: hidden;
|
|
317
|
+
white-space: nowrap;
|
|
307
318
|
}
|
|
308
319
|
#advanced-modal .form-check {
|
|
309
320
|
width: fit-content;
|
|
@@ -447,8 +458,10 @@ button {
|
|
|
447
458
|
|
|
448
459
|
#scan-form #scan-submit {
|
|
449
460
|
border: none;
|
|
450
|
-
padding: 10px
|
|
451
|
-
margin
|
|
461
|
+
padding: 10px 3%;
|
|
462
|
+
margin: 0 15px;
|
|
463
|
+
min-width: 55px;
|
|
464
|
+
height: 42px;
|
|
452
465
|
}
|
|
453
466
|
|
|
454
467
|
/* Button Styling */
|
|
@@ -657,10 +670,6 @@ input[type="range"] {
|
|
|
657
670
|
.table thead tr th.detail-col
|
|
658
671
|
{
|
|
659
672
|
width: 30px;
|
|
660
|
-
/*
|
|
661
|
-
background-color: var(--body-bg);
|
|
662
|
-
border: 1px solid var(--text-almost-hidden);
|
|
663
|
-
*/
|
|
664
673
|
}
|
|
665
674
|
.table td:has(.info-icon-container) {
|
|
666
675
|
width: 30px;
|
|
@@ -709,6 +718,12 @@ input[type="range"] {
|
|
|
709
718
|
span.alt {
|
|
710
719
|
color: var(--text-accent-color);
|
|
711
720
|
}
|
|
721
|
+
span.no-wrap {
|
|
722
|
+
white-space: nowrap;
|
|
723
|
+
overflow: hidden;
|
|
724
|
+
text-overflow: ellipsis;
|
|
725
|
+
display: block;
|
|
726
|
+
}
|
|
712
727
|
.colorful-buttons a{
|
|
713
728
|
margin:2px;
|
|
714
729
|
color: var(--text-color);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
$(document).ready(function() {
|
|
2
2
|
rightSizeDocLayout(0,showFooter);
|
|
3
3
|
initTooltips();
|
|
4
|
+
adjustNoWrap();
|
|
4
5
|
})
|
|
5
6
|
|
|
6
7
|
$(window).on('resize', function() {
|
|
7
8
|
rightSizeDocLayout();
|
|
9
|
+
adjustNoWrap();
|
|
8
10
|
});
|
|
9
11
|
|
|
10
12
|
|
|
@@ -36,4 +38,16 @@ function initTooltips() {
|
|
|
36
38
|
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
37
39
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
|
38
40
|
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
An imperfect approach to adjusting
|
|
45
|
+
text field width within a table
|
|
46
|
+
*/
|
|
47
|
+
function adjustNoWrap() {
|
|
48
|
+
$('.no-wrap').width(0);
|
|
49
|
+
$('.no-wrap').each(function() {
|
|
50
|
+
var parentWidth = $(this).parent().width();
|
|
51
|
+
$(this).width(parseInt(parentWidth));
|
|
52
|
+
});
|
|
39
53
|
}
|
|
@@ -8,6 +8,9 @@ function quietReload() {
|
|
|
8
8
|
var newDoc = new DOMParser().parseFromString(data, 'text/html');
|
|
9
9
|
// replace current body with the new body content
|
|
10
10
|
$('body').html($(newDoc.body).html());
|
|
11
|
+
if (typeof adjustNoWrap === 'function') {
|
|
12
|
+
adjustNoWrap();
|
|
13
|
+
}
|
|
11
14
|
});
|
|
12
15
|
}
|
|
13
16
|
setTimeout(function() {
|
|
@@ -3,45 +3,43 @@
|
|
|
3
3
|
{% block content %}
|
|
4
4
|
<div id="header">
|
|
5
5
|
<!-- Header and Scan Submission Inline -->
|
|
6
|
-
<div class="d-flex justify-content-between align-items-center flex-
|
|
6
|
+
<div class="d-flex justify-content-between align-items-center flex-wrap">
|
|
7
7
|
<a href="/" class="text-decoration-none" aria-label="Go to homepage">
|
|
8
8
|
<h1 class="title">
|
|
9
9
|
<span>LAN</span>scape
|
|
10
10
|
</h1>
|
|
11
11
|
</a>
|
|
12
12
|
<!-- Right side: settings + form -->
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
<div id="subnet-info"></div>
|
|
20
|
-
</div>
|
|
21
|
-
<!-- Subnet input with dropdown -->
|
|
22
|
-
<div class="input-group">
|
|
23
|
-
<button
|
|
24
|
-
type="button"
|
|
25
|
-
id="settings-btn"
|
|
26
|
-
class="btn btn-secondary start"
|
|
27
|
-
data-bs-toggle="tooltip"
|
|
28
|
-
data-bs-placement="bottom"
|
|
29
|
-
title="Advanced scan settings"
|
|
30
|
-
>
|
|
31
|
-
<i class="fa-solid fa-gear"></i>
|
|
32
|
-
</button>
|
|
33
|
-
<input type="text" id="subnet" name="subnet" class="form-control" value="{{ subnet }}" placeholder="Enter subnet">
|
|
34
|
-
<button class="btn btn-secondary dropdown-toggle end" type="button" id="subnet-dropdown" data-bs-toggle="dropdown" aria-expanded="false"></button>
|
|
35
|
-
<ul class="dropdown-menu" aria-labelledby="subnet-dropdown" id="dropdown-list">
|
|
36
|
-
{% for subnet_option in alternate_subnets %}
|
|
37
|
-
<li><a class="dropdown-item" href="#">{{ subnet_option['subnet'] }}</a></li>
|
|
38
|
-
{% endfor %}
|
|
39
|
-
</ul>
|
|
40
|
-
</div>
|
|
13
|
+
<form id="scan-form" class="d-flex align-items-end">
|
|
14
|
+
<div class="form-group me-2">
|
|
15
|
+
<!-- Above subnet input -->
|
|
16
|
+
<div class="label-container">
|
|
17
|
+
<label for="subnet">Subnet:</label>
|
|
18
|
+
<div id="subnet-info"></div>
|
|
41
19
|
</div>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
20
|
+
<!-- Subnet input with dropdown -->
|
|
21
|
+
<div class="input-group">
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
id="settings-btn"
|
|
25
|
+
class="btn btn-secondary start"
|
|
26
|
+
data-bs-toggle="tooltip"
|
|
27
|
+
data-bs-placement="bottom"
|
|
28
|
+
title="Advanced scan settings"
|
|
29
|
+
>
|
|
30
|
+
<i class="fa-solid fa-gear"></i>
|
|
31
|
+
</button>
|
|
32
|
+
<input type="text" id="subnet" name="subnet" class="form-control" value="{{ subnet }}" placeholder="Enter subnet">
|
|
33
|
+
<button class="btn btn-secondary dropdown-toggle end" type="button" id="subnet-dropdown" data-bs-toggle="dropdown" aria-expanded="false"></button>
|
|
34
|
+
<ul class="dropdown-menu" aria-labelledby="subnet-dropdown" id="dropdown-list">
|
|
35
|
+
{% for subnet_option in alternate_subnets %}
|
|
36
|
+
<li><a class="dropdown-item" href="#">{{ subnet_option['subnet'] }}</a></li>
|
|
37
|
+
{% endfor %}
|
|
38
|
+
</ul>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<button type="submit" id="scan-submit" class="btn btn-primary">Scan</button>
|
|
42
|
+
</form>
|
|
45
43
|
</div>
|
|
46
44
|
|
|
47
45
|
<div id="scan-progress-bar"></div>
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
</div>
|
|
95
95
|
<div class="col-1 descriptor">=</div>
|
|
96
96
|
<div class="col-3">
|
|
97
|
-
<label for="total-ping-attempts">Max pings per device</label>
|
|
97
|
+
<label for="total-ping-attempts" id="tpa-label">Max pings per device</label>
|
|
98
98
|
<input type="number" id="total-ping-attempts" class="form-control" readonly>
|
|
99
99
|
</div>
|
|
100
100
|
</div>
|
|
@@ -7,20 +7,20 @@
|
|
|
7
7
|
></i>
|
|
8
8
|
</div>
|
|
9
9
|
</td>
|
|
10
|
-
<td>
|
|
10
|
+
<td class="ip">
|
|
11
11
|
<div>{{ device.ip }}</div>
|
|
12
12
|
{% if device.hostname %}
|
|
13
|
-
<
|
|
13
|
+
<span class="alt no-wrap">{{device.hostname}}</span>
|
|
14
14
|
{% endif %}
|
|
15
15
|
</td>
|
|
16
|
-
<td>
|
|
16
|
+
<td class="mac">
|
|
17
17
|
<div>{{ device.mac_addr or 'Unknown' }}</div>
|
|
18
18
|
{% if device.manufacturer %}
|
|
19
|
-
<
|
|
19
|
+
<span class="alt no-wrap">{{device.manufacturer}}</span>
|
|
20
20
|
{% endif %}
|
|
21
21
|
</td>
|
|
22
|
-
<td>{{ device.ports | join(", ") }}</td>
|
|
23
|
-
<td>
|
|
22
|
+
<td class="ports">{{ device.ports | join(", ") }}</td>
|
|
23
|
+
<td class="stage">
|
|
24
24
|
{% if device.stage == 'complete' %}
|
|
25
25
|
<span class="badge badge-success">complete</span>
|
|
26
26
|
{% elif device.stage == 'found' %}
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
<thead>
|
|
10
10
|
<tr>
|
|
11
11
|
<th class="detail-col" scope="col"></th>
|
|
12
|
-
<th scope="col">IP / <span class="alt">Host</span></th>
|
|
13
|
-
<th scope="col">MAC / <span class="alt">Manufacturer</span></th>
|
|
14
|
-
<th scope="col">Open Ports</th>
|
|
15
|
-
<th scope="col">Stage</th>
|
|
12
|
+
<th class="ip" scope="col">IP / <span class="alt">Host</span></th>
|
|
13
|
+
<th class="mac" scope="col">MAC / <span class="alt">Manufacturer</span></th>
|
|
14
|
+
<th class="ports" scope="col">Open Ports</th>
|
|
15
|
+
<th class="stage" scope="col">Stage</th>
|
|
16
16
|
</tr>
|
|
17
17
|
</thead>
|
|
18
18
|
<tbody>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket interface for LANscape.
|
|
3
|
+
|
|
4
|
+
Provides a standalone WebSocket server that exposes all LANscape functionality,
|
|
5
|
+
allowing clients to initiate scans, manage port lists, and receive real-time
|
|
6
|
+
scan results with delta updates.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from lanscape.ui.ws.server import WebSocketServer, run_server
|
|
10
|
+
from lanscape.ui.ws.protocol import (
|
|
11
|
+
WSMessage,
|
|
12
|
+
WSRequest,
|
|
13
|
+
WSResponse,
|
|
14
|
+
WSError,
|
|
15
|
+
WSEvent,
|
|
16
|
+
MessageType
|
|
17
|
+
)
|
|
18
|
+
from lanscape.ui.ws.delta import DeltaTracker, ScanDeltaTracker
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
'WebSocketServer',
|
|
22
|
+
'run_server',
|
|
23
|
+
'WSMessage',
|
|
24
|
+
'WSRequest',
|
|
25
|
+
'WSResponse',
|
|
26
|
+
'WSError',
|
|
27
|
+
'WSEvent',
|
|
28
|
+
'MessageType',
|
|
29
|
+
'DeltaTracker',
|
|
30
|
+
'ScanDeltaTracker'
|
|
31
|
+
]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Delta tracking for efficient scan result updates.
|
|
3
|
+
|
|
4
|
+
Uses content hashing to detect changes and only send updated data
|
|
5
|
+
to clients, reducing bandwidth and improving performance.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import hashlib
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DeltaState(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
Represents the state of a tracked item.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
hash: Content hash of the serialized data
|
|
21
|
+
data: The actual data being tracked
|
|
22
|
+
"""
|
|
23
|
+
hash: str
|
|
24
|
+
data: Any
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DeltaTracker:
|
|
28
|
+
"""
|
|
29
|
+
Tracks changes to scan results and provides delta updates.
|
|
30
|
+
|
|
31
|
+
Uses MD5 hashing to detect changes in device data and scan state.
|
|
32
|
+
Clients receive only the changed portions of scan results.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
"""Initialize the delta tracker with empty state."""
|
|
37
|
+
self._states: dict[str, DeltaState] = {}
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def compute_hash(data: Any) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Compute MD5 hash of serialized data.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
data: Any JSON-serializable data
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Hex string of the MD5 hash
|
|
49
|
+
"""
|
|
50
|
+
serialized = json.dumps(data, sort_keys=True, default=str)
|
|
51
|
+
return hashlib.md5(serialized.encode()).hexdigest()
|
|
52
|
+
|
|
53
|
+
def update(self, key: str, data: Any) -> Optional[Any]:
|
|
54
|
+
"""
|
|
55
|
+
Update tracked state and return data if changed.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
key: Unique identifier for the tracked item
|
|
59
|
+
data: Current data for the item
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The data if it has changed, None otherwise
|
|
63
|
+
"""
|
|
64
|
+
new_hash = self.compute_hash(data)
|
|
65
|
+
current_state = self._states.get(key)
|
|
66
|
+
|
|
67
|
+
if current_state is None or current_state.hash != new_hash:
|
|
68
|
+
self._states[key] = DeltaState(hash=new_hash, data=data)
|
|
69
|
+
return data
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def get_changes(self, items: dict[str, Any]) -> dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
Get only changed items from a dictionary.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
items: Dictionary of key -> data to check for changes
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Dictionary containing only the changed items
|
|
81
|
+
"""
|
|
82
|
+
changes = {}
|
|
83
|
+
for key, data in items.items():
|
|
84
|
+
result = self.update(key, data)
|
|
85
|
+
if result is not None:
|
|
86
|
+
changes[key] = result
|
|
87
|
+
return changes
|
|
88
|
+
|
|
89
|
+
def reset(self, key: Optional[str] = None) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Reset tracked state.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
key: Specific key to reset, or None to reset all
|
|
95
|
+
"""
|
|
96
|
+
if key is not None:
|
|
97
|
+
self._states.pop(key, None)
|
|
98
|
+
else:
|
|
99
|
+
self._states.clear()
|
|
100
|
+
|
|
101
|
+
def has_key(self, key: str) -> bool:
|
|
102
|
+
"""
|
|
103
|
+
Check if a key is being tracked.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
key: The key to check
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if the key is tracked, False otherwise
|
|
110
|
+
"""
|
|
111
|
+
return key in self._states
|
|
112
|
+
|
|
113
|
+
def get_hash(self, key: str) -> Optional[str]:
|
|
114
|
+
"""
|
|
115
|
+
Get the current hash for a tracked key.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
key: The key to get the hash for
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The current hash, or None if not tracked
|
|
122
|
+
"""
|
|
123
|
+
state = self._states.get(key)
|
|
124
|
+
return state.hash if state else None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ScanDeltaTracker(DeltaTracker):
|
|
128
|
+
"""
|
|
129
|
+
Specialized delta tracker for scan results.
|
|
130
|
+
|
|
131
|
+
Tracks individual devices and overall scan metadata,
|
|
132
|
+
providing efficient delta updates for real-time scan monitoring.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def get_scan_delta(self, scan_results: dict) -> dict:
|
|
136
|
+
"""
|
|
137
|
+
Get delta update for scan results.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
scan_results: Full scan results dictionary
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dictionary containing only changed fields:
|
|
144
|
+
- 'devices': List of changed device data
|
|
145
|
+
- 'metadata': Changed scan metadata (if any)
|
|
146
|
+
- 'has_changes': Boolean indicating if there are any changes
|
|
147
|
+
"""
|
|
148
|
+
delta = {
|
|
149
|
+
'devices': [],
|
|
150
|
+
'metadata': None,
|
|
151
|
+
'has_changes': False
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Track metadata changes (everything except devices)
|
|
155
|
+
metadata = {k: v for k, v in scan_results.items() if k != 'devices'}
|
|
156
|
+
metadata_change = self.update('_metadata', metadata)
|
|
157
|
+
if metadata_change is not None:
|
|
158
|
+
delta['metadata'] = metadata_change
|
|
159
|
+
delta['has_changes'] = True
|
|
160
|
+
|
|
161
|
+
# Track individual device changes
|
|
162
|
+
devices = scan_results.get('devices', [])
|
|
163
|
+
for device in devices:
|
|
164
|
+
device_ip = device.get('ip', str(id(device)))
|
|
165
|
+
device_change = self.update(f'device_{device_ip}', device)
|
|
166
|
+
if device_change is not None:
|
|
167
|
+
delta['devices'].append(device_change)
|
|
168
|
+
delta['has_changes'] = True
|
|
169
|
+
|
|
170
|
+
return delta
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket handlers for LANscape.
|
|
3
|
+
|
|
4
|
+
Provides handler classes for different functional areas:
|
|
5
|
+
- ScanHandler: Network scanning operations
|
|
6
|
+
- PortHandler: Port list management
|
|
7
|
+
- ToolsHandler: Utility functions (subnet validation, etc.)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from lanscape.ui.ws.handlers.base import BaseHandler
|
|
11
|
+
from lanscape.ui.ws.handlers.scan import ScanHandler
|
|
12
|
+
from lanscape.ui.ws.handlers.port import PortHandler
|
|
13
|
+
from lanscape.ui.ws.handlers.tools import ToolsHandler
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
'BaseHandler',
|
|
17
|
+
'ScanHandler',
|
|
18
|
+
'PortHandler',
|
|
19
|
+
'ToolsHandler'
|
|
20
|
+
]
|