diff options
| author | Agando <roduccomm@gmail.com> | 2026-07-09 06:00:32 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-07-09 06:00:32 +0200 |
| commit | cf7461ffeb4f53f3f50c9c692a2e4fdcecf17e82 (patch) | |
| tree | e7780cd41271381cf108c9c81d71a3cb65cfd555 /modules/ext/critical-ext-bundle.js | |
| parent | ea4d09fb5ccba12ed774fcf7c913edfa1e6a851a (diff) | |
auto: 2026-07-09_06:00 changes
Diffstat (limited to 'modules/ext/critical-ext-bundle.js')
| -rw-r--r-- | modules/ext/critical-ext-bundle.js | 315 |
1 files changed, 199 insertions, 116 deletions
diff --git a/modules/ext/critical-ext-bundle.js b/modules/ext/critical-ext-bundle.js index 3c5d54b..2230498 100644 --- a/modules/ext/critical-ext-bundle.js +++ b/modules/ext/critical-ext-bundle.js @@ -186,7 +186,18 @@ class ShellModule { if (!rawInput.trim()) return; this.markDirty(); this.printUserPrompt(rawInput); - + + // Save to command history (preserved from original executeCommand) + const cmdRaw = rawInput.trim(); + if (this.history.length === 0 || this.history[this.history.length - 1] !== cmdRaw) { + this.history.push(cmdRaw); + } + if (this.history.length > this._historyCap) { + this.history = this.history.slice(-this._historyCap); + } + this.historyIndex = this.history.length; + this._saveCommandHistory(); + // For simple single-word commands without special chars, skip shell parsing if (/^[a-zA-Z0-9._\/-]+$/.test(rawInput.trim()) && !rawInput.includes(' ') && !rawInput.includes('$')) { try { @@ -196,19 +207,20 @@ class ShellModule { return; } } - + // Substitute variables in the raw input const substituted = self._substituteVars(rawInput); // Split on ; first (sequence) const seqParts = self._splitSequence(substituted); for (const seqPart of seqParts) { if (!seqPart.trim()) continue; - // Split on && and || + // Split on && and || — operator now attached to the FOLLOWING segment const chainParts = self._splitChain(seqPart.trim()); let lastOk = true; for (const chainPart of chainParts) { if (!chainPart.cmd.trim()) continue; const { cmd: chainCmd, operator } = chainPart; + // Check the operator that PRECEDES this command if (operator === '&&' && !lastOk) continue; if (operator === '||' && lastOk) continue; // Check for redirects @@ -219,28 +231,40 @@ class ShellModule { lastOk = redirectInfo.exitOk; } else { // Normal or piped command - const pipeParts = self.terminal.splitPipe(chainCmd); + const pipeParts = this.splitPipe(chainCmd); if (pipeParts.length > 1) { - await self.terminal.executePipeChain(pipeParts); + await this.executePipeChain(pipeParts); } else { - // Single command + // Single command — check for shell built-ins first const parts = chainCmd.split(/\s+/); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); - try { - await this.security.checkAndRun(chainCmd); - } catch(e) { - this.terminal && this.terminal.print ? this.terminal.print('Error: ' + e.message, 'error') : console.error(e); + if (self.commands[cmd]) { + try { + this.markDirty(); + await self.commands[cmd](args); + lastOk = true; + } catch(e) { + this.print('Error: ' + e.message, 'error'); + lastOk = false; + } + } else { + try { + await this.security.checkAndRun(chainCmd); + lastOk = true; + } catch(e) { + this.print('Error: ' + e.message, 'error'); + lastOk = false; + } } } - lastOk = true; } } } }; this.terminal._shellModule = this; - // Modify pipe chain to read _pipeInput for commands that support it + // Modify pipe chain to add pipe-aware injection and security checks const origPipeChain = this.terminal.executePipeChain.bind(this.terminal); this.terminal.executePipeChain = async function(pipeParts) { // Write pipe input from previous segment as temp file for next segment @@ -251,6 +275,22 @@ class ShellModule { const parts = cmdText.split(/\s+/); const cmd = parts[0].toLowerCase(); let args = parts.slice(1); + + // Security gate — replicate the original executePipeChain checks + const cls = this.security._classify(cmdText); + if (cls.destructive && this.security.level > 0) { + this.print(`⚠️ Security: Destructive command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error'); + return; + } + if (cls.system && this.security.level > 0) { + this.print(`⚠️ Security: System command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error'); + return; + } + if (!cls.known && this.security.level >= 2) { + this.print(`⚠️ Security: Unknown command "${cmd}" blocked in pipe (paranoid mode)`, 'error'); + return; + } + this._pipeInput = pipeInput; if (pipeInput !== null) { @@ -265,16 +305,17 @@ class ShellModule { } } - if (i < pipeParts.length - 1) { - pipeInput = await this.capturePipeOutput(cmd, args); - } else { - await this.dispatchCommand(cmd, args); - } - - // Clean up temp file - if (this.fs.readFile('__pipebuf__') !== null) { - this.fs.root.content['__pipebuf__'] = undefined; - delete this.fs.root.content['__pipebuf__']; + try { + if (i < pipeParts.length - 1) { + pipeInput = await this.capturePipeOutput(cmd, args); + } else { + await this.dispatchCommand(cmd, args); + } + } finally { + // Clean up temp file + if (this.fs.readFile('__pipebuf__') !== null) { + delete this.fs.root.content['__pipebuf__']; + } } } this._pipeInput = null; @@ -307,10 +348,12 @@ class ShellModule { _splitChain(input) { // Split on && and ||, preserving operator info + // Operator is attached to the FOLLOWING segment (the one it guards) const parts = []; let current = ''; let inQuote = false; let quoteChar = ''; + let pendingOp = null; // operator from previous split, applies to next segment let i = 0; while (i < input.length) { const ch = input[i]; @@ -319,16 +362,18 @@ class ShellModule { } else if (ch === quoteChar && inQuote) { inQuote = false; quoteChar = ''; current += ch; i++; } else if (!inQuote && input.substring(i, i+2) === '&&') { - parts.push({ cmd: current.trim(), operator: '&&' }); + parts.push({ cmd: current.trim(), operator: pendingOp }); + pendingOp = '&&'; current = ''; i += 2; } else if (!inQuote && input.substring(i, i+2) === '||') { - parts.push({ cmd: current.trim(), operator: '||' }); + parts.push({ cmd: current.trim(), operator: pendingOp }); + pendingOp = '||'; current = ''; i += 2; } else { current += ch; i++; } } - parts.push({ cmd: current.trim(), operator: null }); + parts.push({ cmd: current.trim(), operator: pendingOp }); return parts; } @@ -351,7 +396,7 @@ class ShellModule { inQuote = false; quoteChar = ''; current += ch; i++; } else if (!inQuote) { if (input.substring(i, i+2) === '>>') { - if (current.trim()) parts.push(current.trim()); + if (current.trim()) { parts.push(current.trim()); current = ''; } hasRedirect = true; redirectOp = '>>'; append = true; @@ -366,7 +411,7 @@ class ShellModule { if (i < input.length && (input[i] === '>' || input[i] === '<')) continue; break; } else if (input.substring(i, i+1) === '>') { - if (current.trim()) parts.push(current.trim()); + if (current.trim()) { parts.push(current.trim()); current = ''; } hasRedirect = true; redirectOp = '>'; append = false; @@ -380,7 +425,7 @@ class ShellModule { if (i < input.length && (input[i] === '>' || input[i] === '<')) continue; break; } else if (input.substring(i, i+1) === '<') { - if (current.trim()) parts.push(current.trim()); + if (current.trim()) { parts.push(current.trim()); current = ''; } hasRedirect = true; redirectOp = '<'; i += 1; @@ -421,28 +466,38 @@ class ShellModule { const inputContent = fs.readFile(redirectFile); if (inputContent === null) { this.terminal.print(`shell: ${redirectFile}: No such file`, 'error'); + redirectInfo.exitOk = false; return; } this.terminal._pipeInput = inputContent; - // Execute the command with pipe input available - const parts = cmdText.split(/\s+/); - const cmd = parts[0].toLowerCase(); - const args = parts.slice(1); - // If command reads files, inject pipe content as temp file - if (this._isPipeAware(cmd)) { - fs.writeFile('__pipebuf__', inputContent); - const newArgs = [...args]; - if (!newArgs.some(a => !a.startsWith('-'))) { - newArgs.push('__pipebuf__'); - } - await this.terminal.dispatchCommand(cmd, newArgs); - if (fs.readFile('__pipebuf__') !== null) { - delete fs.root.content['__pipebuf__']; + try { + const parts = cmdText.split(/\s+/); + const cmd = parts[0].toLowerCase(); + const args = parts.slice(1); + // If command reads files, inject pipe content as temp file + if (this._isPipeAware(cmd)) { + fs.writeFile('__pipebuf__', inputContent); + const newArgs = [...args]; + if (!newArgs.some(a => !a.startsWith('-'))) { + newArgs.push('__pipebuf__'); + } + try { + await this.terminal.dispatchCommand(cmd, newArgs); + } finally { + if (fs.readFile('__pipebuf__') !== null) { + delete fs.root.content['__pipebuf__']; + } + } + } else { + await this.terminal.dispatchCommand(cmd, args); } - } else { - await this.terminal.dispatchCommand(cmd, args); + redirectInfo.exitOk = true; + } catch(e) { + this.terminal.print('Error: ' + e.message, 'error'); + redirectInfo.exitOk = false; + } finally { + this.terminal._pipeInput = null; } - this.terminal._pipeInput = null; return; } @@ -450,42 +505,57 @@ class ShellModule { const originalPrint = this.terminal.print.bind(this.terminal); let output = ''; this.terminal.print = (text, type) => { output += text + '\n'; }; + let cmdOk = true; - // Check for pipes within the command before redirect - const pipeParts = this.terminal.splitPipe(cmdText); - if (pipeParts.length > 1) { - // Execute pipe chain manually to capture all output - let pipeInput = null; - for (let i = 0; i < pipeParts.length; i++) { - const pText = pipeParts[i]; - const pParts = pText.split(/\s+/); - const pCmd = pParts[0].toLowerCase(); - const pArgs = pParts.slice(1); - if (pipeInput !== null) { - fs.writeFile('__pipebuf__', pipeInput); - } - if (i < pipeParts.length - 1) { - const captPrint = this.terminal.print.bind(this.terminal); - let capt = ''; - this.terminal.print = (t, ty) => { capt += t + '\n'; }; - await this.terminal.dispatchCommand(pCmd, pArgs); - this.terminal.print = captPrint; - pipeInput = capt.trim(); - } else { - await this.terminal.dispatchCommand(pCmd, pArgs); - } - if (fs.readFile('__pipebuf__') !== null) { - delete fs.root.content['__pipebuf__']; + try { + // Check for pipes within the command before redirect + const pipeParts = this.terminal.splitPipe(cmdText); + if (pipeParts.length > 1) { + // Execute pipe chain manually to capture all output + let pipeInput = null; + for (let i = 0; i < pipeParts.length; i++) { + const pText = pipeParts[i]; + const pParts = pText.split(/\s+/); + const pCmd = pParts[0].toLowerCase(); + const pArgs = pParts.slice(1); + if (pipeInput !== null) { + fs.writeFile('__pipebuf__', pipeInput); + } + if (i < pipeParts.length - 1) { + const captPrint = this.terminal.print.bind(this.terminal); + let capt = ''; + this.terminal.print = (t, ty) => { capt += t + '\n'; }; + try { + await this.terminal.dispatchCommand(pCmd, pArgs); + } finally { + this.terminal.print = captPrint; + } + pipeInput = capt.trim(); + } else { + await this.terminal.dispatchCommand(pCmd, pArgs); + } + if (fs.readFile('__pipebuf__') !== null) { + delete fs.root.content['__pipebuf__']; + } } + } else { + const parts = cmdText.split(/\s+/); + const cmd = parts[0].toLowerCase(); + const args = parts.slice(1); + await this.terminal.dispatchCommand(cmd, args); } - } else { - const parts = cmdText.split(/\s+/); - const cmd = parts[0].toLowerCase(); - const args = parts.slice(1); - await this.terminal.dispatchCommand(cmd, args); + } catch(e) { + cmdOk = false; + // Restore print before showing the error + this.terminal.print = originalPrint; + this.terminal.print('Error: ' + e.message, 'error'); + redirectInfo.exitOk = false; + return; + } finally { + // Always restore print — critical: without this, terminal is permanently broken + this.terminal.print = originalPrint; } - this.terminal.print = originalPrint; const captured = output.trimEnd(); if (captured) { @@ -503,6 +573,7 @@ class ShellModule { this.terminal.print(`Redirected (empty) to ${redirectFile}`, 'success'); } } + redirectInfo.exitOk = cmdOk; } _isPipeAware(cmd) { @@ -622,54 +693,66 @@ class ShellModule { const content = this.terminal.fs.readFile(path); if (content === null) return this.terminal.print(`script: ${path}: No such file`, 'error'); this._scriptDepth++; - this.terminal.print(`Running script: ${path}`, 'info'); - const lines = content.split('\n'); - let lineNum = 0; - for (const rawLine of lines) { - lineNum++; - const line = rawLine.trim(); - if (!line || line.startsWith('#')) continue; - // Run each line through the shell (by calling executeCommand directly) - // We need to manually call the wrapped executeCommand - this.terminal.print(`[${path}:${lineNum}] ${line}`, 'command-prompt'); - // Parse inline: handle pipe, redirect, chain, sequence - const substituted = this._substituteVars(line); - const seqParts = this._splitSequence(substituted); - for (const seqPart of seqParts) { - if (!seqPart.trim()) continue; - const chainParts = this._splitChain(seqPart.trim()); - let lastOk = true; - for (const chainPart of chainParts) { - if (!chainPart.cmd.trim()) continue; - if (chainPart.operator === '&&' && !lastOk) continue; - if (chainPart.operator === '||' && lastOk) continue; - const redirectInfo = this._extractRedirects(chainPart.cmd); - if (redirectInfo.hasRedirect) { - await this._executeWithRedirect(redirectInfo); - lastOk = redirectInfo.exitOk; - } else { - const pipeParts = this.terminal.splitPipe(chainPart.cmd); - if (pipeParts.length > 1) { - await this.terminal.executePipeChain(pipeParts); + try { + this.terminal.print(`Running script: ${path}`, 'info'); + const lines = content.split('\n'); + let lineNum = 0; + for (const rawLine of lines) { + lineNum++; + const line = rawLine.trim(); + if (!line || line.startsWith('#')) continue; + this.terminal.print(`[${path}:${lineNum}] ${line}`, 'command-prompt'); + const substituted = this._substituteVars(line); + const seqParts = this._splitSequence(substituted); + for (const seqPart of seqParts) { + if (!seqPart.trim()) continue; + const chainParts = this._splitChain(seqPart.trim()); + let lastOk = true; + for (const chainPart of chainParts) { + if (!chainPart.cmd.trim()) continue; + if (chainPart.operator === '&&' && !lastOk) continue; + if (chainPart.operator === '||' && lastOk) continue; + const redirectInfo = this._extractRedirects(chainPart.cmd); + if (redirectInfo.hasRedirect) { + await this._executeWithRedirect(redirectInfo); + lastOk = redirectInfo.exitOk; } else { - const parts = chainPart.cmd.split(/\s+/); - const cmd = parts[0].toLowerCase(); - const args = parts.slice(1); - // Check if it's our built-in command - if (this.commands[cmd]) { - this.terminal.markDirty(); - await this.commands[cmd](args); + const pipeParts = this.terminal.splitPipe(chainPart.cmd); + if (pipeParts.length > 1) { + await this.terminal.executePipeChain(pipeParts); } else { - await this.terminal.security.checkAndRun(chainPart.cmd); + const parts = chainPart.cmd.split(/\s+/); + const cmd = parts[0].toLowerCase(); + const args = parts.slice(1); + // Check if it's a shell built-in command + if (this.commands[cmd]) { + try { + this.terminal.markDirty(); + await this.commands[cmd](args); + lastOk = true; + } catch(e) { + this.terminal.print('Error: ' + e.message, 'error'); + lastOk = false; + } + } else { + try { + await this.terminal.security.checkAndRun(chainPart.cmd); + lastOk = true; + } catch(e) { + this.terminal.print('Error: ' + e.message, 'error'); + lastOk = false; + } + } } } - lastOk = true; } } } + this.terminal.print(`Script ${path} completed (${lineNum} lines)`, 'success'); + } finally { + // Always decrement — even on error or thrown exception + this._scriptDepth--; } - this._scriptDepth--; - this.terminal.print(`Script ${path} completed (${lineNum} lines)`, 'success'); } } if (typeof window.webx11 !== 'undefined' && window.webx11) { |