From 608e47b2fac066a9679f7d214c2fb33825ff914c Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Mon, 2 Jun 2025 14:34:03 -0600 Subject: [PATCH] Made some MASSIVE improvements --- .env.example | 5 +- docker-compose.yml | 4 + src/components/Terminal.tsx | 340 ++++++++++++++++++++++++++++++------ src/pages/root.astro | 43 ++++- 4 files changed, 334 insertions(+), 58 deletions(-) diff --git a/.env.example b/.env.example index a5580c6..462738e 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,7 @@ S3_SECRET_KEY=your_secret_key_here S3_BUCKET_NAME=your_bucket_name SECRET_CODE=super_secret_code ADMIN_CODE=super_secret_code -JWT_SECRET=your_jwt_secret_key_here_make_it_long_and_random \ No newline at end of file +JWT_SECRET=your_jwt_secret_key_here_make_it_long_and_random +SOLUTION_1=29 +SOLUTION_2=31 +SOLUTION_3=5 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c8d99d4..00f4d32 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,4 +15,8 @@ services: SECRET_CODE: ${SECRET_CODE} ADMIN_CODE: ${ADMIN_CODE} JWT_SECRET: ${JWT_SECRET} + # Puzzle Solutions (private) + SOLUTION_1: ${SOLUTION_1} + SOLUTION_2: ${SOLUTION_2} + SOLUTION_3: ${SOLUTION_3} restart: unless-stopped diff --git a/src/components/Terminal.tsx b/src/components/Terminal.tsx index f211a9f..bf092bc 100644 --- a/src/components/Terminal.tsx +++ b/src/components/Terminal.tsx @@ -9,12 +9,16 @@ type FileSystemStructure = { }; interface TerminalProps { - solution1: number; - solution2: number; - solution3: number; + puzzle1Content: string; + puzzle2Content: string; + puzzle3Content: string; } -const TerminalComponent: React.FC = ({ solution1 = 42, solution2 = 87, solution3 = 5 }) => { +const TerminalComponent: React.FC = ({ + puzzle1Content, + puzzle2Content, + puzzle3Content +}) => { const terminalRef = useRef(null); const xtermRef = useRef(null); const fitAddonRef = useRef(null); @@ -57,35 +61,13 @@ const TerminalComponent: React.FC = ({ solution1 = 42, solution2 "- /usr: System-wide shared resources and executables (read-only for you!).\n", // Puzzle 1: Number Base - '/etc/sys_data/core_access.bin': `System memory address reference:\n` + - `Register B1: ${solution1.toString(2).padStart(8, '0')}\n` + - `Note: Always decode binary values when transferring between subsystems.`, + '/etc/sys_data/core_access.bin': puzzle1Content, // Puzzle 2: Hex Code (Color) - '/etc/config/color_palette.config': `# Display Color Settings (RGB hex format) -PRIMARY_COLOR=#FF${solution2.toString(16).padStart(2, '0')}33 -SECONDARY_COLOR=#33FF57 -ALERT_COLOR=#FF0000 - -# System Note: -# Color channels must be isolated when importing to legacy devices. -# Remember middle values carry essential security data.`, + '/etc/config/color_palette.config': puzzle2Content, // Puzzle 3: Log File Analysis - '/var/log/critical_errors.log': "[ERROR 001] System boot failure sequence A.\n" + - "[INFO 002] Retrying boot sequence B.\n" + - "[ERROR 003] Disk read error on /dev/sda1.\n" + - "[WARN 004] Network interface eth0 flapping.\n" + - "[INFO 005] System recovered.\n" + - "[DEBUG 006] User 'admin' login attempt.\n" + - "[ERROR 007] Authentication failure for user 'admin'.\n" + - "[INFO 008] User 'hacker' login successful.\n" + - "[WARN 009] Unusual activity detected from IP 192.168.1.101.\n" + - "[ERROR 010] Critical process 'security_daemon' terminated unexpectedly.\n" + - "[INFO 011] Attempting to restart 'security_daemon'.\n" + - `[ERROR 012] Failed to restart 'security_daemon'. Max retries (${solution3}) reached.\n` + - "[ERROR 014] System shutting down due to critical errors.\n" + - "[RECOVER] Backup facility requires retry count for authentication sequence.", + '/var/log/critical_errors.log': puzzle3Content, // Home directory files with hints '/home/user/puzzle_progress.txt': "My notes on the combination lock:\n- First number: __ (Check core system data)\n- Second number: __ (UI color settings have relevant info)\n- Third number: __ (Error logs contain critical values)\n\nReminder: Always convert between number formats as needed!", @@ -103,7 +85,7 @@ ALERT_COLOR=#FF0000 '/usr/bin/common_commands.txt': "Available commands: ls, cd, cat, clear, help (for README)\n\nImportant system paths:\n- Main config: /etc/config/\n- System data: /etc/sys_data/\n- Error logs: /var/log/", '/usr/share/misc/old_data.bak': "ARCHIVE FILE: Contains outdated research notes from Project Chimera. Access restricted.\n\nPossible combination values tested:\n- 12-45-78 (Failed)\n- 24-68-99 (Failed)\n- 11-22-33 (Failed)\n\nNOTE: New secure generation algorithm implemented. Old combinations no longer valid.\n\nCurrent security relies on: core_access biometrics, color_palette UI safeguards, and error logging thresholds." - }), [solution1, solution2, solution3]); + }), [puzzle1Content, puzzle2Content, puzzle3Content]); const currentPath = useRef('/'); const prompt = () => { @@ -237,7 +219,10 @@ ALERT_COLOR=#FF0000 background: '#1e1e1e', foreground: '#d4d4d4', cursor: '#d4d4d4', - } + }, + allowTransparency: true, + scrollback: 1000, + rightClickSelectsWord: true }); const fit = new FitAddon(); const webLinks = new WebLinksAddon(); @@ -250,11 +235,164 @@ ALERT_COLOR=#FF0000 term.open(terminalRef.current); fit.fit(); + // Enhanced paste handler for Ctrl+V and Cmd+V + term.attachCustomKeyEventHandler((e) => { + // Handle paste with Ctrl+V (Windows/Linux) or Cmd+V (Mac) + if ((e.ctrlKey || e.metaKey) && e.key === 'v') { + e.preventDefault(); + navigator.clipboard.readText().then(text => { + // Process the pasted text character by character to update currentLine + const cleanText = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + for (const char of cleanText) { + if (char === '\n') { + // Handle newline as Enter key + if (currentLine.current.trim()) { + commandHistory.current.push(currentLine.current); + } + historyIndex.current = commandHistory.current.length; + processCommand(currentLine.current); + currentLine.current = ''; + } else { + // Add printable characters to current line and display + term.write(char); + currentLine.current += char; + } + } + }).catch(err => { + console.warn('Failed to read clipboard:', err); + }); + return false; + } + + // Handle copy with Ctrl+C or Cmd+C (when text is selected) + if ((e.ctrlKey || e.metaKey) && e.key === 'c') { + const selection = term.getSelection(); + if (selection) { + navigator.clipboard.writeText(selection).catch(err => { + console.warn('Failed to write to clipboard:', err); + }); + } + return true; // Don't prevent default for copy + } + + return true; + }); + + // Add right-click context menu support + term.element?.addEventListener('contextmenu', (e) => { + e.preventDefault(); + + // Create context menu + const existingMenu = document.getElementById('terminal-context-menu'); + if (existingMenu) { + existingMenu.remove(); + } + + const menu = document.createElement('div'); + menu.id = 'terminal-context-menu'; + menu.style.cssText = ` + position: fixed; + top: ${e.clientY}px; + left: ${e.clientX}px; + background: #2d2d2d; + border: 1px solid #555; + border-radius: 4px; + padding: 4px 0; + box-shadow: 0 2px 8px rgba(0,0,0,0.3); + z-index: 1000; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + min-width: 120px; + `; + + const selection = term.getSelection(); + + // Copy option (only if text is selected) + if (selection) { + const copyItem = document.createElement('div'); + copyItem.textContent = 'Copy'; + copyItem.style.cssText = ` + padding: 8px 16px; + color: #fff; + cursor: pointer; + transition: background-color 0.1s; + `; + copyItem.addEventListener('mouseenter', () => { + copyItem.style.backgroundColor = '#404040'; + }); + copyItem.addEventListener('mouseleave', () => { + copyItem.style.backgroundColor = 'transparent'; + }); + copyItem.addEventListener('click', () => { + navigator.clipboard.writeText(selection).catch(err => { + console.warn('Failed to write to clipboard:', err); + }); + menu.remove(); + }); + menu.appendChild(copyItem); + } + + // Paste option + const pasteItem = document.createElement('div'); + pasteItem.textContent = 'Paste'; + pasteItem.style.cssText = ` + padding: 8px 16px; + color: #fff; + cursor: pointer; + transition: background-color 0.1s; + `; + pasteItem.addEventListener('mouseenter', () => { + pasteItem.style.backgroundColor = '#404040'; + }); + pasteItem.addEventListener('mouseleave', () => { + pasteItem.style.backgroundColor = 'transparent'; + }); + pasteItem.addEventListener('click', () => { + navigator.clipboard.readText().then(text => { + // Process the pasted text character by character to update currentLine + const cleanText = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + for (const char of cleanText) { + if (char === '\n') { + // Handle newline as Enter key + if (currentLine.current.trim()) { + commandHistory.current.push(currentLine.current); + } + historyIndex.current = commandHistory.current.length; + processCommand(currentLine.current); + currentLine.current = ''; + } else { + // Add printable characters to current line and display + term.write(char); + currentLine.current += char; + } + } + }).catch(err => { + console.warn('Failed to read clipboard:', err); + }); + menu.remove(); + }); + menu.appendChild(pasteItem); + + document.body.appendChild(menu); + + // Remove menu when clicking elsewhere + const removeMenu = (event: MouseEvent) => { + if (!menu.contains(event.target as Node)) { + menu.remove(); + document.removeEventListener('click', removeMenu); + } + }; + + // Use setTimeout to avoid immediate removal + setTimeout(() => { + document.addEventListener('click', removeMenu); + }, 0); + }); + term.write('Welcome to Ixabata-shell!\r\n'); term.write('Type a command to begin. (Try ls, cd, cat)\r\n'); prompt(); - term.onKey(({ key, domEvent }) => { if (!xtermRef.current) return; @@ -305,40 +443,142 @@ ALERT_COLOR=#FF0000 let suggestions: string[] = []; const availableCommands = ['ls', 'cd', 'cat', 'clear', 'help']; + // If we're completing a command (first word) if (inputParts.length === 0 && !currentLine.current.includes(' ')) { suggestions = availableCommands.filter(c => c.startsWith(currentArgument)); - } else { - const currentDirItems = Array.isArray(fileSystem[currentPath.current]) ? fileSystem[currentPath.current] as string[] : []; - suggestions = currentDirItems.filter(item => item.startsWith(currentArgument)); + } else { + // For path completion (after a command) + const command = inputParts[0]; + + // Only complete paths for commands that accept file paths + if (['ls', 'cd', 'cat'].includes(command)) { + // Handle absolute paths + if (currentArgument.startsWith('/')) { + const pathParts = currentArgument.split('/'); + const lastPart = pathParts.pop() || ''; + const basePath = pathParts.join('/') || '/'; + + if (fileSystem[basePath] && Array.isArray(fileSystem[basePath])) { + suggestions = (fileSystem[basePath] as string[]) + .filter(item => item.startsWith(lastPart)) + .map(item => { + // Return the full path for absolute completions + return basePath === '/' ? '/' + item : basePath + '/' + item; + }); + } + } else { + // Handle relative paths + const currentDirItems = Array.isArray(fileSystem[currentPath.current]) + ? fileSystem[currentPath.current] as string[] + : []; + suggestions = currentDirItems.filter(item => item.startsWith(currentArgument)); + } + } } if (suggestions.length === 1) { const suggestion = suggestions[0]; let resolvedSuggestionPath = suggestion; - if (!suggestion.startsWith('/')) { - resolvedSuggestionPath = (currentPath.current === '/' ? '' : currentPath.current) + '/' + suggestion; + + // Handle path resolution for the suggestion + if (currentArgument.startsWith('/')) { + // For absolute paths, the suggestion is already the full path + resolvedSuggestionPath = suggestion; + } else { + // For relative paths, use the current directory + resolvedSuggestionPath = (currentPath.current === '/' ? '' : currentPath.current) + '/' + suggestion; } + + // Normalize path if (resolvedSuggestionPath !== '/' && resolvedSuggestionPath.endsWith('/')) { - resolvedSuggestionPath = resolvedSuggestionPath.slice(0, -1); + resolvedSuggestionPath = resolvedSuggestionPath.slice(0, -1); } const isDirectory = Array.isArray(fileSystem[resolvedSuggestionPath]); - const completion = suggestion + (isDirectory ? '/' : ' '); - - xtermRef.current.write(completion.substring(currentArgument.length)); - currentLine.current = (commandPart ? commandPart + ' ' : '') + completion; + + let completion: string; + if (currentArgument.startsWith('/')) { + // For absolute paths, replace the entire argument + completion = suggestion + (isDirectory ? '/' : ' '); + xtermRef.current.write('\x1b[2K\r'); // Clear the line + xtermRef.current.write(`root@ixabatasha.life:${currentPath.current}# `); + xtermRef.current.write((commandPart ? commandPart + ' ' : '') + completion); + currentLine.current = (commandPart ? commandPart + ' ' : '') + completion; + } else { + // For relative paths, append to the existing argument + completion = suggestion + (isDirectory ? '/' : ' '); + xtermRef.current.write(completion.substring(currentArgument.length)); + currentLine.current = (commandPart ? commandPart + ' ' : '') + completion; + } + if (!isDirectory || !completion.endsWith('/')) { - currentLine.current = currentLine.current.trimEnd(); + currentLine.current = currentLine.current.trimEnd(); + } + } else if (suggestions.length > 1) { + // Show all possible completions in a bash-like format + xtermRef.current.write('\r\n'); + + // For display purposes, show just the item names, not full paths + const displaySuggestions = suggestions.map(s => { + if (currentArgument.startsWith('/')) { + return s.split('/').pop() || s; + } + return s; + }); + + // Calculate the longest common prefix + let commonPrefix = ''; + if (displaySuggestions.length > 0) { + const first = displaySuggestions[0]; + for (let i = 0; i < first.length; i++) { + if (displaySuggestions.every(s => s[i] === first[i])) { + commonPrefix += first[i]; + } else { + break; + } + } } - - } else if (suggestions.length > 1) { - xtermRef.current.write('\r\n' + suggestions.join('\t') + '\r\n'); - prompt(); - xtermRef.current.write(currentLine.current); + // If there's a common prefix, complete up to that point + if (commonPrefix.length > (currentArgument.startsWith('/') ? currentArgument.split('/').pop()?.length || 0 : currentArgument.length)) { + if (currentArgument.startsWith('/')) { + const pathParts = currentArgument.split('/'); + pathParts.pop(); + const newPath = pathParts.join('/') + '/' + commonPrefix; + xtermRef.current.write('\x1b[2K\r'); // Clear the line + xtermRef.current.write(`root@ixabatasha.life:${currentPath.current}# `); + xtermRef.current.write((commandPart ? commandPart + ' ' : '') + newPath); + currentLine.current = (commandPart ? commandPart + ' ' : '') + newPath; + } else { + xtermRef.current.write(commonPrefix.substring(currentArgument.length)); + currentLine.current = (commandPart ? commandPart + ' ' : '') + commonPrefix; + } + } else { + // Display all options in columns + const maxLength = Math.max(...displaySuggestions.map(s => s.length)) + 2; + const columns = Math.floor(term.cols / maxLength); + const rows = Math.ceil(displaySuggestions.length / columns); + + for (let row = 0; row < rows; row++) { + let line = ''; + for (let col = 0; col < columns; col++) { + const index = row + (col * rows); + if (index < displaySuggestions.length) { + const item = displaySuggestions[index]; + const fullPath = suggestions[index]; + const isDir = Array.isArray(fileSystem[fullPath.startsWith('/') ? fullPath : (currentPath.current === '/' ? '' : currentPath.current) + '/' + fullPath]); + line += item + (isDir ? '/' : '') + ' '.repeat(maxLength - item.length - (isDir ? 1 : 0)); + } + } + xtermRef.current.write(line + '\r\n'); + } + + // Reprint the prompt and current input + prompt(); + xtermRef.current.write(currentLine.current); + } } - // If no suggestions, do nothing (or beep: xtermRef.current.write('\x07');) - + // If no suggestions, do nothing } else if (printable && key.length === 1) { // Ensure it's a single character, not a special key like 'Shift' xtermRef.current.write(key); currentLine.current += key; diff --git a/src/pages/root.astro b/src/pages/root.astro index 76c3928..61aa089 100644 --- a/src/pages/root.astro +++ b/src/pages/root.astro @@ -26,10 +26,39 @@ const title = "root@ixabatasha.life"; - How to find: Look for the "max retries" count in the security_daemon error */ -// Define the solutions for the combination lock -const solution1 = 29; // Binary puzzle solution -const solution2 = 31; // Hex color code puzzle solution -const solution3 = 5; // Log file analysis puzzle solution +// Get solutions from environment variables (server-side only) +const solution1 = parseInt(process.env.SOLUTION_1 || import.meta.env.SOLUTION_1 || '29'); +const solution2 = parseInt(process.env.SOLUTION_2 || import.meta.env.SOLUTION_2 || '31'); +const solution3 = parseInt(process.env.SOLUTION_3 || import.meta.env.SOLUTION_3 || '5'); + +// Generate puzzle content server-side +const puzzle1Content = `System memory address reference: +Register B1: ${solution1.toString(2).padStart(8, '0')} +Note: Always decode binary values when transferring between subsystems.`; + +const puzzle2Content = `# Display Color Settings (RGB hex format) +PRIMARY_COLOR=#FF${solution2.toString(16).padStart(2, '0')}33 +SECONDARY_COLOR=#33FF57 +ALERT_COLOR=#FF0000 + +# System Note: +# Color channels must be isolated when importing to legacy devices. +# Remember middle values carry essential security data.`; + +const puzzle3Content = `[ERROR 001] System boot failure sequence A. +[INFO 002] Retrying boot sequence B. +[ERROR 003] Disk read error on /dev/sda1. +[WARN 004] Network interface eth0 flapping. +[INFO 005] System recovered. +[DEBUG 006] User 'admin' login attempt. +[ERROR 007] Authentication failure for user 'admin'. +[INFO 008] User 'hacker' login successful. +[WARN 009] Unusual activity detected from IP 192.168.1.101. +[ERROR 010] Critical process 'security_daemon' terminated unexpectedly. +[INFO 011] Attempting to restart 'security_daemon'. +[ERROR 012] Failed to restart 'security_daemon'. Max retries (${solution3}) reached. +[ERROR 014] System shutting down due to critical errors. +[RECOVER] Backup facility requires retry count for authentication sequence.`; --- @@ -48,9 +77,9 @@ const solution3 = 5; // Log file analysis puzzle solution