Files
python-isolator/server.py
Peter Stockings 3b41eaccea Add base64
2025-10-31 22:55:10 +11:00

173 lines
6.7 KiB
Python

import json
import os
from flask import Flask, request, jsonify
from multiprocessing import Pool, TimeoutError
import time
import requests
from bs4 import BeautifulSoup
import math
import random
import hashlib
from html import escape
import base64, hmac
from urllib.parse import urlparse
import urllib.request, urllib.error
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, 'base64': base64,
'BaseException': BaseException,
'Exception': Exception,
'ArithmeticError': ArithmeticError,
'AssertionError': AssertionError,
'AttributeError': AttributeError,
'EOFError': EOFError,
'FloatingPointError': FloatingPointError,
'GeneratorExit': GeneratorExit,
'ImportError': ImportError,
'IndexError': IndexError,
'KeyError': KeyError,
'KeyboardInterrupt': KeyboardInterrupt,
'LookupError': LookupError,
'MemoryError': MemoryError,
'NameError': NameError,
'NotImplementedError': NotImplementedError,
'OSError': OSError,
'OverflowError': OverflowError,
'RecursionError': RecursionError,
'ReferenceError': ReferenceError,
'RuntimeError': RuntimeError,
'StopIteration': StopIteration,
'StopAsyncIteration': StopAsyncIteration,
'SyntaxError': SyntaxError,
'SystemError': SystemError,
'TypeError': TypeError,
'UnboundLocalError': UnboundLocalError,
'UnicodeError': UnicodeError,
'ValueError': ValueError,
'ZeroDivisionError': ZeroDivisionError
},
'request': request_obj,
'environment': environment,
'requests': requests,
'BeautifulSoup': BeautifulSoup,
'json': json,
'escape': escape,
'math': math,
'random': random,
'hashlib': hashlib,
'urlparse': urlparse,
'urllib.request': urllib.request,
'urllib.error': urllib.error
}
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)