From f8cda984de07e8b5e3ba67b70fa78a2ea6efd690 Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Sun, 28 Sep 2025 12:56:45 +1000 Subject: [PATCH] Initial commit --- Procfile | 1 + requirements.txt | 3 ++ server.py | 129 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 Procfile create mode 100644 requirements.txt create mode 100644 server.py diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..d5e2bb1 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: python server.py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0ffbf2a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask==2.2.5 +requests==2.26.0 +beautifulsoup4==4.10.0 \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..cb76771 --- /dev/null +++ b/server.py @@ -0,0 +1,129 @@ +import json +import os +from flask import Flask, request, jsonify +from multiprocessing import Pool, TimeoutError +import time +import requests +from bs4 import BeautifulSoup + +app = Flask(__name__) + +# ────── 1. one-time constants ────────────────────────────────────────── +PORT = int(os.environ.get('PORT', 5001)) +TIMEOUT_SECONDS = 5 +HTTP_STATUS_CODES = { + "OK": 200, + "BAD_REQUEST": 400, + "INTERNAL_SERVER_ERROR": 500, +} + +States = { + "SUCCESS": "SUCCESS", + "NOT_A_FUNCTION": "NOT_A_FUNCTION", + "SCRIPT_ERROR": "ERROR", + "TIMEOUT": "TIMEOUT", +} + +# ────── 2. execution worker ─────────────────────────────────────────── +def execute_code(code, request_obj, environment): + """ + Executes the user's code in a restricted environment. + """ + logs = [] + start_time = time.time() + + def custom_print(*args, **kwargs): + # Concatenate all arguments into a single string, separated by spaces + output = ' '.join(map(str, args)) + logs.append(output) + + restricted_globals = { + '__builtins__': { + 'print': custom_print, + # Whitelist safe builtins + 'abs': abs, 'all': all, 'any': any, 'ascii': ascii, 'bin': bin, + 'bool': bool, 'bytearray': bytearray, 'bytes': bytes, 'callable': callable, + 'chr': chr, 'complex': complex, 'dict': dict, 'divmod': divmod, + 'enumerate': enumerate, 'filter': filter, 'float': float, 'format': format, + 'frozenset': frozenset, 'getattr': getattr, 'hasattr': hasattr, 'hash': hash, + 'hex': hex, 'int': int, 'isinstance': isinstance, 'issubclass': issubclass, + 'iter': iter, 'len': len, 'list': list, 'map': map, 'max': max, + 'min': min, 'next': next, 'object': object, 'oct': oct, 'ord': ord, + 'pow': pow, 'range': range, 'repr': repr, 'reversed': reversed, + 'round': round, 'set': set, 'slice': slice, 'sorted': sorted, + 'str': str, 'sum': sum, 'super': super, 'tuple': tuple, 'type': type, + 'zip': zip + }, + 'request': request_obj, + 'environment': environment, + 'requests': requests, + 'BeautifulSoup': BeautifulSoup, + 'json': json + } + + try: + # Execute the code + exec(code, restricted_globals) + + # Check if a function (e.g., main) is defined and call it + if 'main' in restricted_globals and callable(restricted_globals['main']): + result = restricted_globals['main'](request_obj, environment) + else: + # If no main function, maybe the script just runs top-level + result = None + + execution_time = (time.time() - start_time) * 1000 # in milliseconds + return { + 'status': States['SUCCESS'], + 'result': result, + 'logs': logs, + 'environment': environment, + 'execution_time': execution_time, + } + except Exception as e: + execution_time = (time.time() - start_time) * 1000 # in milliseconds + return { + 'status': States['SCRIPT_ERROR'], + 'result': str(e), + 'logs': logs, + 'environment': environment, + 'execution_time': execution_time, + } + +# ────── 3. process pool ──────────────────────────────────────────────── +# It's generally better to initialize the pool once. +# For simplicity in this example, we'll create it on demand, but a real app should manage it. +# pool = Pool(processes=4) + +# ────── 4. API surface ──────────────────────────────────────────────── +@app.route("/execute", methods=['POST']) +def execute(): + body = request.json + code = body.get('code', '') + request_obj = body.get('request', {}) + environment = body.get('environment', {}) + + with Pool(processes=1) as pool: + async_result = pool.apply_async(execute_code, (code, request_obj, environment)) + try: + payload = async_result.get(timeout=TIMEOUT_SECONDS) + except TimeoutError: + payload = { + 'status': States['TIMEOUT'], + 'result': f'Execution timed out after {TIMEOUT_SECONDS} seconds.', + 'logs': [], + 'environment': environment, + 'execution_time': TIMEOUT_SECONDS * 1000, + } + except Exception as e: + payload = { + 'status': States['SCRIPT_ERROR'], + 'result': str(e), + 'logs': [], + 'environment': environment, + } + + return jsonify(payload) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=PORT) \ No newline at end of file