This commit is contained in:
2025-05-19 22:19:38 -06:00
parent 4a0b1bea7c
commit 8b8c243973
4 changed files with 2099 additions and 1564 deletions

View File

@ -9,25 +9,28 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/node": "^9.1.1",
"@astrojs/react": "^4.2.0",
"@aws-sdk/client-s3": "^3.750.0",
"@tailwindcss/vite": "^4.0.8",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^22.13.5",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"astro": "^5.3.1",
"@astrojs/node": "^9.2.1",
"@astrojs/react": "^4.2.7",
"@aws-sdk/client-s3": "^3.812.0",
"@tailwindcss/vite": "^4.1.7",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^22.15.19",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"astro": "^5.7.13",
"astro-robots": "^2.3.1",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.0.8",
"typescript": "^5.7.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.7",
"typescript": "^5.8.3",
"uuid": "^11.1.0"
},
"devDependencies": {
"daisyui": "5.0.0-beta.6"
"daisyui": "5.0.35"
}
}

3210
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

365
src/components/Terminal.tsx Normal file
View File

@ -0,0 +1,365 @@
import React, { useEffect, useRef, useMemo } from 'react';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import '@xterm/xterm/css/xterm.css';
type FileSystemStructure = {
[key: string]: string[] | string;
};
interface TerminalProps {
solution1: number;
solution2: number;
solution3: number;
}
const TerminalComponent: React.FC<TerminalProps> = ({ solution1 = 42, solution2 = 87, solution3 = 5 }) => {
const terminalRef = useRef<HTMLDivElement>(null);
const xtermRef = useRef<Terminal | null>(null);
const fitAddonRef = useRef<FitAddon | null>(null);
const commandHistory = useRef<string[]>([]);
const historyIndex = useRef<number>(-1);
const currentLine = useRef<string>('');
const fileSystem: FileSystemStructure = useMemo(() => ({
'/': ['home', 'documents', 'etc', 'var', 'usr', 'README.txt', 'secret.txt'],
'/home': ['user', 'guest'],
'/home/user': ['puzzle_progress.txt'],
'/home/guest': [],
'/documents': ['report.doc', 'notes.txt'],
'/etc': ['config', 'sys_data'],
'/etc/config': ['network.conf', 'color_palette.config'],
'/etc/sys_data': ['core_access.bin'],
'/var': ['log', 'tmp'],
'/var/log': ['system.log', 'critical_errors.log'],
'/usr': ['bin', 'share'],
'/usr/bin': ['common_commands.txt'],
'/usr/share': ['docs', 'misc'],
'/usr/share/docs': ['filesystem_layout.txt'],
'/usr/share/misc': ['old_data.bak'],
// File Contents
'/README.txt': "Welcome, challenger. Your mission, should you choose to accept it...\n" +
"Is to find three numbers to unlock a combination lock.\n" +
"These numbers are hidden within this simulated filesystem.\n" +
"Use commands like 'ls', 'cd', and 'cat' to explore.\n" +
"A good starting point might be to understand the lay of the land: 'cat /usr/share/docs/filesystem_layout.txt'\n" +
"Good luck!",
'/secret.txt': "SECURITY NOTICE:\n\nSystem access restricted to authorized personnel only.\nAll activities on this terminal are logged and monitored.\n\nNote to admins: The master backup codes have been moved to a more secure location.\nPlease refer to the new security protocol documentation.\n\nCheck system configuration in /etc and examine logs in /var/log if issues arise.",
'/usr/share/docs/filesystem_layout.txt': "System Architecture Overview:\n" +
"- /home: User-specific files.\n" +
"- /etc: System configuration files and critical data.\n" +
" |- /config: Display and network settings.\n" +
" |- /sys_data: Core system binary data.\n" +
"- /var: Variable data, such as logs and temporary files.\n" +
" |- /log: System logs and error reports.\n" +
"- /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.`,
// 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.`,
// 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.",
// 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!",
// Document files with hints
'/documents/report.doc': "Project Phoenix - Q3 Financial Report Summary\n\nStrictly Confidential\n\nNote: System core access requires binary authentication module as referenced in maintenance guide section 4.2.1",
'/documents/notes.txt': "Grocery List:\n- Milk\n- Eggs\n- Bread\n- Mystery item #42 (NOT the combination number!)\n- Remember to check system logs for maintenance schedule\n\nReminders:\n- Review critical error logs for system failures\n- Update color palette settings per design team request",
// Network config with hint
'/etc/config/network.conf': "HOSTNAME=ixabatasha-virtual-env\nIP_ADDR=10.1.33.7\nGATEWAY=10.1.33.1\nDNS_SERVER=8.8.8.8\n\n# Note: Display color palette in the same directory needs to be accessible for UI integration",
// System log with hint
'/var/log/system.log': "[INFO] System startup sequence initiated.\n[INFO] All core services loaded successfully.\n[WARN] Low virtual memory. Consider rebooting non-essential modules.\n[INFO] User 'root' logged in from console.\n[NOTICE] Combination lock module initialized with secure parameters.\n[DEBUG] Numbers not stored in plaintext for security reasons.\n[INFO] Memory address values in binary format stored in /etc/sys_data\n[WARN] Check critical_errors.log for security daemon failures",
'/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]);
const currentPath = useRef<string>('/');
const prompt = () => {
if (xtermRef.current) {
xtermRef.current.write(`\r\nroot@ixabatasha:${currentPath.current}# `);
}
};
const processCommand = (command: string) => {
if (!xtermRef.current) return;
const [cmd, ...args] = command.trim().split(' ');
switch (cmd) {
case 'ls':
const currentDirContent = fileSystem[currentPath.current];
if (Array.isArray(currentDirContent)) {
const displayItems = currentDirContent.map(item => {
let itemFullPath = (currentPath.current === '/' ? '' : currentPath.current) + '/' + item;
if (itemFullPath !== '/' && itemFullPath.endsWith('/')) itemFullPath = itemFullPath.slice(0, -1);
if (Array.isArray(fileSystem[itemFullPath])) {
return item + '/';
}
return item;
});
xtermRef.current.write('\r\n' + displayItems.join('\t'));
} else {
xtermRef.current.write(`\r\nls: cannot access '${currentPath.current}': Not a directory`);
}
break;
case 'cd':
const targetDir = args[0];
if (!targetDir) {
// cd to root
currentPath.current = '/';
} else if (targetDir === '..') {
const parts = currentPath.current.split('/').filter(p => p);
parts.pop();
currentPath.current = '/' + parts.join('/');
if (currentPath.current === '//') currentPath.current = '/';
} else {
let newPath: string;
if (targetDir.startsWith('/')) {
newPath = targetDir;
} else {
newPath = (currentPath.current === '/' ? '' : currentPath.current) + '/' + targetDir;
}
// Normalize path (remove trailing slashes unless it's the root)
if (newPath !== '/' && newPath.endsWith('/')) {
newPath = newPath.slice(0, -1);
}
if (fileSystem[newPath] !== undefined) {
if (Array.isArray(fileSystem[newPath])) {
currentPath.current = newPath;
} else {
xtermRef.current.write(`\r\ncd: ${targetDir}: Not a directory`);
}
} else {
xtermRef.current.write(`\r\ncd: ${targetDir}: No such file or directory`);
}
}
break;
case 'cat':
const targetFileArg = args[0];
if (!targetFileArg) {
xtermRef.current.write('\r\ncat: missing file operand');
break;
}
let fullTargetPath: string;
if (targetFileArg.startsWith('/')) {
fullTargetPath = targetFileArg;
} else {
fullTargetPath = (currentPath.current === '/' ? '' : currentPath.current) + '/' + targetFileArg;
}
// Normalize path
if (fullTargetPath !== '/' && fullTargetPath.endsWith('/')) {
fullTargetPath = fullTargetPath.slice(0, -1);
}
const fileContent = fileSystem[fullTargetPath];
if (fileContent === undefined) {
xtermRef.current.write(`\r\ncat: ${targetFileArg}: No such file or directory`);
} else if (Array.isArray(fileContent)) {
xtermRef.current.write(`\r\ncat: ${targetFileArg}: Is a directory`);
} else if (typeof fileContent === 'string') {
xtermRef.current.write('\r\n' + fileContent.split('\n').join('\r\n'));
} else {
// Should not happen with current structure
xtermRef.current.write(`\r\ncat: ${targetFileArg}: Cannot read this type of file`);
}
break;
case 'clear':
xtermRef.current.clear();
break;
case 'help':
// Display available commands and tips
const helpText =
"Available Commands:\n" +
" ls List directory contents\n" +
" cd <directory> Change directory\n" +
" cat <file> Display file contents\n" +
" clear Clear the screen\n" +
" help Display this help message\n\n" +
"Tips:\n" +
"- Use Tab for command/path completion\n" +
"- Up/Down arrows navigate command history\n" +
"- Type 'cat /usr/bin/common_commands.txt' for more system information\n" +
"- Read the README.txt for mission details";
xtermRef.current.write('\r\n' + helpText);
break;
case '': // Handle empty command
break;
default:
xtermRef.current.write(`\r\ncommand not found: ${cmd}\r\nTry 'help' for a list of available commands`);
break;
}
prompt();
};
useEffect(() => {
if (terminalRef.current && !xtermRef.current) {
const term = new Terminal({
cursorBlink: true,
fontFamily: 'monospace',
fontSize: 14,
theme: {
background: '#1e1e1e',
foreground: '#d4d4d4',
cursor: '#d4d4d4',
}
});
const fit = new FitAddon();
const webLinks = new WebLinksAddon();
xtermRef.current = term;
fitAddonRef.current = fit;
term.loadAddon(fit);
term.loadAddon(webLinks);
term.open(terminalRef.current);
fit.fit();
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;
const printable = !domEvent.altKey && !domEvent.ctrlKey && !domEvent.metaKey;
if (domEvent.key === 'Enter') {
if (currentLine.current.trim()) {
commandHistory.current.push(currentLine.current);
}
historyIndex.current = commandHistory.current.length;
processCommand(currentLine.current);
currentLine.current = '';
} else if (domEvent.key === 'Backspace') {
if (currentLine.current.length > 0) {
xtermRef.current.write('\b \b'); // Erase character
currentLine.current = currentLine.current.slice(0, -1);
}
} else if (domEvent.key === 'ArrowUp') {
if (commandHistory.current.length > 0 && historyIndex.current > 0) {
historyIndex.current--;
// Clear current line and write prompt + history item
xtermRef.current.write('\x1b[2K\r');
xtermRef.current.write(`root@ixabatasha:${currentPath.current}# `);
xtermRef.current.write(commandHistory.current[historyIndex.current]);
currentLine.current = commandHistory.current[historyIndex.current];
}
} else if (domEvent.key === 'ArrowDown') {
if (commandHistory.current.length > 0 && historyIndex.current < commandHistory.current.length - 1) {
historyIndex.current++;
// Clear current line and write prompt + history item
xtermRef.current.write('\x1b[2K\r');
xtermRef.current.write(`root@ixabatasha:${currentPath.current}# `);
xtermRef.current.write(commandHistory.current[historyIndex.current]);
currentLine.current = commandHistory.current[historyIndex.current];
} else if (historyIndex.current === commandHistory.current.length - 1) {
historyIndex.current++;
// Clear current line and write just the prompt
xtermRef.current.write('\x1b[2K\r');
xtermRef.current.write(`root@ixabatasha:${currentPath.current}# `);
currentLine.current = "";
}
} else if (domEvent.key === 'Tab') {
domEvent.preventDefault(); // Prevent default tab behavior (focus change)
const inputParts = currentLine.current.split(' ');
const currentArgument = inputParts.pop() || '';
const commandPart = inputParts.join(' ');
let suggestions: string[] = [];
const availableCommands = ['ls', 'cd', 'cat', 'clear', 'help'];
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));
}
if (suggestions.length === 1) {
const suggestion = suggestions[0];
let resolvedSuggestionPath = suggestion;
if (!suggestion.startsWith('/')) {
resolvedSuggestionPath = (currentPath.current === '/' ? '' : currentPath.current) + '/' + suggestion;
}
if (resolvedSuggestionPath !== '/' && resolvedSuggestionPath.endsWith('/')) {
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;
if (!isDirectory || !completion.endsWith('/')) {
currentLine.current = currentLine.current.trimEnd();
}
} else if (suggestions.length > 1) {
xtermRef.current.write('\r\n' + suggestions.join('\t') + '\r\n');
prompt();
xtermRef.current.write(currentLine.current);
}
// If no suggestions, do nothing (or beep: xtermRef.current.write('\x07');)
} 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;
}
});
// Handle resize
const handleResize = () => {
fitAddonRef.current?.fit();
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
term.dispose();
xtermRef.current = null;
};
}
}, []);
return <div ref={terminalRef} style={{ width: '100%', height: '100%' }} />;
};
export default TerminalComponent;

View File

@ -0,0 +1,57 @@
---
import TerminalComponent from '../components/Terminal.tsx';
const title = "Secret Terminal";
/*
Escape Room Terminal Puzzle
==========================
This virtual Linux terminal contains three puzzles that reveal the
combination lock code. Each number can be configured below.
Puzzle Locations & Solutions:
1. Binary Number Challenge
- Location: /etc/sys_data/core_access.bin
- How to find: Look for binary data in system files and convert to decimal
2. Hex Color Code Challenge
- Location: /etc/config/color_palette.config
- How to find: Extract the green component from a hex color code (FF**XX**33)
and convert from hex to decimal
3. Log File Analysis Challenge
- Location: /var/log/critical_errors.log
- 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
---
<!doctype html>
<html lang="en" class="h-full m-0 p-0 overflow-hidden">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤫</text></svg>"
/>
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
<link rel="stylesheet" href="/src/styles/global.css" />
</head>
<body class="h-full m-0 p-0 overflow-hidden bg-black">
<div id="terminal-container" class="w-full h-full">
<TerminalComponent
client:only="react"
solution1={solution1}
solution2={solution2}
solution3={solution3}
/>
</div>
</body>
</html>