fix(security): harden execution sandbox and add dedicated admin execution logs

This commit is contained in:
Stalin
2026-04-19 23:03:08 +05:30
parent d19c4e4d15
commit 14765d7d18
12 changed files with 1296 additions and 834 deletions
+94
View File
@@ -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():
+92 -63
View File
@@ -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 = {
+210 -530
View File
@@ -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 <stdio.h>\nint main(){ printf(\"ok\\n\"); return 0; }",
"cpp": "#include <iostream>\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,
)
+423 -228
View File
@@ -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()
+10
View File
@@ -0,0 +1,10 @@
.next
node_modules
.env*
*.log
coverage
.git
.github
.vscode
.npmrc
pnpm-lock.yaml
+2
View File
@@ -0,0 +1,2 @@
@th30d4y:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
+31
View File
@@ -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
+273 -7
View File
@@ -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<AdminLog[]>([])
const [executionLogs, setExecutionLogs] = useState<ExecutionLog[]>([])
const [selectedLog, setSelectedLog] = useState<AdminLog | null>(null)
const [selectedExecutionLog, setSelectedExecutionLog] = useState<ExecutionLog | null>(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 (
<div className="space-y-6">
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-900">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Security and Activity Logs</h1>
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Admin Logs</h1>
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
Filter authentication, access-control, suspicious payload, and admin activity events.
Switch between security/activity logs and a separate execution log stream.
</p>
<div className="mt-4 flex flex-wrap gap-2">
<button
onClick={() => {
setLogView("security")
setSelectedLog(null)
setSelectedExecutionLog(null)
fetchLogs(1)
}}
className={`rounded-md px-3 py-2 text-sm font-medium ${logView === "security" ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-900 dark:bg-gray-700 dark:text-gray-100"}`}
>
Security and Activity
</button>
<button
onClick={() => {
setLogView("execution")
setSelectedLog(null)
setSelectedExecutionLog(null)
fetchExecutionLogs(1)
}}
className={`rounded-md px-3 py-2 text-sm font-medium ${logView === "execution" ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-900 dark:bg-gray-700 dark:text-gray-100"}`}
>
Execution Logs
</button>
</div>
<div className="mt-3 flex flex-wrap gap-2">
{logView === "security" ? (
<>
<button
onClick={() => exportLogs("json")}
disabled={exporting}
@@ -231,11 +312,14 @@ export default function AdminLogsPage() {
>
Export Logs CSV
</button>
</>
) : null}
</div>
{message ? <p className="mt-2 text-sm text-gray-700 dark:text-gray-200">{message}</p> : null}
</div>
<div className="rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900">
{logView === "security" ? (
<div className="grid grid-cols-1 gap-3 border-b border-gray-100 p-4 md:grid-cols-6 dark:border-gray-800">
<input
placeholder="Search action, path, IP"
@@ -293,8 +377,61 @@ export default function AdminLogsPage() {
</button>
</div>
</div>
) : (
<div className="grid grid-cols-1 gap-3 border-b border-gray-100 p-4 md:grid-cols-5 dark:border-gray-800">
<input
placeholder="Search execution id, source, IP"
value={executionFilters.search}
onChange={(e) => 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"
/>
<select
value={executionFilters.language}
onChange={(e) => setExecutionFilters({ ...executionFilters, language: e.target.value })}
className="rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-800"
>
<option value="">All Languages</option>
<option value="python">Python</option>
<option value="javascript">JavaScript</option>
<option value="c">C</option>
<option value="cpp">C++</option>
<option value="java">Java</option>
<option value="go">Go</option>
<option value="rust">Rust</option>
</select>
<select
value={executionFilters.status}
onChange={(e) => setExecutionFilters({ ...executionFilters, status: e.target.value })}
className="rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-800"
>
<option value="">All Status</option>
<option value="success">Success</option>
<option value="failed">Failed</option>
<option value="blocked">Blocked</option>
</select>
<div className="flex gap-2">
<button
onClick={() => fetchExecutionLogs(1)}
className="w-full rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Apply
</button>
<button
onClick={() => {
const reset = { language: "", status: "", search: "" }
setExecutionFilters(reset)
fetchExecutionLogs(1, reset)
}}
className="w-full rounded-md bg-gray-200 px-3 py-2 text-sm font-medium text-gray-900 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-100"
>
Clear
</button>
</div>
</div>
)}
<div className="overflow-x-auto">
{logView === "security" ? (
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
<thead className="bg-gray-50 dark:bg-gray-800/50">
<tr>
@@ -338,23 +475,76 @@ export default function AdminLogsPage() {
)}
</tbody>
</table>
) : (
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
<thead className="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium uppercase text-gray-500">Time</th>
<th className="px-4 py-2 text-left text-xs font-medium uppercase text-gray-500">Source</th>
<th className="px-4 py-2 text-left text-xs font-medium uppercase text-gray-500">Language</th>
<th className="px-4 py-2 text-left text-xs font-medium uppercase text-gray-500">Execution ID</th>
<th className="px-4 py-2 text-left text-xs font-medium uppercase text-gray-500">Status</th>
<th className="px-4 py-2 text-left text-xs font-medium uppercase text-gray-500">Time (s)</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{loading ? (
<tr>
<td className="px-4 py-4 text-sm text-gray-600" colSpan={6}>Loading execution logs...</td>
</tr>
) : executionLogs.length === 0 ? (
<tr>
<td className="px-4 py-4 text-sm text-gray-500" colSpan={6}>No execution logs found for selected filters.</td>
</tr>
) : (
executionLogs.map((log) => (
<tr
key={log.id}
onClick={() => setSelectedExecutionLog(log)}
className="cursor-pointer hover:bg-blue-50 dark:hover:bg-gray-800/60"
title="Click to view execution request and response details"
>
<td className="px-4 py-3 text-xs text-gray-700 dark:text-gray-300">{new Date(log.timestamp).toLocaleString()}</td>
<td className="px-4 py-3 text-xs text-gray-700 dark:text-gray-300">{log.source || "compiler"}</td>
<td className="px-4 py-3 text-xs text-gray-700 dark:text-gray-300">{log.language}</td>
<td className="px-4 py-3 text-xs text-gray-700 dark:text-gray-300">{log.execution_id || "-"}</td>
<td className="px-4 py-3 text-xs">
<span className={`rounded px-2 py-1 ${log.status === "success" ? "bg-green-100 text-green-700" : log.status === "blocked" ? "bg-amber-100 text-amber-700" : "bg-red-100 text-red-700"}`}>
{log.status}
</span>
</td>
<td className="px-4 py-3 text-xs text-gray-700 dark:text-gray-300">{typeof log.execution_time === "number" ? log.execution_time.toFixed(3) : "0.000"}</td>
</tr>
))
)}
</tbody>
</table>
)}
</div>
<div className="flex items-center justify-between border-t border-gray-100 px-4 py-3 text-sm text-gray-600 dark:border-gray-800 dark:text-gray-300">
<span>
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}`}
</span>
<div className="flex gap-2">
<button
onClick={() => fetchLogs(Math.max(1, pagination.page - 1))}
disabled={pagination.page <= 1}
onClick={() => {
if (logView === "security") fetchLogs(Math.max(1, pagination.page - 1))
else fetchExecutionLogs(Math.max(1, executionPagination.page - 1))
}}
disabled={logView === "security" ? pagination.page <= 1 : executionPagination.page <= 1}
className="rounded bg-gray-100 px-3 py-1.5 disabled:opacity-50 dark:bg-gray-800"
>
Previous
</button>
<button
onClick={() => fetchLogs(Math.min(pagination.pages, pagination.page + 1))}
disabled={pagination.page >= pagination.pages}
onClick={() => {
if (logView === "security") fetchLogs(Math.min(pagination.pages, pagination.page + 1))
else fetchExecutionLogs(Math.min(executionPagination.pages, executionPagination.page + 1))
}}
disabled={logView === "security" ? pagination.page >= pagination.pages : executionPagination.page >= executionPagination.pages}
className="rounded bg-gray-100 px-3 py-1.5 disabled:opacity-50 dark:bg-gray-800"
>
Next
@@ -462,6 +652,82 @@ export default function AdminLogsPage() {
</div>
</div>
) : null}
{selectedExecutionLog ? (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div className="w-full max-w-4xl rounded-xl border border-gray-200 bg-white shadow-xl dark:border-gray-800 dark:bg-gray-900">
<div className="flex items-center justify-between border-b border-gray-100 p-4 dark:border-gray-800">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Execution Request and Response Details</h2>
<button
onClick={() => setSelectedExecutionLog(null)}
className="rounded-md bg-gray-200 px-3 py-1.5 text-sm text-gray-900 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-100"
>
Close
</button>
</div>
<div className="max-h-[75vh] space-y-4 overflow-auto p-4">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<div className="rounded-lg border border-gray-200 p-3 dark:border-gray-700">
<p className="text-xs font-semibold uppercase text-gray-500">Source</p>
<p className="mt-1 text-sm text-gray-900 dark:text-white">{selectedExecutionLog.source || "compiler"}</p>
</div>
<div className="rounded-lg border border-gray-200 p-3 dark:border-gray-700">
<p className="text-xs font-semibold uppercase text-gray-500">Language</p>
<p className="mt-1 text-sm text-gray-900 dark:text-white">{selectedExecutionLog.language}</p>
</div>
<div className="rounded-lg border border-gray-200 p-3 dark:border-gray-700">
<p className="text-xs font-semibold uppercase text-gray-500">Execution ID</p>
<p className="mt-1 break-all text-sm text-gray-900 dark:text-white">{selectedExecutionLog.execution_id || "-"}</p>
</div>
<div className="rounded-lg border border-gray-200 p-3 dark:border-gray-700">
<p className="text-xs font-semibold uppercase text-gray-500">Status</p>
<p className="mt-1 text-sm text-gray-900 dark:text-white">{selectedExecutionLog.status}</p>
</div>
<div className="rounded-lg border border-gray-200 p-3 dark:border-gray-700">
<p className="text-xs font-semibold uppercase text-gray-500">Execution Time</p>
<p className="mt-1 text-sm text-gray-900 dark:text-white">{typeof selectedExecutionLog.execution_time === "number" ? selectedExecutionLog.execution_time.toFixed(3) : "0.000"} s</p>
</div>
<div className="rounded-lg border border-gray-200 p-3 dark:border-gray-700">
<p className="text-xs font-semibold uppercase text-gray-500">Client</p>
<p className="mt-1 break-all text-sm text-gray-900 dark:text-white">{selectedExecutionLog.ip || "Unknown"}</p>
<p className="mt-1 break-all text-xs text-gray-600 dark:text-gray-300">{selectedExecutionLog.user_agent || "Unknown user agent"}</p>
</div>
</div>
<div className="rounded-lg border border-gray-200 p-3 dark:border-gray-700">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold uppercase text-gray-500">Request Body</p>
<button
onClick={() => copyText(safeJson(selectedExecutionLog.request_body ?? { note: "No request body captured for this execution log" }))}
className="rounded bg-blue-600 px-2 py-1 text-xs text-white hover:bg-blue-700"
>
Copy
</button>
</div>
<pre className="mt-2 max-h-64 overflow-auto whitespace-pre-wrap break-words rounded bg-gray-50 p-3 text-sm leading-6 text-gray-800 dark:bg-gray-800 dark:text-gray-100">
{safeJson(selectedExecutionLog.request_body ?? { note: "No request body captured for this execution log" })}
</pre>
</div>
<div className="rounded-lg border border-gray-200 p-3 dark:border-gray-700">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold uppercase text-gray-500">Response Body</p>
<button
onClick={() => copyText(safeJson(selectedExecutionLog.response_body ?? { note: "No response body captured for this execution log" }))}
className="rounded bg-emerald-600 px-2 py-1 text-xs text-white hover:bg-emerald-700"
>
Copy
</button>
</div>
<pre className="mt-2 max-h-64 overflow-auto whitespace-pre-wrap break-words rounded bg-gray-50 p-3 text-sm leading-6 text-gray-800 dark:bg-gray-800 dark:text-gray-100">
{safeJson(selectedExecutionLog.response_body ?? { note: "No response body captured for this execution log" })}
</pre>
</div>
</div>
</div>
</div>
) : null}
</div>
)
}
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
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.
+24 -5
View File
@@ -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"
}
}
},
"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"
]
}
View File
+136
View File
@@ -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"