lanscape 1.3.3__tar.gz → 1.3.5a1__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-1.3.3/lanscape.egg-info → lanscape-1.3.5a1}/PKG-INFO +1 -1
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/net_tools.py +20 -7
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/runtime_args.py +12 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/scan_config.py +50 -2
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/web_browser.py +62 -11
- lanscape-1.3.5a1/lanscape/ui/__init__.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/app.py +11 -33
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/blueprints/api/scan.py +1 -5
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/blueprints/api/tools.py +16 -1
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/blueprints/web/routes.py +0 -3
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/main.py +16 -6
- lanscape-1.3.5a1/lanscape/ui/shutdown_handler.py +53 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/css/style.css +94 -20
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/js/main.js +25 -48
- lanscape-1.3.5a1/lanscape/ui/static/js/scan-config.js +107 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/lanscape.webmanifest +4 -3
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/templates/main.html +39 -36
- lanscape-1.3.5a1/lanscape/ui/templates/scan/config.html +168 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1/lanscape.egg-info}/PKG-INFO +1 -1
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape.egg-info/SOURCES.txt +4 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/pyproject.toml +1 -1
- {lanscape-1.3.3 → lanscape-1.3.5a1}/tests/test_api.py +1 -2
- {lanscape-1.3.3 → lanscape-1.3.5a1}/LICENSE +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/MANIFEST.in +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/README.md +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/__init__.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/__main__.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/__init__.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/app_scope.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/decorators.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/errors.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/ip_parser.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/logger.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/mac_lookup.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/port_manager.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/service_scan.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/subnet_scan.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/libraries/version_manager.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/resources/mac_addresses/convert_csv.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/resources/mac_addresses/mac_db.json +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/resources/ports/convert_csv.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/resources/ports/full.json +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/resources/ports/large.json +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/resources/ports/medium.json +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/resources/ports/small.json +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/resources/services/definitions.jsonc +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/blueprints/__init__.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/blueprints/api/__init__.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/blueprints/api/port.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/blueprints/web/__init__.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/img/ico/favicon.ico +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/js/core.js +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/js/layout-sizing.js +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/js/on-tab-close.js +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/js/quietReload.js +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/js/shutdown-server.js +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/js/subnet-info.js +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/static/js/subnet-selector.js +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/templates/base.html +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/templates/core/head.html +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/templates/core/scripts.html +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/templates/error.html +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/templates/info.html +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/templates/scan/export.html +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/templates/scan/ip-table-row.html +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/templates/scan/ip-table.html +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/templates/scan/overview.html +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/templates/scan/scan-error.html +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/templates/scan.html +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape/ui/templates/shutdown.html +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape.egg-info/dependency_links.txt +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape.egg-info/requires.txt +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/lanscape.egg-info/top_level.txt +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/setup.cfg +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/tests/test_env.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/tests/test_library.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/tests/test_logging.py +0 -0
- {lanscape-1.3.3 → lanscape-1.3.5a1}/tests/test_utils.py +0 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import platform
|
|
3
2
|
import ipaddress
|
|
4
3
|
import traceback
|
|
5
4
|
import subprocess
|
|
@@ -28,6 +27,9 @@ log = logging.getLogger('NetTools')
|
|
|
28
27
|
class IPAlive(JobStatsMixin):
|
|
29
28
|
"""Class to check if a device is alive using ARP and/or ping scans."""
|
|
30
29
|
caught_errors: List[DeviceError] = []
|
|
30
|
+
_icmp_alive: bool = False
|
|
31
|
+
_arp_alive: bool = False
|
|
32
|
+
|
|
31
33
|
|
|
32
34
|
@job_tracker
|
|
33
35
|
def is_alive(
|
|
@@ -95,11 +97,21 @@ class IPAlive(JobStatsMixin):
|
|
|
95
97
|
self.caught_errors.append(DeviceError(e))
|
|
96
98
|
# Fallback to system ping command
|
|
97
99
|
try:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
if psutil.WINDOWS:
|
|
101
|
+
cmd = [
|
|
102
|
+
"ping", "-n", str(cfg.ping_count),
|
|
103
|
+
"-w", str(int(cfg.timeout * 1000)), ip
|
|
104
|
+
]
|
|
105
|
+
else:
|
|
106
|
+
cmd = ["ping", "-c", str(cfg.ping_count), "-W", str(cfg.timeout), ip]
|
|
107
|
+
|
|
108
|
+
result = subprocess.run(
|
|
109
|
+
cmd, stdout=subprocess.PIPE,
|
|
110
|
+
stderr=subprocess.PIPE,
|
|
111
|
+
text=True, check=False
|
|
112
|
+
)
|
|
101
113
|
return result.returncode == 0
|
|
102
|
-
except
|
|
114
|
+
except subprocess.CalledProcessError as fallback_error:
|
|
103
115
|
self.caught_errors.append(DeviceError(fallback_error))
|
|
104
116
|
return False
|
|
105
117
|
|
|
@@ -117,6 +129,7 @@ class IPAlive(JobStatsMixin):
|
|
|
117
129
|
|
|
118
130
|
class Device(IPAlive):
|
|
119
131
|
"""Represents a network device with metadata and scanning capabilities."""
|
|
132
|
+
|
|
120
133
|
def __init__(self, ip: str):
|
|
121
134
|
super().__init__()
|
|
122
135
|
self.ip: str = ip
|
|
@@ -245,7 +258,7 @@ def get_ip_address(interface: str):
|
|
|
245
258
|
def unix_like(): # Combined Linux and macOS
|
|
246
259
|
try:
|
|
247
260
|
# pylint: disable=import-outside-toplevel, import-error
|
|
248
|
-
import fcntl
|
|
261
|
+
import fcntl
|
|
249
262
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
250
263
|
ip_address = socket.inet_ntoa(fcntl.ioctl(
|
|
251
264
|
sock.fileno(),
|
|
@@ -280,7 +293,7 @@ def get_netmask(interface: str):
|
|
|
280
293
|
def unix_like(): # Combined Linux and macOS
|
|
281
294
|
try:
|
|
282
295
|
# pylint: disable=import-outside-toplevel, import-error
|
|
283
|
-
import fcntl
|
|
296
|
+
import fcntl
|
|
284
297
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
285
298
|
netmask = socket.inet_ntoa(fcntl.ioctl(
|
|
286
299
|
sock.fileno(),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Runtime argument handler for LANscape as module"""
|
|
2
|
+
|
|
1
3
|
from dataclasses import dataclass, fields
|
|
2
4
|
import argparse
|
|
3
5
|
from typing import Any, Dict, Optional
|
|
@@ -5,6 +7,7 @@ from typing import Any, Dict, Optional
|
|
|
5
7
|
|
|
6
8
|
@dataclass
|
|
7
9
|
class RuntimeArgs:
|
|
10
|
+
"""Class representing runtime arguments for the application."""
|
|
8
11
|
reloader: bool = False
|
|
9
12
|
port: int = 5001
|
|
10
13
|
logfile: Optional[str] = None
|
|
@@ -14,6 +17,9 @@ class RuntimeArgs:
|
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
def parse_args() -> RuntimeArgs:
|
|
20
|
+
"""
|
|
21
|
+
Parse command line arguments and return a RuntimeArgs instance.
|
|
22
|
+
"""
|
|
17
23
|
parser = argparse.ArgumentParser(description='LANscape')
|
|
18
24
|
|
|
19
25
|
parser.add_argument('--reloader', action='store_true',
|
|
@@ -27,6 +33,8 @@ def parse_args() -> RuntimeArgs:
|
|
|
27
33
|
help='Enable flask logging (disables click output)')
|
|
28
34
|
parser.add_argument('--persistent', action='store_true',
|
|
29
35
|
help='Don\'t exit after browser is closed')
|
|
36
|
+
parser.add_argument('--debug', action='store_true',
|
|
37
|
+
help='Shorthand debug mode (equivalent to "--loglevel DEBUG --reloader")')
|
|
30
38
|
|
|
31
39
|
# Parse the arguments
|
|
32
40
|
args = parser.parse_args()
|
|
@@ -37,6 +45,10 @@ def parse_args() -> RuntimeArgs:
|
|
|
37
45
|
field_names = {field.name for field in fields(
|
|
38
46
|
RuntimeArgs)} # Get dataclass field names
|
|
39
47
|
|
|
48
|
+
if args.debug:
|
|
49
|
+
args_dict['loglevel'] = 'DEBUG'
|
|
50
|
+
args_dict['reloader'] = True
|
|
51
|
+
|
|
40
52
|
# Only pass arguments that exist in the Args dataclass
|
|
41
53
|
filtered_args = {name: args_dict[name]
|
|
42
54
|
for name in field_names if name in args_dict}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import List, Dict
|
|
2
2
|
import ipaddress
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
from enum import Enum
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
from lanscape.libraries.port_manager import PortManager
|
|
7
8
|
from lanscape.libraries.ip_parser import parse_ip_input
|
|
8
9
|
|
|
@@ -82,7 +83,9 @@ class ScanConfig(BaseModel):
|
|
|
82
83
|
return cls.model_validate(data)
|
|
83
84
|
|
|
84
85
|
def to_dict(self) -> dict:
|
|
85
|
-
|
|
86
|
+
dump = self.model_dump()
|
|
87
|
+
dump['lookup_type'] = self.lookup_type.value
|
|
88
|
+
return dump
|
|
86
89
|
|
|
87
90
|
def get_ports(self) -> List[int]:
|
|
88
91
|
return PortManager().get_port_list(self.port_list).keys()
|
|
@@ -95,3 +98,48 @@ class ScanConfig(BaseModel):
|
|
|
95
98
|
b = f'ports={self.port_list}'
|
|
96
99
|
c = f'scan_type={self.lookup_type.value}'
|
|
97
100
|
return f'ScanConfig({a}, {b}, {c})'
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
|
|
104
|
+
'balanced': ScanConfig(subnet='', port_list='medium'),
|
|
105
|
+
'accurate': ScanConfig(
|
|
106
|
+
subnet='',
|
|
107
|
+
port_list='large',
|
|
108
|
+
t_cnt_port_scan=5,
|
|
109
|
+
t_cnt_port_test=64,
|
|
110
|
+
t_cnt_isalive=64,
|
|
111
|
+
task_scan_ports=True,
|
|
112
|
+
task_scan_port_services=False,
|
|
113
|
+
lookup_type=ScanType.BOTH,
|
|
114
|
+
arp_config=ArpConfig(
|
|
115
|
+
attempts=3,
|
|
116
|
+
timeout=2.5
|
|
117
|
+
),
|
|
118
|
+
ping_config=PingConfig(
|
|
119
|
+
attempts=3,
|
|
120
|
+
ping_count=2,
|
|
121
|
+
timeout=1.5,
|
|
122
|
+
retry_delay=0.5
|
|
123
|
+
)
|
|
124
|
+
),
|
|
125
|
+
'fast': ScanConfig(
|
|
126
|
+
subnet='',
|
|
127
|
+
port_list='small',
|
|
128
|
+
t_cnt_port_scan=20,
|
|
129
|
+
t_cnt_port_test=256,
|
|
130
|
+
t_cnt_isalive=512,
|
|
131
|
+
task_scan_ports=True,
|
|
132
|
+
task_scan_port_services=False,
|
|
133
|
+
lookup_type=ScanType.BOTH,
|
|
134
|
+
arp_config=ArpConfig(
|
|
135
|
+
attempts=1,
|
|
136
|
+
timeout=1.0
|
|
137
|
+
),
|
|
138
|
+
ping_config=PingConfig(
|
|
139
|
+
attempts=1,
|
|
140
|
+
ping_count=1,
|
|
141
|
+
timeout=0.5,
|
|
142
|
+
retry_delay=0.25
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
}
|
|
@@ -60,18 +60,8 @@ def open_webapp(url: str) -> bool:
|
|
|
60
60
|
|
|
61
61
|
def get_default_browser_executable() -> Optional[str]:
|
|
62
62
|
if sys.platform.startswith("win"):
|
|
63
|
-
|
|
64
|
-
import winreg
|
|
65
|
-
# On Windows the HKEY_CLASSES_ROOT\http\shell\open\command key
|
|
66
|
-
# holds the command for opening HTTP URLs.
|
|
67
|
-
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r"http\shell\open\command") as key:
|
|
68
|
-
cmd, _ = winreg.QueryValueEx(key, None)
|
|
69
|
-
except Exception:
|
|
70
|
-
return None
|
|
63
|
+
return windows_get_browser_from_registry()
|
|
71
64
|
|
|
72
|
-
# cmd usually looks like: '"C:\\Program Files\\Foo\\foo.exe" %1'
|
|
73
|
-
m = re.match(r'\"?(.+?\.exe)\"?', cmd)
|
|
74
|
-
return m.group(1) if m else None
|
|
75
65
|
|
|
76
66
|
elif sys.platform.startswith("linux"):
|
|
77
67
|
# First, find the .desktop file name
|
|
@@ -140,3 +130,64 @@ def get_default_browser_executable() -> Optional[str]:
|
|
|
140
130
|
|
|
141
131
|
else:
|
|
142
132
|
raise NotImplementedError(f"Unsupported platform: {sys.platform!r}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def windows_get_browser_from_registry() -> Optional[str]:
|
|
136
|
+
"""Get the default web browser executable path on Windows."""
|
|
137
|
+
|
|
138
|
+
import winreg # pylint: disable=import-outside-toplevel
|
|
139
|
+
|
|
140
|
+
def get_reg(base, path, key=None):
|
|
141
|
+
"""Helper function to read a registry key."""
|
|
142
|
+
try:
|
|
143
|
+
with winreg.OpenKey(base, path) as reg:
|
|
144
|
+
return winreg.QueryValueEx(reg, key)[0]
|
|
145
|
+
except FileNotFoundError:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def extract_executable(cmd: str) -> Optional[str]:
|
|
149
|
+
"""Extract the executable path from a command string."""
|
|
150
|
+
match = re.match(r'"?([^"]+)"?', cmd)
|
|
151
|
+
return match.group(1) if match else None
|
|
152
|
+
|
|
153
|
+
def get_user_preferred_browser():
|
|
154
|
+
"""Get the user preferred browser from the registry."""
|
|
155
|
+
progid = get_reg(
|
|
156
|
+
winreg.HKEY_CURRENT_USER,
|
|
157
|
+
r'Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice',
|
|
158
|
+
'ProgId'
|
|
159
|
+
)
|
|
160
|
+
if not progid:
|
|
161
|
+
log.debug('No user preferred browser found in registry')
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
browser_path = get_reg(
|
|
165
|
+
winreg.HKEY_CLASSES_ROOT,
|
|
166
|
+
f'{progid}\\shell\\open\\command'
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if not browser_path:
|
|
170
|
+
log.debug(f'progid {progid} does not have a command in registry')
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
return extract_executable(browser_path)
|
|
174
|
+
|
|
175
|
+
def get_system_default_browser():
|
|
176
|
+
"""Get the system default browser from the registry."""
|
|
177
|
+
reg = get_reg(
|
|
178
|
+
winreg.HKEY_CLASSES_ROOT,
|
|
179
|
+
r'http\shell\open\command'
|
|
180
|
+
)
|
|
181
|
+
if not reg:
|
|
182
|
+
log.debug('No system default browser found in registry')
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
return extract_executable(reg)
|
|
186
|
+
|
|
187
|
+
user_browser = get_user_preferred_browser()
|
|
188
|
+
if user_browser:
|
|
189
|
+
return extract_executable(user_browser)
|
|
190
|
+
|
|
191
|
+
system_browser = get_system_default_browser()
|
|
192
|
+
if system_browser:
|
|
193
|
+
return extract_executable(system_browser)
|
|
File without changes
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
import traceback
|
|
3
3
|
import threading
|
|
4
4
|
import logging
|
|
5
|
-
import
|
|
6
|
-
|
|
5
|
+
from flask import Flask, render_template
|
|
6
|
+
from lanscape.ui.blueprints.web import web_bp, routes # pylint: ignore=unused-import
|
|
7
|
+
from lanscape.ui.blueprints.api import api_bp, tools, port, scan # pylint: ignore=unused-import
|
|
7
8
|
from lanscape.libraries.runtime_args import RuntimeArgs, parse_args
|
|
8
9
|
from lanscape.libraries.version_manager import is_update_available, get_installed_version, lookup_latest_version
|
|
9
10
|
from lanscape.libraries.app_scope import is_local_run
|
|
10
11
|
from lanscape.libraries.net_tools import is_arp_supported
|
|
12
|
+
from lanscape.ui.shutdown_handler import FlaskShutdownHandler
|
|
11
13
|
|
|
12
14
|
app = Flask(
|
|
13
15
|
__name__
|
|
@@ -15,9 +17,7 @@ app = Flask(
|
|
|
15
17
|
log = logging.getLogger('flask')
|
|
16
18
|
|
|
17
19
|
# Import and register BPs
|
|
18
|
-
|
|
19
|
-
from lanscape.ui.blueprints.api import api_bp, tools, port, scan
|
|
20
|
-
from lanscape.ui.blueprints.web import web_bp, routes
|
|
20
|
+
#################################
|
|
21
21
|
|
|
22
22
|
app.register_blueprint(api_bp)
|
|
23
23
|
app.register_blueprint(web_bp)
|
|
@@ -65,32 +65,8 @@ set_global_safe('is_arp_supported', is_arp_supported)
|
|
|
65
65
|
# External hook to kill flask server
|
|
66
66
|
################################
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@app.route("/shutdown", methods=['GET', 'POST'])
|
|
72
|
-
def exit_app():
|
|
73
|
-
|
|
74
|
-
req_type = request.args.get('type')
|
|
75
|
-
if req_type == 'browser-close':
|
|
76
|
-
args = parse_args()
|
|
77
|
-
if args.persistent:
|
|
78
|
-
log.info('Dectected browser close, not exiting flask.')
|
|
79
|
-
return "Ignored"
|
|
80
|
-
log.info('Web browser closed, terminating flask. (disable with --peristent)')
|
|
81
|
-
elif req_type == 'core':
|
|
82
|
-
log.info('Core requested exit, terminating flask.')
|
|
83
|
-
else:
|
|
84
|
-
log.info('Received external exit request. Terminating flask.')
|
|
85
|
-
global exiting
|
|
86
|
-
exiting = True
|
|
87
|
-
return "Done"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@app.teardown_request
|
|
91
|
-
def teardown(exception):
|
|
92
|
-
if exiting:
|
|
93
|
-
os._exit(0)
|
|
68
|
+
shutdown_handler = FlaskShutdownHandler(app)
|
|
69
|
+
shutdown_handler.register_endpoints()
|
|
94
70
|
|
|
95
71
|
# Generalized error handling
|
|
96
72
|
################################
|
|
@@ -111,14 +87,16 @@ def internal_error(e):
|
|
|
111
87
|
|
|
112
88
|
|
|
113
89
|
def start_webserver_daemon(args: RuntimeArgs) -> threading.Thread:
|
|
90
|
+
"""Start the web server in a daemon thread."""
|
|
114
91
|
proc = threading.Thread(target=start_webserver, args=(args,))
|
|
115
92
|
proc.daemon = True # Kill thread when main thread exits
|
|
116
93
|
proc.start()
|
|
117
|
-
log.info('Flask server initializing as
|
|
94
|
+
log.info('Flask server initializing as daemon')
|
|
118
95
|
return proc
|
|
119
96
|
|
|
120
97
|
|
|
121
98
|
def start_webserver(args: RuntimeArgs) -> int:
|
|
99
|
+
"""Start webserver (blocking)"""
|
|
122
100
|
run_args = {
|
|
123
101
|
'host': '0.0.0.0',
|
|
124
102
|
'port': args.port,
|
|
@@ -68,8 +68,4 @@ def get_scan_config():
|
|
|
68
68
|
pulls config from the request body
|
|
69
69
|
"""
|
|
70
70
|
data = request.get_json()
|
|
71
|
-
return ScanConfig(
|
|
72
|
-
subnet=data['subnet'],
|
|
73
|
-
port_list=data['port_list'],
|
|
74
|
-
t_multiplier=data.get('parallelism', 1.0)
|
|
75
|
-
)
|
|
71
|
+
return ScanConfig.from_dict(data)
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API endpoints for subnet testing and listing.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import traceback
|
|
1
6
|
from flask import request, jsonify
|
|
2
7
|
from lanscape.ui.blueprints.api import api_bp
|
|
3
8
|
from lanscape.libraries.net_tools import get_all_network_subnets
|
|
4
9
|
from lanscape.libraries.ip_parser import parse_ip_input
|
|
5
10
|
from lanscape.libraries.errors import SubnetTooLargeError
|
|
6
|
-
import
|
|
11
|
+
from lanscape.libraries.scan_config import DEFAULT_CONFIGS
|
|
7
12
|
|
|
8
13
|
|
|
9
14
|
@api_bp.route('/api/tools/subnet/test')
|
|
@@ -34,3 +39,13 @@ def list_subnet():
|
|
|
34
39
|
return jsonify(get_all_network_subnets())
|
|
35
40
|
except BaseException:
|
|
36
41
|
return jsonify({'error': traceback.format_exc()})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@api_bp.route('/api/tools/config/defaults')
|
|
45
|
+
def get_default_configs():
|
|
46
|
+
"""
|
|
47
|
+
Get default scan configurations.
|
|
48
|
+
"""
|
|
49
|
+
return jsonify(
|
|
50
|
+
{key: config.to_dict() for key, config in DEFAULT_CONFIGS.items()}
|
|
51
|
+
)
|
|
@@ -16,12 +16,10 @@ def index():
|
|
|
16
16
|
subnet = smart_select_primary_subnet(subnets)
|
|
17
17
|
|
|
18
18
|
port_list = 'medium'
|
|
19
|
-
parallelism = 1
|
|
20
19
|
if scan_id := request.args.get('scan_id'):
|
|
21
20
|
if scan := scan_manager.get_scan(scan_id):
|
|
22
21
|
subnet = scan.cfg.subnet
|
|
23
22
|
port_list = scan.cfg.port_list
|
|
24
|
-
parallelism = scan.cfg.t_multiplier
|
|
25
23
|
|
|
26
24
|
else:
|
|
27
25
|
log.debug(f'Redirecting, scan {scan_id} doesnt exist in memory')
|
|
@@ -30,7 +28,6 @@ def index():
|
|
|
30
28
|
'main.html',
|
|
31
29
|
subnet=subnet,
|
|
32
30
|
port_list=port_list,
|
|
33
|
-
parallelism=parallelism,
|
|
34
31
|
alternate_subnets=subnets
|
|
35
32
|
)
|
|
36
33
|
|
|
@@ -7,8 +7,10 @@ import time
|
|
|
7
7
|
import logging
|
|
8
8
|
import traceback
|
|
9
9
|
import os
|
|
10
|
+
import requests
|
|
11
|
+
|
|
10
12
|
from lanscape.libraries.logger import configure_logging
|
|
11
|
-
from lanscape.libraries.runtime_args import parse_args
|
|
13
|
+
from lanscape.libraries.runtime_args import parse_args
|
|
12
14
|
from lanscape.libraries.web_browser import open_webapp
|
|
13
15
|
from lanscape.libraries.net_tools import is_arp_supported
|
|
14
16
|
from lanscape.libraries.version_manager import get_installed_version, is_update_available
|
|
@@ -25,6 +27,7 @@ IS_FLASK_RELOAD = os.environ.get("WERKZEUG_RUN_MAIN")
|
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def main():
|
|
30
|
+
"""core entry point for running lanscape as a module."""
|
|
28
31
|
try:
|
|
29
32
|
_main()
|
|
30
33
|
except KeyboardInterrupt:
|
|
@@ -47,10 +50,15 @@ def _main():
|
|
|
47
50
|
args.port = get_valid_port(args.port)
|
|
48
51
|
|
|
49
52
|
if not is_arp_supported():
|
|
50
|
-
|
|
53
|
+
warn = (
|
|
54
|
+
'ARP is not supported, device discovery is degraded. ',
|
|
55
|
+
'For more information, see the help guide: ',
|
|
56
|
+
'https://github.com/mdennis281/LANscape/blob/main/support/arp-issues.md'
|
|
57
|
+
)
|
|
58
|
+
log.warning(''.join(warn))
|
|
51
59
|
|
|
52
60
|
try:
|
|
53
|
-
start_webserver_ui(
|
|
61
|
+
start_webserver_ui()
|
|
54
62
|
log.info('Exiting...')
|
|
55
63
|
except Exception as e:
|
|
56
64
|
# showing error in debug only because this is handled gracefully
|
|
@@ -60,6 +68,7 @@ def _main():
|
|
|
60
68
|
|
|
61
69
|
|
|
62
70
|
def try_check_update():
|
|
71
|
+
"""Check for updates and log if available."""
|
|
63
72
|
try:
|
|
64
73
|
if is_update_available():
|
|
65
74
|
log.info('An update is available!')
|
|
@@ -86,7 +95,8 @@ def open_browser(url: str, wait=2) -> bool:
|
|
|
86
95
|
return False
|
|
87
96
|
|
|
88
97
|
|
|
89
|
-
def start_webserver_ui(
|
|
98
|
+
def start_webserver_ui():
|
|
99
|
+
"""Start the web server and open the UI in a browser."""
|
|
90
100
|
uri = f'http://127.0.0.1:{args.port}'
|
|
91
101
|
|
|
92
102
|
# running reloader requires flask to run in main thread
|
|
@@ -128,9 +138,9 @@ def get_valid_port(port: int):
|
|
|
128
138
|
|
|
129
139
|
|
|
130
140
|
def terminate():
|
|
131
|
-
|
|
141
|
+
"""send a request to the shutdown flask"""
|
|
132
142
|
log.info('Attempting flask shutdown')
|
|
133
|
-
requests.get(f'http://127.0.0.1:{args.port}/shutdown?type=core')
|
|
143
|
+
requests.get(f'http://127.0.0.1:{args.port}/shutdown?type=core', timeout=2)
|
|
134
144
|
|
|
135
145
|
|
|
136
146
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from flask import request
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from lanscape.libraries.runtime_args import parse_args
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger('shutdown_handler')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FlaskShutdownHandler:
|
|
13
|
+
"""Handles shutdown requests for the Flask application.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, app):
|
|
17
|
+
self.app = app
|
|
18
|
+
self._exiting = False
|
|
19
|
+
|
|
20
|
+
def register_endpoints(self):
|
|
21
|
+
"""Register shutdown endpoints to the Flask app."""
|
|
22
|
+
|
|
23
|
+
@self.app.route('/shutdown', methods=['POST', 'GET'])
|
|
24
|
+
def shutdown():
|
|
25
|
+
req_type = request.args.get('type')
|
|
26
|
+
self.shutdown_request(req_type)
|
|
27
|
+
return "Done"
|
|
28
|
+
|
|
29
|
+
@self.app.teardown_request
|
|
30
|
+
def teardown(_):
|
|
31
|
+
self.exit_if_requested()
|
|
32
|
+
|
|
33
|
+
def shutdown_request(self, req_type: str):
|
|
34
|
+
"""Handles shutdown requests based on the type of request.
|
|
35
|
+
Args:
|
|
36
|
+
req_type (str): The type of shutdown request.
|
|
37
|
+
"""
|
|
38
|
+
if req_type == 'browser-close':
|
|
39
|
+
args = parse_args()
|
|
40
|
+
if args.persistent:
|
|
41
|
+
log.info('Detected browser close, not exiting flask.')
|
|
42
|
+
return "Ignored"
|
|
43
|
+
log.info('Web browser closed, terminating flask. (disable with --persistent)')
|
|
44
|
+
elif req_type == 'core':
|
|
45
|
+
log.info('Core requested exit, terminating flask.')
|
|
46
|
+
else:
|
|
47
|
+
log.info('Received external exit request. Terminating flask.')
|
|
48
|
+
self._exiting = True
|
|
49
|
+
|
|
50
|
+
def exit_if_requested(self):
|
|
51
|
+
"""Exits the application if a shutdown request has been made."""
|
|
52
|
+
if self._exiting:
|
|
53
|
+
os._exit(0)
|