-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
130 lines (110 loc) · 3.69 KB
/
Copy pathapp.py
File metadata and controls
130 lines (110 loc) · 3.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
from flask import Flask, request, jsonify
import os
import json
import subprocess
import uuid
app = Flask(__name__)
NSJAIL_CONFIG_PATH = "/etc/nsjail.cfg"
PYTHON_BIN = "/usr/local/bin/python3"
SANDBOX_ROOT = "/sandbox"
WRAPPER_PATH = "/app/executor.py"
RESULT_PREFIX = "___RESULT_JSON___:"
def run_in_nsjail(script_path: str):
"""
Run the given script inside nsjail using executor.py as a wrapper.
Returns:
stdout (str), stderr (str), exit_code (int)
"""
cmd = [
"nsjail",
"--config", NSJAIL_CONFIG_PATH,
"--",
PYTHON_BIN,
WRAPPER_PATH,
script_path,
]
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
stdout, stderr = proc.communicate()
return stdout, stderr, proc.returncode
@app.route("/execute", methods=["POST"])
def execute():
"""
Main execution endpoint.
Expects JSON body:
{ "script": "<python code defining main()>" }
Returns JSON:
{
"result": <main() return value as JSON>,
"stdout": "<captured stdout>"
}
"""
# Input validation
if not request.is_json:
return jsonify({"error": "Request body must be JSON"}), 400
# Parse Request body as JSON
body = request.get_json(silent=True)
if body is None:
return jsonify({"error": "Invalid JSON"}), 400
# Validate Script
script = body.get("script")
if not isinstance(script, str) or not script.strip():
return jsonify({"error": "`script` must be a non-empty string"}), 400
# Save the script into a temporary file with a unique id to avoid collisions
os.makedirs(SANDBOX_ROOT, exist_ok=True)
script_id = str(uuid.uuid4())
script_filename = f"user_script_{script_id}.py"
script_path = os.path.join(SANDBOX_ROOT, script_filename)
with open(script_path, "w") as f:
f.write(script)
# Execute the script via nsjail
stdout, stderr, code = run_in_nsjail(script_path)
# If the execution failed return an error
if code != 0:
return jsonify({
"error": "Script execution failed",
"exit_code": code,
"stderr": stderr,
"stdout": stdout,
}), 400
# Parse stdout lines:
result_json_str = None
user_stdout_lines = []
for line in stdout.splitlines():
if line.startswith(RESULT_PREFIX):
# Retrieve the rest of the line after the prefix (We only retrieve the last one which shouldn't be an issue since we only output one)
result_json_str = line[len(RESULT_PREFIX):]
else:
user_stdout_lines.append(line)
# If the execution didn't return a result return an error
if result_json_str is None:
return jsonify({
"error": "main() result not found. Ensure script defines main() and returns JSON.",
"stderr": stderr,
"stdout": stdout,
}), 400
# Try to parse the result into a JSON, if it fails return an error
try:
result = json.loads(result_json_str)
except json.JSONDecodeError:
return jsonify({
"error": "main() did not return JSON-serializable object",
"stderr": stderr,
"stdout": stdout,
}), 400
# Return the result jsonified
return jsonify({
"result": result,
"stdout": "\n".join(user_stdout_lines),
})
# Simple endpoint to check that the service is up and running, for debugging
@app.route("/status", methods=["GET"])
def status():
"""Simple endpoint to check that the service is up."""
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)