From 14765d7d1856d564747c55c5412e2f38feab079e Mon Sep 17 00:00:00 2001 From: Stalin Date: Sun, 19 Apr 2026 23:03:08 +0530 Subject: [PATCH] fix(security): harden execution sandbox and add dedicated admin execution logs --- backend/routes/admin.py | 94 +++ backend/routes/coding.py | 155 +++-- backend/routes/compiler.py | 740 ++++++---------------- backend/services/real_compiler_service.py | 651 ++++++++++++------- frontend/.npmignore | 10 + frontend/.npmrc | 2 + frontend/README.md | 31 + frontend/app/admin/logs/page.tsx | 280 +++++++- frontend/next-env.d.ts | 2 +- frontend/package.json | 29 +- nohup.out | 0 start-local-secure.sh | 136 ++++ 12 files changed, 1296 insertions(+), 834 deletions(-) create mode 100644 frontend/.npmignore create mode 100644 frontend/.npmrc create mode 100644 frontend/README.md create mode 100644 nohup.out create mode 100755 start-local-secure.sh diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 5695027..5025ad3 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -371,6 +371,100 @@ def get_admin_logs(): return jsonify({"error": str(e)}), 500 +@bp.route("/logs/executions", methods=["GET"]) +@admin_required +def get_execution_logs(): + """Query compiler/coding execution events as a dedicated log stream.""" + try: + language = request.args.get("language", "").strip().lower() + status = request.args.get("status", "").strip().lower() + search = request.args.get("search", "").strip() + from_ts = request.args.get("from", "").strip() + to_ts = request.args.get("to", "").strip() + limit = min(max(int(request.args.get("limit", 100)), 1), 500) + page = max(int(request.args.get("page", 1)), 1) + + query = {} + if language: + query["language"] = language + if status: + query["status"] = status + + ts_filter = {} + if from_ts: + try: + ts_filter["$gte"] = datetime.fromisoformat(from_ts) + except Exception: + pass + if to_ts: + try: + ts_filter["$lte"] = datetime.fromisoformat(to_ts) + except Exception: + pass + if ts_filter: + query["timestamp"] = ts_filter + + if search: + safe = re.escape(search) + query["$or"] = [ + {"execution_id": {"$regex": safe, "$options": "i"}}, + {"language": {"$regex": safe, "$options": "i"}}, + {"source": {"$regex": safe, "$options": "i"}}, + {"ip": {"$regex": safe, "$options": "i"}}, + {"status": {"$regex": safe, "$options": "i"}}, + {"error": {"$regex": safe, "$options": "i"}}, + ] + + skip = (page - 1) * limit + total = db.code_execution_events.count_documents(query) + docs = list(db.code_execution_events.find(query).sort("timestamp", -1).skip(skip).limit(limit)) + + logs = [] + for doc in docs: + item = _json_safe(doc) + item["id"] = str(item.get("_id")) + item.pop("_id", None) + + if not item.get("source"): + item["source"] = "compiler" + + if not item.get("request_body"): + item["request_body"] = { + "language": item.get("language", "unknown"), + "code": "Legacy log entry (request code body was not captured at execution time)", + "code_size": item.get("code_size", 0), + } + + if not item.get("response_body"): + item["response_body"] = { + "success": item.get("status") == "success", + "blocked": bool(item.get("blocked")), + "execution_id": item.get("execution_id"), + "error": item.get("error", ""), + "security_violations": item.get("security_violations", []), + "execution_time": item.get("execution_time", 0), + "memory_used": item.get("memory_used", 0), + "exit_code": item.get("exit_code", 0), + "note": "Legacy log entry (response payload was not captured at execution time)", + } + + logs.append(item) + + return jsonify({ + "success": True, + "logs": logs, + "pagination": { + "page": page, + "limit": limit, + "total": total, + "pages": (total + limit - 1) // limit, + }, + }) + except Exception as e: + print(f"Error getting execution logs: {str(e)}") + return jsonify({"error": str(e)}), 500 + + @bp.route("/users", methods=["GET"]) @admin_required def get_admin_users(): diff --git a/backend/routes/coding.py b/backend/routes/coding.py index faa780e..c057364 100644 --- a/backend/routes/coding.py +++ b/backend/routes/coding.py @@ -10,6 +10,7 @@ import docker import psutil from pymongo import MongoClient from activity_logger import log_user_activity, resolve_user_identity +from services.real_compiler_service import real_compiler_service bp = Blueprint('coding', __name__) @@ -86,10 +87,81 @@ def execute_code(): # Log coding attempt log_coding_attempt(session['coding_session_id'], code, language) - # Execute code in secure container - result = execute_in_container(code, language, test_cases) - - return jsonify(result) + # Execute code in hardened Docker sandbox + result = real_compiler_service.execute_code(code=code, language=language, input_data="") + + event_type = "coding_execution_success" if result.get("success") else "coding_execution_blocked" + severity = "info" if result.get("success") else "warning" + + execution_status = "success" if result.get("success") else "failed" + if result.get("blocked"): + execution_status = "blocked" + + db.security_logs.insert_one({ + "timestamp": datetime.utcnow(), + "event_type": event_type, + "action": "secure_coding_execute", + "status_code": 200 if result.get("success") else 400, + "severity": severity, + "path": request.path, + "method": request.method, + "ip": request.remote_addr or "unknown", + "user_agent": request.headers.get("User-Agent", ""), + "metadata": { + "language": language, + "execution_id": result.get("execution_id"), + "blocked": bool(result.get("blocked")), + "security_violations": result.get("security_violations", []), + "execution_time": result.get("execution_time", 0), + "memory_used": result.get("memory_used", 0), + "exit_code": result.get("exit_code", -1), + }, + "metadata_text": str(result.get("security_violations", [])), + }) + + try: + request_payload = { + "language": language, + "code": (code or "")[:4000], + "code_size": len(code or ""), + "test_case_count": len(test_cases) if isinstance(test_cases, list) else 0, + } + response_payload = { + "success": bool(result.get("success")), + "blocked": bool(result.get("blocked")), + "execution_id": result.get("execution_id"), + "output": (result.get("output") or "")[:4000], + "error": result.get("error", ""), + "security_violations": result.get("security_violations", []), + "execution_time": result.get("execution_time", 0), + "memory_used": result.get("memory_used", 0), + "exit_code": result.get("exit_code", -1), + } + + db.code_execution_events.insert_one({ + "timestamp": datetime.utcnow(), + "event_type": "execution", + "source": "coding", + "language": language, + "execution_id": result.get("execution_id"), + "execution_time": result.get("execution_time", 0), + "memory_used": result.get("memory_used", 0), + "exit_code": result.get("exit_code", -1), + "status": execution_status, + "blocked": bool(result.get("blocked")), + "security_violations": result.get("security_violations", []), + "error": result.get("error", ""), + "request_body": request_payload, + "response_body": response_payload, + "ip": request.remote_addr or "unknown", + "user_agent": request.headers.get("User-Agent", ""), + }) + except Exception: + pass + + if result.get("success"): + return jsonify({"success": True, **result}) + return jsonify({"success": False, **result}), 400 except Exception as e: return jsonify({"error": str(e)}), 500 @@ -156,66 +228,23 @@ def submit_coding_test(): return jsonify({"error": str(e)}), 500 def execute_in_container(code, language, test_cases): - """Execute code in secure Docker container""" - try: - client = docker.from_env() - - # Language-specific container configuration - containers = { - 'python': 'python:3.9-alpine', - 'java': 'openjdk:11-alpine', - 'javascript': 'node:16-alpine' + """Backward-compatible wrapper around the hardened compiler service.""" + result = real_compiler_service.execute_code(code=code, language=language, input_data="") + if result.get("success"): + return { + "success": True, + "output": result.get("output", ""), + "test_results": [], + "execution_time": result.get("execution_time", 0), + "memory_used": result.get("memory_used", 0), + "execution_id": result.get("execution_id"), } - - if language not in containers: - return {"error": "Unsupported language"} - - # Create temporary file - with tempfile.NamedTemporaryFile(mode='w', suffix=f'.{get_file_extension(language)}', delete=False) as f: - f.write(code) - temp_file = f.name - - try: - # Run container with security restrictions - container = client.containers.run( - containers[language], - command=get_run_command(language, temp_file), - volumes={os.path.dirname(temp_file): {'bind': '/app', 'mode': 'ro'}}, - working_dir='/app', - mem_limit='128m', - cpu_period=100000, - cpu_quota=50000, # 50% CPU limit - network_mode='none', # No network access - remove=True, - timeout=10, # 10 second timeout - detach=False - ) - - output = container.decode('utf-8') - - # Run test cases if provided - test_results = [] - if test_cases: - for test in test_cases: - test_result = run_test_case(code, language, test) - test_results.append(test_result) - - return { - "success": True, - "output": output, - "test_results": test_results, - "execution_time": "< 10s" - } - - finally: - os.unlink(temp_file) - - except docker.errors.ContainerError as e: - return {"error": f"Runtime error: {e}"} - except docker.errors.ImageNotFound: - return {"error": "Language runtime not available"} - except Exception as e: - return {"error": f"Execution failed: {str(e)}"} + return { + "success": False, + "error": result.get("error", "Execution failed"), + "security_violations": result.get("security_violations", []), + "execution_id": result.get("execution_id"), + } def get_file_extension(language): extensions = { diff --git a/backend/routes/compiler.py b/backend/routes/compiler.py index 02d436c..1c6da2c 100644 --- a/backend/routes/compiler.py +++ b/backend/routes/compiler.py @@ -1,546 +1,226 @@ -from flask import Blueprint, request, jsonify -import subprocess -import tempfile -import os -import time -import docker from datetime import datetime +import os -bp = Blueprint('compiler', __name__) +from flask import Blueprint, jsonify, request +from pymongo import MongoClient -def get_db(): - """Get MongoDB database connection""" - from pymongo import MongoClient - from flask import current_app - client = MongoClient(current_app.config['MONGODB_URI']) - return client.openlearnx +from services.real_compiler_service import real_compiler_service -@bp.route('/execute', methods=['POST', 'OPTIONS']) +bp = Blueprint("compiler", __name__) + +mongo_uri = os.getenv("MONGODB_URI", "mongodb://localhost:27017/") +client = MongoClient(mongo_uri) +db = client.openlearnx + + +def _json_response(payload, status=200): + response = jsonify(payload) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "GET,POST,OPTIONS") + return response, status + + +def _client_ip(): + forwarded_for = request.headers.get("X-Forwarded-For", "") + if forwarded_for: + return forwarded_for.split(",")[0].strip() + return request.remote_addr or "unknown" + + +def _log_security(event_type, action, severity="info", status_code=200, metadata=None): + try: + log_doc = { + "timestamp": datetime.utcnow(), + "event_type": event_type, + "action": action, + "status_code": int(status_code), + "severity": severity, + "path": request.path, + "method": request.method, + "ip": _client_ip(), + "user_agent": request.headers.get("User-Agent", ""), + "metadata": metadata or {}, + "metadata_text": str(metadata or {}), + } + db.security_logs.insert_one(log_doc) + except Exception as e: + print(f"Compiler security log failure: {e}") + + +@bp.route("/execute", methods=["POST", "OPTIONS"]) def execute_code(): - """Execute code in specified language with Docker support""" if request.method == "OPTIONS": - response = jsonify({'status': 'ok'}) - response.headers.add("Access-Control-Allow-Origin", "*") - response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") - response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") - return response - - try: - data = request.get_json() - language = data.get('language', 'python').lower() - code = data.get('code', '').strip() - input_data = data.get('input', '') - - print(f"🔧 Executing {language} code") - print(f"📝 Code length: {len(code)} characters") - - if not code: - return jsonify({"success": False, "error": "No code provided"}), 400 - - # Execute based on language - if language == 'python': - return execute_python(code, input_data) - elif language == 'java': - return execute_java(code, input_data) - elif language == 'javascript' or language == 'js': - return execute_javascript(code, input_data) - elif language == 'cpp' or language == 'c++': - return execute_cpp(code, input_data) - elif language == 'c': - return execute_c(code, input_data) - else: - return jsonify({ - "success": False, - "error": f"Language '{language}' not supported. Available: python, java, javascript, cpp, c" - }), 400 - - except Exception as e: - print(f"❌ Compiler error: {str(e)}") - import traceback - traceback.print_exc() - return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500 + return _json_response({"status": "ok"}, 200) -def execute_python(code, input_data=""): - """Execute Python code""" try: - # Create temporary file - with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: - f.write(code) - temp_file = f.name - + data = request.get_json(silent=True) or {} + language = str(data.get("language", "python")).strip().lower() + code = str(data.get("code", "")) + input_data = str(data.get("input", "")) + + if not code.strip(): + _log_security( + "compiler_input_invalid", + "empty_code_submission", + severity="warning", + status_code=400, + metadata={"language": language}, + ) + return _json_response({"success": False, "error": "No code provided"}, 400) + + result = real_compiler_service.execute_code(code=code, language=language, input_data=input_data) + + log_metadata = { + "language": language, + "code_size": len(code), + "execution_id": result.get("execution_id"), + "exit_code": result.get("exit_code"), + "execution_time": result.get("execution_time", 0), + "memory_used": result.get("memory_used", 0), + "blocked": bool(result.get("blocked")), + "security_violations": result.get("security_violations", []), + "error": result.get("error", ""), + } + try: - # Execute with subprocess - start_time = time.time() - result = subprocess.run( - ['python3', temp_file], - input=input_data, - text=True, - capture_output=True, - timeout=10, # 10 second timeout - cwd=tempfile.gettempdir() - ) - execution_time = time.time() - start_time - - if result.returncode == 0: - return jsonify({ - "success": True, - "output": result.stdout or "Code executed successfully (no output)", - "error": result.stderr if result.stderr else None, - "language": "python", - "execution_time": round(execution_time, 3) - }) - else: - return jsonify({ - "success": False, - "error": result.stderr or f"Process exited with code {result.returncode}", - "language": "python" - }) - - finally: - # Clean up temp file - try: - os.unlink(temp_file) - except: - pass - - except subprocess.TimeoutExpired: - return jsonify({ - "success": False, - "error": "Code execution timed out (10s limit)" - }), 400 - except FileNotFoundError: - return jsonify({ - "success": False, - "error": "Python interpreter not found. Please install Python 3." - }), 500 - except Exception as e: - return jsonify({ - "success": False, - "error": f"Python execution error: {str(e)}" - }), 500 + status = "success" + if result.get("blocked"): + status = "blocked" + elif result.get("error") and not result.get("success", False): + status = "failed" -def execute_java(code, input_data=""): - """Execute Java code""" - try: - # Extract class name from code - import re - class_match = re.search(r'public\s+class\s+(\w+)', code) - if not class_match: - return jsonify({ - "success": False, - "error": "No public class found. Java code must contain 'public class ClassName'" - }), 400 - - class_name = class_match.group(1) - - # Create temporary directory - temp_dir = tempfile.mkdtemp() - java_file = os.path.join(temp_dir, f"{class_name}.java") - - try: - # Write Java code to file - with open(java_file, 'w') as f: - f.write(code) - - # Compile Java code - compile_result = subprocess.run( - ['javac', java_file], - capture_output=True, - text=True, - timeout=30, - cwd=temp_dir - ) - - if compile_result.returncode != 0: - return jsonify({ - "success": False, - "error": f"Compilation error:\n{compile_result.stderr}", - "language": "java" - }) - - # Execute Java code - start_time = time.time() - result = subprocess.run( - ['java', class_name], - input=input_data, - text=True, - capture_output=True, - timeout=10, - cwd=temp_dir - ) - execution_time = time.time() - start_time - - if result.returncode == 0: - return jsonify({ - "success": True, - "output": result.stdout or "Code executed successfully (no output)", - "error": result.stderr if result.stderr else None, - "language": "java", - "execution_time": round(execution_time, 3) - }) - else: - return jsonify({ - "success": False, - "error": result.stderr or f"Runtime error (exit code {result.returncode})", - "language": "java" - }) - - finally: - # Clean up temp files - import shutil - try: - shutil.rmtree(temp_dir) - except: - pass - - except subprocess.TimeoutExpired: - return jsonify({ - "success": False, - "error": "Code execution timed out" - }), 400 - except FileNotFoundError: - return jsonify({ - "success": False, - "error": "Java compiler/runtime not found. Please install JDK." - }), 500 - except Exception as e: - return jsonify({ - "success": False, - "error": f"Java execution error: {str(e)}" - }), 500 - -def execute_javascript(code, input_data=""): - """Execute JavaScript code""" - try: - # Create temporary file - with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f: - # Add input handling if needed - if input_data: - js_code = f""" -const input = `{input_data}`; -const readline = {{ question: () => input }}; -{code} -""" - else: - js_code = code - - f.write(js_code) - temp_file = f.name - - try: - # Execute with Node.js - start_time = time.time() - result = subprocess.run( - ['node', temp_file], - input=input_data, - text=True, - capture_output=True, - timeout=10, - cwd=tempfile.gettempdir() - ) - execution_time = time.time() - start_time - - if result.returncode == 0: - return jsonify({ - "success": True, - "output": result.stdout or "Code executed successfully (no output)", - "error": result.stderr if result.stderr else None, - "language": "javascript", - "execution_time": round(execution_time, 3) - }) - else: - return jsonify({ - "success": False, - "error": result.stderr or f"Runtime error (exit code {result.returncode})", - "language": "javascript" - }) - - finally: - try: - os.unlink(temp_file) - except: - pass - - except subprocess.TimeoutExpired: - return jsonify({ - "success": False, - "error": "Code execution timed out" - }), 400 - except FileNotFoundError: - return jsonify({ - "success": False, - "error": "Node.js not found. Please install Node.js." - }), 500 - except Exception as e: - return jsonify({ - "success": False, - "error": f"JavaScript execution error: {str(e)}" - }), 500 - -def execute_cpp(code, input_data=""): - """Execute C++ code""" - try: - # Create temporary files - temp_dir = tempfile.mkdtemp() - cpp_file = os.path.join(temp_dir, "main.cpp") - exe_file = os.path.join(temp_dir, "main.exe") if os.name == 'nt' else os.path.join(temp_dir, "main") - - try: - # Write C++ code to file - with open(cpp_file, 'w') as f: - f.write(code) - - # Compile C++ code - compile_cmd = ['g++', '-o', exe_file, cpp_file, '-std=c++17'] - compile_result = subprocess.run( - compile_cmd, - capture_output=True, - text=True, - timeout=30, - cwd=temp_dir - ) - - if compile_result.returncode != 0: - return jsonify({ - "success": False, - "error": f"Compilation error:\n{compile_result.stderr}", - "language": "cpp" - }) - - # Execute compiled program - start_time = time.time() - result = subprocess.run( - [exe_file], - input=input_data, - text=True, - capture_output=True, - timeout=10, - cwd=temp_dir - ) - execution_time = time.time() - start_time - - if result.returncode == 0: - return jsonify({ - "success": True, - "output": result.stdout or "Code executed successfully (no output)", - "error": result.stderr if result.stderr else None, - "language": "cpp", - "execution_time": round(execution_time, 3) - }) - else: - return jsonify({ - "success": False, - "error": result.stderr or f"Runtime error (exit code {result.returncode})", - "language": "cpp" - }) - - finally: - # Clean up temp files - import shutil - try: - shutil.rmtree(temp_dir) - except: - pass - - except subprocess.TimeoutExpired: - return jsonify({ - "success": False, - "error": "Code execution timed out" - }), 400 - except FileNotFoundError: - return jsonify({ - "success": False, - "error": "G++ compiler not found. Please install GCC/G++." - }), 500 - except Exception as e: - return jsonify({ - "success": False, - "error": f"C++ execution error: {str(e)}" - }), 500 - -def execute_c(code, input_data=""): - """Execute C code""" - try: - # Create temporary files - temp_dir = tempfile.mkdtemp() - c_file = os.path.join(temp_dir, "main.c") - exe_file = os.path.join(temp_dir, "main.exe") if os.name == 'nt' else os.path.join(temp_dir, "main") - - try: - # Write C code to file - with open(c_file, 'w') as f: - f.write(code) - - # Compile C code - compile_cmd = ['gcc', '-o', exe_file, c_file, '-std=c99'] - compile_result = subprocess.run( - compile_cmd, - capture_output=True, - text=True, - timeout=30, - cwd=temp_dir - ) - - if compile_result.returncode != 0: - return jsonify({ - "success": False, - "error": f"Compilation error:\n{compile_result.stderr}", - "language": "c" - }) - - # Execute compiled program - start_time = time.time() - result = subprocess.run( - [exe_file], - input=input_data, - text=True, - capture_output=True, - timeout=10, - cwd=temp_dir - ) - execution_time = time.time() - start_time - - if result.returncode == 0: - return jsonify({ - "success": True, - "output": result.stdout or "Code executed successfully (no output)", - "error": result.stderr if result.stderr else None, - "language": "c", - "execution_time": round(execution_time, 3) - }) - else: - return jsonify({ - "success": False, - "error": result.stderr or f"Runtime error (exit code {result.returncode})", - "language": "c" - }) - - finally: - # Clean up temp files - import shutil - try: - shutil.rmtree(temp_dir) - except: - pass - - except subprocess.TimeoutExpired: - return jsonify({ - "success": False, - "error": "Code execution timed out" - }), 400 - except FileNotFoundError: - return jsonify({ - "success": False, - "error": "GCC compiler not found. Please install GCC." - }), 500 - except Exception as e: - return jsonify({ - "success": False, - "error": f"C execution error: {str(e)}" - }), 500 - -@bp.route('/languages', methods=['GET', 'OPTIONS']) -def get_supported_languages(): - """Get list of supported programming languages""" - if request.method == "OPTIONS": - response = jsonify({'status': 'ok'}) - response.headers.add("Access-Control-Allow-Origin", "*") - response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") - response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS") - return response - - try: - languages = { - "python": { - "name": "Python", - "version": "3.x", - "extension": ".py", - "available": check_language_availability("python3") - }, - "java": { - "name": "Java", - "version": "JDK 8+", - "extension": ".java", - "available": check_language_availability("javac") - }, - "javascript": { - "name": "JavaScript", - "version": "Node.js", - "extension": ".js", - "available": check_language_availability("node") - }, - "cpp": { - "name": "C++", - "version": "GCC/G++", - "extension": ".cpp", - "available": check_language_availability("g++") - }, - "c": { - "name": "C", - "version": "GCC", - "extension": ".c", - "available": check_language_availability("gcc") + request_payload = { + "language": language, + "code": code[:4000], + "code_size": len(code), + "input": input_data[:2000], + "input_size": len(input_data), } - } - - return jsonify({ - "success": True, - "languages": languages, - "total": len(languages), - "available_count": sum(1 for lang in languages.values() if lang["available"]) - }) - + response_payload = { + "success": bool(result.get("success")), + "blocked": bool(result.get("blocked")), + "execution_id": result.get("execution_id"), + "output": (result.get("output") or "")[:4000], + "error": result.get("error", ""), + "security_violations": result.get("security_violations", []), + "execution_time": result.get("execution_time", 0), + "memory_used": result.get("memory_used", 0), + "exit_code": result.get("exit_code", 0), + } + + db.code_execution_events.insert_one( + { + "timestamp": datetime.utcnow(), + "event_type": "execution", + "source": "compiler", + "language": language, + "execution_id": result.get("execution_id"), + "execution_time": result.get("execution_time", 0), + "memory_used": result.get("memory_used", 0), + "exit_code": result.get("exit_code", 0), + "status": status, + "blocked": bool(result.get("blocked")), + "security_violations": result.get("security_violations", []), + "error": result.get("error", ""), + "request_body": request_payload, + "response_body": response_payload, + "ip": _client_ip(), + "user_agent": request.headers.get("User-Agent", ""), + } + ) + except Exception: + pass + + if result.get("blocked"): + _log_security( + "compiler_security_block", + "static_policy_blocked_submission", + severity="warning", + status_code=400, + metadata=log_metadata, + ) + return _json_response({"success": False, **result}, 400) + + if result.get("error") and not result.get("success", False): + _log_security( + "compiler_execution_failed", + "secure_container_execution_failed", + severity="warning", + status_code=400, + metadata=log_metadata, + ) + return _json_response({"success": False, **result}, 400) + + _log_security( + "compiler_execution_success", + "secure_container_execution_completed", + severity="info", + status_code=200, + metadata=log_metadata, + ) + + return _json_response(result, 200) + except Exception as e: - return jsonify({"success": False, "error": str(e)}), 500 + _log_security( + "compiler_internal_error", + "compiler_route_exception", + severity="error", + status_code=500, + metadata={"error": str(e)}, + ) + return _json_response({"success": False, "error": f"Server error: {str(e)}"}, 500) + + +@bp.route("/languages", methods=["GET", "OPTIONS"]) +def get_supported_languages(): + if request.method == "OPTIONS": + return _json_response({"status": "ok"}, 200) -def check_language_availability(command): - """Check if a language compiler/interpreter is available""" try: - result = subprocess.run([command, '--version'], - capture_output=True, - timeout=5) - return result.returncode == 0 - except (FileNotFoundError, subprocess.TimeoutExpired): - return False + return _json_response({"success": True, "languages": real_compiler_service.get_supported_languages()}, 200) + except Exception as e: + return _json_response({"success": False, "error": str(e)}, 500) -@bp.route('/health', methods=['GET']) + +@bp.route("/test", methods=["POST", "OPTIONS"]) +def compiler_test(): + if request.method == "OPTIONS": + return _json_response({"status": "ok"}, 200) + + data = request.get_json(silent=True) or {} + language = str(data.get("language", "python")).strip().lower() + + smoke_code = { + "python": 'print("ok")', + "javascript": 'console.log("ok")', + "java": "public class Main { public static void main(String[] args){ System.out.println(\"ok\"); } }", + "c": "#include \nint main(){ printf(\"ok\\n\"); return 0; }", + "cpp": "#include \nint main(){ std::cout << \"ok\\n\"; return 0; }", + "go": "package main\nimport \"fmt\"\nfunc main(){ fmt.Println(\"ok\") }", + "rust": "fn main(){ println!(\"ok\"); }", + } + + if language not in smoke_code: + return _json_response({"success": False, "error": f"Unsupported language: {language}"}, 400) + + result = real_compiler_service.execute_code(smoke_code[language], language, "") + if result.get("success"): + return _json_response(result, 200) + return _json_response({"success": False, **result}, 400) + + +@bp.route("/health", methods=["GET"]) def compiler_health(): - """Health check for compiler service""" - try: - languages_status = { - "python": check_language_availability("python3"), - "java": check_language_availability("javac"), - "javascript": check_language_availability("node"), - "cpp": check_language_availability("g++"), - "c": check_language_availability("gcc") - } - - available_languages = sum(languages_status.values()) - total_languages = len(languages_status) - - status = "healthy" if available_languages > 0 else "unavailable" - - return jsonify({ - "status": status, - "timestamp": datetime.now().isoformat(), - "languages": languages_status, - "available_languages": available_languages, - "total_languages": total_languages, - "docker_available": check_docker_availability() - }) - - except Exception as e: - return jsonify({ - "status": "error", - "error": str(e), - "timestamp": datetime.now().isoformat() - }), 500 - -def check_docker_availability(): - """Check if Docker is available for containerized execution""" - try: - client = docker.from_env() - client.ping() - return True - except: - return False + docker_ok = real_compiler_service._get_docker_client() is not None and real_compiler_service.docker_available + return _json_response( + { + "status": "healthy" if docker_ok else "degraded", + "timestamp": datetime.utcnow().isoformat(), + "docker_available": docker_ok, + "secure_execution_only": True, + "supported_languages": [l["id"] for l in real_compiler_service.get_supported_languages()], + }, + 200 if docker_ok else 503, + ) diff --git a/backend/services/real_compiler_service.py b/backend/services/real_compiler_service.py index 98801b6..1dfac46 100644 --- a/backend/services/real_compiler_service.py +++ b/backend/services/real_compiler_service.py @@ -1,117 +1,198 @@ -import docker -import tempfile +import ast import os -import subprocess +import queue +import re +import tempfile +import threading import time import uuid -import json -import threading -from typing import Dict, List, Any, Optional from datetime import datetime -import queue -import signal +from typing import Any, Dict, List, Optional, Tuple + +import docker + class RealCompilerService: def __init__(self): - self.client = None # Lazy initialization + self.client = None self.execution_queue = queue.Queue() - self.active_executions = {} - self.max_concurrent_executions = 5 + self.active_executions: Dict[str, Dict[str, Any]] = {} + self.max_code_size = 20000 self.docker_available = False - - # Enhanced language configurations with real execution + + # Docker is mandatory for secure execution. self.language_configs = { - 'python': { - 'image': 'python:3.11-slim', - 'file_ext': '.py', - 'compile_command': None, # Python doesn't need compilation - 'run_command': 'python /app/code.py', - 'timeout': 30, - 'memory_limit': '256m', - 'cpu_limit': '0.5' + "python": { + "image": "python:3.11-alpine", + "file_name": "code.py", + "compile_command": None, + "run_command": "python /workspace/code.py", + "timeout": 8, + "memory_limit": "128m", + "cpu_limit": 0.35, }, - 'java': { - 'image': 'openjdk:17-alpine', - 'file_ext': '.java', - 'compile_command': 'javac /app/Main.java', - 'run_command': 'java -cp /app Main', - 'timeout': 30, - 'memory_limit': '512m', - 'cpu_limit': '0.5' + "javascript": { + "image": "node:20-alpine", + "file_name": "code.js", + "compile_command": None, + "run_command": "node /workspace/code.js", + "timeout": 8, + "memory_limit": "128m", + "cpu_limit": 0.35, }, - 'cpp': { - 'image': 'gcc:latest', - 'file_ext': '.cpp', - 'compile_command': 'g++ -o /app/program /app/code.cpp -std=c++17', - 'run_command': '/app/program', - 'timeout': 30, - 'memory_limit': '256m', - 'cpu_limit': '0.5' + "c": { + "image": "gcc:13", + "file_name": "code.c", + "compile_command": "gcc -O2 -o /workspace/program /workspace/code.c", + "run_command": "/workspace/program", + "timeout": 10, + "memory_limit": "192m", + "cpu_limit": 0.5, }, - 'c': { - 'image': 'gcc:latest', - 'file_ext': '.c', - 'compile_command': 'gcc -o /app/program /app/code.c', - 'run_command': '/app/program', - 'timeout': 30, - 'memory_limit': '256m', - 'cpu_limit': '0.5' + "cpp": { + "image": "gcc:13", + "file_name": "code.cpp", + "compile_command": "g++ -O2 -std=c++17 -o /workspace/program /workspace/code.cpp", + "run_command": "/workspace/program", + "timeout": 10, + "memory_limit": "256m", + "cpu_limit": 0.5, }, - 'javascript': { - 'image': 'node:18-alpine', - 'file_ext': '.js', - 'compile_command': None, - 'run_command': 'node /app/code.js', - 'timeout': 30, - 'memory_limit': '256m', - 'cpu_limit': '0.5' + "java": { + "image": "openjdk:17-alpine", + "file_name": "Main.java", + "compile_command": "javac /workspace/Main.java", + "run_command": "java -cp /workspace Main", + "timeout": 12, + "memory_limit": "256m", + "cpu_limit": 0.5, }, - 'bash': { - 'image': 'bash:5.2-alpine3.18', - 'file_ext': '.sh', - 'compile_command': None, - 'run_command': 'bash /app/code.sh', - 'timeout': 30, - 'memory_limit': '128m', - 'cpu_limit': '0.3' + "go": { + "image": "golang:1.22-alpine", + "file_name": "code.go", + "compile_command": "go build -o /workspace/program /workspace/code.go", + "run_command": "/workspace/program", + "timeout": 14, + "memory_limit": "256m", + "cpu_limit": 0.6, }, - 'go': { - 'image': 'golang:1.21-alpine', - 'file_ext': '.go', - 'compile_command': 'go build -o /app/program /app/code.go', - 'run_command': '/app/program', - 'timeout': 30, - 'memory_limit': '512m', - 'cpu_limit': '0.5' + "rust": { + "image": "rust:1.77-alpine", + "file_name": "code.rs", + "compile_command": "rustc /workspace/code.rs -o /workspace/program", + "run_command": "/workspace/program", + "timeout": 20, + "memory_limit": "512m", + "cpu_limit": 0.8, }, - 'rust': { - 'image': 'rust:1.75-alpine', - 'file_ext': '.rs', - 'compile_command': 'rustc /app/code.rs -o /app/program', - 'run_command': '/app/program', - 'timeout': 60, # Rust compilation can be slow - 'memory_limit': '1g', - 'cpu_limit': '1.0' - } } - - # Start execution worker + + self.blocked_python_modules = { + "os", + "socket", + "subprocess", + "pty", + "multiprocessing", + "ctypes", + "resource", + "pwd", + "grp", + "signal", + "fcntl", + "selectors", + "pathlib", + "shutil", + } + self.blocked_python_calls = { + "eval", + "exec", + "compile", + "__import__", + "open", + "input", + "globals", + "locals", + "vars", + "getattr", + "setattr", + "delattr", + } + self.blocked_python_attrs = { + "fork", + "forkpty", + "spawn", + "spawnl", + "spawnlp", + "spawnv", + "spawnvp", + "system", + "popen", + "execl", + "execle", + "execlp", + "execv", + "execve", + "execvp", + "setsid", + "dup2", + } + self.blocked_patterns = { + "javascript": [ + r"require\s*\(\s*['\"]child_process['\"]\s*\)", + r"require\s*\(\s*['\"]net['\"]\s*\)", + r"require\s*\(\s*['\"]dgram['\"]\s*\)", + r"process\.env", + r"process\.binding", + r"fs\.readFile|fs\.writeFile|fs\.open|fs\.create", + ], + "java": [ + r"Runtime\.getRuntime\s*\(", + r"ProcessBuilder\s*\(", + r"java\.net\.", + r"java\.nio\.file\.", + r"System\.getenv\s*\(", + ], + "c": [ + r"\bsystem\s*\(", + r"\bpopen\s*\(", + r"\bfork\s*\(", + r"\bexec[a-z]*\s*\(", + r"\bsocket\s*\(", + ], + "cpp": [ + r"\bsystem\s*\(", + r"\bpopen\s*\(", + r"\bfork\s*\(", + r"\bexec[a-z]*\s*\(", + r"\bsocket\s*\(", + ], + "go": [ + r"\bexec\.Command\s*\(", + r"\bnet\.", + r"\bos\.StartProcess\s*\(", + r"\bos\.Exec\s*\(", + ], + "rust": [ + r"std::process::Command", + r"std::net::", + r"unsafe\s*\{", + ], + } + self.start_execution_worker() def _get_docker_client(self): - """Lazily initialize Docker client""" if self.client is None: try: self.client = docker.from_env() + self.client.ping() self.docker_available = True - except Exception as e: - print(f"⚠️ Docker initialization failed: {e}") + except Exception: self.docker_available = False self.client = None return self.client def start_execution_worker(self): - """Start background worker for code execution""" def worker(): while True: try: @@ -122,212 +203,326 @@ class RealCompilerService: continue except Exception as e: print(f"Execution worker error: {e}") - + worker_thread = threading.Thread(target=worker, daemon=True) worker_thread.start() - def execute_code(self, code: str, language: str, input_data: str = "", - execution_id: str = None) -> Dict[str, Any]: - """Execute code with real output capture""" + def _execute_task(self, _execution_task): + # Queue worker placeholder kept for backward compatibility. + return None + + def execute_code(self, code: str, language: str, input_data: str = "", execution_id: str = None) -> Dict[str, Any]: + language = (language or "").lower().strip() + if language == "js": + language = "javascript" + if language == "c++": + language = "cpp" + if language not in self.language_configs: return {"error": f"Language '{language}' not supported"} - + if not execution_id: execution_id = str(uuid.uuid4()) - - config = self.language_configs[language] - - try: - # Create execution context - execution_context = { - 'execution_id': execution_id, - 'code': code, - 'language': language, - 'input_data': input_data, - 'config': config, - 'start_time': datetime.now(), - 'status': 'running' + + if not code or not code.strip(): + return {"error": "No code provided", "execution_id": execution_id, "language": language} + + if len(code) > self.max_code_size: + return { + "error": f"Code too large. Maximum size is {self.max_code_size} characters.", + "execution_id": execution_id, + "language": language, + "blocked": True, } - + + ok, violations = self._validate_code_static(code, language) + if not ok: + return { + "error": "Code rejected by security policy", + "execution_id": execution_id, + "language": language, + "blocked": True, + "security_violations": violations, + } + + config = self.language_configs[language] + execution_context = { + "execution_id": execution_id, + "code": code, + "language": language, + "input_data": input_data or "", + "config": config, + "start_time": datetime.utcnow(), + "status": "running", + } + + try: self.active_executions[execution_id] = execution_context - - # Execute in Docker container result = self._execute_in_container(execution_context) - - # Update execution context - execution_context['status'] = 'completed' - execution_context['end_time'] = datetime.now() - execution_context['result'] = result - + execution_context["status"] = "completed" + execution_context["end_time"] = datetime.utcnow() + execution_context["result"] = result + + if result.get("error"): + return { + "success": False, + "execution_id": execution_id, + "output": result.get("output", ""), + "error": result.get("error", ""), + "execution_time": result.get("execution_time", 0), + "memory_used": result.get("memory_used", 0), + "exit_code": result.get("exit_code", -1), + "language": language, + "timestamp": datetime.utcnow().isoformat(), + } + return { "success": True, "execution_id": execution_id, - "output": result.get('output', ''), - "error": result.get('error', ''), - "execution_time": result.get('execution_time', 0), - "memory_used": result.get('memory_used', 0), - "exit_code": result.get('exit_code', 0), + "output": result.get("output", ""), + "error": "", + "execution_time": result.get("execution_time", 0), + "memory_used": result.get("memory_used", 0), + "exit_code": result.get("exit_code", 0), "language": language, - "timestamp": datetime.now().isoformat() + "timestamp": datetime.utcnow().isoformat(), } - except Exception as e: return { "error": f"Execution failed: {str(e)}", "execution_id": execution_id, - "language": language + "language": language, } finally: - # Clean up - if execution_id in self.active_executions: - del self.active_executions[execution_id] + self.active_executions.pop(execution_id, None) - def _execute_in_container(self, context: Dict) -> Dict[str, Any]: - """Execute code in secure Docker container""" - code = context['code'] - language = context['language'] - input_data = context['input_data'] - config = context['config'] - - # Check Docker availability + def _validate_code_static(self, code: str, language: str) -> Tuple[bool, List[str]]: + violations: List[str] = [] + + # Generic payload patterns often used for sandbox escape and exfiltration. + generic_patterns = [ + r"/bin/sh", + r"/bin/bash", + r"nc\s+-l|nc\s+-e", + r"reverse\s*shell", + r"bash\s+-i", + r"wget\s+http|curl\s+http", + ] + for pattern in generic_patterns: + if re.search(pattern, code, flags=re.IGNORECASE): + violations.append(f"Blocked high-risk pattern: {pattern}") + + if language == "python": + try: + tree = ast.parse(code) + except SyntaxError as e: + return False, [f"Python syntax error: {e}"] + + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + base = alias.name.split(".")[0] + if base in self.blocked_python_modules: + violations.append(f"Blocked module import: {base}") + + if isinstance(node, ast.ImportFrom): + if node.module: + base = node.module.split(".")[0] + if base in self.blocked_python_modules: + violations.append(f"Blocked module import: {base}") + + if isinstance(node, ast.Call): + fn = node.func + if isinstance(fn, ast.Name) and fn.id in self.blocked_python_calls: + violations.append(f"Blocked function call: {fn.id}") + if isinstance(fn, ast.Attribute) and fn.attr in self.blocked_python_attrs: + violations.append(f"Blocked dangerous call: {fn.attr}") + + for pattern in self.blocked_patterns.get(language, []): + if re.search(pattern, code, flags=re.IGNORECASE | re.MULTILINE): + violations.append(f"Blocked pattern for {language}: {pattern}") + + return len(violations) == 0, violations + + def _execute_in_container(self, context: Dict[str, Any]) -> Dict[str, Any]: docker_client = self._get_docker_client() if docker_client is None or not self.docker_available: return { "output": "", - "error": "Docker service is not available. Compiler service cannot execute code.", + "error": "Docker service is not available. Secure execution requires Docker.", "exit_code": -1, "execution_time": 0, - "memory_used": 0 + "memory_used": 0, } - - with tempfile.TemporaryDirectory() as temp_dir: - # Prepare code file - filename = f"code{config['file_ext']}" if language != 'java' else "Main.java" - file_path = os.path.join(temp_dir, filename) - - with open(file_path, 'w', encoding='utf-8') as f: + + code = context["code"] + language = context["language"] + input_data = context["input_data"] + config = context["config"] + + with tempfile.TemporaryDirectory(prefix="openlearnx_exec_") as temp_dir: + os.chmod(temp_dir, 0o755) + code_path = os.path.join(temp_dir, config["file_name"]) + with open(code_path, "w", encoding="utf-8") as f: f.write(code) - - # Prepare input file - input_file = os.path.join(temp_dir, 'input.txt') - with open(input_file, 'w', encoding='utf-8') as f: + os.chmod(code_path, 0o644) + + input_path = os.path.join(temp_dir, "input.txt") + with open(input_path, "w", encoding="utf-8") as f: f.write(input_data) - + os.chmod(input_path, 0o644) + + container = None + start = time.time() try: - start_time = time.time() - - # Create and run container + cpu_quota = int(float(config["cpu_limit"]) * 100000) container = docker_client.containers.run( - config['image'], - command=self._build_execution_command(config, filename), - volumes={temp_dir: {'bind': '/app', 'mode': 'rw'}}, - working_dir='/app', - mem_limit=config['memory_limit'], + config["image"], + command=self._build_execution_command(config), + volumes={temp_dir: {"bind": "/workspace", "mode": "rw"}}, + working_dir="/workspace", + mem_limit=config["memory_limit"], + memswap_limit=config["memory_limit"], cpu_period=100000, - cpu_quota=int(float(config['cpu_limit']) * 100000), - network_mode='none', # No network access - remove=True, - detach=False, - stdin_open=True, + cpu_quota=cpu_quota, + pids_limit=64, + network_mode="none", + detach=True, + stdin_open=False, tty=False, - timeout=config['timeout'], - # Security options - cap_drop=['ALL'], - security_opt=['no-new-privileges'], - read_only=False, - tmpfs={'/tmp': 'rw,noexec,nosuid,size=100m'} + cap_drop=["ALL"], + security_opt=["no-new-privileges:true"], + read_only=True, + user="65534:65534", + tmpfs={ + "/tmp": "rw,noexec,nosuid,size=64m", + }, + labels={ + "openlearnx.sandbox": "true", + "openlearnx.execution_id": context["execution_id"], + }, ) - - execution_time = time.time() - start_time - output = container.decode('utf-8') - + + wait_result = container.wait(timeout=config["timeout"] + 2) + logs = container.logs(stdout=True, stderr=True).decode("utf-8", errors="replace") + status_code = int(wait_result.get("StatusCode", -1)) + execution_time = round(time.time() - start, 3) + memory_used = self._get_memory_usage(container) + + if status_code != 0: + return { + "output": "", + "error": self._sanitize_error_output(language, logs.strip() or f"Runtime exited with code {status_code}"), + "exit_code": status_code, + "execution_time": execution_time, + "memory_used": memory_used, + } + return { - "output": output.strip(), + "output": logs.strip(), "error": "", "exit_code": 0, - "execution_time": round(execution_time, 3), - "memory_used": self._get_memory_usage(container) - } - - except docker.errors.ContainerError as e: - return { - "output": "", - "error": f"Runtime error (exit code {e.exit_status}): {e.stderr.decode('utf-8') if e.stderr else 'Unknown error'}", - "exit_code": e.exit_status, - "execution_time": time.time() - start_time, - "memory_used": 0 - } - except docker.errors.APIError as e: - return { - "output": "", - "error": f"Docker API error: {str(e)}", - "exit_code": -1, - "execution_time": 0, - "memory_used": 0 - } - except Exception as e: - return { - "output": "", - "error": f"Execution error: {str(e)}", - "exit_code": -1, - "execution_time": 0, - "memory_used": 0 + "execution_time": execution_time, + "memory_used": memory_used, } - def _build_execution_command(self, config: Dict, filename: str) -> str: - """Build the execution command for the container""" - commands = [] - - # Add compilation step if needed - if config.get('compile_command'): - commands.append(config['compile_command']) - - # Add execution command with input redirection - run_cmd = config['run_command'] - if '<' not in run_cmd: # Add input redirection if not present - run_cmd += ' < /app/input.txt 2>&1' - commands.append(run_cmd) - - # Combine commands - return f"sh -c '{' && '.join(commands)}'" + except Exception as e: + if container is not None: + try: + container.kill() + except Exception: + pass + return { + "output": "", + "error": self._sanitize_error_output(language, f"Execution failed or timed out: {str(e)}"), + "exit_code": -1, + "execution_time": round(time.time() - start, 3), + "memory_used": 0, + } + finally: + if container is not None: + try: + container.remove(force=True) + except Exception: + pass + + def _sanitize_error_output(self, language: str, raw_error: str) -> str: + if not raw_error: + return "Runtime error" + + text = str(raw_error) + # Avoid leaking container-internal paths. + text = re.sub(r"/workspace/", "", text) + lines = [line.rstrip() for line in text.splitlines() if line.strip()] + + if language == "python": + cleaned: List[str] = [] + for line in lines: + stripped = line.strip() + if stripped.startswith("Traceback"): + continue + if stripped.startswith("File "): + continue + if stripped.startswith("^"): + continue + cleaned.append(stripped) + + for line in reversed(cleaned): + if "Error" in line or "Exception" in line: + return line + if cleaned: + return cleaned[-1] + return "Python runtime error" + + # Keep non-python errors concise. + tail = lines[-3:] if len(lines) > 3 else lines + sanitized = "\n".join(tail).strip() + return sanitized or "Runtime error" + + def _build_execution_command(self, config: Dict[str, Any]) -> str: + commands: List[str] = [] + if config.get("compile_command"): + commands.append(config["compile_command"]) + + run_cmd = config["run_command"] + if "< /workspace/input.txt" not in run_cmd: + run_cmd = f"{run_cmd} < /workspace/input.txt" + + # ulimit adds an additional in-container CPU-time and file-size restriction. + shell_cmd = " && ".join(commands + [run_cmd]) + return f"sh -c 'ulimit -t {config['timeout']} -f 1024; {shell_cmd} 2>&1'" def _get_memory_usage(self, container) -> int: - """Get memory usage from container stats""" try: stats = container.stats(stream=False) - memory_usage = stats['memory']['usage'] - return memory_usage - except: + return int(stats.get("memory_stats", {}).get("usage", 0)) + except Exception: return 0 def get_supported_languages(self) -> List[Dict[str, str]]: - """Get list of supported languages with details""" return [ { - 'id': lang_id, - 'name': lang_id.title(), - 'extension': config['file_ext'], - 'timeout': config['timeout'], - 'memory_limit': config['memory_limit'] + "id": lang_id, + "name": lang_id.title(), + "extension": os.path.splitext(config["file_name"])[1], + "timeout": config["timeout"], + "memory_limit": config["memory_limit"], } for lang_id, config in self.language_configs.items() ] - def get_execution_status(self, execution_id: str) -> Optional[Dict]: - """Get status of a running execution""" + def get_execution_status(self, execution_id: str) -> Optional[Dict[str, Any]]: return self.active_executions.get(execution_id) def cancel_execution(self, execution_id: str) -> bool: - """Cancel a running execution""" if execution_id in self.active_executions: - # Implementation would involve stopping the Docker container del self.active_executions[execution_id] return True return False -# Create global instance + try: real_compiler_service = RealCompilerService() except Exception as e: - print(f"⚠️ Failed to initialize RealCompilerService: {e}") - real_compiler_service = RealCompilerService() # Still create instance for graceful fallback + print(f"WARNING: Failed to initialize RealCompilerService: {e}") + real_compiler_service = RealCompilerService() diff --git a/frontend/.npmignore b/frontend/.npmignore new file mode 100644 index 0000000..8602839 --- /dev/null +++ b/frontend/.npmignore @@ -0,0 +1,10 @@ +.next +node_modules +.env* +*.log +coverage +.git +.github +.vscode +.npmrc +pnpm-lock.yaml diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..5846088 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1,2 @@ +@th30d4y:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..aea2d20 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,31 @@ +# OpenLearnX + +OpenLearnX is an AI-powered learning platform with adaptive quizzes, coding practice, course tracking, and dashboard analytics. + +## Install + +```bash +npm i openlearnx +``` + +## Project + +This package contains the OpenLearnX frontend (Next.js). + +## Quick Start (development) + +```bash +npm install +npm run dev +``` + +## Build + +```bash +npm run build +npm start +``` + +## Repository + +https://github.com/th30d4y/OpenLearnX diff --git a/frontend/app/admin/logs/page.tsx b/frontend/app/admin/logs/page.tsx index cdb49a4..bb5dc98 100644 --- a/frontend/app/admin/logs/page.tsx +++ b/frontend/app/admin/logs/page.tsx @@ -23,6 +23,24 @@ type AdminLog = { origin?: string } +type ExecutionLog = { + id: string + timestamp: string + source?: string + language: string + execution_id?: string + status: string + exit_code?: number + execution_time?: number + memory_used?: number + blocked?: boolean + error?: string + ip?: string + request_body?: unknown + response_body?: unknown + user_agent?: string +} + const API_BASE = "http://127.0.0.1:5000" export default function AdminLogsPage() { @@ -31,8 +49,11 @@ export default function AdminLogsPage() { const [loading, setLoading] = useState(false) const [exporting, setExporting] = useState(false) const [message, setMessage] = useState("") + const [logView, setLogView] = useState<"security" | "execution">("security") const [logs, setLogs] = useState([]) + const [executionLogs, setExecutionLogs] = useState([]) const [selectedLog, setSelectedLog] = useState(null) + const [selectedExecutionLog, setSelectedExecutionLog] = useState(null) const safeJson = (value: unknown) => { if (value === null || value === undefined || value === "") return "No data" @@ -89,6 +110,12 @@ export default function AdminLogsPage() { search: "", }) const [pagination, setPagination] = useState({ page: 1, limit: 50, total: 0, pages: 1 }) + const [executionFilters, setExecutionFilters] = useState({ + language: "", + status: "", + search: "", + }) + const [executionPagination, setExecutionPagination] = useState({ page: 1, limit: 50, total: 0, pages: 1 }) const getToken = () => localStorage.getItem("admin_token") const headers = () => { @@ -142,6 +169,34 @@ export default function AdminLogsPage() { } } + const fetchExecutionLogs = async (page = 1, nextFilters = executionFilters) => { + setLoading(true) + setMessage("") + const params = new URLSearchParams() + params.set("page", String(page)) + params.set("limit", String(executionPagination.limit)) + if (nextFilters.language) params.set("language", nextFilters.language) + if (nextFilters.status) params.set("status", nextFilters.status) + if (nextFilters.search) params.set("search", nextFilters.search) + + try { + const resp = await fetch(`${API_BASE}/api/admin/logs/executions?${params.toString()}`, { headers: headers() }) + if (resp.ok) { + const data = await resp.json() + setExecutionLogs(Array.isArray(data.logs) ? data.logs : []) + if (data.pagination) { + setExecutionPagination(data.pagination) + } + } else { + setExecutionLogs([]) + } + } catch { + setExecutionLogs([]) + } finally { + setLoading(false) + } + } + const triggerDownload = (content: string, filename: string, mimeType: string) => { const blob = new Blob([content], { type: mimeType }) const url = URL.createObjectURL(blob) @@ -212,11 +267,37 @@ export default function AdminLogsPage() { return (
-

Security and Activity Logs

+

Admin Logs

- Filter authentication, access-control, suspicious payload, and admin activity events. + Switch between security/activity logs and a separate execution log stream.

+ + +
+
+ {logView === "security" ? ( + <> + + ) : null}
{message ?

{message}

: null}
+ {logView === "security" ? (
+ ) : ( +
+ setExecutionFilters({ ...executionFilters, search: e.target.value })} + className="md:col-span-2 rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-800" + /> + + +
+ + +
+
+ )}
+ {logView === "security" ? ( @@ -338,23 +475,76 @@ export default function AdminLogsPage() { )}
+ ) : ( + + + + + + + + + + + + + {loading ? ( + + + + ) : executionLogs.length === 0 ? ( + + + + ) : ( + executionLogs.map((log) => ( + setSelectedExecutionLog(log)} + className="cursor-pointer hover:bg-blue-50 dark:hover:bg-gray-800/60" + title="Click to view execution request and response details" + > + + + + + + + + )) + )} + +
TimeSourceLanguageExecution IDStatusTime (s)
Loading execution logs...
No execution logs found for selected filters.
{new Date(log.timestamp).toLocaleString()}{log.source || "compiler"}{log.language}{log.execution_id || "-"} + + {log.status} + + {typeof log.execution_time === "number" ? log.execution_time.toFixed(3) : "0.000"}
+ )}
- Page {pagination.page} of {pagination.pages} • Total {pagination.total} + {logView === "security" + ? `Page ${pagination.page} of ${pagination.pages} • Total ${pagination.total}` + : `Page ${executionPagination.page} of ${executionPagination.pages} • Total ${executionPagination.total}`}
) : null} + + {selectedExecutionLog ? ( +
+
+
+

Execution Request and Response Details

+ +
+ +
+
+
+

Source

+

{selectedExecutionLog.source || "compiler"}

+
+
+

Language

+

{selectedExecutionLog.language}

+
+
+

Execution ID

+

{selectedExecutionLog.execution_id || "-"}

+
+
+

Status

+

{selectedExecutionLog.status}

+
+
+

Execution Time

+

{typeof selectedExecutionLog.execution_time === "number" ? selectedExecutionLog.execution_time.toFixed(3) : "0.000"} s

+
+
+

Client

+

{selectedExecutionLog.ip || "Unknown"}

+

{selectedExecutionLog.user_agent || "Unknown user agent"}

+
+
+ +
+
+

Request Body

+ +
+
+{safeJson(selectedExecutionLog.request_body ?? { note: "No request body captured for this execution log" })}
+                
+
+ +
+
+

Response Body

+ +
+
+{safeJson(selectedExecutionLog.response_body ?? { note: "No response body captured for this execution log" })}
+                
+
+
+
+
+ ) : null}
) } diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/frontend/next-env.d.ts +++ b/frontend/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/frontend/package.json b/frontend/package.json index 62f2ad6..5df2c03 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { - "name": "my-v0-project", - "version": "0.1.0", - "private": true, + "name": "openlearnx", + "version": "2.0.3", + "private": false, "scripts": { "build": "next build", "dev": "next dev", @@ -81,5 +81,24 @@ "postcss": "^8.5", "tailwindcss": "^3.4.17", "typescript": "^5" - } -} \ No newline at end of file + }, + "repository": "https://github.com/th30d4y/OpenLearnX.git", + "publishConfig": { + "registry": "https://registry.npmjs.org" + }, + "files": [ + "README.md", + "package.json", + "app", + "components", + "context", + "hooks", + "lib", + "public", + "styles", + "next.config.mjs", + "postcss.config.mjs", + "tailwind.config.ts", + "tsconfig.json" + ] +} diff --git a/nohup.out b/nohup.out new file mode 100644 index 0000000..e69de29 diff --git a/start-local-secure.sh b/start-local-secure.sh new file mode 100755 index 0000000..720c618 --- /dev/null +++ b/start-local-secure.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" +MONGO_DBPATH="${HOME}/mongodata" +MONGO_LOG="/tmp/openlearnx_mongod.log" +BACKEND_LOG="/tmp/openlearnx_backend.log" +FRONTEND_LOG="/tmp/openlearnx_frontend.log" +FRONTEND_PID_FILE="/tmp/openlearnx_frontend.pid" +VENV_PYTHON="${ROOT_DIR}/venv_openlearnx/bin/python3" + +cd "$ROOT_DIR" + +echo "[1/8] Checking prerequisites" +command -v mongod >/dev/null 2>&1 || { echo "ERROR: mongod not found"; exit 1; } +command -v pnpm >/dev/null 2>&1 || { echo "ERROR: pnpm not found"; exit 1; } +command -v docker >/dev/null 2>&1 || { echo "ERROR: docker not found"; exit 1; } +[[ -x "$VENV_PYTHON" ]] || { echo "ERROR: Python venv not found at $VENV_PYTHON"; exit 1; } + +ensure_docker_access() { + if docker info >/dev/null 2>&1; then + return 0 + fi + + echo "[Docker] Current user cannot access Docker. Attempting automatic fix..." + + if ! sudo -n true >/dev/null 2>&1; then + echo "[Docker] sudo authentication required once to configure Docker access." + sudo -v + fi + + sudo systemctl enable --now docker + + if ! getent group docker >/dev/null 2>&1; then + sudo groupadd docker + fi + + sudo usermod -aG docker "$USER" + sudo chgrp docker /var/run/docker.sock || true + sudo chmod 660 /var/run/docker.sock || true + + if docker info >/dev/null 2>&1; then + return 0 + fi + + echo "[Docker] Group refresh required. Testing with sg docker context..." + if sg docker -c 'docker info >/dev/null 2>&1'; then + return 0 + fi + + echo "ERROR: Docker access is still unavailable after auto-fix." + echo "Run: newgrp docker (or log out/in) and rerun this script." + exit 1 +} + +run_backend() { + if docker info >/dev/null 2>&1; then + nohup "$VENV_PYTHON" backend/main.py >"$BACKEND_LOG" 2>&1 & + sleep 1 + pgrep -f "python3 .*backend/main.py" | head -n1 || true + return 0 + fi + + # Start backend in docker group context when group refresh has not propagated. + sg docker -c "nohup '$VENV_PYTHON' '$ROOT_DIR/backend/main.py' >'$BACKEND_LOG' 2>&1 &" + sleep 1 + pgrep -f "python3 .*backend/main.py" | head -n1 || true +} + +echo "[2/8] Ensuring Docker access" +ensure_docker_access + +echo "[3/8] Stopping old local processes" +pkill -f "mongod.*--dbpath ${MONGO_DBPATH}" 2>/dev/null || true +pkill -f "python3 .*backend/main.py" 2>/dev/null || true +pkill -f "pnpm dev" 2>/dev/null || true +pkill -f "next dev" 2>/dev/null || true + +echo "[4/8] Starting MongoDB" +mkdir -p "$MONGO_DBPATH" +if pgrep -f "mongod.*--dbpath ${MONGO_DBPATH}" >/dev/null 2>&1; then + echo "MongoDB already running for ${MONGO_DBPATH}; reusing existing process" +else + set +e + mongod --dbpath "$MONGO_DBPATH" --bind_ip 127.0.0.1 --port 27017 --logpath "$MONGO_LOG" --fork >/tmp/openlearnx_mongod_fork.out 2>&1 + mongo_start_code=$? + set -e + if [[ $mongo_start_code -ne 0 ]]; then + echo "WARNING: mongod --fork returned ${mongo_start_code}. Checking if service is still running..." + fi +fi +MONGO_PID="$(pgrep -f "mongod.*--dbpath ${MONGO_DBPATH}" | head -n1 || true)" +if [[ -z "$MONGO_PID" ]]; then + echo "ERROR: MongoDB did not start. Check logs: ${MONGO_LOG} and /tmp/openlearnx_mongod_fork.out" + exit 1 +fi +echo "Mongo PID: ${MONGO_PID:-N/A}" + +echo "[5/8] Starting backend" +BACKEND_PID="$(run_backend)" +if [[ -z "$BACKEND_PID" ]]; then + echo "ERROR: Backend did not start. Check log: $BACKEND_LOG" + exit 1 +fi +echo "Backend PID: ${BACKEND_PID:-N/A}" + +echo "[6/8] Starting frontend" +( + cd frontend + nohup pnpm dev >"$FRONTEND_LOG" 2>&1 & + echo "$!" >"$FRONTEND_PID_FILE" +) +FRONTEND_PID="$(cat "$FRONTEND_PID_FILE")" +echo "Frontend PID: ${FRONTEND_PID:-N/A}" + +echo "[7/8] Waiting for services" +backend_code="$(curl -sS -o /tmp/openlearnx_backend_health_body.txt -w "%{http_code}" --retry 30 --retry-all-errors --retry-connrefused --retry-delay 1 http://127.0.0.1:5000/api/health || true)" +frontend_code="$(curl -sS -o /tmp/openlearnx_frontend_body.txt -w "%{http_code}" --retry 30 --retry-all-errors --retry-connrefused --retry-delay 1 http://127.0.0.1:3000 || true)" + +echo "[8/8] Verifying secure compiler execution" +compiler_code="$(curl -sS -o /tmp/openlearnx_compiler_smoke.json -w "%{http_code}" -X POST http://127.0.0.1:5000/api/compiler/execute -H "Content-Type: application/json" -d '{"language":"python","code":"print(\"ok\")"}' || true)" + +echo "" +echo "RESULT" +echo " Mongo PID: ${MONGO_PID:-N/A}" +echo " Backend PID: ${BACKEND_PID:-N/A}" +echo " Frontend PID: ${FRONTEND_PID:-N/A}" +echo " Backend health: ${backend_code:-000} (http://127.0.0.1:5000/api/health)" +echo " Frontend health:${frontend_code:-000} (http://127.0.0.1:3000)" +echo " Compiler smoke: ${compiler_code:-000} (/api/compiler/execute)" + +echo "" +echo "Logs:" +echo " MongoDB: $MONGO_LOG" +echo " Backend: $BACKEND_LOG" +echo " Frontend: $FRONTEND_LOG"