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
|
@@ -1,103 +1,37 @@
|
|
|
1
1
|
<tr>
|
|
2
2
|
<td>
|
|
3
|
+
<div class="info-icon-container">
|
|
4
|
+
<i
|
|
5
|
+
class="fa-solid fa-circle-info info-icon"
|
|
6
|
+
onclick="parent.openDeviceDetail('{{ device.ip }}')"
|
|
7
|
+
></i>
|
|
8
|
+
</div>
|
|
9
|
+
</td>
|
|
10
|
+
<td class="ip">
|
|
3
11
|
<div>{{ device.ip }}</div>
|
|
4
12
|
{% if device.hostname %}
|
|
5
|
-
<
|
|
13
|
+
<span class="alt no-wrap">{{device.hostname}}</span>
|
|
6
14
|
{% endif %}
|
|
7
15
|
</td>
|
|
8
|
-
<td>
|
|
16
|
+
<td class="mac">
|
|
9
17
|
<div>{{ device.mac_addr or 'Unknown' }}</div>
|
|
10
18
|
{% if device.manufacturer %}
|
|
11
|
-
<
|
|
19
|
+
<span class="alt no-wrap">{{device.manufacturer}}</span>
|
|
12
20
|
{% endif %}
|
|
13
21
|
</td>
|
|
14
|
-
<td>{{ device.ports | join(", ") }}</td>
|
|
15
|
-
<td>
|
|
22
|
+
<td class="ports">{{ device.ports | join(", ") }}</td>
|
|
23
|
+
<td class="stage">
|
|
16
24
|
{% if device.stage == 'complete' %}
|
|
17
25
|
<span class="badge badge-success">complete</span>
|
|
18
26
|
{% elif device.stage == 'found' %}
|
|
19
27
|
<span class="badge badge-secondary">found</span>
|
|
20
28
|
{% elif device.stage == 'scanning' %}
|
|
21
|
-
<span class="badge badge-info">
|
|
29
|
+
<span class="badge badge-info">
|
|
30
|
+
scanning
|
|
31
|
+
{{ (device.ports_scanned/data.port_list_length*100)|round|int if data.port_list_length > 0 else 0 }}%
|
|
32
|
+
</span>
|
|
22
33
|
{% else %}
|
|
23
34
|
<span class="badge badge-warning">{{device.stage}}</span>
|
|
24
35
|
{% endif %}
|
|
25
36
|
</td>
|
|
26
|
-
<td class="colorful-buttons">
|
|
27
|
-
{% if 80 in device.ports %}
|
|
28
|
-
<a href="http://{{ device.ip }}" class="btn btn-sm" target="_blank">HTTP</a>
|
|
29
|
-
{% endif %}
|
|
30
|
-
{% if 443 in device.ports %}
|
|
31
|
-
<a href="https://{{ device.ip }}" class="btn btn-sm" target="_blank">HTTPS</a>
|
|
32
|
-
{% endif %}
|
|
33
|
-
{% if 22 in device.ports %}
|
|
34
|
-
<a href="ssh://{{ device.ip }}" class="btn btn-sm " target="_blank">SSH</a>
|
|
35
|
-
{% endif %}
|
|
36
|
-
{% if 21 in device.ports %}
|
|
37
|
-
<a href="ftp://{{ device.ip }}" class="btn btn-sm " target="_blank">FTP</a>
|
|
38
|
-
{% endif %}
|
|
39
|
-
{% if 23 in device.ports %}
|
|
40
|
-
<a href="telnet://{{ device.ip }}" class="btn btn-sm " target="_blank">Telnet</a>
|
|
41
|
-
{% endif %}
|
|
42
|
-
{% if 445 in device.ports %}
|
|
43
|
-
<a href="smb://{{ device.ip }}" class="btn btn-sm " target="_blank">SMB</a>
|
|
44
|
-
{% endif %}
|
|
45
|
-
{% if 3306 in device.ports %}
|
|
46
|
-
<a href="mysql://{{ device.ip }}" class="btn btn-sm " target="_blank">MySQL</a>
|
|
47
|
-
{% endif %}
|
|
48
|
-
{% if 5432 in device.ports %}
|
|
49
|
-
<a href="postgresql://{{ device.ip }}" class="btn btn-sm " target="_blank">PostgreSQL</a>
|
|
50
|
-
{% endif %}
|
|
51
|
-
{% if 3389 in device.ports %}
|
|
52
|
-
<a href="rdp://{{ device.ip }}" class="btn btn-sm " target="_blank">RDP</a>
|
|
53
|
-
{% endif %}
|
|
54
|
-
{% if 5900 in device.ports %}
|
|
55
|
-
<a href="vnc://{{ device.ip }}" class="btn btn-sm " target="_blank">VNC</a>
|
|
56
|
-
{% endif %}
|
|
57
|
-
{% if 27017 in device.ports %}
|
|
58
|
-
<a href="mongodb://{{ device.ip }}" class="btn btn-sm " target="_blank">MongoDB</a>
|
|
59
|
-
{% endif %}
|
|
60
|
-
{% if 6379 in device.ports %}
|
|
61
|
-
<a href="redis://{{ device.ip }}" class="btn btn-sm " target="_blank">Redis</a>
|
|
62
|
-
{% endif %}
|
|
63
|
-
{% if 11211 in device.ports %}
|
|
64
|
-
<a href="memcached://{{ device.ip }}" class="btn btn-sm " target="_blank">Memcached</a>
|
|
65
|
-
{% endif %}
|
|
66
|
-
{% if 1433 in device.ports %}
|
|
67
|
-
<a href="mssql://{{ device.ip }}" class="btn btn-sm " target="_blank">MSSQL</a>
|
|
68
|
-
{% endif %}
|
|
69
|
-
{% if 1521 in device.ports %}
|
|
70
|
-
<a href="oracle://{{ device.ip }}" class="btn btn-sm " target="_blank">Oracle</a>
|
|
71
|
-
{% endif %}
|
|
72
|
-
{% if 27015 in device.ports %}
|
|
73
|
-
<a href="source://{{ device.ip }}" class="btn btn-sm " target="_blank">Source</a>
|
|
74
|
-
{% endif %}
|
|
75
|
-
{% if 27015 in device.ports %}
|
|
76
|
-
<a href="minecraft://{{ device.ip }}" class="btn btn-sm " target="_blank">Minecraft</a>
|
|
77
|
-
{% endif %}
|
|
78
|
-
{% if 27015 in device.ports %}
|
|
79
|
-
<a href="teamspeak://{{ device.ip }}" class="btn btn-sm " target="_blank">Teamspeak</a>
|
|
80
|
-
{% endif %}
|
|
81
|
-
{% if 27015 in device.ports %}
|
|
82
|
-
<a href="gmod://{{ device.ip }}" class="btn btn-sm " target="_blank">Garry's Mod</a>
|
|
83
|
-
{% endif %}
|
|
84
|
-
{% if 27015 in device.ports %}
|
|
85
|
-
<a href="rust://{{ device.ip }}" class="btn btn-sm " target="_blank">Rust</a>
|
|
86
|
-
{% endif %}
|
|
87
|
-
{% if 27015 in device.ports %}
|
|
88
|
-
<a href="ark://{{ device.ip }}" class="btn btn-sm " target="_blank">ARK</a>
|
|
89
|
-
{% endif %}
|
|
90
|
-
{% if 27015 in device.ports %}
|
|
91
|
-
<a href="terraria://{{ device.ip }}" class="btn btn-sm " target="_blank">Terraria</a>
|
|
92
|
-
{% endif %}
|
|
93
|
-
{% if 27015 in device.ports %}
|
|
94
|
-
<a href="factorio://{{ device.ip }}" class="btn btn-sm " target="_blank">Factorio</a>
|
|
95
|
-
{% endif %}
|
|
96
|
-
{% if 27015 in device.ports %}
|
|
97
|
-
<a href="starmade://{{ device.ip }}" class="btn btn-sm " target="_blank">StarMade</a>
|
|
98
|
-
{% endif %}
|
|
99
|
-
{% if 27015 in device.ports %}
|
|
100
|
-
<a href="7dtd://{{ device.ip }}" class="btn btn-sm " target="_blank">7 Days to Die</a>
|
|
101
|
-
{% endif %}
|
|
102
|
-
</td>
|
|
103
37
|
</tr>
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
<table class="table table-bordered table-striped">
|
|
9
9
|
<thead>
|
|
10
10
|
<tr>
|
|
11
|
-
<th
|
|
12
|
-
<th scope="col">
|
|
13
|
-
<th scope="col">
|
|
14
|
-
<th scope="col">
|
|
15
|
-
<th scope="col">
|
|
11
|
+
<th class="detail-col" scope="col"></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
|
+
]
|
lanscape/ui/ws/delta.py
ADDED
|
@@ -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
|
+
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base handler class for WebSocket handlers.
|
|
3
|
+
|
|
4
|
+
Provides common functionality and interface for all handlers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import traceback
|
|
10
|
+
from typing import Any, Callable, Optional
|
|
11
|
+
|
|
12
|
+
from lanscape.ui.ws.protocol import WSRequest, WSResponse, WSError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseHandler:
|
|
16
|
+
"""
|
|
17
|
+
Base class for WebSocket message handlers.
|
|
18
|
+
|
|
19
|
+
Provides registration of action handlers and dispatch logic.
|
|
20
|
+
Subclasses should register their handlers in __init__.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
"""Initialize the handler with an empty action registry."""
|
|
25
|
+
self._actions: dict[str, Callable] = {}
|
|
26
|
+
self.log = logging.getLogger(self.__class__.__name__)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def prefix(self) -> str:
|
|
30
|
+
"""
|
|
31
|
+
The action prefix for this handler.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
String prefix (e.g., 'scan', 'port', 'tools')
|
|
35
|
+
"""
|
|
36
|
+
raise NotImplementedError("Subclasses must define a prefix")
|
|
37
|
+
|
|
38
|
+
def register(self, action: str, handler: Callable) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Register an action handler.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
action: The action name (without prefix)
|
|
44
|
+
handler: The callable to handle the action
|
|
45
|
+
"""
|
|
46
|
+
full_action = f"{self.prefix}.{action}"
|
|
47
|
+
self._actions[full_action] = handler
|
|
48
|
+
self.log.debug(f"Registered handler for action: {full_action}")
|
|
49
|
+
|
|
50
|
+
def can_handle(self, action: str) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Check if this handler can process the given action.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
action: The full action name
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if this handler can process the action
|
|
59
|
+
"""
|
|
60
|
+
return action in self._actions
|
|
61
|
+
|
|
62
|
+
def get_actions(self) -> list[str]:
|
|
63
|
+
"""
|
|
64
|
+
Get all registered actions.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
List of action names this handler supports
|
|
68
|
+
"""
|
|
69
|
+
return list(self._actions.keys())
|
|
70
|
+
|
|
71
|
+
async def handle(
|
|
72
|
+
self,
|
|
73
|
+
request: WSRequest,
|
|
74
|
+
send_event: Optional[Callable] = None
|
|
75
|
+
) -> WSResponse | WSError:
|
|
76
|
+
"""
|
|
77
|
+
Handle a WebSocket request.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
request: The incoming request
|
|
81
|
+
send_event: Optional callback to send events to the client
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
WSResponse on success, WSError on failure
|
|
85
|
+
"""
|
|
86
|
+
action = request.action
|
|
87
|
+
handler = self._actions.get(action)
|
|
88
|
+
|
|
89
|
+
if handler is None:
|
|
90
|
+
return WSError(
|
|
91
|
+
id=request.id,
|
|
92
|
+
action=action,
|
|
93
|
+
error=f"Unknown action: {action}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
params = request.params or {}
|
|
98
|
+
# Check if handler is async
|
|
99
|
+
if asyncio.iscoroutinefunction(handler):
|
|
100
|
+
result = await handler(params, send_event)
|
|
101
|
+
else:
|
|
102
|
+
result = handler(params, send_event)
|
|
103
|
+
|
|
104
|
+
return WSResponse(
|
|
105
|
+
id=request.id,
|
|
106
|
+
action=action,
|
|
107
|
+
data=result,
|
|
108
|
+
success=True
|
|
109
|
+
)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
self.log.error(f"Error handling {action}: {e}")
|
|
112
|
+
self.log.debug(traceback.format_exc())
|
|
113
|
+
return WSError(
|
|
114
|
+
id=request.id,
|
|
115
|
+
action=action,
|
|
116
|
+
error=str(e),
|
|
117
|
+
traceback=traceback.format_exc()
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _get_param(
|
|
121
|
+
self,
|
|
122
|
+
params: dict[str, Any],
|
|
123
|
+
key: str,
|
|
124
|
+
required: bool = False,
|
|
125
|
+
default: Any = None
|
|
126
|
+
) -> Any:
|
|
127
|
+
"""
|
|
128
|
+
Get a parameter from the params dict with optional validation.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
params: The parameters dictionary
|
|
132
|
+
key: The parameter key
|
|
133
|
+
required: Whether the parameter is required
|
|
134
|
+
default: Default value if not present
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
The parameter value
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
ValueError: If required parameter is missing
|
|
141
|
+
"""
|
|
142
|
+
value = params.get(key, default)
|
|
143
|
+
if required and value is None:
|
|
144
|
+
raise ValueError(f"Missing required parameter: {key}")
|
|
145
|
+
return value
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket handler for port list management.
|
|
3
|
+
|
|
4
|
+
Provides handlers for:
|
|
5
|
+
- Listing port lists
|
|
6
|
+
- Getting port list details
|
|
7
|
+
- Creating, updating, deleting port lists
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, Callable, Optional
|
|
11
|
+
|
|
12
|
+
from lanscape.core.port_manager import PortManager
|
|
13
|
+
from lanscape.ui.ws.handlers.base import BaseHandler
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PortHandler(BaseHandler):
|
|
17
|
+
"""
|
|
18
|
+
Handler for port list management WebSocket actions.
|
|
19
|
+
|
|
20
|
+
Supports actions:
|
|
21
|
+
- port.list: Get all port list names
|
|
22
|
+
- port.list_summary: Get port lists with port counts
|
|
23
|
+
- port.get: Get a specific port list
|
|
24
|
+
- port.create: Create a new port list
|
|
25
|
+
- port.update: Update an existing port list
|
|
26
|
+
- port.delete: Delete a port list
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, port_manager: Optional[PortManager] = None):
|
|
30
|
+
"""
|
|
31
|
+
Initialize the port handler.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
port_manager: Optional PortManager instance.
|
|
35
|
+
"""
|
|
36
|
+
super().__init__()
|
|
37
|
+
self._port_manager = port_manager or PortManager()
|
|
38
|
+
|
|
39
|
+
# Register handlers
|
|
40
|
+
self.register('list', self._handle_list)
|
|
41
|
+
self.register('list_summary', self._handle_list_summary)
|
|
42
|
+
self.register('get', self._handle_get)
|
|
43
|
+
self.register('create', self._handle_create)
|
|
44
|
+
self.register('update', self._handle_update)
|
|
45
|
+
self.register('delete', self._handle_delete)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def prefix(self) -> str:
|
|
49
|
+
"""Return the action prefix for this handler."""
|
|
50
|
+
return 'port'
|
|
51
|
+
|
|
52
|
+
def _handle_list(
|
|
53
|
+
self,
|
|
54
|
+
params: dict[str, Any], # pylint: disable=unused-argument
|
|
55
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
56
|
+
) -> list:
|
|
57
|
+
"""
|
|
58
|
+
Get all available port lists.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
List of port list names
|
|
62
|
+
"""
|
|
63
|
+
return self._port_manager.get_port_lists()
|
|
64
|
+
|
|
65
|
+
def _handle_list_summary(
|
|
66
|
+
self,
|
|
67
|
+
params: dict[str, Any], # pylint: disable=unused-argument
|
|
68
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
69
|
+
) -> list:
|
|
70
|
+
"""
|
|
71
|
+
Get port list names with their port counts.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of dicts with 'name' and 'count' keys
|
|
75
|
+
"""
|
|
76
|
+
summaries = []
|
|
77
|
+
for name in self._port_manager.get_port_lists():
|
|
78
|
+
ports = self._port_manager.get_port_list(name) or {}
|
|
79
|
+
summaries.append({
|
|
80
|
+
'name': name,
|
|
81
|
+
'count': len(ports)
|
|
82
|
+
})
|
|
83
|
+
return summaries
|
|
84
|
+
|
|
85
|
+
def _handle_get(
|
|
86
|
+
self,
|
|
87
|
+
params: dict[str, Any],
|
|
88
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
89
|
+
) -> dict:
|
|
90
|
+
"""
|
|
91
|
+
Get a specific port list by name.
|
|
92
|
+
|
|
93
|
+
Params:
|
|
94
|
+
name: Name of the port list to retrieve
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Dict mapping port numbers to service names
|
|
98
|
+
"""
|
|
99
|
+
name = self._get_param(params, 'name', required=True)
|
|
100
|
+
port_list = self._port_manager.get_port_list(name)
|
|
101
|
+
|
|
102
|
+
if port_list is None:
|
|
103
|
+
raise ValueError(f"Port list not found: {name}")
|
|
104
|
+
|
|
105
|
+
return port_list
|
|
106
|
+
|
|
107
|
+
def _handle_create(
|
|
108
|
+
self,
|
|
109
|
+
params: dict[str, Any],
|
|
110
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
111
|
+
) -> dict:
|
|
112
|
+
"""
|
|
113
|
+
Create a new port list.
|
|
114
|
+
|
|
115
|
+
Params:
|
|
116
|
+
name: Name for the new port list
|
|
117
|
+
ports: Dict mapping port numbers to service names
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Dict with success status
|
|
121
|
+
"""
|
|
122
|
+
name = self._get_param(params, 'name', required=True)
|
|
123
|
+
ports = self._get_param(params, 'ports', required=True)
|
|
124
|
+
|
|
125
|
+
success = self._port_manager.create_port_list(name, ports)
|
|
126
|
+
|
|
127
|
+
if not success:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"Failed to create port list '{name}'. "
|
|
130
|
+
"It may already exist or have invalid port data."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return {'success': True, 'name': name}
|
|
134
|
+
|
|
135
|
+
def _handle_update(
|
|
136
|
+
self,
|
|
137
|
+
params: dict[str, Any],
|
|
138
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
139
|
+
) -> dict:
|
|
140
|
+
"""
|
|
141
|
+
Update an existing port list.
|
|
142
|
+
|
|
143
|
+
Params:
|
|
144
|
+
name: Name of the port list to update
|
|
145
|
+
ports: New dict mapping port numbers to service names
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dict with success status
|
|
149
|
+
"""
|
|
150
|
+
name = self._get_param(params, 'name', required=True)
|
|
151
|
+
ports = self._get_param(params, 'ports', required=True)
|
|
152
|
+
|
|
153
|
+
success = self._port_manager.update_port_list(name, ports)
|
|
154
|
+
|
|
155
|
+
if not success:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"Failed to update port list '{name}'. "
|
|
158
|
+
"It may not exist or have invalid port data."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return {'success': True, 'name': name}
|
|
162
|
+
|
|
163
|
+
def _handle_delete(
|
|
164
|
+
self,
|
|
165
|
+
params: dict[str, Any],
|
|
166
|
+
send_event: Optional[Callable] = None # pylint: disable=unused-argument
|
|
167
|
+
) -> dict:
|
|
168
|
+
"""
|
|
169
|
+
Delete a port list.
|
|
170
|
+
|
|
171
|
+
Params:
|
|
172
|
+
name: Name of the port list to delete
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Dict with success status
|
|
176
|
+
"""
|
|
177
|
+
name = self._get_param(params, 'name', required=True)
|
|
178
|
+
|
|
179
|
+
success = self._port_manager.delete_port_list(name)
|
|
180
|
+
|
|
181
|
+
if not success:
|
|
182
|
+
raise ValueError(f"Failed to delete port list '{name}'. It may not exist.")
|
|
183
|
+
|
|
184
|
+
return {'success': True, 'name': name}
|