Added Shell Page
This commit is contained in:
109
src/css/shell.css
Normal file
109
src/css/shell.css
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/* css/shell.css — Themed terminal styling (dark/light aware) */
|
||||||
|
|
||||||
|
.shell-page {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px var(--shadow);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
transition: background 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-page h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-output {
|
||||||
|
background: #171717;
|
||||||
|
color: #01b501;
|
||||||
|
padding: 1rem;
|
||||||
|
height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
box-shadow: inset 0 0 6px rgba(0,0,0,0.4);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .shell-output {
|
||||||
|
background: #f9f9f9;
|
||||||
|
color: #004400;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
box-shadow: inset 0 0 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
background: var(--bg);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: inset 0 0 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-form .prompt {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.6rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .shell-input {
|
||||||
|
border: 1px solid rgba(0,0,0,0.1);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-run {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-run:hover {
|
||||||
|
background: var(--accentHover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-run:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.line {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.2rem 0;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.line.cmd { color: var(--accent); }
|
||||||
|
pre.line.err { color: #f33; }
|
||||||
|
pre.line.out { color: #01b501; }
|
||||||
|
body.light-mode pre.line.out { color: #007700; }
|
||||||
46
src/includes/shell_exec.php
Normal file
46
src/includes/shell_exec.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
// includes/shell_exec.php — Executes shell commands securely (admin only)
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (empty($_SESSION['is_admin'])) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Not authorized']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cmd = trim($_POST['command'] ?? '');
|
||||||
|
if ($cmd === '') {
|
||||||
|
echo json_encode(['output' => '']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restrict dangerous commands for safety
|
||||||
|
$blacklist = ['rm', 'shutdown', 'reboot', 'passwd', 'dd', ':(){'];
|
||||||
|
foreach ($blacklist as $bad) {
|
||||||
|
if (stripos($cmd, $bad) !== false) {
|
||||||
|
echo json_encode(['output' => "⚠️ Command '$bad' not allowed"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute safely (captures both stdout and stderr)
|
||||||
|
$descriptor = [
|
||||||
|
1 => ['pipe', 'w'],
|
||||||
|
2 => ['pipe', 'w']
|
||||||
|
];
|
||||||
|
$process = proc_open($cmd, $descriptor, $pipes, '/home');
|
||||||
|
if (is_resource($process)) {
|
||||||
|
$output = stream_get_contents($pipes[1]);
|
||||||
|
$error = stream_get_contents($pipes[2]);
|
||||||
|
fclose($pipes[1]);
|
||||||
|
fclose($pipes[2]);
|
||||||
|
$status = proc_close($process);
|
||||||
|
echo json_encode([
|
||||||
|
'output' => trim($output . "\n" . $error),
|
||||||
|
'status' => $status
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['output' => 'Failed to execute command']);
|
||||||
|
}
|
||||||
90
src/js/shell.js
Normal file
90
src/js/shell.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// js/shell.js — SPA-safe interactive shell with history + Ctrl-L clear
|
||||||
|
(() => {
|
||||||
|
if (window.shellInitialized) return;
|
||||||
|
window.shellInitialized = true;
|
||||||
|
|
||||||
|
document.addEventListener('page-loaded', (e) => {
|
||||||
|
if (e.detail.page !== 'shell') return;
|
||||||
|
|
||||||
|
console.log('[shell.js] Initializing shell...');
|
||||||
|
|
||||||
|
const form = document.getElementById('shellForm');
|
||||||
|
const input = document.getElementById('shellInput');
|
||||||
|
const output = document.getElementById('shellOutput');
|
||||||
|
const runBtn = document.getElementById('runBtn');
|
||||||
|
if (!form || !input || !output || !runBtn) return;
|
||||||
|
|
||||||
|
// ----- History -----
|
||||||
|
const history = [];
|
||||||
|
let histIndex = -1;
|
||||||
|
|
||||||
|
function appendLine(text, type = 'out') {
|
||||||
|
const line = document.createElement('pre');
|
||||||
|
line.className = `line ${type}`;
|
||||||
|
line.textContent = text;
|
||||||
|
output.appendChild(line);
|
||||||
|
output.scrollTop = output.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const cmd = input.value.trim();
|
||||||
|
if (!cmd) return;
|
||||||
|
|
||||||
|
history.push(cmd);
|
||||||
|
histIndex = history.length;
|
||||||
|
appendLine(`$ ${cmd}`, 'cmd');
|
||||||
|
input.value = '';
|
||||||
|
runBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('includes/shell_exec.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({ command: cmd })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.error) appendLine(data.error, 'err');
|
||||||
|
if (data.output) appendLine(data.output, 'out');
|
||||||
|
} catch (err) {
|
||||||
|
appendLine(`Error: ${err.message}`, 'err');
|
||||||
|
} finally {
|
||||||
|
runBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----- Keyboard handlers -----
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
if (histIndex > 0) {
|
||||||
|
histIndex--;
|
||||||
|
input.value = history[histIndex];
|
||||||
|
setTimeout(() => input.setSelectionRange(input.value.length, input.value.length), 0);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'ArrowDown') {
|
||||||
|
if (histIndex < history.length - 1) {
|
||||||
|
histIndex++;
|
||||||
|
input.value = history[histIndex];
|
||||||
|
} else {
|
||||||
|
histIndex = history.length;
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key.toLowerCase() === 'l' && e.ctrlKey) {
|
||||||
|
// Ctrl+L clear screen
|
||||||
|
output.innerHTML = '';
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[shell.js] Shell initialized.');
|
||||||
|
console.log('💡 Usage');
|
||||||
|
console.log('Shortcut Action');
|
||||||
|
console.log('↑ / ↓ Browse previous commands');
|
||||||
|
console.log('Ctrl + L Clear the screen');
|
||||||
|
console.log('Enter Execute command');
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
// pages/shell.php — Admin shell console
|
||||||
|
require_once __DIR__ . '/../includes/config.php';
|
||||||
|
session_start();
|
||||||
|
if (empty($_SESSION['is_admin'])) {
|
||||||
|
echo "<section class='center'><p class='error'>Access denied. Admin login required.</p></section>";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<section class="shell-page">
|
||||||
|
<h2>Interactive Shell</h2>
|
||||||
|
|
||||||
|
<div id="shellOutput" class="shell-output"></div>
|
||||||
|
|
||||||
|
<form id="shellForm" class="shell-form">
|
||||||
|
<span class="prompt">$</span>
|
||||||
|
<input id="shellInput" class="shell-input" type="text" placeholder="Enter command..." autocomplete="off" />
|
||||||
|
<button id="runBtn" class="btn-run" type="submit">Run</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script src="js/shell.js" defer></script>
|
||||||
|
<link rel="stylesheet" href="css/shell.css">
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="css/shell.css">
|
||||||
|
<script src="js/shell.js" defer></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user