diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..68fba14 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +piptools: ## install pinned version of pip-compile and pip-sync + pip install -r requirements/pip-tools.txt + +upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade +upgrade: piptools ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in + pip-compile --upgrade -o requirements/pip-tools.txt requirements/pip-tools.in + pip-compile --upgrade -o requirements/base.txt requirements/base.in diff --git a/codejailservice/__init__.py b/codejailservice/__init__.py new file mode 100644 index 0000000..c07c459 --- /dev/null +++ b/codejailservice/__init__.py @@ -0,0 +1 @@ +from .app import app diff --git a/codejailservice/app.py b/codejailservice/app.py new file mode 100644 index 0000000..3ee53b0 --- /dev/null +++ b/codejailservice/app.py @@ -0,0 +1,119 @@ +import os +import json +import logging +import sys +import timeit + +from codejail import jail_code + +from copy import deepcopy +from flask import Flask, Response, jsonify, request +from logging.config import dictConfig + + +dictConfig({ + 'version': 1, + 'formatters': { + 'default': { + 'format': '%(asctime)s %(levelname)s %(process)d ' '[%(name)s] %(filename)s:%(lineno)d - %(message)s', + } + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'stream': sys.stdout, + 'formatter': 'default' + } + }, + 'root': { + 'level': 'DEBUG', + 'handlers': ['console'] + } +}) + +app = Flask(__name__) +env_config = os.getenv("FLASK_APP_SETTINGS", "codejailservice.config.ProductionConfig") +app.config.from_object(env_config) + +def configure_codejail(app): + code_jail_settings = app.config["CODE_JAIL"] + python_bin = code_jail_settings.get('python_bin') + if python_bin: + user = code_jail_settings['user'] + jail_code.configure("python", python_bin, user=user) + limits = code_jail_settings.get('limits', {}) + for name, value in limits.items(): + jail_code.set_limit( + limit_name=name, + value=value, + ) + limit_overrides = code_jail_settings.get('limit_overrides', {}) + for context, overrides in limit_overrides.items(): + for name, value in overrides.items(): + jail_code.override_limit( + limit_name=name, + value=value, + limit_overrides_context=context, + ) + +configure_codejail(app) + +from codejail.safe_exec import SafeExecException, json_safe +from codejail.safe_exec import not_safe_exec as codejail_not_safe_exec +from codejail.safe_exec import safe_exec as codejail_safe_exec + +@app.route("/") +def index(): + return Response("Edx Codejail Service", status=200) + +@app.route("/health") +def health(): + return Response("OK", status=200) + +@app.post("/api/v0/code-exec") +def code_exec(): + payload = json.loads(request.form["payload"]) + globals_dict = deepcopy(payload["globals_dict"]) + + unsafely = payload["unsafely"] + if unsafely: + exec_fn = codejail_not_safe_exec + else: + exec_fn = codejail_safe_exec + + try: + python_path=payload["python_path"] + if python_path: + extra_files=[(python_path[0], request.files[python_path[0]].read())] + else: + extra_files=[] + course_id = payload["limit_overrides_context"] + problem_id = payload["slug"] + app.logger.info("Running problem_id:%s jailed code for course_id:%s ...", problem_id, course_id) + start = timeit.default_timer() + exec_fn( + payload["code"], + globals_dict, + python_path=python_path, + extra_files=extra_files, + limit_overrides_context=course_id, + slug=problem_id, + ) + end = timeit.default_timer() + + except SafeExecException as e: + # Saving SafeExecException e in exception to be used later. + app.logger.error("Error found while executing jailed code.") + exception = e + emsg = str(e) + else: + app.logger.info("Jailed code was executed in %s seconds.", str(end-start)) + exception = None + emsg = None + + response = { + "globals_dict": globals_dict, + "emsg": emsg + } + + return jsonify(response) diff --git a/codejailservice/config.py b/codejailservice/config.py new file mode 100644 index 0000000..40c8d03 --- /dev/null +++ b/codejailservice/config.py @@ -0,0 +1,42 @@ +import os + +class BaseConfig: + DEBUG = False + DEVELOPMENT = False + SECRET_KEY = os.getenv("SECRET_KEY", "this-is-the-default-key") + + CODE_JAIL = { + 'python_bin': '/sandbox/venv/bin/python', + # User to run as in the sandbox. + 'user': 'sandbox', + + # Configurable limits. + # Setting all of them to 0 to disable limits in containers. + 'limits': { + # + 'NPROC': 0, + # How many CPU seconds can jailed code use? + 'CPU': 0, + # Limit the memory of the jailed process to something high but not + # infinite (512MiB in bytes) + 'VMEM': 0, + # Time in seconds that the jailed process has to run. + 'REALTIME': 0, + # Needs to be non-zero so that jailed code can use it as their temp directory.(10MiB in bytes) + 'FSIZE': 10485760, + # Disable usage of proxy (force thread-safe) + 'PROXY': 0, + }, + + # Overrides to default configurable 'limits' (above). + # Keys should be course run ids. + # Values should be dictionaries that look like 'limits'. + "limit_overrides": {}, + } + +class DevelopmentConfig(BaseConfig): + DEBUG = True + DEVELOPMENT = True + +class ProductionConfig(BaseConfig): + pass diff --git a/codejailservice/tutor.py b/codejailservice/tutor.py new file mode 100755 index 0000000..e69de29 diff --git a/requirements/base.in b/requirements/base.in new file mode 100644 index 0000000..2b4dd18 --- /dev/null +++ b/requirements/base.in @@ -0,0 +1,7 @@ + +-c constraints.txt + +Flask +requests + +-e git+https://github.com/edx/codejail.git@3.1.3#egg=codejail==3.1.3 diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000..ae9a749 --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,32 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# make upgrade +# +-e git+https://github.com/edx/codejail.git@3.1.3#egg=codejail==3.1.3 + # via -r requirements/base.in +certifi==2021.5.30 + # via requests +chardet==4.0.0 + # via requests +click==8.0.1 + # via flask +flask==2.0.1 + # via -r requirements/base.in +idna==2.10 + # via requests +itsdangerous==2.0.1 + # via flask +jinja2==3.0.1 + # via flask +markupsafe==2.0.1 + # via jinja2 +requests==2.25.1 + # via -r requirements/base.in +six==1.16.0 + # via codejail +urllib3==1.26.5 + # via requests +werkzeug==2.0.1 + # via flask diff --git a/requirements/constraints.txt b/requirements/constraints.txt new file mode 100644 index 0000000..52ac6d0 --- /dev/null +++ b/requirements/constraints.txt @@ -0,0 +1,9 @@ +# Version constraints for pip installation. +# +# This file doesn't install any packages. It specifies version constraints +# that will be applied if a package is needed. +# +# When pinning something here, please provide an explanation of why. Ideally, +# link to other information that will help people in the future to remove the +# pin when possible. Writing an issue against the offending project and +# linking to it here is good. diff --git a/requirements/pip-tools.in b/requirements/pip-tools.in new file mode 100644 index 0000000..de76110 --- /dev/null +++ b/requirements/pip-tools.in @@ -0,0 +1,7 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# make upgrade +# +pip-tools diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt new file mode 100644 index 0000000..88f1620 --- /dev/null +++ b/requirements/pip-tools.txt @@ -0,0 +1,17 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# make upgrade +# +click==8.0.1 + # via pip-tools +pep517==0.10.0 + # via pip-tools +pip-tools==6.1.0 + # via -r requirements/pip-tools.in +toml==0.10.2 + # via pep517 + +# The following packages are considered to be unsafe in a requirements file: +# pip diff --git a/requirements/pip.txt b/requirements/pip.txt new file mode 100644 index 0000000..c65dc9a --- /dev/null +++ b/requirements/pip.txt @@ -0,0 +1,2 @@ +pip==20.2.3 +setuptools==50.3.0 \ No newline at end of file diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..a4375d6 --- /dev/null +++ b/wsgi.py @@ -0,0 +1,8 @@ +import os + +from codejailservice import app + +if __name__ == "__main__": + host = os.getenv("FLASK_CODEJAILSERVICE_HOST", "0.0.0.0") + port = os.getenv("FLASK_CODEJAILSERVICE_PORT", 8000) + app.run(host=host, port=port)