lanscape 1.3.8a1__py3-none-any.whl → 2.4.0a2__py3-none-any.whl
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.
Potentially problematic release.
This version of lanscape might be problematic. Click here for more details.
- lanscape/__init__.py +8 -4
- lanscape/{libraries → core}/app_scope.py +21 -3
- lanscape/core/decorators.py +231 -0
- lanscape/{libraries → core}/device_alive.py +83 -16
- lanscape/{libraries → core}/ip_parser.py +2 -26
- lanscape/{libraries → core}/net_tools.py +209 -66
- lanscape/{libraries → core}/runtime_args.py +6 -0
- lanscape/{libraries → core}/scan_config.py +103 -5
- lanscape/core/service_scan.py +222 -0
- lanscape/{libraries → core}/subnet_scan.py +30 -14
- lanscape/{libraries → core}/version_manager.py +15 -17
- lanscape/resources/ports/test_port_list_scan.json +4 -0
- lanscape/resources/services/definitions.jsonc +576 -400
- lanscape/ui/app.py +17 -5
- lanscape/ui/blueprints/__init__.py +1 -1
- lanscape/ui/blueprints/api/port.py +15 -1
- lanscape/ui/blueprints/api/scan.py +1 -1
- lanscape/ui/blueprints/api/tools.py +4 -4
- lanscape/ui/blueprints/web/routes.py +29 -2
- lanscape/ui/main.py +46 -19
- lanscape/ui/shutdown_handler.py +2 -2
- lanscape/ui/static/css/style.css +186 -20
- lanscape/ui/static/js/core.js +14 -0
- lanscape/ui/static/js/main.js +30 -2
- lanscape/ui/static/js/quietReload.js +3 -0
- lanscape/ui/static/js/scan-config.js +56 -6
- lanscape/ui/templates/base.html +6 -8
- lanscape/ui/templates/core/head.html +1 -1
- lanscape/ui/templates/info.html +20 -5
- lanscape/ui/templates/main.html +33 -36
- lanscape/ui/templates/scan/config.html +214 -176
- lanscape/ui/templates/scan/device-detail.html +111 -0
- lanscape/ui/templates/scan/ip-table-row.html +17 -83
- lanscape/ui/templates/scan/ip-table.html +5 -5
- lanscape/ui/ws/__init__.py +31 -0
- lanscape/ui/ws/delta.py +170 -0
- lanscape/ui/ws/handlers/__init__.py +20 -0
- lanscape/ui/ws/handlers/base.py +145 -0
- lanscape/ui/ws/handlers/port.py +184 -0
- lanscape/ui/ws/handlers/scan.py +352 -0
- lanscape/ui/ws/handlers/tools.py +145 -0
- lanscape/ui/ws/protocol.py +86 -0
- lanscape/ui/ws/server.py +375 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/METADATA +18 -3
- lanscape-2.4.0a2.dist-info/RECORD +85 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/WHEEL +1 -1
- lanscape-2.4.0a2.dist-info/entry_points.txt +2 -0
- lanscape/libraries/decorators.py +0 -170
- lanscape/libraries/service_scan.py +0 -50
- lanscape/libraries/web_browser.py +0 -210
- lanscape-1.3.8a1.dist-info/RECORD +0 -74
- /lanscape/{libraries → core}/__init__.py +0 -0
- /lanscape/{libraries → core}/errors.py +0 -0
- /lanscape/{libraries → core}/logger.py +0 -0
- /lanscape/{libraries → core}/mac_lookup.py +0 -0
- /lanscape/{libraries → core}/port_manager.py +0 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/licenses/LICENSE +0 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket handler for network scanning operations.
|
|
3
|
+
|
|
4
|
+
Provides handlers for:
|
|
5
|
+
- Starting scans (threaded and synchronous)
|
|
6
|
+
- Getting scan results (full and delta)
|
|
7
|
+
- Getting scan summary/status
|
|
8
|
+
- Terminating scans
|
|
9
|
+
- Subscribing to real-time scan updates
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any, Callable, Optional
|
|
16
|
+
|
|
17
|
+
from lanscape.core.subnet_scan import ScanManager, ScanConfig
|
|
18
|
+
from lanscape.ui.ws.handlers.base import BaseHandler
|
|
19
|
+
from lanscape.ui.ws.delta import ScanDeltaTracker
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ScanHandler(BaseHandler):
|
|
23
|
+
"""
|
|
24
|
+
Handler for scan-related WebSocket actions.
|
|
25
|
+
|
|
26
|
+
Supports actions:
|
|
27
|
+
- scan.start: Start a new scan (non-blocking)
|
|
28
|
+
- scan.start_sync: Start a scan and wait for completion
|
|
29
|
+
- scan.get: Get full scan results
|
|
30
|
+
- scan.get_delta: Get only changed results since last request
|
|
31
|
+
- scan.summary: Get scan summary/progress
|
|
32
|
+
- scan.terminate: Stop a running scan
|
|
33
|
+
- scan.subscribe: Subscribe to real-time scan updates
|
|
34
|
+
- scan.unsubscribe: Unsubscribe from scan updates
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, scan_manager: Optional[ScanManager] = None):
|
|
38
|
+
"""
|
|
39
|
+
Initialize the scan handler.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
scan_manager: Optional ScanManager instance. If not provided,
|
|
43
|
+
uses the singleton instance.
|
|
44
|
+
"""
|
|
45
|
+
super().__init__()
|
|
46
|
+
self._scan_manager = scan_manager or ScanManager()
|
|
47
|
+
self._delta_trackers: dict[str, ScanDeltaTracker] = {}
|
|
48
|
+
self._subscriptions: dict[str, set] = {} # scan_id -> set of client_ids
|
|
49
|
+
self.log = logging.getLogger('ScanHandler')
|
|
50
|
+
|
|
51
|
+
# Register handlers
|
|
52
|
+
self.register('start', self._handle_start)
|
|
53
|
+
self.register('start_sync', self._handle_start_sync)
|
|
54
|
+
self.register('get', self._handle_get)
|
|
55
|
+
self.register('get_delta', self._handle_get_delta)
|
|
56
|
+
self.register('summary', self._handle_summary)
|
|
57
|
+
self.register('terminate', self._handle_terminate)
|
|
58
|
+
self.register('subscribe', self._handle_subscribe)
|
|
59
|
+
self.register('unsubscribe', self._handle_unsubscribe)
|
|
60
|
+
self.register('list', self._handle_list)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def prefix(self) -> str:
|
|
64
|
+
"""Return the action prefix for this handler."""
|
|
65
|
+
return 'scan'
|
|
66
|
+
|
|
67
|
+
def _handle_start(
|
|
68
|
+
self,
|
|
69
|
+
params: dict[str, Any],
|
|
70
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
71
|
+
) -> dict:
|
|
72
|
+
"""
|
|
73
|
+
Start a new network scan.
|
|
74
|
+
|
|
75
|
+
Params:
|
|
76
|
+
subnet: Target subnet to scan
|
|
77
|
+
port_list: Name of the port list to use
|
|
78
|
+
... (other ScanConfig parameters)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dict with scan_id and status
|
|
82
|
+
"""
|
|
83
|
+
config = ScanConfig.from_dict(params)
|
|
84
|
+
scan = self._scan_manager.new_scan(config)
|
|
85
|
+
self.log.info(f"Started scan {scan.uid} for {config.subnet}")
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
'scan_id': scan.uid,
|
|
89
|
+
'status': 'running'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async def _handle_start_sync(
|
|
93
|
+
self,
|
|
94
|
+
params: dict[str, Any],
|
|
95
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
96
|
+
) -> dict:
|
|
97
|
+
"""
|
|
98
|
+
Start a scan and wait for completion.
|
|
99
|
+
|
|
100
|
+
Params:
|
|
101
|
+
Same as _handle_start
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Dict with scan_id and status='complete'
|
|
105
|
+
"""
|
|
106
|
+
config = ScanConfig.from_dict(params)
|
|
107
|
+
scan = self._scan_manager.new_scan(config)
|
|
108
|
+
self.log.info(f"Started sync scan {scan.uid} for {config.subnet}")
|
|
109
|
+
|
|
110
|
+
# Wait for completion in a non-blocking way
|
|
111
|
+
while scan.running:
|
|
112
|
+
await asyncio.sleep(0.5)
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
'scan_id': scan.uid,
|
|
116
|
+
'status': 'complete'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
def _handle_get(
|
|
120
|
+
self,
|
|
121
|
+
params: dict[str, Any],
|
|
122
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
123
|
+
) -> dict:
|
|
124
|
+
"""
|
|
125
|
+
Get full scan results.
|
|
126
|
+
|
|
127
|
+
Params:
|
|
128
|
+
scan_id: The scan ID to retrieve
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Full scan results as dict
|
|
132
|
+
"""
|
|
133
|
+
scan_id = self._get_param(params, 'scan_id', required=True)
|
|
134
|
+
scan = self._scan_manager.get_scan(scan_id)
|
|
135
|
+
|
|
136
|
+
if scan is None:
|
|
137
|
+
raise ValueError(f"Scan not found: {scan_id}")
|
|
138
|
+
|
|
139
|
+
return json.loads(scan.results.export(str))
|
|
140
|
+
|
|
141
|
+
def _handle_get_delta(
|
|
142
|
+
self,
|
|
143
|
+
params: dict[str, Any],
|
|
144
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
145
|
+
) -> dict:
|
|
146
|
+
"""
|
|
147
|
+
Get only changed scan results since last request.
|
|
148
|
+
|
|
149
|
+
Params:
|
|
150
|
+
scan_id: The scan ID to retrieve
|
|
151
|
+
client_id: Client identifier for tracking deltas
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Delta update containing only changed devices and metadata
|
|
155
|
+
"""
|
|
156
|
+
scan_id = self._get_param(params, 'scan_id', required=True)
|
|
157
|
+
client_id = self._get_param(params, 'client_id', default='default')
|
|
158
|
+
|
|
159
|
+
scan = self._scan_manager.get_scan(scan_id)
|
|
160
|
+
if scan is None:
|
|
161
|
+
raise ValueError(f"Scan not found: {scan_id}")
|
|
162
|
+
|
|
163
|
+
# Get or create delta tracker for this client
|
|
164
|
+
tracker_key = f"{scan_id}_{client_id}"
|
|
165
|
+
if tracker_key not in self._delta_trackers:
|
|
166
|
+
self._delta_trackers[tracker_key] = ScanDeltaTracker()
|
|
167
|
+
|
|
168
|
+
tracker = self._delta_trackers[tracker_key]
|
|
169
|
+
full_results = json.loads(scan.results.export(str))
|
|
170
|
+
delta = tracker.get_scan_delta(full_results)
|
|
171
|
+
|
|
172
|
+
# Add scan status info
|
|
173
|
+
delta['scan_id'] = scan_id
|
|
174
|
+
delta['running'] = scan.running
|
|
175
|
+
|
|
176
|
+
# Add calculated progress from backend (more accurate than simple host count)
|
|
177
|
+
if delta.get('metadata') is None:
|
|
178
|
+
delta['metadata'] = {}
|
|
179
|
+
delta['metadata']['percent_complete'] = scan.calc_percent_complete()
|
|
180
|
+
|
|
181
|
+
return delta
|
|
182
|
+
|
|
183
|
+
def _handle_summary(
|
|
184
|
+
self,
|
|
185
|
+
params: dict[str, Any],
|
|
186
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
187
|
+
) -> dict:
|
|
188
|
+
"""
|
|
189
|
+
Get scan summary/progress.
|
|
190
|
+
|
|
191
|
+
Params:
|
|
192
|
+
scan_id: The scan ID to get summary for
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Dict with running status, percent complete, stage, runtime, and device counts
|
|
196
|
+
"""
|
|
197
|
+
scan_id = self._get_param(params, 'scan_id', required=True)
|
|
198
|
+
scan = self._scan_manager.get_scan(scan_id)
|
|
199
|
+
|
|
200
|
+
if scan is None:
|
|
201
|
+
raise ValueError(f"Scan not found: {scan_id}")
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
'scan_id': scan_id,
|
|
205
|
+
'running': scan.running,
|
|
206
|
+
'percent_complete': scan.calc_percent_complete(),
|
|
207
|
+
'stage': scan.results.stage,
|
|
208
|
+
'runtime': scan.results.get_runtime(),
|
|
209
|
+
'devices': {
|
|
210
|
+
'scanned': scan.results.devices_scanned,
|
|
211
|
+
'alive': len(scan.results.devices),
|
|
212
|
+
'total': scan.results.devices_total
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
def _handle_terminate(
|
|
217
|
+
self,
|
|
218
|
+
params: dict[str, Any],
|
|
219
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
220
|
+
) -> dict:
|
|
221
|
+
"""
|
|
222
|
+
Terminate a running scan.
|
|
223
|
+
|
|
224
|
+
Params:
|
|
225
|
+
scan_id: The scan ID to terminate
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Dict with success status
|
|
229
|
+
"""
|
|
230
|
+
scan_id = self._get_param(params, 'scan_id', required=True)
|
|
231
|
+
scan = self._scan_manager.get_scan(scan_id)
|
|
232
|
+
|
|
233
|
+
if scan is None:
|
|
234
|
+
raise ValueError(f"Scan not found: {scan_id}")
|
|
235
|
+
|
|
236
|
+
scan.terminate()
|
|
237
|
+
self.log.info(f"Terminated scan {scan_id}")
|
|
238
|
+
|
|
239
|
+
return {'success': True, 'scan_id': scan_id}
|
|
240
|
+
|
|
241
|
+
def _handle_subscribe(
|
|
242
|
+
self,
|
|
243
|
+
params: dict[str, Any],
|
|
244
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
245
|
+
) -> dict:
|
|
246
|
+
"""
|
|
247
|
+
Subscribe to real-time scan updates.
|
|
248
|
+
|
|
249
|
+
Params:
|
|
250
|
+
scan_id: The scan ID to subscribe to
|
|
251
|
+
client_id: Client identifier for the subscription
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Dict with subscription confirmation
|
|
255
|
+
"""
|
|
256
|
+
scan_id = self._get_param(params, 'scan_id', required=True)
|
|
257
|
+
client_id = self._get_param(params, 'client_id', required=True)
|
|
258
|
+
|
|
259
|
+
scan = self._scan_manager.get_scan(scan_id)
|
|
260
|
+
if scan is None:
|
|
261
|
+
raise ValueError(f"Scan not found: {scan_id}")
|
|
262
|
+
|
|
263
|
+
if scan_id not in self._subscriptions:
|
|
264
|
+
self._subscriptions[scan_id] = set()
|
|
265
|
+
self._subscriptions[scan_id].add(client_id)
|
|
266
|
+
|
|
267
|
+
self.log.debug(f"Client {client_id} subscribed to scan {scan_id}")
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
'subscribed': True,
|
|
271
|
+
'scan_id': scan_id,
|
|
272
|
+
'client_id': client_id
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
def _handle_unsubscribe(
|
|
276
|
+
self,
|
|
277
|
+
params: dict[str, Any],
|
|
278
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
279
|
+
) -> dict:
|
|
280
|
+
"""
|
|
281
|
+
Unsubscribe from scan updates.
|
|
282
|
+
|
|
283
|
+
Params:
|
|
284
|
+
scan_id: The scan ID to unsubscribe from
|
|
285
|
+
client_id: Client identifier for the subscription
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Dict with unsubscription confirmation
|
|
289
|
+
"""
|
|
290
|
+
scan_id = self._get_param(params, 'scan_id', required=True)
|
|
291
|
+
client_id = self._get_param(params, 'client_id', required=True)
|
|
292
|
+
|
|
293
|
+
if scan_id in self._subscriptions:
|
|
294
|
+
self._subscriptions[scan_id].discard(client_id)
|
|
295
|
+
|
|
296
|
+
# Clean up delta tracker
|
|
297
|
+
tracker_key = f"{scan_id}_{client_id}"
|
|
298
|
+
self._delta_trackers.pop(tracker_key, None)
|
|
299
|
+
|
|
300
|
+
self.log.debug(f"Client {client_id} unsubscribed from scan {scan_id}")
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
'unsubscribed': True,
|
|
304
|
+
'scan_id': scan_id,
|
|
305
|
+
'client_id': client_id
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
def _handle_list(
|
|
309
|
+
self,
|
|
310
|
+
params: dict[str, Any], # pylint: disable=unused-argument
|
|
311
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
312
|
+
) -> list:
|
|
313
|
+
"""
|
|
314
|
+
List all scans.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
List of scan summaries
|
|
318
|
+
"""
|
|
319
|
+
scans = []
|
|
320
|
+
for scan in self._scan_manager.scans:
|
|
321
|
+
scans.append({
|
|
322
|
+
'scan_id': scan.uid,
|
|
323
|
+
'subnet': scan.subnet_str,
|
|
324
|
+
'running': scan.running,
|
|
325
|
+
'stage': scan.results.stage,
|
|
326
|
+
'devices_found': len(scan.results.devices)
|
|
327
|
+
})
|
|
328
|
+
return scans
|
|
329
|
+
|
|
330
|
+
def get_subscriptions(self, scan_id: str) -> set:
|
|
331
|
+
"""
|
|
332
|
+
Get all client IDs subscribed to a scan.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
scan_id: The scan ID
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Set of client IDs
|
|
339
|
+
"""
|
|
340
|
+
return self._subscriptions.get(scan_id, set())
|
|
341
|
+
|
|
342
|
+
def cleanup_client(self, client_id: str) -> None:
|
|
343
|
+
"""
|
|
344
|
+
Clean up all subscriptions for a client.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
client_id: The client ID to clean up
|
|
348
|
+
"""
|
|
349
|
+
for scan_id in list(self._subscriptions.keys()):
|
|
350
|
+
self._subscriptions[scan_id].discard(client_id)
|
|
351
|
+
tracker_key = f"{scan_id}_{client_id}"
|
|
352
|
+
self._delta_trackers.pop(tracker_key, None)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket handler for utility tools.
|
|
3
|
+
|
|
4
|
+
Provides handlers for:
|
|
5
|
+
- Subnet validation and listing
|
|
6
|
+
- Default configuration retrieval
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import traceback
|
|
10
|
+
from typing import Any, Callable, Optional
|
|
11
|
+
|
|
12
|
+
from lanscape.core.net_tools import get_all_network_subnets, is_arp_supported
|
|
13
|
+
from lanscape.core.ip_parser import parse_ip_input
|
|
14
|
+
from lanscape.core.errors import SubnetTooLargeError
|
|
15
|
+
from lanscape.core.scan_config import DEFAULT_CONFIGS
|
|
16
|
+
from lanscape.ui.ws.handlers.base import BaseHandler
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ToolsHandler(BaseHandler):
|
|
20
|
+
"""
|
|
21
|
+
Handler for utility tool WebSocket actions.
|
|
22
|
+
|
|
23
|
+
Supports actions:
|
|
24
|
+
- tools.subnet_test: Validate a subnet string
|
|
25
|
+
- tools.subnet_list: List all network subnets on the system
|
|
26
|
+
- tools.config_defaults: Get default scan configurations
|
|
27
|
+
- tools.arp_supported: Check if ARP is supported on this system
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
"""Initialize the tools handler."""
|
|
32
|
+
super().__init__()
|
|
33
|
+
|
|
34
|
+
# Register handlers
|
|
35
|
+
self.register('subnet_test', self._handle_subnet_test)
|
|
36
|
+
self.register('subnet_list', self._handle_subnet_list)
|
|
37
|
+
self.register('config_defaults', self._handle_config_defaults)
|
|
38
|
+
self.register('arp_supported', self._handle_arp_supported)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def prefix(self) -> str:
|
|
42
|
+
"""Return the action prefix for this handler."""
|
|
43
|
+
return 'tools'
|
|
44
|
+
|
|
45
|
+
def _handle_subnet_test(
|
|
46
|
+
self,
|
|
47
|
+
params: dict[str, Any],
|
|
48
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
49
|
+
) -> dict:
|
|
50
|
+
"""
|
|
51
|
+
Validate a subnet string.
|
|
52
|
+
|
|
53
|
+
Params:
|
|
54
|
+
subnet: The subnet string to validate
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Dict with 'valid', 'msg', and 'count' fields
|
|
58
|
+
"""
|
|
59
|
+
subnet = self._get_param(params, 'subnet')
|
|
60
|
+
|
|
61
|
+
if not subnet:
|
|
62
|
+
return {'valid': False, 'msg': 'Subnet cannot be blank', 'count': -1}
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
ips = parse_ip_input(subnet)
|
|
66
|
+
length = len(ips)
|
|
67
|
+
return {
|
|
68
|
+
'valid': True,
|
|
69
|
+
'msg': f"{length} IP{'s' if length > 1 else ''}",
|
|
70
|
+
'count': length
|
|
71
|
+
}
|
|
72
|
+
except SubnetTooLargeError:
|
|
73
|
+
return {
|
|
74
|
+
'valid': False,
|
|
75
|
+
'msg': 'subnet too large',
|
|
76
|
+
'error': traceback.format_exc(),
|
|
77
|
+
'count': -1
|
|
78
|
+
}
|
|
79
|
+
except Exception:
|
|
80
|
+
return {
|
|
81
|
+
'valid': False,
|
|
82
|
+
'msg': 'invalid subnet',
|
|
83
|
+
'error': traceback.format_exc(),
|
|
84
|
+
'count': -1
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def _handle_subnet_list(
|
|
88
|
+
self,
|
|
89
|
+
params: dict[str, Any], # pylint: disable=unused-argument
|
|
90
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
91
|
+
) -> list | dict:
|
|
92
|
+
"""
|
|
93
|
+
List all network subnets on the system.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of subnet information or error dict
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
return get_all_network_subnets()
|
|
100
|
+
except Exception:
|
|
101
|
+
return {'error': traceback.format_exc()}
|
|
102
|
+
|
|
103
|
+
def _handle_config_defaults(
|
|
104
|
+
self,
|
|
105
|
+
params: dict[str, Any], # pylint: disable=unused-argument
|
|
106
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
107
|
+
) -> dict:
|
|
108
|
+
"""
|
|
109
|
+
Get default scan configurations.
|
|
110
|
+
|
|
111
|
+
Adjusts presets that rely on ARP_LOOKUP when ARP is not supported.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Dict of preset name -> ScanConfig dict
|
|
115
|
+
"""
|
|
116
|
+
arp_supported = is_arp_supported()
|
|
117
|
+
|
|
118
|
+
configs = {}
|
|
119
|
+
for key, config in DEFAULT_CONFIGS.items():
|
|
120
|
+
config_dict = config.to_dict()
|
|
121
|
+
|
|
122
|
+
if not arp_supported:
|
|
123
|
+
lookup_types = list(config_dict.get('lookup_type') or [])
|
|
124
|
+
if 'ARP_LOOKUP' in lookup_types:
|
|
125
|
+
lookup_types = [lt for lt in lookup_types if lt != 'ARP_LOOKUP']
|
|
126
|
+
if 'POKE_THEN_ARP' not in lookup_types:
|
|
127
|
+
lookup_types.append('POKE_THEN_ARP')
|
|
128
|
+
config_dict['lookup_type'] = lookup_types
|
|
129
|
+
|
|
130
|
+
configs[key] = config_dict
|
|
131
|
+
|
|
132
|
+
return configs
|
|
133
|
+
|
|
134
|
+
def _handle_arp_supported(
|
|
135
|
+
self,
|
|
136
|
+
params: dict[str, Any], # pylint: disable=unused-argument
|
|
137
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
138
|
+
) -> dict:
|
|
139
|
+
"""
|
|
140
|
+
Check if ARP is supported on this system.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dict with 'supported' boolean
|
|
144
|
+
"""
|
|
145
|
+
return {'supported': is_arp_supported()}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket protocol definitions for LANscape.
|
|
3
|
+
|
|
4
|
+
Defines the message format and types used for communication between
|
|
5
|
+
WebSocket clients and the LANscape server.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MessageType(str, Enum):
|
|
15
|
+
"""Types of WebSocket messages."""
|
|
16
|
+
# Requests
|
|
17
|
+
REQUEST = "request"
|
|
18
|
+
# Responses
|
|
19
|
+
RESPONSE = "response"
|
|
20
|
+
ERROR = "error"
|
|
21
|
+
# Push notifications (server-initiated)
|
|
22
|
+
EVENT = "event"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WSMessage(BaseModel):
|
|
26
|
+
"""Base WebSocket message structure."""
|
|
27
|
+
type: MessageType
|
|
28
|
+
id: Optional[str] = None # Message ID for request/response correlation
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WSRequest(WSMessage):
|
|
32
|
+
"""
|
|
33
|
+
WebSocket request message from client.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
action: The action to perform (e.g., 'scan.start', 'port.list')
|
|
37
|
+
params: Optional parameters for the action
|
|
38
|
+
"""
|
|
39
|
+
type: MessageType = Field(default=MessageType.REQUEST)
|
|
40
|
+
action: str
|
|
41
|
+
params: Optional[dict[str, Any]] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class WSResponse(WSMessage):
|
|
45
|
+
"""
|
|
46
|
+
WebSocket response message from server.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
action: The action this is responding to
|
|
50
|
+
data: The response data
|
|
51
|
+
success: Whether the action was successful
|
|
52
|
+
"""
|
|
53
|
+
type: MessageType = Field(default=MessageType.RESPONSE)
|
|
54
|
+
action: str
|
|
55
|
+
data: Any = None
|
|
56
|
+
success: bool = True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class WSError(WSMessage):
|
|
60
|
+
"""
|
|
61
|
+
WebSocket error message from server.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
action: The action that caused the error
|
|
65
|
+
error: Error message
|
|
66
|
+
traceback: Optional full traceback for debugging
|
|
67
|
+
"""
|
|
68
|
+
type: MessageType = Field(default=MessageType.ERROR)
|
|
69
|
+
action: Optional[str] = None
|
|
70
|
+
error: str
|
|
71
|
+
traceback: Optional[str] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class WSEvent(WSMessage):
|
|
75
|
+
"""
|
|
76
|
+
WebSocket event message from server (push notification).
|
|
77
|
+
|
|
78
|
+
Used for real-time updates like scan progress and results.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
event: The event name (e.g., 'scan.progress', 'scan.device_found')
|
|
82
|
+
data: Event data
|
|
83
|
+
"""
|
|
84
|
+
type: MessageType = Field(default=MessageType.EVENT)
|
|
85
|
+
event: str
|
|
86
|
+
data: Any = None
|