lanscape 2.1.2__tar.gz → 2.4.0a1__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.
Potentially problematic release.
This version of lanscape might be problematic. Click here for more details.
- {lanscape-2.1.2/lanscape.egg-info → lanscape-2.4.0a1}/PKG-INFO +6 -1
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/net_tools.py +18 -3
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/runtime_args.py +6 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/service_scan.py +38 -3
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/api/port.py +14 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/main.py +44 -16
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/css/style.css +25 -10
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/core.js +14 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/quietReload.js +3 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/scan-config.js +17 -6
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/main.html +30 -32
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/config.html +6 -4
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/ip-table-row.html +6 -6
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/ip-table.html +4 -4
- lanscape-2.4.0a1/lanscape/ui/ws/__init__.py +31 -0
- lanscape-2.4.0a1/lanscape/ui/ws/delta.py +170 -0
- lanscape-2.4.0a1/lanscape/ui/ws/handlers/__init__.py +20 -0
- lanscape-2.4.0a1/lanscape/ui/ws/handlers/base.py +145 -0
- lanscape-2.4.0a1/lanscape/ui/ws/handlers/port.py +184 -0
- lanscape-2.4.0a1/lanscape/ui/ws/handlers/scan.py +352 -0
- lanscape-2.4.0a1/lanscape/ui/ws/handlers/tools.py +145 -0
- lanscape-2.4.0a1/lanscape/ui/ws/protocol.py +86 -0
- lanscape-2.4.0a1/lanscape/ui/ws/server.py +375 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1/lanscape.egg-info}/PKG-INFO +6 -1
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape.egg-info/SOURCES.txt +12 -2
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape.egg-info/requires.txt +5 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/pyproject.toml +8 -3
- {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_api.py +30 -1
- lanscape-2.4.0a1/tests/test_port_scan_linux_fd_exhaustion.py +297 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_utils.py +2 -2
- lanscape-2.4.0a1/tests/test_websocket.py +940 -0
- lanscape-2.1.2/lanscape/core/web_browser.py +0 -210
- {lanscape-2.1.2 → lanscape-2.4.0a1}/LICENSE +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/MANIFEST.in +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/README.md +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/__init__.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/__main__.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/__init__.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/app_scope.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/decorators.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/device_alive.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/errors.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/ip_parser.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/logger.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/mac_lookup.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/port_manager.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/scan_config.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/subnet_scan.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/version_manager.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/mac_addresses/convert_csv.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/mac_addresses/mac_db.json +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/ports/convert_csv.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/ports/full.json +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/ports/large.json +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/ports/medium.json +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/ports/small.json +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/ports/test_port_list_scan.json +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/services/definitions.jsonc +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/__init__.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/app.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/__init__.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/api/__init__.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/api/scan.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/api/tools.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/web/__init__.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/web/routes.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/shutdown_handler.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/favicon.ico +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/layout-sizing.js +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/main.js +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/on-tab-close.js +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/shutdown-server.js +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/subnet-info.js +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/subnet-selector.js +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/lanscape.webmanifest +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/base.html +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/core/head.html +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/core/scripts.html +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/error.html +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/info.html +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/device-detail.html +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/export.html +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/overview.html +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/scan-error.html +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan.html +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/shutdown.html +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape.egg-info/dependency_links.txt +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape.egg-info/entry_points.txt +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape.egg-info/top_level.txt +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/setup.cfg +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_decorators.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_env.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_globals.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_library.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_logging.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_port_scan.py +0 -0
- {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_service_scan.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lanscape
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0a1
|
|
4
4
|
Summary: A python based local network scanner
|
|
5
5
|
Author-email: Michael Dennis <michael@dipduo.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,11 +25,16 @@ Requires-Dist: scapy<3.0,>=2.3.2
|
|
|
25
25
|
Requires-Dist: tabulate==0.9.0
|
|
26
26
|
Requires-Dist: pydantic
|
|
27
27
|
Requires-Dist: icmplib
|
|
28
|
+
Requires-Dist: pwa-launcher>=1.1.0
|
|
29
|
+
Requires-Dist: websockets<14.0,>=12.0
|
|
28
30
|
Provides-Extra: dev
|
|
29
31
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
32
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
31
33
|
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
32
35
|
Requires-Dist: openai>=1.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pylint>=3.0; extra == "dev"
|
|
37
|
+
Requires-Dist: autopep8>=2.0; extra == "dev"
|
|
33
38
|
Dynamic: license-file
|
|
34
39
|
|
|
35
40
|
# LANscape
|
|
@@ -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:
|
|
@@ -138,18 +142,29 @@ class Device(BaseModel):
|
|
|
138
142
|
@timeout_enforcer(enforcer_timeout, False)
|
|
139
143
|
def do_test():
|
|
140
144
|
for attempt in range(port_config.retries + 1):
|
|
141
|
-
sock =
|
|
142
|
-
sock.settimeout(port_config.timeout)
|
|
145
|
+
sock = None
|
|
143
146
|
try:
|
|
147
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
148
|
+
sock.settimeout(port_config.timeout)
|
|
144
149
|
result = sock.connect_ex((self.ip, port))
|
|
145
150
|
if result == 0:
|
|
146
151
|
if port not in self.ports:
|
|
147
152
|
self.ports.append(port)
|
|
148
153
|
return True
|
|
154
|
+
except OSError as e:
|
|
155
|
+
# Handle socket creation failures (e.g., "Too many open files")
|
|
156
|
+
# Log and continue to retry if attempts remain
|
|
157
|
+
log = logging.getLogger('Device.test_port')
|
|
158
|
+
log.debug(f"OSError on {self.ip}:{port} attempt {attempt + 1}: {e}")
|
|
149
159
|
except Exception:
|
|
150
160
|
pass # Connection failed, try again if retries remain
|
|
151
161
|
finally:
|
|
152
|
-
|
|
162
|
+
# Always close socket if it was created
|
|
163
|
+
if sock is not None:
|
|
164
|
+
try:
|
|
165
|
+
sock.close()
|
|
166
|
+
except Exception:
|
|
167
|
+
pass # Ignore errors during cleanup
|
|
153
168
|
|
|
154
169
|
# Wait before retry (except on last attempt)
|
|
155
170
|
if attempt < port_config.retries:
|
|
@@ -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()
|
|
@@ -154,7 +154,9 @@ def get_port_probes(port: int, strategy: ServiceScanStrategy):
|
|
|
154
154
|
|
|
155
155
|
def scan_service(ip: str, port: int, cfg: ServiceScanConfig) -> str:
|
|
156
156
|
"""
|
|
157
|
-
Synchronous function that attempts to identify the service
|
|
157
|
+
Synchronous function that attempts to identify the service
|
|
158
|
+
running on a given port.
|
|
159
|
+
TODO: This is AI slop and needs to be reworked properly.
|
|
158
160
|
"""
|
|
159
161
|
|
|
160
162
|
async def _async_scan_service(
|
|
@@ -183,5 +185,38 @@ def scan_service(ip: str, port: int, cfg: ServiceScanConfig) -> str:
|
|
|
183
185
|
log.debug(traceback.format_exc())
|
|
184
186
|
return "Unknown"
|
|
185
187
|
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
+
# Create and properly manage event loop to avoid file descriptor leaks
|
|
189
|
+
# Using new_event_loop + explicit close is safer in threaded environments
|
|
190
|
+
# than asyncio.run() which can leave resources open under heavy load
|
|
191
|
+
loop = None
|
|
192
|
+
try:
|
|
193
|
+
try:
|
|
194
|
+
# Try to get existing loop first (if running in async context)
|
|
195
|
+
loop = asyncio.get_running_loop()
|
|
196
|
+
# If we're already in an async context, just await directly
|
|
197
|
+
return asyncio.run_coroutine_threadsafe(
|
|
198
|
+
_async_scan_service(ip, port, cfg=cfg), loop
|
|
199
|
+
).result(timeout=cfg.timeout + 5)
|
|
200
|
+
except RuntimeError:
|
|
201
|
+
# No running loop, create a new one
|
|
202
|
+
loop = asyncio.new_event_loop()
|
|
203
|
+
asyncio.set_event_loop(loop)
|
|
204
|
+
try:
|
|
205
|
+
return loop.run_until_complete(_async_scan_service(ip, port, cfg=cfg))
|
|
206
|
+
finally:
|
|
207
|
+
# Clean up the loop properly
|
|
208
|
+
try:
|
|
209
|
+
# Cancel all remaining tasks
|
|
210
|
+
pending = asyncio.all_tasks(loop)
|
|
211
|
+
for task in pending:
|
|
212
|
+
task.cancel()
|
|
213
|
+
# Run loop once more to process cancellations
|
|
214
|
+
if pending:
|
|
215
|
+
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
finally:
|
|
219
|
+
loop.close()
|
|
220
|
+
except Exception as e:
|
|
221
|
+
log.error(f"Event loop error scanning {ip}:{port}: {e}")
|
|
222
|
+
return "Unknown"
|
|
@@ -21,6 +21,20 @@ def get_port_lists():
|
|
|
21
21
|
return jsonify(PortManager().get_port_lists())
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
@api_bp.route('/api/port/list/summary', methods=['GET'])
|
|
25
|
+
def get_port_lists_summary():
|
|
26
|
+
"""Get port list names with their port counts."""
|
|
27
|
+
manager = PortManager()
|
|
28
|
+
summaries = []
|
|
29
|
+
for name in manager.get_port_lists():
|
|
30
|
+
ports = manager.get_port_list(name) or {}
|
|
31
|
+
summaries.append({
|
|
32
|
+
'name': name,
|
|
33
|
+
'count': len(ports)
|
|
34
|
+
})
|
|
35
|
+
return jsonify(summaries)
|
|
36
|
+
|
|
37
|
+
|
|
24
38
|
@api_bp.route('/api/port/list/<port_list>', methods=['GET'])
|
|
25
39
|
def get_port_list(port_list):
|
|
26
40
|
"""
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
"""Main entry point for the LANscape application when running as a module."""
|
|
2
2
|
import socket
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
import threading
|
|
6
4
|
import time
|
|
7
5
|
import logging
|
|
8
6
|
import traceback
|
|
9
7
|
import os
|
|
8
|
+
from subprocess import Popen
|
|
9
|
+
import webbrowser
|
|
10
10
|
import requests
|
|
11
11
|
|
|
12
|
+
from pwa_launcher import open_pwa, ChromiumNotFoundError
|
|
13
|
+
|
|
14
|
+
|
|
12
15
|
from lanscape.core.logger import configure_logging
|
|
13
16
|
from lanscape.core.runtime_args import parse_args
|
|
14
|
-
from lanscape.core.web_browser import open_webapp
|
|
15
17
|
from lanscape.core.version_manager import get_installed_version, is_update_available
|
|
16
18
|
from lanscape.ui.app import start_webserver_daemon, start_webserver
|
|
19
|
+
from lanscape.ui.ws.server import run_server
|
|
17
20
|
# do this so any logs generated on import are displayed
|
|
18
21
|
args = parse_args()
|
|
19
22
|
configure_logging(args.loglevel, args.logfile, args.flask_logging)
|
|
@@ -46,6 +49,11 @@ def _main():
|
|
|
46
49
|
else:
|
|
47
50
|
log.info('Flask reloaded app.')
|
|
48
51
|
|
|
52
|
+
# Check if WebSocket server mode is requested
|
|
53
|
+
if args.ws_server:
|
|
54
|
+
start_websocket_server()
|
|
55
|
+
return
|
|
56
|
+
|
|
49
57
|
args.port = get_valid_port(args.port)
|
|
50
58
|
|
|
51
59
|
try:
|
|
@@ -70,7 +78,24 @@ def try_check_update():
|
|
|
70
78
|
log.warning('Unable to check for updates.')
|
|
71
79
|
|
|
72
80
|
|
|
73
|
-
def
|
|
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
|
+
# Bind to localhost only for better Chrome compatibility
|
|
89
|
+
run_server(host='127.0.0.1', port=args.ws_port)
|
|
90
|
+
except KeyboardInterrupt:
|
|
91
|
+
log.info('WebSocket server stopped by user')
|
|
92
|
+
except Exception as e:
|
|
93
|
+
log.critical(f'WebSocket server failed: {e}')
|
|
94
|
+
log.debug(traceback.format_exc())
|
|
95
|
+
raise
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def open_browser(url: str, wait=2) -> Popen | None:
|
|
74
99
|
"""
|
|
75
100
|
Open a browser window to the specified
|
|
76
101
|
url after waiting for the server to start
|
|
@@ -78,12 +103,18 @@ def open_browser(url: str, wait=2) -> bool:
|
|
|
78
103
|
try:
|
|
79
104
|
time.sleep(wait)
|
|
80
105
|
log.info(f'Starting UI - http://127.0.0.1:{args.port}')
|
|
81
|
-
return
|
|
82
|
-
|
|
106
|
+
return open_pwa(url)
|
|
107
|
+
|
|
108
|
+
except ChromiumNotFoundError:
|
|
109
|
+
success = webbrowser.open(url)
|
|
110
|
+
if success:
|
|
111
|
+
log.warning("Chromium browser not found. Falling back to default web browser.")
|
|
112
|
+
else:
|
|
113
|
+
log.warning(f"Cannot find any web browser. LANScape UI running on {url}")
|
|
83
114
|
except BaseException:
|
|
84
115
|
log.debug(traceback.format_exc())
|
|
85
116
|
log.info(f'Unable to open web browser, server running on {url}')
|
|
86
|
-
return
|
|
117
|
+
return None
|
|
87
118
|
|
|
88
119
|
|
|
89
120
|
def start_webserver_ui():
|
|
@@ -97,19 +128,16 @@ def start_webserver_ui():
|
|
|
97
128
|
# if it was, dont open the browser again
|
|
98
129
|
log.info('Opening UI as daemon')
|
|
99
130
|
if not IS_FLASK_RELOAD:
|
|
100
|
-
|
|
101
|
-
target=open_browser,
|
|
102
|
-
args=(uri,),
|
|
103
|
-
daemon=True
|
|
104
|
-
).start()
|
|
131
|
+
open_browser(uri)
|
|
105
132
|
start_webserver(args)
|
|
106
133
|
else:
|
|
107
134
|
flask_thread = start_webserver_daemon(args)
|
|
108
|
-
|
|
135
|
+
proc = open_browser(uri)
|
|
136
|
+
if proc:
|
|
137
|
+
app_closed = proc.wait()
|
|
138
|
+
else:
|
|
139
|
+
app_closed = False
|
|
109
140
|
|
|
110
|
-
# depending on env, open_browser may or
|
|
111
|
-
# may not be coupled with the closure of UI
|
|
112
|
-
# (if in browser tab, it's uncoupled)
|
|
113
141
|
if not app_closed or args.persistent:
|
|
114
142
|
# not doing a direct join so i can still
|
|
115
143
|
# terminate the app with ctrl+c
|
|
@@ -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 */
|
|
@@ -564,8 +577,8 @@ input[type="range"] {
|
|
|
564
577
|
/* Service Strategy Select Styles */
|
|
565
578
|
.service-strategy-wrapper {
|
|
566
579
|
position: relative;
|
|
567
|
-
display:
|
|
568
|
-
width:
|
|
580
|
+
display: block;
|
|
581
|
+
width: 100%;
|
|
569
582
|
}
|
|
570
583
|
|
|
571
584
|
.service-strategy {
|
|
@@ -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() {
|
|
@@ -139,15 +139,26 @@ function getScanConfig() {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
function getPortLists(callback=null) {
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
const customSelectDropdown = $('#port_list');
|
|
143
|
+
|
|
144
|
+
const renderOptions = (items) => {
|
|
144
145
|
customSelectDropdown.empty();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
items.forEach((item) => {
|
|
147
|
+
const name = item.name || item;
|
|
148
|
+
const count = item.count;
|
|
149
|
+
const label = count !== undefined ? `${name} (${count} ports)` : name;
|
|
150
|
+
customSelectDropdown.append(`<option value="${name}">${label}</option>`);
|
|
149
151
|
});
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
$.get('/api/port/list/summary', function(data) {
|
|
155
|
+
renderOptions(data || []);
|
|
150
156
|
if (callback) callback();
|
|
157
|
+
}).fail(function() {
|
|
158
|
+
$.get('/api/port/list', function(data) {
|
|
159
|
+
renderOptions(data || []);
|
|
160
|
+
if (callback) callback();
|
|
161
|
+
});
|
|
151
162
|
});
|
|
152
163
|
}
|
|
153
164
|
|
|
@@ -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>
|
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
<div id="section-service-scan" class="form-group mt-2 config-section div-hide">
|
|
202
202
|
<h6>Service Scanning</h6>
|
|
203
203
|
<div class="row">
|
|
204
|
-
<div class="col">
|
|
204
|
+
<div class="col-12">
|
|
205
205
|
<label for="service_lookup_type" class="form-label">Strategy</label>
|
|
206
206
|
<div class="service-strategy-wrapper">
|
|
207
207
|
<select id="service_lookup_type" class="service-strategy">
|
|
@@ -211,11 +211,13 @@
|
|
|
211
211
|
</select>
|
|
212
212
|
</div>
|
|
213
213
|
</div>
|
|
214
|
-
|
|
214
|
+
</div>
|
|
215
|
+
<div class="row mt-2">
|
|
216
|
+
<div class="col-12 col-md-6">
|
|
215
217
|
<label for="service_timeout" class="form-label">Timeout (sec)</label>
|
|
216
218
|
<input type="number" step="0.1" id="service_timeout" class="form-control">
|
|
217
219
|
</div>
|
|
218
|
-
<div class="col">
|
|
220
|
+
<div class="col-12 col-md-6 mt-2 mt-md-0">
|
|
219
221
|
<label for="service_max_concurrent_probes" class="form-label">Max Concurrent Probes</label>
|
|
220
222
|
<input type="number" id="service_max_concurrent_probes" class="form-control">
|
|
221
223
|
</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
|
+
]
|