diff options
| -rw-r--r-- | .gitignore | 5 | ||||
| -rw-r--r-- | Dockerfile | 12 | ||||
| -rw-r--r-- | config.yaml | 3 | ||||
| -rwxr-xr-x | remote-https-runner | 3 | ||||
| -rw-r--r-- | requirements.txt | 8 | ||||
| -rw-r--r-- | runner.py | 78 |
6 files changed, 109 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..915f3b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +venv/ +*.pyc +__pycache__/ +.vscode/ +output.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2d01eac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python + +RUN python --version + +ARG configfile +ADD requirements.txt . +RUN pip install -r requirements.txt +ADD ${configfile} remote-https-runner *.py ./ +ENV FLASK_APP=runner.py +EXPOSE 5000 + +CMD ["./remote-https-runner"]
\ No newline at end of file diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..f148545 --- /dev/null +++ b/config.yaml @@ -0,0 +1,3 @@ +example: + auth: "super secret password" + command: "echo ${MESSAGE} | tee output.txt" diff --git a/remote-https-runner b/remote-https-runner new file mode 100755 index 0000000..5821986 --- /dev/null +++ b/remote-https-runner @@ -0,0 +1,3 @@ +#!/bin/sh + +gunicorn -w 4 -b 0.0.0.0:5000 runner:app diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0703955 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +Click==7.0 +Flask==1.0.2 +gunicorn==19.9.0 +itsdangerous==1.1.0 +Jinja2==2.10 +MarkupSafe==1.1.0 +PyYAML==3.13 +Werkzeug==0.14.1 diff --git a/runner.py b/runner.py new file mode 100644 index 0000000..c550555 --- /dev/null +++ b/runner.py @@ -0,0 +1,78 @@ +import os +import sys +import subprocess +import time +from flask import Flask, Response, request, jsonify +app = Flask(__name__) + +import logging +logging.basicConfig(format='%(asctime)s %(levelname)s - %(message)s', level=logging.INFO) + +from yaml import load, dump +if "CONFIG_PATH" in os.environ: + filename = os.environ["CONFIG_PATH"] +else: + filename = "config.yaml" + +try: + with open(filename) as f: + text = f.read() + config = load(text) +except: + print("Could not open file {}".format(filename)) + sys.exit(1) + +config_keys = config.keys() + +@app.route("/<key>", methods=["POST"]) +def key(key): + logging.info("User requested route \"{}\"".format(key)) + if not key in config_keys: + logging.warning("Route \"{}\" does not exist!".format(key)) + return Response("", status=404) + + options = config[key] + auth_header = request.headers.get('Authorization') + protocol = request.url.split("://")[0] + logging.info("User uses protocol {}".format(protocol)) + #if protocol != "https": + # return Response("HTTP requests not allowed! Use HTTPS!", status=400) + + auth = options["auth"] + if auth != auth_header: + logging.warning("User request rejected due to incorrect Authorization header!") + return Response("", status=401) + + if request.content_type != "application/json": + logging.warning("User request rejected due to incorrect content type (must be application/json)!") + return Response("", status=401) + + content = request.json + + cmd = options["command"] + for content_key in content: + cmd = cmd.replace("${" + str(content_key) + "}", content[content_key]) + + logging.info("User runs command:") + logging.info(cmd) + + start = time.time() + output = subprocess.check_output(cmd, shell=True) + end = time.time() + + output = output.decode("utf-8") if type(output) == bytes else output + duration = "{:.2f}".format(end - start) + + logging.info("Command output:") + logging.info(output) + logging.info("Command duration: {}".format(duration)) + + return jsonify({ + "command": cmd, + "output": str(output), + "duration": duration + }) + +@app.route("/") +def hello(): + return "Welcome to remote HTTPS runner API!" |
