Devity4756 commited on
Commit
7cdbef0
·
verified ·
1 Parent(s): 4fa00b5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +281 -64
app.py CHANGED
@@ -4,16 +4,21 @@ import subprocess
4
  import shutil
5
  import threading
6
  import io
 
 
 
 
 
7
  from huggingface_hub import HfApi, snapshot_download
8
- from flask import Flask
9
- from fastapi import FastAPI
10
- from fastapi.middleware.wsgi import WSGIMiddleware
11
- from starlette.responses import StreamingResponse
12
 
13
  # === Config ===
14
  DATASET_REPO = "Devity4756/Terminal"
15
  HF_TOKEN = os.environ.get("HF_TOKEN")
16
- api = HfApi(token=HF_TOKEN)
 
 
 
 
17
 
18
  WORKDIR = "workspace"
19
  os.makedirs(WORKDIR, exist_ok=True)
@@ -25,12 +30,15 @@ PATH_MAP = {"Alex": VIRTUAL_HOME}
25
 
26
  # === Restore state ===
27
  try:
28
- snapshot_path = snapshot_download(
29
- repo_id=DATASET_REPO,
30
- repo_type="dataset",
31
- token=HF_TOKEN
32
- )
33
- shutil.copytree(snapshot_path, WORKDIR, dirs_exist_ok=True)
 
 
 
34
  except Exception as e:
35
  print("No previous data restored:", e)
36
 
@@ -42,6 +50,29 @@ open_file_path = None
42
  # === Flask apps & logs ===
43
  flask_apps = {}
44
  flask_logs = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  def expand_path(path: str) -> str:
47
  for alias, real in PATH_MAP.items():
@@ -51,10 +82,109 @@ def expand_path(path: str) -> str:
51
  return os.path.join(WORKDIR, path.lstrip("/"))
52
  return os.path.join(current_dir, path)
53
 
54
- def run_flask_app(app_name: str, code: str, fastapi_app: FastAPI):
55
- """Start a Flask app under /flask/<app_name>"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  log_buffer = io.StringIO()
57
  flask_logs[app_name] = log_buffer
 
 
 
58
 
59
  def log(msg):
60
  log_buffer.write(msg + "\n")
@@ -62,44 +192,87 @@ def run_flask_app(app_name: str, code: str, fastapi_app: FastAPI):
62
  print(f"[{app_name}] {msg}")
63
 
64
  try:
 
 
 
 
 
 
 
65
  local_flask = Flask(app_name)
66
 
67
  # Default route
68
  @local_flask.route("/")
69
  def home():
70
- return f"Hello from {app_name}! (mounted at /flask/{app_name}/)"
71
-
72
- # Execute user code (can add more routes, HTML, APIs)
73
- exec(code, {"app": local_flask, "log": log})
74
-
75
- # Mount into FastAPI
76
- fastapi_app.mount(f"/flask/{app_name}", WSGIMiddleware(local_flask))
 
 
 
 
 
 
 
 
77
  flask_apps[app_name] = local_flask
78
- log(f"Started Flask app: {app_name} mounted at /flask/{app_name}/")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
  except Exception as e:
81
  log(f"Error starting Flask app {app_name}: {e}")
82
 
83
  def view_logs(app_name: str):
84
- """Stream logs of a Flask app"""
85
  if app_name not in flask_logs:
86
  return f"No logs for {app_name}"
87
-
88
- def stream():
89
- log_buf = flask_logs[app_name]
90
- last_pos = 0
91
- while True:
92
- log_buf.seek(last_pos)
93
- new_data = log_buf.read()
94
- if new_data:
95
- yield new_data
96
- last_pos = log_buf.tell()
97
-
98
- return StreamingResponse(stream(), media_type="text/plain")
99
-
100
- def run_command(cmd: str, fastapi_app: FastAPI):
 
 
 
 
 
 
 
 
 
 
 
 
101
  """Handle terminal-like commands including Flask"""
102
  global current_dir, running_process, open_file_path
 
103
  try:
104
  raw_cmd = cmd.strip()
105
 
@@ -117,12 +290,23 @@ def run_command(cmd: str, fastapi_app: FastAPI):
117
  if len(parts) < 3:
118
  return f"$ {cmd}\n\nUsage: flaskrun <appname> <python_code>", "", None
119
  appname, code = parts[1], parts[2]
120
- threading.Thread(target=run_flask_app, args=(appname, code, fastapi_app), daemon=True).start()
121
- return f"$ {cmd}\n\nFlask app '{appname}' started at /flask/{appname}/", "", None
 
 
 
 
 
 
 
 
 
 
 
122
 
123
  # === Change directory (cd) ===
124
- if raw_cmd.startswith("cd/") or raw_cmd.startswith("cd "):
125
- new_path = raw_cmd.split(" ", 1)[-1].replace("cd/", "", 1)
126
  target = expand_path(new_path)
127
  if os.path.isdir(target):
128
  current_dir = target
@@ -174,6 +358,19 @@ def run_command(cmd: str, fastapi_app: FastAPI):
174
  except Exception as e:
175
  return f"$ {cmd}\n\nError reading file: {e}", "", None
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  # === Normal shell command ===
178
  running_process = subprocess.Popen(
179
  raw_cmd,
@@ -199,16 +396,19 @@ def save_file(new_content: str, file_path: str):
199
  with open(file_path, "w", encoding="utf-8") as f:
200
  f.write(new_content)
201
 
202
- try:
203
- api.upload_folder(
204
- folder_path=WORKDIR,
205
- repo_id=DATASET_REPO,
206
- repo_type="dataset",
207
- commit_message=f"Edited file: {file_path}",
208
- token=HF_TOKEN
209
- )
210
- except Exception as e:
211
- print("Upload failed:", e)
 
 
 
212
 
213
  return f"Saved file: {file_path}", new_content
214
  except Exception as e:
@@ -216,17 +416,23 @@ def save_file(new_content: str, file_path: str):
216
 
217
  # === Gradio UI ===
218
  with gr.Blocks() as demo:
219
- gr.Markdown("## 🖥️ Hugging Face Terminal + Editor + Flask Apps")
 
220
 
221
  with gr.Row():
222
  cmd = gr.Textbox(
223
  label="Command",
224
  placeholder="Examples:\n"
225
- "flaskrun app1 'app.route(\"/hello\")(lambda: \"Hello App1\")'\n"
226
- "cd/Alex/project\n"
227
- "create /myapp/app.py\n"
228
- "delete /myapp/app.py\n"
229
- "open /myapp/app.py\n"
 
 
 
 
 
230
  "close"
231
  )
232
  run_btn = gr.Button("Run")
@@ -237,13 +443,24 @@ with gr.Blocks() as demo:
237
  hidden_file = gr.Textbox(visible=False)
238
 
239
  # Attach events
240
- run_btn.click(lambda c: run_command(c, app), inputs=cmd, outputs=[out, editor, hidden_file])
241
  save_btn.click(save_file, inputs=[editor, hidden_file], outputs=[out, editor])
242
 
243
- # === Main FastAPI app ===
244
- app = FastAPI()
245
- app = gr.mount_gradio_app(app, demo, path="/")
246
-
247
- @app.get("/flask/{appname}/logs")
248
- def flask_logs_stream(appname: str):
249
- return view_logs(appname)
 
 
 
 
 
 
 
 
 
 
 
 
4
  import shutil
5
  import threading
6
  import io
7
+ import socket
8
+ import random
9
+ import string
10
+ import ast
11
+ import re
12
  from huggingface_hub import HfApi, snapshot_download
 
 
 
 
13
 
14
  # === Config ===
15
  DATASET_REPO = "Devity4756/Terminal"
16
  HF_TOKEN = os.environ.get("HF_TOKEN")
17
+ if HF_TOKEN:
18
+ api = HfApi(token=HF_TOKEN)
19
+ else:
20
+ api = None
21
+ print("Warning: HF_TOKEN not found. Save functionality will be limited.")
22
 
23
  WORKDIR = "workspace"
24
  os.makedirs(WORKDIR, exist_ok=True)
 
30
 
31
  # === Restore state ===
32
  try:
33
+ if HF_TOKEN:
34
+ snapshot_path = snapshot_download(
35
+ repo_id=DATASET_REPO,
36
+ repo_type="dataset",
37
+ token=HF_TOKEN
38
+ )
39
+ shutil.copytree(snapshot_path, WORKDIR, dirs_exist_ok=True)
40
+ else:
41
+ print("Cannot restore state without HF_TOKEN")
42
  except Exception as e:
43
  print("No previous data restored:", e)
44
 
 
50
  # === Flask apps & logs ===
51
  flask_apps = {}
52
  flask_logs = {}
53
+ flask_ports = {}
54
+ next_port = 5000
55
+
56
+ # Get the public URL of the Gradio app
57
+ public_url = os.environ.get("SPACE_URL", "http://localhost:7860")
58
+
59
+ # === Security Configuration ===
60
+ ALLOWED_FLASK_IMPORTS = {
61
+ 'Flask', 'request', 'jsonify', 'render_template',
62
+ 'redirect', 'url_for', 'send_file', 'abort'
63
+ }
64
+
65
+ RESTRICTED_KEYWORDS = {
66
+ 'os.', 'subprocess.', 'sys.', 'importlib.', 'eval(', 'exec(',
67
+ 'open(', 'file(', 'compile(', '__import__', 'globals()', 'locals()',
68
+ 'getattr', 'setattr', 'delattr', 'input(', 'execfile'
69
+ }
70
+
71
+ MAX_CODE_LENGTH = 5000 # Maximum characters for Flask code
72
+
73
+ def generate_random_name(length=12):
74
+ """Generate a random function/class name"""
75
+ return ''.join(random.choice(string.ascii_letters) for _ in range(length))
76
 
77
  def expand_path(path: str) -> str:
78
  for alias, real in PATH_MAP.items():
 
82
  return os.path.join(WORKDIR, path.lstrip("/"))
83
  return os.path.join(current_dir, path)
84
 
85
+ def get_local_ip():
86
+ """Get the local IP address of the machine"""
87
+ try:
88
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
89
+ s.connect(("8.8.8.8", 80))
90
+ ip = s.getsockname()[0]
91
+ s.close()
92
+ return ip
93
+ except:
94
+ return "localhost"
95
+
96
+ def validate_flask_code(code):
97
+ """Validate Flask code for security"""
98
+ # Check length
99
+ if len(code) > MAX_CODE_LENGTH:
100
+ return False, "Code too long (max 5000 characters)"
101
+
102
+ # Check for restricted patterns
103
+ for pattern in RESTRICTED_KEYWORDS:
104
+ if pattern in code:
105
+ return False, f"Restricted pattern found: {pattern}"
106
+
107
+ # Parse AST to check for dangerous constructs
108
+ try:
109
+ tree = ast.parse(code)
110
+
111
+ for node in ast.walk(tree):
112
+ # Check for imports
113
+ if isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom):
114
+ for alias in (node.names if isinstance(node, ast.Import) else [node]):
115
+ module_name = alias.name if isinstance(node, ast.Import) else node.module
116
+ if module_name and not any(module_name.startswith(allowed)
117
+ for allowed in ['flask', 'werkzeug']):
118
+ return False, f"Restricted import: {module_name}"
119
+
120
+ # Check for function definitions with dangerous names
121
+ if isinstance(node, ast.FunctionDef):
122
+ if node.name.startswith('_') or node.name in ['exit', 'quit', 'help']:
123
+ return False, f"Restricted function name: {node.name}"
124
+
125
+ except SyntaxError as e:
126
+ return False, f"Syntax error: {e}"
127
+
128
+ return True, "Code validated successfully"
129
+
130
+ def secure_execute_flask_code(app_instance, code, log_func, request_obj):
131
+ """Safely execute Flask code with randomized function names"""
132
+ # Generate random names for functions to avoid conflicts
133
+ random_prefix = generate_random_name(8)
134
+
135
+ # Replace function definitions with randomized names
136
+ # This pattern matches function definitions and adds a random prefix
137
+ code = re.sub(
138
+ r'def (\w+)\s*\(',
139
+ lambda m: f'def {random_prefix}_{m.group(1)}(',
140
+ code
141
+ )
142
+
143
+ # Create a secure environment
144
+ secure_globals = {
145
+ 'app': app_instance,
146
+ 'log': log_func,
147
+ 'request': request_obj,
148
+ '__builtins__': {
149
+ 'str': str, 'int': int, 'float': float, 'bool': bool, 'list': list,
150
+ 'dict': dict, 'tuple': tuple, 'set': set, 'len': len, 'range': range,
151
+ 'print': print, 'isinstance': isinstance, 'type': type, 'repr': repr
152
+ }
153
+ }
154
+
155
+ # Add allowed Flask imports
156
+ try:
157
+ from flask import Flask, request, jsonify, render_template, redirect, url_for, send_file, abort
158
+ secure_globals.update({
159
+ 'Flask': Flask,
160
+ 'request': request,
161
+ 'jsonify': jsonify,
162
+ 'render_template': render_template,
163
+ 'redirect': redirect,
164
+ 'url_for': url_for,
165
+ 'send_file': send_file,
166
+ 'abort': abort
167
+ })
168
+ except ImportError:
169
+ pass
170
+
171
+ try:
172
+ # Compile and execute in restricted environment
173
+ compiled_code = compile(code, '<string>', 'exec')
174
+ exec(compiled_code, secure_globals)
175
+ return True, "Code executed successfully"
176
+ except Exception as e:
177
+ return False, f"Execution error: {e}"
178
+
179
+ def run_flask_app(app_name: str, code: str):
180
+ """Start a Flask app on a dedicated port"""
181
+ global next_port
182
+
183
  log_buffer = io.StringIO()
184
  flask_logs[app_name] = log_buffer
185
+ port = next_port
186
+ next_port += 1
187
+ flask_ports[app_name] = port
188
 
189
  def log(msg):
190
  log_buffer.write(msg + "\n")
 
192
  print(f"[{app_name}] {msg}")
193
 
194
  try:
195
+ # Validate code first
196
+ is_valid, validation_msg = validate_flask_code(code)
197
+ if not is_valid:
198
+ log(f"Code validation failed: {validation_msg}")
199
+ return
200
+
201
+ from flask import Flask, request
202
  local_flask = Flask(app_name)
203
 
204
  # Default route
205
  @local_flask.route("/")
206
  def home():
207
+ return f"Hello from {app_name}!<br><br>App is running successfully."
208
+
209
+ # Secure execution of user code
210
+ success, exec_msg = secure_execute_flask_code(local_flask, code, log, request)
211
+ if not success:
212
+ log(f"Failed to execute code: {exec_msg}")
213
+ return
214
+
215
+ # Start the Flask app in a separate thread
216
+ def run_app():
217
+ local_flask.run(host='0.0.0.0', port=port, debug=True, use_reloader=False)
218
+
219
+ thread = threading.Thread(target=run_app, daemon=True)
220
+ thread.start()
221
+
222
  flask_apps[app_name] = local_flask
223
+
224
+ # Get the correct URLs
225
+ local_ip = get_local_ip()
226
+ local_url = f"http://{local_ip}:{port}"
227
+
228
+ # For Hugging Face Spaces
229
+ if "hf.space" in public_url:
230
+ space_id = public_url.split("https://")[1].split(".hf.space")[0]
231
+ live_url = f"https://{space_id}.hf.space/proxy/{port}/"
232
+ else:
233
+ live_url = f"{public_url}/proxy/{port}/"
234
+
235
+ log(f"Started Flask app: {app_name} on port {port}")
236
+ log(f"Local URL: {local_url}")
237
+ log(f"Live URL: {live_url}")
238
+ log("App is now live and accessible!")
239
 
240
  except Exception as e:
241
  log(f"Error starting Flask app {app_name}: {e}")
242
 
243
  def view_logs(app_name: str):
244
+ """Get logs of a Flask app"""
245
  if app_name not in flask_logs:
246
  return f"No logs for {app_name}"
247
+
248
+ log_buf = flask_logs[app_name]
249
+ log_buf.seek(0)
250
+ return log_buf.read()
251
+
252
+ def get_flask_apps():
253
+ """Get list of running Flask apps with their URLs"""
254
+ if not flask_ports:
255
+ return "No Flask apps running"
256
+
257
+ local_ip = get_local_ip()
258
+ result = "Running Flask apps:\n\n"
259
+
260
+ for app_name, port in flask_ports.items():
261
+ local_url = f"http://{local_ip}:{port}"
262
+
263
+ if "hf.space" in public_url:
264
+ space_id = public_url.split("https://")[1].split(".hf.space")[0]
265
+ live_url = f"https://{space_id}.hf.space/proxy/{port}/"
266
+ result += f"- {app_name}:\n Local: {local_url}\n Live: {live_url}\n\n"
267
+ else:
268
+ result += f"- {app_name}:\n Local: {local_url}\n Live: {public_url}/proxy/{port}/\n\n"
269
+
270
+ return result
271
+
272
+ def run_command(cmd: str):
273
  """Handle terminal-like commands including Flask"""
274
  global current_dir, running_process, open_file_path
275
+
276
  try:
277
  raw_cmd = cmd.strip()
278
 
 
290
  if len(parts) < 3:
291
  return f"$ {cmd}\n\nUsage: flaskrun <appname> <python_code>", "", None
292
  appname, code = parts[1], parts[2]
293
+ threading.Thread(target=run_flask_app, args=(appname, code), daemon=True).start()
294
+ return f"$ {cmd}\n\nStarting Flask app '{appname}'... Check status with 'flasklist' or view logs with 'logs {appname}'", "", None
295
+
296
+ # === List Flask apps ===
297
+ if raw_cmd == "flasklist":
298
+ apps_info = get_flask_apps()
299
+ return f"$ {cmd}\n\n{apps_info}", "", None
300
+
301
+ # === View logs ===
302
+ if raw_cmd.startswith("logs "):
303
+ appname = raw_cmd.split(" ", 1)[1]
304
+ logs = view_logs(appname)
305
+ return f"$ {cmd}\n\nLogs for {appname}:\n{logs}", "", None
306
 
307
  # === Change directory (cd) ===
308
+ if raw_cmd.startswith("cd "):
309
+ new_path = raw_cmd.split(" ", 1)[1]
310
  target = expand_path(new_path)
311
  if os.path.isdir(target):
312
  current_dir = target
 
358
  except Exception as e:
359
  return f"$ {cmd}\n\nError reading file: {e}", "", None
360
 
361
+ # === List files ===
362
+ elif raw_cmd == "ls":
363
+ items = os.listdir(current_dir)
364
+ return f"$ {cmd}\n\n" + ("\n".join(items) if items else "(empty directory)"), "", None
365
+
366
+ # === Print working directory ===
367
+ elif raw_cmd == "pwd":
368
+ return f"$ {cmd}\n\n{current_dir}", "", None
369
+
370
+ # === Clear screen ===
371
+ elif raw_cmd == "clear":
372
+ return "", "", None
373
+
374
  # === Normal shell command ===
375
  running_process = subprocess.Popen(
376
  raw_cmd,
 
396
  with open(file_path, "w", encoding="utf-8") as f:
397
  f.write(new_content)
398
 
399
+ if HF_TOKEN and api:
400
+ try:
401
+ api.upload_folder(
402
+ folder_path=WORKDIR,
403
+ repo_id=DATASET_REPO,
404
+ repo_type="dataset",
405
+ commit_message=f"Edited file: {file_path}",
406
+ token=HF_TOKEN
407
+ )
408
+ except Exception as e:
409
+ print("Upload failed:", e)
410
+ else:
411
+ print("Save local only - no HF token configured")
412
 
413
  return f"Saved file: {file_path}", new_content
414
  except Exception as e:
 
416
 
417
  # === Gradio UI ===
418
  with gr.Blocks() as demo:
419
+ gr.Markdown("## 🖥️ Secure Terminal + Editor + Flask Apps")
420
+ gr.Markdown("Create Flask apps with `flaskrun <name> <code>` - they will be accessible at the URLs shown in the logs")
421
 
422
  with gr.Row():
423
  cmd = gr.Textbox(
424
  label="Command",
425
  placeholder="Examples:\n"
426
+ "flaskrun myapp '@app.route(\"/hello\")\\ndef hello(): return \"Hello World!\"'\n"
427
+ "flaskrun api '@app.route(\"/data\")\\ndef data(): return {\"message\": \"API working!\"}'\n"
428
+ "flasklist\n"
429
+ "logs myapp\n"
430
+ "cd Alex/project\n"
431
+ "create app.py\n"
432
+ "open app.py\n"
433
+ "ls\n"
434
+ "pwd\n"
435
+ "clear\n"
436
  "close"
437
  )
438
  run_btn = gr.Button("Run")
 
443
  hidden_file = gr.Textbox(visible=False)
444
 
445
  # Attach events
446
+ run_btn.click(run_command, inputs=cmd, outputs=[out, editor, hidden_file])
447
  save_btn.click(save_file, inputs=[editor, hidden_file], outputs=[out, editor])
448
 
449
+ # For Hugging Face Spaces, we need to set up the proxy routes
450
+ if "hf.space" in public_url:
451
+ # This will be handled by Hugging Face's built-in proxy
452
+ pass
453
+ else:
454
+ # For local development, we can set up a simple proxy
455
+ @demo.app.get("/proxy/{port}/{path:path}")
456
+ async def proxy_to_flask(port: int, path: str):
457
+ import httpx
458
+ try:
459
+ async with httpx.AsyncClient() as client:
460
+ response = await client.get(f"http://localhost:{port}/{path}")
461
+ return response.content
462
+ except:
463
+ return "Flask app not available"
464
+
465
+ if __name__ == "__main__":
466
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=True)