Rémy
commited on
Commit
·
0694075
1
Parent(s):
d45df14
Improvement
Browse files- app.py +24 -6
- static/index.html +35 -12
app.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
-
from fastapi import FastAPI, HTTPException
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
from fastapi.staticfiles import StaticFiles
|
| 4 |
from fastapi.responses import FileResponse
|
| 5 |
from pydantic import BaseModel
|
| 6 |
import subprocess
|
| 7 |
import shlex
|
| 8 |
-
|
|
|
|
| 9 |
app = FastAPI()
|
| 10 |
|
| 11 |
# Add CORS middleware
|
|
@@ -28,8 +28,16 @@ class Command(BaseModel):
|
|
| 28 |
command: str
|
| 29 |
|
| 30 |
@app.post("/execute")
|
| 31 |
-
async def execute_command(command: Command):
|
| 32 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
# Parse the command to get the base command
|
| 34 |
base_command = shlex.split(command.command)[0]
|
| 35 |
|
|
@@ -37,12 +45,22 @@ async def execute_command(command: Command):
|
|
| 37 |
if base_command not in ALLOWED_COMMANDS:
|
| 38 |
raise HTTPException(status_code=403, detail=f"Command '{base_command}' is not allowed")
|
| 39 |
|
| 40 |
-
#
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
return {
|
| 43 |
"output": result.stdout,
|
| 44 |
"error": result.stderr,
|
| 45 |
-
"returncode": result.returncode
|
|
|
|
| 46 |
}
|
| 47 |
except subprocess.TimeoutExpired:
|
| 48 |
raise HTTPException(status_code=408, detail="Command execution timed out")
|
|
|
|
|
|
|
| 1 |
from fastapi.middleware.cors import CORSMiddleware
|
| 2 |
from fastapi.staticfiles import StaticFiles
|
| 3 |
from fastapi.responses import FileResponse
|
| 4 |
from pydantic import BaseModel
|
| 5 |
import subprocess
|
| 6 |
import shlex
|
| 7 |
+
import os
|
| 8 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 9 |
app = FastAPI()
|
| 10 |
|
| 11 |
# Add CORS middleware
|
|
|
|
| 28 |
command: str
|
| 29 |
|
| 30 |
@app.post("/execute")
|
| 31 |
+
async def execute_command(command: Command, request: Request):
|
| 32 |
try:
|
| 33 |
+
# Get or create a session-specific directory
|
| 34 |
+
session = request.session
|
| 35 |
+
if 'working_dir' not in session:
|
| 36 |
+
session['working_dir'] = '/tmp/user_' + os.urandom(8).hex()
|
| 37 |
+
os.makedirs(session['working_dir'], exist_ok=True)
|
| 38 |
+
|
| 39 |
+
working_dir = session['working_dir']
|
| 40 |
+
|
| 41 |
# Parse the command to get the base command
|
| 42 |
base_command = shlex.split(command.command)[0]
|
| 43 |
|
|
|
|
| 45 |
if base_command not in ALLOWED_COMMANDS:
|
| 46 |
raise HTTPException(status_code=403, detail=f"Command '{base_command}' is not allowed")
|
| 47 |
|
| 48 |
+
# Handle 'cd' command specially
|
| 49 |
+
if base_command == 'cd':
|
| 50 |
+
new_dir = os.path.join(working_dir, shlex.split(command.command)[1])
|
| 51 |
+
if os.path.isdir(new_dir):
|
| 52 |
+
session['working_dir'] = new_dir
|
| 53 |
+
return {"output": "", "error": "", "returncode": 0, "currentDirectory": new_dir}
|
| 54 |
+
else:
|
| 55 |
+
return {"output": "", "error": "No such directory", "returncode": 1, "currentDirectory": working_dir}
|
| 56 |
+
|
| 57 |
+
# Execute the command in the controlled environment
|
| 58 |
+
result = subprocess.run(command.command, shell=True, capture_output=True, text=True, timeout=5, cwd=working_dir)
|
| 59 |
return {
|
| 60 |
"output": result.stdout,
|
| 61 |
"error": result.stderr,
|
| 62 |
+
"returncode": result.returncode,
|
| 63 |
+
"currentDirectory": working_dir
|
| 64 |
}
|
| 65 |
except subprocess.TimeoutExpired:
|
| 66 |
raise HTTPException(status_code=408, detail="Command execution timed out")
|
static/index.html
CHANGED
|
@@ -6,11 +6,11 @@
|
|
| 6 |
<title>Linux Practice Tool</title>
|
| 7 |
<style>
|
| 8 |
body {
|
| 9 |
-
font-family: monospace;
|
| 10 |
-
background-color: #
|
| 11 |
-
color: #
|
| 12 |
margin: 0;
|
| 13 |
-
padding:
|
| 14 |
}
|
| 15 |
#terminal {
|
| 16 |
width: 100%;
|
|
@@ -25,17 +25,23 @@
|
|
| 25 |
margin-top: 10px;
|
| 26 |
}
|
| 27 |
#prompt {
|
|
|
|
| 28 |
margin-right: 5px;
|
| 29 |
}
|
| 30 |
#command-input {
|
| 31 |
flex-grow: 1;
|
| 32 |
background-color: transparent;
|
| 33 |
border: none;
|
| 34 |
-
color: #
|
| 35 |
font-family: inherit;
|
| 36 |
font-size: inherit;
|
| 37 |
outline: none;
|
| 38 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
</style>
|
| 40 |
</head>
|
| 41 |
<body>
|
|
@@ -49,9 +55,9 @@
|
|
| 49 |
<script>
|
| 50 |
const output = document.getElementById('output');
|
| 51 |
const input = document.getElementById('command-input');
|
| 52 |
-
|
| 53 |
let commandHistory = [];
|
| 54 |
let historyIndex = -1;
|
|
|
|
| 55 |
|
| 56 |
input.addEventListener('keydown', async (event) => {
|
| 57 |
if (event.key === 'Enter') {
|
|
@@ -85,7 +91,7 @@
|
|
| 85 |
if (command.toLowerCase() === 'help') {
|
| 86 |
displayHelp();
|
| 87 |
} else {
|
| 88 |
-
output.innerHTML +=
|
| 89 |
|
| 90 |
try {
|
| 91 |
const response = await fetch('/execute', {
|
|
@@ -93,27 +99,44 @@
|
|
| 93 |
headers: {
|
| 94 |
'Content-Type': 'application/json',
|
| 95 |
},
|
| 96 |
-
body: JSON.stringify({ command }),
|
| 97 |
});
|
| 98 |
|
| 99 |
const data = await response.json();
|
| 100 |
|
| 101 |
if (response.ok) {
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
| 103 |
if (data.error) {
|
| 104 |
-
output.innerHTML +=
|
| 105 |
}
|
| 106 |
} else {
|
| 107 |
-
output.innerHTML +=
|
| 108 |
}
|
| 109 |
} catch (error) {
|
| 110 |
-
output.innerHTML +=
|
| 111 |
}
|
| 112 |
}
|
| 113 |
|
| 114 |
output.scrollTop = output.scrollHeight;
|
| 115 |
}
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
function displayHelp() {
|
| 118 |
const helpText = `
|
| 119 |
Available commands:
|
|
|
|
| 6 |
<title>Linux Practice Tool</title>
|
| 7 |
<style>
|
| 8 |
body {
|
| 9 |
+
font-family: 'Courier New', monospace;
|
| 10 |
+
background-color: #300a24;
|
| 11 |
+
color: #ffffff;
|
| 12 |
margin: 0;
|
| 13 |
+
padding: 10px;
|
| 14 |
}
|
| 15 |
#terminal {
|
| 16 |
width: 100%;
|
|
|
|
| 25 |
margin-top: 10px;
|
| 26 |
}
|
| 27 |
#prompt {
|
| 28 |
+
color: #4e9a06;
|
| 29 |
margin-right: 5px;
|
| 30 |
}
|
| 31 |
#command-input {
|
| 32 |
flex-grow: 1;
|
| 33 |
background-color: transparent;
|
| 34 |
border: none;
|
| 35 |
+
color: #ffffff;
|
| 36 |
font-family: inherit;
|
| 37 |
font-size: inherit;
|
| 38 |
outline: none;
|
| 39 |
}
|
| 40 |
+
.directory { color: #3465a4; }
|
| 41 |
+
.executable { color: #4e9a06; }
|
| 42 |
+
.image { color: #75507b; }
|
| 43 |
+
.archive { color: #c4a000; }
|
| 44 |
+
.text { color: #cc0000; }
|
| 45 |
</style>
|
| 46 |
</head>
|
| 47 |
<body>
|
|
|
|
| 55 |
<script>
|
| 56 |
const output = document.getElementById('output');
|
| 57 |
const input = document.getElementById('command-input');
|
|
|
|
| 58 |
let commandHistory = [];
|
| 59 |
let historyIndex = -1;
|
| 60 |
+
let currentDirectory = '/';
|
| 61 |
|
| 62 |
input.addEventListener('keydown', async (event) => {
|
| 63 |
if (event.key === 'Enter') {
|
|
|
|
| 91 |
if (command.toLowerCase() === 'help') {
|
| 92 |
displayHelp();
|
| 93 |
} else {
|
| 94 |
+
output.innerHTML += `<span id="prompt">$</span> ${command}\n`;
|
| 95 |
|
| 96 |
try {
|
| 97 |
const response = await fetch('/execute', {
|
|
|
|
| 99 |
headers: {
|
| 100 |
'Content-Type': 'application/json',
|
| 101 |
},
|
| 102 |
+
body: JSON.stringify({ command, currentDirectory }),
|
| 103 |
});
|
| 104 |
|
| 105 |
const data = await response.json();
|
| 106 |
|
| 107 |
if (response.ok) {
|
| 108 |
+
if (command.startsWith('cd ')) {
|
| 109 |
+
currentDirectory = data.currentDirectory;
|
| 110 |
+
}
|
| 111 |
+
output.innerHTML += colorize(data.output);
|
| 112 |
if (data.error) {
|
| 113 |
+
output.innerHTML += `<span style="color: #cc0000;">Error: ${data.error}</span>\n`;
|
| 114 |
}
|
| 115 |
} else {
|
| 116 |
+
output.innerHTML += `<span style="color: #cc0000;">Error: ${data.detail}</span>\n`;
|
| 117 |
}
|
| 118 |
} catch (error) {
|
| 119 |
+
output.innerHTML += `<span style="color: #cc0000;">Error: ${error.message}</span>\n`;
|
| 120 |
}
|
| 121 |
}
|
| 122 |
|
| 123 |
output.scrollTop = output.scrollHeight;
|
| 124 |
}
|
| 125 |
|
| 126 |
+
function colorize(text) {
|
| 127 |
+
const lines = text.split('\n');
|
| 128 |
+
return lines.map(line => {
|
| 129 |
+
return line.replace(/(\S+)/g, (match) => {
|
| 130 |
+
if (match.endsWith('/')) return `<span class="directory">${match}</span>`;
|
| 131 |
+
if (match.endsWith('.exe') || match.endsWith('.sh')) return `<span class="executable">${match}</span>`;
|
| 132 |
+
if (match.match(/\.(jpg|jpeg|png|gif|bmp)$/i)) return `<span class="image">${match}</span>`;
|
| 133 |
+
if (match.match(/\.(zip|tar|gz|rar)$/i)) return `<span class="archive">${match}</span>`;
|
| 134 |
+
if (match.match(/\.(txt|md|log)$/i)) return `<span class="text">${match}</span>`;
|
| 135 |
+
return match;
|
| 136 |
+
});
|
| 137 |
+
}).join('\n');
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
function displayHelp() {
|
| 141 |
const helpText = `
|
| 142 |
Available commands:
|