Skip to content
This repository was archived by the owner on Dec 8, 2022. It is now read-only.

Commit ce3eb9e

Browse files
committed
backend: use sandbox for evaluating JS/Python code
1 parent 14e5b44 commit ce3eb9e

File tree

7 files changed

+116
-83
lines changed

7 files changed

+116
-83
lines changed

CodeChallenge/api/questions.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from hmac import compare_digest as str_cmp
44

55
import requests
6-
from flask import Blueprint, current_app, jsonify, request, redirect, url_for
6+
from flask import Blueprint, current_app, jsonify, request, redirect, url_for, abort
77
from flask_jwt_extended import get_current_user, jwt_required
88

99
from .. import core
@@ -119,7 +119,7 @@ def answer_next_question():
119119
@bp.route("/history", methods=["GET"])
120120
@jwt_required
121121
def history():
122-
""" Returns all past questions and answers for the currrent user"""
122+
""" Returns all past questions and answers for the current user"""
123123

124124
u = get_current_user()
125125

@@ -148,7 +148,7 @@ def reset_all():
148148
u = get_current_user()
149149
u.rank = 0
150150

151-
for ans in Answer.query.filter_by(user_id= u.id): # type: Answer
151+
for ans in Answer.query.filter_by(user_id=u.id): # type: Answer
152152
db.session.delete(ans)
153153

154154
db.session.commit()
@@ -183,9 +183,18 @@ def answer_eval():
183183
return jsonify(status="error",
184184
reason="missing 'text' property in JSON body"), 400
185185

186+
try:
187+
language = request.json["language"]
188+
except KeyError:
189+
return jsonify(status="error",
190+
reason="missing 'language' property in JSON body"), 400
191+
186192
# designated output variable for evaluation
187-
code += ";output"
188-
r = requests.post(current_app.config["DUKTAPE_API"], json={"code": code})
193+
if language == "js":
194+
code += ";output"
195+
196+
r = requests.post(current_app.config["SANDBOX_API_URL"],
197+
json={"code": code, "language": language})
189198

190199
if not r.ok:
191200
if r.status_code >= 500:
@@ -196,7 +205,7 @@ def answer_eval():
196205
eval_data = r.json()
197206
except ValueError:
198207
return jsonify(status="error",
199-
reason="response from duktape API was not JSON"), 500
208+
reason="response from sandbox API was not JSON"), 500
200209

201210
eval_error = eval_data["error"]
202211
eval_output = str(eval_data["output"])

CodeChallenge/config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,14 @@ class DefaultConfig:
3333
# no trailing /
3434
EXTERNAL_URL = "https://hackcwhq.com"
3535

36-
DUKTAPE_API = "http://localhost:5001/js/eval"
36+
SANDBOX_API_URL = "http://sandbox.cwhq-apps.com:3000/"
3737

3838
ALLOW_RESET = False
39+
MAX_VOTES = 2
40+
41+
# number of days to leave CodeChallenge open
42+
# past the final rank
43+
CHALLENGE_ENDS = 1
3944

4045
@property
4146
def ROOT_DIR(self):

docker/Dockerfile

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
FROM alpine:latest
22
LABEL maintainer="Sam Hoffman <sam@codewizardshq.com>"
33

4+
ENV LANGUAGE=""
5+
ENV CODEFILE=""
6+
47
RUN apk --update add --no-cache python3 bash curl binutils libc-dev python3-dev gcc
58
RUN pip3 install -U pip
6-
RUN pip3 install flask flask-cors pyduktape gunicorn
7-
8-
RUN adduser -D -H webapp
9-
RUN mkdir -p "/var/www/flaskapp"
10-
11-
# default Flask port
12-
EXPOSE 5000/tcp
9+
RUN pip3 install pyduktape
1310

14-
USER webapp
11+
RUN adduser -D -H sandbox-user
12+
USER sandbox-user
1513

16-
COPY "start.sh" "/tmp/start.sh"
17-
COPY "app.py" "/var/www/flaskapp/app.py"
14+
COPY "main.py" "/tmp/main.py"
15+
VOLUME "/mnt/code"
1816

19-
ENTRYPOINT [ "/tmp/start.sh" ]
17+
ENTRYPOINT [ "/tmp/main.py" ]

docker/app.py

Lines changed: 0 additions & 33 deletions
This file was deleted.

docker/main.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/python3
2+
3+
import json
4+
import os
5+
import sys
6+
from subprocess import Popen, PIPE
7+
from typing import Tuple
8+
9+
from pyduktape import DuktapeContext
10+
11+
CODE_FOLDER = "/mnt/code/"
12+
13+
CLIOutput = Tuple[str, str] # stdout/stderr
14+
15+
16+
def echo(output="", error="") -> None:
17+
print(json.dumps({
18+
"output": output,
19+
"error": error
20+
}))
21+
22+
23+
def exec_python(code: str) -> CLIOutput:
24+
p = Popen("/usr/bin/python3", stdin=PIPE, stderr=PIPE, stdout=PIPE)
25+
stdout, stderr = p.communicate(code.encode("utf8"))
26+
27+
return stdout.decode("utf8"), stderr.decode("utf8")
28+
29+
30+
def exec_js(code: str) -> CLIOutput:
31+
ctx = DuktapeContext()
32+
33+
output = ""
34+
error = ""
35+
36+
try:
37+
output = ctx.eval_js(code)
38+
except Exception as e:
39+
error = str(e)
40+
41+
return output, error
42+
43+
44+
def main() -> int:
45+
language = os.getenv("LANGUAGE")
46+
filename = os.getenv("CODEFILE")
47+
code = ""
48+
49+
with open(os.path.join(CODE_FOLDER, filename), "r") as contents:
50+
code = contents.read()
51+
52+
stdout, stderr = "", ""
53+
54+
if language not in ("python", "js"):
55+
echo(error=f"unsupported language {language!r}")
56+
return -1
57+
58+
if language == "python":
59+
stdout, stderr = exec_python(code)
60+
61+
elif language == "js":
62+
stdout, stderr = exec_js(code)
63+
64+
echo(stdout, stderr)
65+
66+
return 0
67+
68+
69+
if __name__ == "__main__":
70+
sys.exit(main())

docker/start.sh

Lines changed: 0 additions & 4 deletions
This file was deleted.

tests/test_question.py

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88

99
app = CodeChallenge.create_app("DefaultConfig")
1010

11+
NOW = datetime.now(timezone.utc)
12+
13+
CC_CLOSED = (NOW - timedelta(days=5)).timestamp()
14+
CC_2D_PRIOR = (NOW - timedelta(days=2)).timestamp()
15+
CC_4D_PRIOR = (NOW - timedelta(days=4)).timestamp()
16+
CC_2D_FUTURE = (NOW + timedelta(days=2)).timestamp()
17+
1118

1219
@pytest.fixture(scope="module")
1320
def client_challenge_today():
@@ -39,12 +46,9 @@ def client_challenge_today():
3946

4047
@pytest.fixture(scope="module")
4148
def client_challenge_future():
42-
now = datetime.now(timezone.utc)
43-
future = now + timedelta(days=2)
44-
4549
app.config["TESTING"] = True
4650
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
47-
app.config["CODE_CHALLENGE_START"] = future.timestamp()
51+
app.config["CODE_CHALLENGE_START"] = CC_2D_FUTURE
4852

4953
with app.test_client() as client:
5054
with app.app_context():
@@ -54,12 +58,9 @@ def client_challenge_future():
5458

5559
@pytest.fixture(scope="module")
5660
def client_challenge_past():
57-
now = datetime.now(timezone.utc)
58-
past = now - timedelta(days=2)
59-
6061
app.config["TESTING"] = True
6162
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
62-
app.config["CODE_CHALLENGE_START"] = past.timestamp()
63+
app.config["CODE_CHALLENGE_START"] = CC_2D_PRIOR
6364

6465
with app.test_client() as client:
6566
with app.app_context():
@@ -69,12 +70,9 @@ def client_challenge_past():
6970

7071
@pytest.fixture(scope="module")
7172
def client_challenge_lastq():
72-
now = datetime.now(timezone.utc)
73-
past = now - timedelta(days=4)
74-
7573
app.config["TESTING"] = True
7674
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
77-
app.config["CODE_CHALLENGE_START"] = past.timestamp()
75+
app.config["CODE_CHALLENGE_START"] = CC_4D_PRIOR
7876

7977
with app.test_client() as client:
8078
with app.app_context():
@@ -276,46 +274,36 @@ def test_answer_exceed_attempts(client_challenge_past):
276274
assert "X-RateLimit-Remaining" in retval.headers
277275

278276

279-
@pytest.mark.skipif(not os.getenv("DUKTAPE_API"), reason="envvar DUKTAPE_API is not set")
277+
@pytest.mark.skipif(not os.getenv("SANDBOX_API_URL"), reason="envvar SANDBOX_API_URL is not set")
280278
def test_answer_finalq_wrong(client_challenge_lastq):
281279
login(client_challenge_lastq,
282280
"cwhqsam",
283281
"supersecurepassword")
284282

285283
rv = client_challenge_lastq.post("/api/v1/questions/final", json=dict(
286-
text="var output; output = 11"
284+
text="var output; output = 11",
285+
language="js"
287286
))
288287

289288
assert rv.status_code == 200
290289
assert rv.json["correct"] is False
291290

292291

293-
@pytest.mark.skipif(not os.getenv("DUKTAPE_API"), reason="envvar DUKTAPE_API is not set")
292+
@pytest.mark.skipif(not os.getenv("SANDBOX_API_URL"), reason="envvar SANDBOX_API_URL is not set")
294293
def test_answer_finalq_right(client_challenge_lastq):
295294
login(client_challenge_lastq,
296295
"cwhqsam",
297296
"supersecurepassword")
298297

299298
rv = client_challenge_lastq.post("/api/v1/questions/final", json=dict(
300-
text="var output; output = 10"
299+
text="var output; output = 10",
300+
language="js"
301301
))
302302

303303
assert rv.status_code == 200
304304
assert rv.json["correct"] is True
305305

306306

307-
def test_reset_all(client_challenge_past):
308-
retval = client_challenge_past.delete("/api/v1/questions/reset")
309-
310-
assert retval.status_code == 200
311-
312-
retval = client_challenge_past.get("/api/v1/users/hello")
313-
data = retval.get_json()
314-
315-
assert retval.status_code == 200
316-
assert data["rank"] == 0
317-
318-
319307
def test_leaderboard(client_challenge_past):
320308

321309
# register a bunch of fake users

0 commit comments

Comments
 (0)