superchatai commited on
Commit
324849f
·
verified ·
1 Parent(s): a225cdd

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +393 -0
app.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import logging
3
+ import subprocess
4
+ import os
5
+ import uuid
6
+ import tempfile
7
+ import sys
8
+ from datetime import datetime, timedelta
9
+
10
+ def install_dependencies():
11
+ required_packages = ['fastapi', 'uvicorn']
12
+ missing_packages = []
13
+
14
+ for package in required_packages:
15
+ try:
16
+ __import__(package.replace('-', '_'))
17
+ except ImportError:
18
+ missing_packages.append(package)
19
+
20
+ if missing_packages:
21
+ print(f"Installing missing dependencies: {', '.join(missing_packages)}")
22
+ try:
23
+ subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--user'] + missing_packages)
24
+ print("Dependencies installed successfully!")
25
+ except subprocess.CalledProcessError as e:
26
+ print(f"Failed to install dependencies: {e}")
27
+ sys.exit(1)
28
+
29
+ def install_docker():
30
+ try:
31
+ subprocess.check_output(['docker', '--version'], stderr=subprocess.STDOUT)
32
+ return True
33
+ except (subprocess.CalledProcessError, FileNotFoundError):
34
+ pass
35
+
36
+ import platform
37
+ system = platform.system().lower()
38
+
39
+ try:
40
+ if system == "linux":
41
+ install_commands = [
42
+ ['sudo', 'apt-get', 'update'],
43
+ ['sudo', 'apt-get', 'install', '-y', 'ca-certificates', 'curl', 'gnupg', 'lsb-release'],
44
+ ['sudo', 'mkdir', '-p', '/etc/apt/keyrings'],
45
+ ['curl', '-fsSL', 'https://download.docker.com/linux/ubuntu/gpg', '|', 'sudo', 'gpg', '--dearmor', '-o', '/etc/apt/keyrings/docker.gpg'],
46
+ ['echo', '"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"', '|', 'sudo', 'tee', '/etc/apt/sources.list.d/docker.list', '>', '/dev/null'],
47
+ ['sudo', 'apt-get', 'update'],
48
+ ['sudo', 'apt-get', 'install', '-y', 'docker-ce', 'docker-ce-cli', 'containerd.io', 'docker-buildx-plugin', 'docker-compose-plugin'],
49
+ ['sudo', 'systemctl', 'start', 'docker'],
50
+ ['sudo', 'systemctl', 'enable', 'docker']
51
+ ]
52
+
53
+ for cmd in install_commands:
54
+ try:
55
+ subprocess.check_call(cmd)
56
+ except subprocess.CalledProcessError:
57
+ continue
58
+
59
+ elif system == "darwin":
60
+ return False
61
+
62
+ elif system == "windows":
63
+ return False
64
+
65
+ try:
66
+ subprocess.check_output(['docker', '--version'], stderr=subprocess.STDOUT)
67
+ return True
68
+ except (subprocess.CalledProcessError, FileNotFoundError):
69
+ return False
70
+
71
+ except Exception:
72
+ return False
73
+
74
+ install_dependencies()
75
+ install_docker()
76
+
77
+ from fastapi import FastAPI, Request, HTTPException
78
+ from fastapi.responses import PlainTextResponse, StreamingResponse, HTMLResponse
79
+
80
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
81
+
82
+ app = FastAPI()
83
+
84
+ def validate_command(command: str):
85
+ if '..' in command or command.startswith('/') or '../' in command:
86
+ raise HTTPException(status_code=403, detail="Access denied: cannot access files outside files directory")
87
+
88
+ dangerous_commands = ['rm', 'del', 'format', 'fdisk', 'mkfs', 'dd', 'wget', 'curl', 'nc', 'netcat', 'ssh', 'scp', 'chmod', 'chown', 'sudo', 'su']
89
+ for cmd in dangerous_commands:
90
+ if cmd in command.lower():
91
+ raise HTTPException(status_code=403, detail=f"Dangerous command not allowed: {cmd}")
92
+
93
+ allowed_commands = ['python3', 'node', 'npm', 'pip', 'ls', 'cat', 'head', 'tail', 'grep', 'find', 'wc', 'sort', 'echo', 'whoami']
94
+ command_parts = command.split()
95
+ if command_parts and command_parts[0] not in allowed_commands:
96
+ if not (command_parts[0].startswith('python3') or command_parts[0] in ['node', 'npm']):
97
+ raise HTTPException(status_code=403, detail=f"Command not allowed: {command_parts[0]}")
98
+
99
+ files = []
100
+
101
+ ip_timestamps = {}
102
+
103
+ async def check_stale_ips():
104
+ while True:
105
+ current_time = datetime.now()
106
+ stale_ips = []
107
+ for ip, timestamp in ip_timestamps.items():
108
+ time_diff = current_time - timestamp
109
+ if time_diff > timedelta(seconds=15):
110
+ stale_ips.append(ip)
111
+
112
+ if stale_ips:
113
+ logging.info(f"Resetting files array due to stale IPs: {stale_ips}")
114
+ files.clear()
115
+ for ip in stale_ips:
116
+ del ip_timestamps[ip]
117
+
118
+ await asyncio.sleep(5)
119
+
120
+ @app.on_event("startup")
121
+ async def startup_event():
122
+ asyncio.create_task(check_stale_ips())
123
+
124
+ HTML_CONTENT = """<!DOCTYPE html>
125
+ <html>
126
+ <head>
127
+ <title>Forge Code</title>
128
+ </head>
129
+ <body style="margin:20px;font-family:monospace;">
130
+ <h1>Forge Code</h1>
131
+
132
+ <div style="margin:20px 0;">
133
+ <h3>app.py</h3>
134
+ <div style="margin:10px 0;">
135
+ <button onclick="loadTimeTemplate()">Time Printer</button>
136
+ <button onclick="loadServerTemplate()">Web Server</button>
137
+ </div>
138
+ <textarea id="filecontent" placeholder="Write your Python code here..." style="width:100%;height:300px;margin:5px;"></textarea><br>
139
+ <button onclick="saveFile()">Save</button>
140
+ <button onclick="runApp()">Run App</button>
141
+ </div>
142
+
143
+ <div style="margin:20px 0;">
144
+ <h3>Terminal Output</h3>
145
+ <button onclick="clearTerminal()">Clear</button><br>
146
+ <pre id="terminal" style="background:#000;color:#0f0;padding:10px;height:300px;overflow:auto;margin:5px 0;"></pre>
147
+ </div>
148
+
149
+ <script>
150
+ function loadFile() {
151
+ const stored = localStorage.getItem('app_py');
152
+ if (stored) {
153
+ document.getElementById('filecontent').value = stored;
154
+ }
155
+ }
156
+
157
+ function saveFile() {
158
+ const content = document.getElementById('filecontent').value;
159
+ localStorage.setItem('app_py', content);
160
+ log('Saved app.py');
161
+ }
162
+
163
+ function loadTimeTemplate() {
164
+ const template = [
165
+ 'import time',
166
+ 'import datetime',
167
+ '',
168
+ 'while True:',
169
+ ' now = datetime.datetime.now()',
170
+ ' print(f"Current time: {now.strftime(\\'%Y-%m-%d %H:%M:%S\\')}")',
171
+ ' time.sleep(1)'
172
+ ].join('\\n');
173
+ document.getElementById('filecontent').value = template;
174
+ log('Loaded Time Printer template');
175
+ }
176
+
177
+ function loadServerTemplate() {
178
+ const template = [
179
+ 'from flask import Flask',
180
+ 'import time',
181
+ '',
182
+ 'app = Flask(__name__)',
183
+ '',
184
+ '@app.route(\\'/\\')',
185
+ 'def home():',
186
+ ' return f"Hello from Forge Code! Time: {time.strftime(\\'%H:%M:%S\\')}"',
187
+ '',
188
+ 'if __name__ == \\'__main__\\':',
189
+ ' print("Starting server on port 5000...")',
190
+ ' print("Visit: http://localhost:5000")',
191
+ ' app.run(host=\\'0.0.0.0\\', port=5000, debug=True)'
192
+ ].join('\\n');
193
+ document.getElementById('filecontent').value = template;
194
+ log('Loaded Web Server template');
195
+ }
196
+
197
+ function runApp() {
198
+ const content = document.getElementById('filecontent').value;
199
+ if (!content.trim()) return alert('Write some code first!');
200
+
201
+ document.getElementById('terminal').textContent = '';
202
+
203
+ const formData = new FormData();
204
+ formData.append('command', 'python3 app.py');
205
+ formData.append('files', new Blob([content]), 'app.py');
206
+
207
+ fetch('/execute', {
208
+ method: 'POST',
209
+ body: formData
210
+ })
211
+ .then(response => {
212
+ const reader = response.body.getReader();
213
+ const decoder = new TextDecoder();
214
+
215
+ function readStream() {
216
+ reader.read().then(({done, value}) => {
217
+ if (done) {
218
+ return;
219
+ }
220
+ const chunk = decoder.decode(value);
221
+ const lines = chunk.split('\\n');
222
+ lines.forEach(line => {
223
+ if (line.startsWith('data: ')) {
224
+ log(line.replace('data: ', ''));
225
+ }
226
+ });
227
+ readStream();
228
+ });
229
+ }
230
+ readStream();
231
+ })
232
+ .catch(e => log('Error: ' + e.message));
233
+ }
234
+
235
+ function log(msg) {
236
+ const terminal = document.getElementById('terminal');
237
+ terminal.textContent += msg + '\\n';
238
+ terminal.scrollTop = terminal.scrollHeight;
239
+ }
240
+
241
+ function clearTerminal() {
242
+ document.getElementById('terminal').textContent = '';
243
+ }
244
+
245
+ window.onload = loadFile;
246
+ </script>
247
+ </body>
248
+ </html>"""
249
+
250
+ @app.get("/", response_class=HTMLResponse)
251
+ async def root():
252
+ return HTMLResponse(content=HTML_CONTENT)
253
+
254
+ @app.get("/execute")
255
+ async def execute_get(command: str, files=None):
256
+ if not command or not command.strip():
257
+ raise HTTPException(status_code=400, detail="Command cannot be empty")
258
+
259
+ if command.startswith('python '):
260
+ command = 'python3 ' + command[7:]
261
+ validate_command(command)
262
+
263
+ container_id = str(uuid.uuid4())[:8]
264
+ logging.info(f"Creating container {container_id} for command: {command}")
265
+
266
+ if files is None:
267
+ files = []
268
+
269
+ async def generate():
270
+ try:
271
+ import asyncio
272
+ import time
273
+ import shutil
274
+
275
+ start_time = time.time()
276
+ timeout_seconds = 30.0
277
+
278
+ with tempfile.TemporaryDirectory() as temp_dir:
279
+ workspace_dir = os.path.join(temp_dir, 'workspace')
280
+ os.makedirs(workspace_dir)
281
+
282
+ if files:
283
+ for file in files:
284
+ content = await file.read()
285
+ file_path = os.path.join(workspace_dir, file.filename)
286
+ with open(file_path, 'wb') as f:
287
+ f.write(content)
288
+
289
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False, dir=temp_dir) as script_file:
290
+ script_content = f"""#!/bin/bash
291
+ cd /workspace
292
+ {command}
293
+ """
294
+ script_file.write(script_content)
295
+ script_path = script_file.name
296
+
297
+ os.chmod(script_path, 0o755)
298
+
299
+ docker_cmd = [
300
+ 'docker', 'run',
301
+ '--rm',
302
+ '--name', f'forge-exec-{container_id}',
303
+ '--cpus', '0.25',
304
+ '--memory', '1g',
305
+ '--memory-swap', '1g',
306
+ '--network', 'none',
307
+ '--read-only',
308
+ '--tmpfs', '/tmp:rw,noexec,nosuid,size=100m',
309
+ '--env', 'PYTHONUNBUFFERED=1',
310
+ '--volume', f'{workspace_dir}:/workspace:rw',
311
+ '--volume', f'{script_path}:/script.sh:ro',
312
+ '--workdir', '/workspace',
313
+ 'python:3.11-slim',
314
+ 'bash', '/script.sh'
315
+ ]
316
+
317
+ process = subprocess.Popen(
318
+ docker_cmd,
319
+ stdout=subprocess.PIPE,
320
+ stderr=subprocess.STDOUT,
321
+ text=True,
322
+ bufsize=1
323
+ )
324
+
325
+ while True:
326
+ if time.time() - start_time > timeout_seconds:
327
+ try:
328
+ subprocess.run(['docker', 'kill', f'forge-exec-{container_id}'], timeout=5)
329
+ except:
330
+ pass
331
+ yield f"data: Command timed out after 30 seconds\n\n"
332
+ break
333
+
334
+ try:
335
+ line = await asyncio.wait_for(
336
+ asyncio.get_event_loop().run_in_executor(None, process.stdout.readline),
337
+ timeout=1.0
338
+ )
339
+ if line:
340
+ yield f"data: {line.rstrip()}\n\n"
341
+ else:
342
+ break
343
+ except asyncio.TimeoutError:
344
+ continue
345
+
346
+ process.wait()
347
+
348
+ if process.returncode != 0:
349
+ yield f"data: Command failed with exit code {process.returncode}\n\n"
350
+
351
+ except Exception as e:
352
+ yield f"data: Error: {str(e)}\n\n"
353
+ finally:
354
+ try:
355
+ subprocess.run(['docker', 'rm', '-f', f'forge-exec-{container_id}'], timeout=5, capture_output=True)
356
+ except:
357
+ pass
358
+
359
+ return StreamingResponse(
360
+ generate(),
361
+ media_type="text/event-stream",
362
+ headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}
363
+ )
364
+
365
+ @app.post("/execute")
366
+ async def execute_post(request: Request):
367
+ form = await request.form()
368
+ command = form.get("command")
369
+ files = []
370
+
371
+ for field_name, field_value in form.items():
372
+ if field_name == "command":
373
+ command = field_value
374
+ elif hasattr(field_value, 'filename'):
375
+ files.append(field_value)
376
+
377
+ if not command:
378
+ raise HTTPException(status_code=400, detail="Command is required")
379
+
380
+ return await execute_get(command, files)
381
+
382
+ @app.get("/ping")
383
+ async def ping(request: Request):
384
+ client_ip = request.client.host
385
+ current_time = datetime.now()
386
+
387
+ ip_timestamps[client_ip] = current_time
388
+
389
+ return {"message": "pong", "files_count": len(files)}
390
+
391
+ if __name__ == "__main__":
392
+ import uvicorn
393
+ uvicorn.run(app, host="0.0.0.0", port=7860)