iotsploit-cli 0.0.6__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.
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env python
2
+
3
+ import cmd2
4
+ from cmd2 import ansi
5
+ import sys
6
+ import subprocess
7
+ import time
8
+ from .base_commands import BaseCommands
9
+ from iotsploit_core.utils import iots_logger
10
+ import os
11
+ import signal
12
+ import socket
13
+ from typing import Tuple
14
+ try:
15
+ import redis # type: ignore
16
+ except Exception: # pragma: no cover
17
+ redis = None
18
+
19
+ from django.conf import settings
20
+
21
+ logger = iots_logger.get_logger(__name__)
22
+
23
+
24
+ class DjangoCommands(BaseCommands):
25
+ """Django-related commands for the SAT Shell"""
26
+
27
+ def _check_redis_available(self) -> Tuple[bool, str]:
28
+ """
29
+ Preflight check for Redis reachability using Django settings.
30
+
31
+ Returns (ok, message). If ok is False, message contains human-friendly guidance.
32
+ """
33
+ redis_host = getattr(settings, 'REDIS_HOST', '127.0.0.1')
34
+ redis_port = int(getattr(settings, 'REDIS_PORT', 6379))
35
+ redis_db = int(getattr(settings, 'REDIS_DB', 0))
36
+
37
+ # Quick TCP reachability check first to avoid long client timeouts
38
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
39
+ sock.settimeout(0.5)
40
+ try:
41
+ sock.connect((redis_host, redis_port))
42
+ except Exception:
43
+ guide = (
44
+ f"Redis is not reachable at {redis_host}:{redis_port}.\n"
45
+ "Celery and WebSocket features require Redis.\n\n"
46
+ "How to start Redis:\n"
47
+ " - If you use Docker: docker run --name redis -p 6379:6379 -d redis:latest\n"
48
+ " - On Ubuntu/Debian: sudo apt install redis-server && sudo systemctl start redis-server\n"
49
+ " - Or inside project (docker-compose): enable the redis service\n\n"
50
+ "Please start Redis and try again."
51
+ )
52
+ return False, guide
53
+
54
+ # If redis-py is available, ping for certainty
55
+ if redis is not None:
56
+ try:
57
+ client = redis.Redis(host=redis_host, port=redis_port, db=redis_db)
58
+ client.ping()
59
+ except Exception as e: # pragma: no cover
60
+ return False, (
61
+ f"Connected to {redis_host}:{redis_port} but Redis ping failed: {e}\n"
62
+ "Please ensure Redis is healthy and try again."
63
+ )
64
+
65
+ return True, "Redis is available"
66
+
67
+ @cmd2.with_category('Django Commands')
68
+ def do_runserver(self, arg):
69
+ 'Start Django development server, Daphne WebSocket server, MCP WebSocket bridge, and Celery worker in the background'
70
+ if self.django_server_process or self.daphne_server_process:
71
+ self.poutput("Servers are already running.")
72
+ return
73
+
74
+ try:
75
+ logger.info("Attempting to start Django, Daphne, MCP WebSocket bridge, and Celery servers in background...")
76
+
77
+ # Preflight: check Redis and fail fast if unavailable
78
+ redis_ok, redis_msg = self._check_redis_available()
79
+ if not redis_ok:
80
+ self.poutput(ansi.style("\n[ERROR] Redis is unavailable. Cannot start services that depend on it.", fg=ansi.Fg.RED, bold=True))
81
+ self.poutput(redis_msg)
82
+ return False
83
+
84
+ # Prepare the commands
85
+ django_cmd = [sys.executable, '-m', 'django', 'runserver', '--noreload', '0.0.0.0:8888']
86
+ daphne_cmd = [
87
+ sys.executable,
88
+ '-m',
89
+ 'daphne',
90
+ '-b',
91
+ '0.0.0.0',
92
+ '-p',
93
+ '9999',
94
+ 'iotsploit_django.asgi:application'
95
+ ]
96
+ mcp_bridge_cmd = [
97
+ sys.executable,
98
+ '-m',
99
+ 'iotsploit_mcp.cli',
100
+ 'ws',
101
+ '--host',
102
+ '0.0.0.0',
103
+ '--port',
104
+ '9998',
105
+ ]
106
+ celery_cmd = [
107
+ sys.executable,
108
+ '-m',
109
+ 'celery',
110
+ '-A',
111
+ 'iotsploit_django.tasks.celery_app:app',
112
+ 'worker',
113
+ '--loglevel=info'
114
+ ]
115
+
116
+ logger.info(f"Running Django command: {' '.join(django_cmd)}")
117
+ logger.info(f"Running Daphne command: {' '.join(daphne_cmd)}")
118
+ logger.info(f"Running MCP WebSocket Bridge command: {' '.join(mcp_bridge_cmd)}")
119
+ logger.info(f"Running Celery command: {' '.join(celery_cmd)}")
120
+
121
+ # Start the processes with direct output to stdout/stderr
122
+ self.django_server_process = subprocess.Popen(
123
+ django_cmd,
124
+ stdout=sys.stdout, # 直接输出到控制台
125
+ stderr=sys.stderr,
126
+ universal_newlines=True
127
+ )
128
+
129
+ self.daphne_server_process = subprocess.Popen(
130
+ daphne_cmd,
131
+ stdout=sys.stdout, # 直接输出到控制台
132
+ stderr=sys.stderr,
133
+ universal_newlines=True
134
+ )
135
+
136
+ # Start the WebSocket bridge in its own process group so that we can
137
+ # later terminate the entire group (bridge + sat_fastmcp child)
138
+ # Set up environment variables for MCP bridge (Django API URL)
139
+ mcp_env = os.environ.copy()
140
+ mcp_env.setdefault('IOTSPLOIT_DJANGO_API_BASE_URL', 'http://127.0.0.1:8888')
141
+
142
+ self.mcp_bridge_process = subprocess.Popen(
143
+ mcp_bridge_cmd,
144
+ stdout=sys.stdout, # 直接输出到控制台
145
+ stderr=sys.stderr,
146
+ universal_newlines=True,
147
+ start_new_session=True, # create new session = new PGID on POSIX
148
+ env=mcp_env
149
+ )
150
+
151
+ self.celery_worker_process = subprocess.Popen(
152
+ celery_cmd,
153
+ stdout=sys.stdout, # 直接输出到控制台
154
+ stderr=sys.stderr,
155
+ universal_newlines=True
156
+ )
157
+
158
+ logger.info("All servers started successfully in the background.")
159
+ logger.info("Services running on:")
160
+ logger.info(" - Django HTTP API: http://localhost:8888")
161
+ logger.info(" - Daphne WebSocket: ws://localhost:9999")
162
+ logger.info(" - MCP WebSocket Bridge: ws://localhost:9998")
163
+ logger.info(" - Celery Worker: background task processing")
164
+
165
+ # Wait for HTTP server to be available and initialize devices
166
+ import requests
167
+ max_retries = 30
168
+ retry_interval = 1
169
+
170
+ logger.info("Waiting for HTTP server to be available...")
171
+ for i in range(max_retries):
172
+ try:
173
+ # Try to initialize devices using the HTTP endpoint (GET method)
174
+ response = requests.get('http://127.0.0.1:8888/api/initialize_devices/')
175
+ if response.status_code == 200:
176
+ logger.info("Devices initialized successfully via HTTP API")
177
+ break
178
+ else:
179
+ logger.error(f"Failed to initialize devices: {response.text}")
180
+ break
181
+ except requests.exceptions.ConnectionError:
182
+ if i < max_retries - 1:
183
+ time.sleep(retry_interval)
184
+ else:
185
+ logger.error("HTTP server did not become available in time")
186
+ continue
187
+ except Exception as e:
188
+ logger.error(f"Error initializing devices: {str(e)}")
189
+ break
190
+
191
+ return None
192
+
193
+ except Exception as e:
194
+ logger.error(f"Failed to start servers: {str(e)}")
195
+ logger.debug("Detailed traceback:", exc_info=True)
196
+ return False
197
+
198
+ @cmd2.with_category('Django Commands')
199
+ def do_stop_server(self, arg):
200
+ 'Stop Django development server, Daphne WebSocket server, MCP WebSocket bridge, and Celery worker'
201
+ try:
202
+ # Cleanup devices using HTTP endpoint (GET method)
203
+ # Only attempt HTTP cleanup if the Django server process is still alive
204
+ if self.django_server_process and self.django_server_process.poll() is None:
205
+ import requests
206
+ try:
207
+ response = requests.get('http://127.0.0.1:8888/api/cleanup_devices/')
208
+ if response.status_code == 200:
209
+ logger.info("Devices cleaned up successfully via HTTP API")
210
+ else:
211
+ logger.error(f"Failed to cleanup devices: {response.text}")
212
+ except requests.exceptions.ConnectionError:
213
+ logger.warning("Could not reach HTTP server for device cleanup")
214
+ except Exception as e:
215
+ logger.error(f"Error during device cleanup: {str(e)}")
216
+ else:
217
+ logger.debug("Django server not running, skipping HTTP device cleanup")
218
+
219
+ # Stop the servers
220
+ if self.django_server_process:
221
+ self.django_server_process.terminate()
222
+ self.django_server_process = None
223
+
224
+ if self.daphne_server_process:
225
+ self.daphne_server_process.terminate()
226
+ self.daphne_server_process = None
227
+
228
+ if self.mcp_bridge_process:
229
+ try:
230
+ # Terminate the entire process group so that child processes
231
+ # such as sat_fastmcp are also stopped.
232
+ os.killpg(os.getpgid(self.mcp_bridge_process.pid), signal.SIGTERM)
233
+ except ProcessLookupError:
234
+ # Process already gone
235
+ pass
236
+ except Exception as e:
237
+ logger.error(f"Failed to terminate MCP bridge process group: {e}")
238
+ finally:
239
+ self.mcp_bridge_process = None
240
+
241
+ if self.celery_worker_process:
242
+ self.celery_worker_process.terminate()
243
+ self.celery_worker_process = None
244
+
245
+ if not any([self.django_server_process, self.daphne_server_process,
246
+ getattr(self, 'mcp_bridge_process', None),
247
+ getattr(self, 'celery_worker_process', None)]):
248
+ logger.info("All servers stopped.")
249
+ else:
250
+ logger.error("No servers were running.")
251
+
252
+ except Exception as e:
253
+ logger.error(f"Error stopping servers: {str(e)}")
254
+ logger.debug("Detailed error:", exc_info=True)
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env python
2
+
3
+ import cmd2
4
+ from cmd2 import ansi
5
+ from .base_commands import BaseCommands
6
+ from iotsploit_django.tools.input_mgr import Input_Mgr
7
+ from iotsploit_core.core.tool_service import get_firmware_service
8
+ from iotsploit_core.utils import iots_logger
9
+
10
+ logger = iots_logger.get_logger(__name__)
11
+
12
+
13
+ class FirmwareCommands(BaseCommands):
14
+ """Firmware-related commands for the SAT Shell"""
15
+
16
+ @cmd2.with_category('Firmware Commands')
17
+ def do_list_firmware(self, arg):
18
+ 'List all available firmware'
19
+ try:
20
+ firmware_service = get_firmware_service()
21
+ firmware_list = firmware_service.list_firmware()
22
+
23
+ if not firmware_list:
24
+ self.poutput(ansi.style("No firmware found in registry.", fg=ansi.Fg.YELLOW))
25
+ return
26
+
27
+ self.poutput(ansi.style(f"\nFound {len(firmware_list)} firmware(s):", fg=ansi.Fg.GREEN))
28
+ self.poutput("-" * 80)
29
+
30
+ for firmware in firmware_list:
31
+ name = firmware['name']
32
+ device_type = firmware.get('device_type', 'unknown')
33
+ version = firmware.get('version', 'unknown')
34
+ path = firmware.get('path', 'unknown')
35
+
36
+ # Check if file exists
37
+ from pathlib import Path
38
+ file_exists = Path(path).exists() if path != 'unknown' else False
39
+ status_color = ansi.Fg.GREEN if file_exists else ansi.Fg.RED
40
+ status_text = "✓" if file_exists else "✗"
41
+
42
+ self.poutput(ansi.style(f" {name}:", fg=ansi.Fg.CYAN, bold=True))
43
+ self.poutput(f" Device Type: {device_type}")
44
+ self.poutput(f" Version: {version}")
45
+ self.poutput(f" Path: {path}")
46
+ self.poutput(f" Status: {ansi.style(status_text, fg=status_color)} {'(File exists)' if file_exists else '(File missing)'}")
47
+
48
+ # Show flash options if available
49
+ flash_options = firmware.get('flash_options', {})
50
+ if flash_options:
51
+ self.poutput(f" Flash Options: {flash_options}")
52
+
53
+ self.poutput("")
54
+
55
+ except Exception as e:
56
+ logger.error(f"Error listing firmware: {str(e)}")
57
+ self.poutput(ansi.style(f"Error listing firmware: {str(e)}", fg=ansi.Fg.RED))
58
+
59
+ do_lsfw = do_list_firmware
60
+
61
+ @cmd2.with_category('Firmware Commands')
62
+ def do_add_firmware(self, arg):
63
+ 'Add new firmware to the system'
64
+ try:
65
+ # Parse arguments
66
+ args = arg.split()
67
+ if len(args) < 4:
68
+ self.poutput(ansi.style("Usage: add_firmware <name> <path> <device_type> <version> [flash_options_json]", fg=ansi.Fg.YELLOW))
69
+ self.poutput("Example: add_firmware my_esp32 /path/to/firmware.bin esp32 1.0.0")
70
+ return
71
+
72
+ name = args[0]
73
+ path = args[1]
74
+ device_type = args[2]
75
+ version = args[3]
76
+
77
+ # Parse optional flash options
78
+ flash_options = None
79
+ if len(args) > 4:
80
+ import json
81
+ try:
82
+ flash_options = json.loads(' '.join(args[4:]))
83
+ except json.JSONDecodeError:
84
+ self.poutput(ansi.style("Invalid JSON format for flash options", fg=ansi.Fg.RED))
85
+ return
86
+
87
+ firmware_service = get_firmware_service()
88
+ success = firmware_service.add_firmware(name, path, device_type, version, flash_options)
89
+
90
+ if success:
91
+ self.poutput(ansi.style(f"Successfully added firmware: {name}", fg=ansi.Fg.GREEN))
92
+ else:
93
+ self.poutput(ansi.style(f"Failed to add firmware: {name}", fg=ansi.Fg.RED))
94
+
95
+ except Exception as e:
96
+ logger.error(f"Error adding firmware: {str(e)}")
97
+ self.poutput(ansi.style(f"Error adding firmware: {str(e)}", fg=ansi.Fg.RED))
98
+
99
+ do_addfw = do_add_firmware
100
+
101
+ @cmd2.with_category('Firmware Commands')
102
+ def do_flash_firmware(self, arg):
103
+ 'Flash firmware to a device'
104
+ try:
105
+ # Parse arguments
106
+ args = arg.split()
107
+ if len(args) < 2:
108
+ self.poutput(ansi.style("Usage: flash_firmware <firmware_name> <device_name> [options_json]", fg=ansi.Fg.YELLOW))
109
+ self.poutput("Example: flash_firmware esp32s3_wifi_penetration_tool esp32")
110
+ return
111
+
112
+ firmware_name = args[0]
113
+ device_name = args[1]
114
+
115
+ # Parse optional options
116
+ options = None
117
+ if len(args) > 2:
118
+ import json
119
+ try:
120
+ options = json.loads(' '.join(args[2:]))
121
+ except json.JSONDecodeError:
122
+ self.poutput(ansi.style("Invalid JSON format for options", fg=ansi.Fg.RED))
123
+ return
124
+
125
+ firmware_service = get_firmware_service()
126
+
127
+ # Check if firmware exists
128
+ firmware_info = firmware_service.get_firmware_info(firmware_name)
129
+ if not firmware_info:
130
+ self.poutput(ansi.style(f"Firmware '{firmware_name}' not found", fg=ansi.Fg.RED))
131
+ return
132
+
133
+ self.poutput(ansi.style(f"Flashing firmware '{firmware_name}' to device '{device_name}'...", fg=ansi.Fg.YELLOW))
134
+
135
+ # Flash the firmware
136
+ success = firmware_service.flash_registered_firmware(firmware_name, options)
137
+
138
+ if success:
139
+ self.poutput(ansi.style(f"Successfully flashed firmware: {firmware_name}", fg=ansi.Fg.GREEN))
140
+ else:
141
+ self.poutput(ansi.style(f"Failed to flash firmware: {firmware_name}", fg=ansi.Fg.RED))
142
+
143
+ except Exception as e:
144
+ logger.error(f"Error flashing firmware: {str(e)}")
145
+ self.poutput(ansi.style(f"Error flashing firmware: {str(e)}", fg=ansi.Fg.RED))
146
+
147
+ do_flashfw = do_flash_firmware
148
+
149
+ @cmd2.with_category('Firmware Commands')
150
+ def do_remove_firmware(self, arg):
151
+ 'Remove firmware from the system'
152
+ try:
153
+ if not arg.strip():
154
+ self.poutput(ansi.style("Usage: remove_firmware <firmware_name>", fg=ansi.Fg.YELLOW))
155
+ self.poutput("Example: remove_firmware esp32_blink")
156
+ return
157
+
158
+ firmware_name = arg.strip()
159
+ firmware_service = get_firmware_service()
160
+
161
+ # Check if firmware exists
162
+ firmware_info = firmware_service.get_firmware_info(firmware_name)
163
+ if not firmware_info:
164
+ self.poutput(ansi.style(f"Firmware '{firmware_name}' not found", fg=ansi.Fg.RED))
165
+ return
166
+
167
+ # Confirm removal
168
+ confirm = input(f"Are you sure you want to remove firmware '{firmware_name}'? (y/N): ")
169
+ if confirm.lower() != 'y':
170
+ self.poutput(ansi.style("Firmware removal cancelled", fg=ansi.Fg.YELLOW))
171
+ return
172
+
173
+ success = firmware_service.remove_firmware(firmware_name)
174
+
175
+ if success:
176
+ self.poutput(ansi.style(f"Successfully removed firmware: {firmware_name}", fg=ansi.Fg.GREEN))
177
+ else:
178
+ self.poutput(ansi.style(f"Failed to remove firmware: {firmware_name}", fg=ansi.Fg.RED))
179
+
180
+ except Exception as e:
181
+ logger.error(f"Error removing firmware: {str(e)}")
182
+ self.poutput(ansi.style(f"Error removing firmware: {str(e)}", fg=ansi.Fg.RED))
183
+
184
+ do_rmfw = do_remove_firmware
185
+
186
+ @cmd2.with_category('Firmware Commands')
187
+ def do_download_firmware(self, arg):
188
+ 'Download firmware from URL'
189
+ try:
190
+ if not arg.strip():
191
+ self.poutput(ansi.style("Usage: download_firmware <url> [output_path]", fg=ansi.Fg.YELLOW))
192
+ self.poutput("Example: download_firmware https://example.com/firmware.bin")
193
+ return
194
+
195
+ args = arg.split()
196
+ url = args[0]
197
+ output_path = args[1] if len(args) > 1 else None
198
+
199
+ firmware_service = get_firmware_service()
200
+ self.poutput(ansi.style(f"Downloading firmware from: {url}", fg=ansi.Fg.YELLOW))
201
+
202
+ result_path = firmware_service.download_firmware(url, output_path)
203
+
204
+ if result_path:
205
+ self.poutput(ansi.style(f"Successfully downloaded firmware to: {result_path}", fg=ansi.Fg.GREEN))
206
+ else:
207
+ self.poutput(ansi.style("Failed to download firmware", fg=ansi.Fg.RED))
208
+
209
+ except Exception as e:
210
+ logger.error(f"Error downloading firmware: {str(e)}")
211
+ self.poutput(ansi.style(f"Error downloading firmware: {str(e)}", fg=ansi.Fg.RED))
212
+
213
+ do_dlfw = do_download_firmware
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env python
2
+
3
+ import cmd2
4
+ from cmd2 import ansi
5
+ import subprocess
6
+ from .base_commands import BaseCommands
7
+ from iotsploit_core.utils import iots_logger
8
+
9
+ logger = iots_logger.get_logger(__name__)
10
+
11
+
12
+ class LinuxCommands(BaseCommands):
13
+ """Linux system commands for the SAT Shell"""
14
+
15
+ @cmd2.with_category('Linux Commands')
16
+ def do_ls(self, arg):
17
+ 'List directory contents'
18
+ try:
19
+ result = subprocess.run(['ls'] + arg.split(), capture_output=True, text=True)
20
+ self.poutput(result.stdout)
21
+ if result.stderr:
22
+ self.poutput(result.stderr)
23
+ except Exception as e:
24
+ logger.error(f"Error executing ls: {str(e)}")
25
+
26
+ @cmd2.with_category('Linux Commands')
27
+ def do_lsusb(self, arg):
28
+ 'List USB devices'
29
+ try:
30
+ result = subprocess.run(['lsusb'] + arg.split(), capture_output=True, text=True)
31
+ self.poutput(result.stdout)
32
+ if result.stderr:
33
+ self.poutput(result.stderr)
34
+ except Exception as e:
35
+ logger.error(f"Error executing lsusb: {str(e)}")
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env python
2
+
3
+ import cmd2
4
+ from cmd2 import ansi
5
+ import time
6
+ from .base_commands import BaseCommands
7
+ from iotsploit_django.tools.wifi_mgr import WiFi_Mgr
8
+ from iotsploit_django.tools.input_mgr import Input_Mgr
9
+ from iotsploit_core.utils import iots_logger
10
+
11
+ logger = iots_logger.get_logger(__name__)
12
+
13
+
14
+ class NetworkCommands(BaseCommands):
15
+ """Network-related commands for the SAT Shell"""
16
+
17
+ @cmd2.with_category('Network Commands')
18
+ def do_connect_wifi(self, arg):
19
+ 'Connect to WiFi network by providing SSID and password'
20
+ logger.info(ansi.style("WiFi Connection Setup", fg=ansi.Fg.CYAN))
21
+
22
+ # Get SSID from user
23
+ ssid = Input_Mgr.Instance().string_input("Enter WiFi SSID")
24
+
25
+ # Get password from user
26
+ password = Input_Mgr.Instance().string_input("Enter WiFi password")
27
+
28
+ # Attempt to connect
29
+ logger.info(ansi.style(f"Attempting to connect to {ssid}...", fg=ansi.Fg.CYAN))
30
+ WiFi_Mgr.Instance().sta_connect_wifi(ssid, password)
31
+
32
+ # Wait for connection to establish
33
+ time.sleep(2)
34
+
35
+ # Show connection status
36
+ WiFi_Mgr.Instance().status()