diff options
| author | 2019-06-05 09:11:26 -0700 | |
|---|---|---|
| committer | 2019-06-05 09:11:26 -0700 | |
| commit | 26069d1412b94df1ec74194ee975d7f901f40489 (patch) | |
| tree | 9baf1ebbdbb866a59dd0d58425ef6ccb4c03b66e | |
| parent | Configure gunicorn logging (diff) | |
| parent | Merge pull request #23 from python-discord/falcon (diff) | |
Merge remote-tracking branch 'origin/revitalisation' into refactor/nsjail
| -rw-r--r-- | .coveragerc | 14 | ||||
| -rw-r--r-- | Pipfile | 5 | ||||
| -rw-r--r-- | Pipfile.lock | 125 | ||||
| -rw-r--r-- | snekbox/api/__init__.py | 3 | ||||
| -rw-r--r-- | snekbox/api/app.py | 3 | ||||
| -rw-r--r-- | snekbox/api/middleware/__init__.py | 3 | ||||
| -rw-r--r-- | snekbox/api/middleware/logger.py | 11 | ||||
| -rw-r--r-- | snekbox/api/resources/__init__.py | 3 | ||||
| -rw-r--r-- | snekbox/api/resources/eval.py | 74 | ||||
| -rw-r--r-- | snekbox/api/snekapi.py | 27 | ||||
| -rw-r--r-- | snekbox/site/snekapp.py | 35 | ||||
| -rw-r--r-- | snekbox/site/templates/index.html | 14 | ||||
| -rw-r--r-- | snekbox/site/templates/result.html | 9 | ||||
| -rw-r--r-- | tests/__init__.py | 1 | ||||
| -rw-r--r-- | tests/api/__init__.py | 17 | ||||
| -rw-r--r-- | tests/api/test_eval.py | 49 | 
16 files changed, 257 insertions, 136 deletions
| diff --git a/.coveragerc b/.coveragerc index c1e718b..d1877d4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,10 +1,6 @@  [run] -omit = .venv/*, -       tests/*, -       snekweb.py -       config.py - -[report] -exclude_lines = return jsonify, -                raise RuntimeError, -                return +branch = True +omit = +    .venv/*, +    snekbox/api/app.py, +    tests/* @@ -4,8 +4,9 @@ verify_ssl = true  name = "pypi"  [packages] -flask = "*" +falcon = "*"  gunicorn = "*" +jsonschema = "*"  [dev-packages]  pytest = "*" @@ -30,7 +31,7 @@ lint = "flake8"  precommit = "pre-commit install"  test = "pytest tests --cov . --cov-report term-missing -v"  report = "pytest tests --cov . --cov-report=html" -snekbox = "gunicorn -w 2 -b 0.0.0.0:8060 --logger-class snekbox.GunicornLogger --access-logfile - snekbox.site.snekapp:app" +snekbox = "gunicorn -w 2 -b 0.0.0.0:8060 --logger-class snekbox.GunicornLogger --access-logfile - snekbox.api.app"  buildbox = "docker build -t pythondiscord/snekbox:latest -f docker/Dockerfile ."  pushbox = "docker push pythondiscord/snekbox:latest"  buildboxbase = "docker build -t pythondiscord/snekbox-base:latest -f docker/base.Dockerfile ." diff --git a/Pipfile.lock b/Pipfile.lock index b73b997..c09c916 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@  {      "_meta": {          "hash": { -            "sha256": "c1f4c3df791d8c4758f72cb8fb148a34d5c1ca02298b0d660844899f15f6ba85" +            "sha256": "af53793d2c00001698021096041ed23e5de0a5553f974822e9ad7e58f70de4a9"          },          "pipfile-spec": 6,          "requires": { @@ -16,20 +16,32 @@          ]      },      "default": { -        "click": { +        "attrs": {              "hashes": [ -                "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", -                "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" +                "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", +                "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"              ], -            "version": "==7.0" +            "version": "==19.1.0"          }, -        "flask": { -            "hashes": [ -                "sha256:ad7c6d841e64296b962296c2c2dabc6543752985727af86a975072dea984b6f3", -                "sha256:e7d32475d1de5facaa55e3958bc4ec66d3762076b074296aa50ef8fdc5b9df61" +        "falcon": { +            "hashes": [ +                "sha256:18157af2a4fc3feedf2b5dcc6196f448639acf01c68bc33d4d5a04c3ef87f494", +                "sha256:24adcd2b29a8ffa9d552dc79638cd21736a3fb04eda7d102c6cebafdaadb88ad", +                "sha256:54f2cb4b687035b2a03206dbfc538055cc48b59a953187b0458aa1b574d47b53", +                "sha256:59d1e8c993b9a37ea06df9d72cf907a46cc8063b30717cdac2f34d1658b6f936", +                "sha256:733033ec80c896e30a43ab3e776856096836787197a44eb21022320a61311983", +                "sha256:74cf1d18207381c665b9e6292d65100ce146d958707793174b03869dc6e614f4", +                "sha256:95bf6ce986c1119aef12c9b348f4dee9c6dcc58391bdd0bc2b0bf353c2b15986", +                "sha256:9712975adcf8c6e12876239085ad757b8fdeba223d46d23daef82b47658f83a9", +                "sha256:a5ebb22a04c9cc65081938ee7651b4e3b4d2a28522ea8ec04c7bdd2b3e9e8cd8", +                "sha256:aa184895d1ad4573fbfaaf803563d02f019ebdf4790e41cc568a330607eae439", +                "sha256:e3782b7b92fefd46a6ad1fd8fe63fe6c6f1b7740a95ca56957f48d1aee34b357", +                "sha256:e9efa0791b5d9f9dd9689015ea6bce0a27fcd5ecbcd30e6d940bffa4f7f03389", +                "sha256:eea593cf466b9c126ce667f6d30503624ef24459f118c75594a69353b6c3d5fc", +                "sha256:f93351459f110b4c1ee28556aef9a791832df6f910bea7b3f616109d534df06b"              ],              "index": "pypi", -            "version": "==1.0.3" +            "version": "==2.0.0"          },          "gunicorn": {              "hashes": [ @@ -39,59 +51,26 @@              "index": "pypi",              "version": "==19.9.0"          }, -        "itsdangerous": { -            "hashes": [ -                "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", -                "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" -            ], -            "version": "==1.1.0" -        }, -        "jinja2": { -            "hashes": [ -                "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", -                "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" -            ], -            "version": "==2.10.1" -        }, -        "markupsafe": { -            "hashes": [ -                "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", -                "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", -                "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", -                "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", -                "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", -                "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", -                "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", -                "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", -                "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", -                "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", -                "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", -                "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", -                "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", -                "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", -                "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", -                "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", -                "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", -                "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", -                "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", -                "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", -                "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", -                "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", -                "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", -                "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", -                "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", -                "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", -                "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", -                "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" -            ], -            "version": "==1.1.1" -        }, -        "werkzeug": { -            "hashes": [ -                "sha256:865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c", -                "sha256:a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6" -            ], -            "version": "==0.15.4" +        "jsonschema": { +            "hashes": [ +                "sha256:0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d", +                "sha256:a5f6559964a3851f59040d3b961de5e68e70971afb88ba519d27e6a039efff1a" +            ], +            "index": "pypi", +            "version": "==3.0.1" +        }, +        "pyrsistent": { +            "hashes": [ +                "sha256:16692ee739d42cf5e39cef8d27649a8c1fdb7aa99887098f1460057c5eb75c3a" +            ], +            "version": "==0.15.2" +        }, +        "six": { +            "hashes": [ +                "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", +                "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" +            ], +            "version": "==1.12.0"          }      },      "develop": { @@ -284,6 +263,13 @@              ],              "version": "==1.3.3"          }, +        "packaging": { +            "hashes": [ +                "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", +                "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" +            ], +            "version": "==19.0" +        },          "pluggy": {              "hashes": [                  "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", @@ -328,13 +314,20 @@              ],              "version": "==2.1.1"          }, +        "pyparsing": { +            "hashes": [ +                "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", +                "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" +            ], +            "version": "==2.4.0" +        },          "pytest": {              "hashes": [ -                "sha256:1a8aa4fa958f8f451ac5441f3ac130d9fc86ea38780dd2715e6d5c5882700b24", -                "sha256:b8bf138592384bd4e87338cb0f256bf5f615398a649d4bd83915f0e4047a5ca6" +                "sha256:6032845e68a17a96e8da3088037f899b56357769a724122056265ca2ea1890ee", +                "sha256:bea27a646a3d74cbbcf8d3d4a06b2dfc336baf3dc2cc85cf70ad0157e73e8322"              ],              "index": "pypi", -            "version": "==4.5.0" +            "version": "==4.6.2"          },          "pytest-cov": {              "hashes": [ diff --git a/snekbox/api/__init__.py b/snekbox/api/__init__.py new file mode 100644 index 0000000..d106f26 --- /dev/null +++ b/snekbox/api/__init__.py @@ -0,0 +1,3 @@ +from .snekapi import SnekAPI + +__all__ = ("SnekAPI",) diff --git a/snekbox/api/app.py b/snekbox/api/app.py new file mode 100644 index 0000000..c71e246 --- /dev/null +++ b/snekbox/api/app.py @@ -0,0 +1,3 @@ +from . import SnekAPI + +application = SnekAPI() diff --git a/snekbox/api/middleware/__init__.py b/snekbox/api/middleware/__init__.py new file mode 100644 index 0000000..ba97ca6 --- /dev/null +++ b/snekbox/api/middleware/__init__.py @@ -0,0 +1,3 @@ +from .logger import LoggingMiddleware + +__all__ = ("LoggingMiddleware",) diff --git a/snekbox/api/middleware/logger.py b/snekbox/api/middleware/logger.py new file mode 100644 index 0000000..393fc64 --- /dev/null +++ b/snekbox/api/middleware/logger.py @@ -0,0 +1,11 @@ +import logging + +log = logging.getLogger("snekbox.api") + + +class LoggingMiddleware: +    """Log basic information about responses.""" + +    def process_response(self, req, resp, resource, req_succeeded): +        """Log the method, route, and status of a response.""" +        log.info(f"{req.method} {req.relative_uri} {resp.status}") diff --git a/snekbox/api/resources/__init__.py b/snekbox/api/resources/__init__.py new file mode 100644 index 0000000..fe422b8 --- /dev/null +++ b/snekbox/api/resources/__init__.py @@ -0,0 +1,3 @@ +from .eval import EvalResource + +__all__ = ("EvalResource",) diff --git a/snekbox/api/resources/eval.py b/snekbox/api/resources/eval.py new file mode 100644 index 0000000..b2f4260 --- /dev/null +++ b/snekbox/api/resources/eval.py @@ -0,0 +1,74 @@ +import logging + +import falcon +from falcon.media.validators.jsonschema import validate + +from snekbox.nsjail import NsJail + +log = logging.getLogger(__name__) + + +class EvalResource: +    """ +    Evaluation of Python code. + +    Supported methods: + +    - POST /eval +        Evaluate Python code and return the result +    """ + +    REQ_SCHEMA = { +        "type": "object", +        "properties": { +            "input": { +                "type": "string" +            } +        }, +        "required": [ +            "input" +        ] +    } + +    def __init__(self): +        self.nsjail = NsJail() + +    @validate(REQ_SCHEMA) +    def on_post(self, req, resp): +        """ +        Evaluate Python code and return the result. + +        Request body: + +        >>> { +        ...     "input": "print(1 + 1)" +        ... } + +        Response format: + +        >>> { +        ...     "input": "print(1 + 1)", +        ...     "output": "2\\n" +        ... } + +        Status codes: + +        - 200 +            Successful evaluation; not indicative that the input code itself works +        - 400 +           Input's JSON schema is invalid +        - 415 +            Unsupported content type; only application/JSON is supported +        """ +        code = req.media["input"] + +        try: +            output = self.nsjail.python3(code) +        except Exception: +            log.exception("An exception occurred while trying to process the request") +            raise falcon.HTTPInternalServerError + +        resp.media = { +            "input": code, +            "output": output +        } diff --git a/snekbox/api/snekapi.py b/snekbox/api/snekapi.py new file mode 100644 index 0000000..849e7d6 --- /dev/null +++ b/snekbox/api/snekapi.py @@ -0,0 +1,27 @@ +import falcon + +from .middleware import LoggingMiddleware +from .resources import EvalResource + + +class SnekAPI(falcon.API): +    """ +    The main entry point to the snekbox JSON API. + +    Routes: + +    - /eval +        Evaluation of Python code + +    Error response format: + +    >>> { +    ...     "title": "Unsupported media type", +    ...     "description": "application/xml is an unsupported media type." +    ... } +    """ + +    def __init__(self, *args, **kwargs): +        super().__init__(middleware=[LoggingMiddleware()], *args, **kwargs) + +        self.add_route("/eval", EvalResource()) diff --git a/snekbox/site/snekapp.py b/snekbox/site/snekapp.py deleted file mode 100644 index 3954238..0000000 --- a/snekbox/site/snekapp.py +++ /dev/null @@ -1,35 +0,0 @@ -from flask import Flask, jsonify, render_template, request - -from snekbox.nsjail import NsJail - -nsjail = NsJail() - -# Load app -app = Flask(__name__) -app.use_reloader = False - -# Logging -log = app.logger - - [email protected]("/") -def index(): -    """Return a page with a form for inputting code to be executed.""" -    return render_template("index.html") - - [email protected]("/result", methods=["POST", "GET"]) -def result(): -    """Execute code and return a page displaying the results.""" -    if request.method == "POST": -        code = request.form["Code"] -        output = nsjail.python3(code) -        return render_template("result.html", code=code, result=output) - - [email protected]("/input", methods=["POST"]) -def code_input(): -    """Execute code and return the results.""" -    body = request.get_json() -    output = nsjail.python3(body["code"]) -    return jsonify(input=body["code"], output=output) diff --git a/snekbox/site/templates/index.html b/snekbox/site/templates/index.html deleted file mode 100644 index 41980d1..0000000 --- a/snekbox/site/templates/index.html +++ /dev/null @@ -1,14 +0,0 @@ -<!DOCTYPE html> -<html> -    <meta charset="utf-8" /> -    <title>snekboxweb</title> -    <body> -        <form action="/result" method="POST"> -            <p>Code:<br><textarea name="Code" rows="20" cols="80"> -def sum(a,b): -    return a+b -print( sum(1,2) )</textarea></p> -            <p><input type="submit" value="Run"></p> -        </form> -    </body> -</html> diff --git a/snekbox/site/templates/result.html b/snekbox/site/templates/result.html deleted file mode 100644 index e339605..0000000 --- a/snekbox/site/templates/result.html +++ /dev/null @@ -1,9 +0,0 @@ -<!DOCTYPE html> -<html> -    <meta charset="utf-8" /> -    <title>snekboxweb</title> -    <body> -        <p>Code Evaluated:<br><pre>{{ code }}</pre></p> -        <p>Results:<br><pre>{{ result }}</pre></p> -    </body> -</html> diff --git a/tests/__init__.py b/tests/__init__.py index 792d600..e69de29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +0,0 @@ -# diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000..fd4679a --- /dev/null +++ b/tests/api/__init__.py @@ -0,0 +1,17 @@ +from unittest import mock + +from falcon import testing + +from snekbox.api import SnekAPI + + +class SnekAPITestCase(testing.TestCase): +    def setUp(self): +        super().setUp() + +        self.patcher = mock.patch("snekbox.api.resources.eval.NsJail", autospec=True) +        self.mock_nsjail = self.patcher.start() +        self.mock_nsjail.return_value.python3.return_value = "test output" +        self.addCleanup(self.patcher.stop) + +        self.app = SnekAPI() diff --git a/tests/api/test_eval.py b/tests/api/test_eval.py new file mode 100644 index 0000000..a5b83fd --- /dev/null +++ b/tests/api/test_eval.py @@ -0,0 +1,49 @@ +from tests.api import SnekAPITestCase + + +class TestEvalResource(SnekAPITestCase): +    PATH = "/eval" + +    def test_post_valid_200(self): +        body = {"input": "foo"} +        result = self.simulate_post(self.PATH, json=body) + +        self.assertEqual(result.status_code, 200) +        self.assertEqual(body["input"], result.json["input"]) +        self.assertEqual("test output", result.json["output"]) + +    def test_post_invalid_schema_400(self): +        body = {"stuff": "foo"} +        result = self.simulate_post(self.PATH, json=body) + +        self.assertEqual(result.status_code, 400) + +        expected = { +            "title": "Request data failed validation", +            "description": "'input' is a required property" +        } + +        self.assertEqual(expected, result.json) + +    def test_post_invalid_content_type_415(self): +        body = "{\"input\": \"foo\"}" +        headers = {"Content-Type": "application/xml"} +        result = self.simulate_post(self.PATH, body=body, headers=headers) + +        self.assertEqual(result.status_code, 415) + +        expected = { +            "title": "Unsupported media type", +            "description": "application/xml is an unsupported media type." +        } + +        self.assertEqual(expected, result.json) + +    def test_disallowed_method_405(self): +        result = self.simulate_get(self.PATH) +        self.assertEqual(result.status_code, 405) + +    def test_options_allow_post_only(self): +        result = self.simulate_options(self.PATH) +        self.assertEqual(result.status_code, 200) +        self.assertEqual(result.headers.get("Allow"), "POST") | 
