diff options
| -rw-r--r-- | .flake8 | 16 | ||||
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | Pipfile | 1 | ||||
| -rw-r--r-- | Pipfile.lock | 50 | ||||
| -rw-r--r-- | config.py | 5 | ||||
| -rw-r--r-- | rmq.py | 54 | ||||
| -rw-r--r-- | snekbox.py | 34 | ||||
| -rw-r--r-- | snekweb.py | 7 | 
8 files changed, 113 insertions, 58 deletions
| @@ -1,6 +1,18 @@  [flake8]  max-line-length=100  application_import_names=snekbox,config,logs -ignore=P102,B311,W503,E226,S311 -exclude=__pycache__, venv, .venv, tests +ignore= +    P102,B311,W503,E226,S311, +    # Missing Docstrings +    D100,D104,D107, +    # Docstring Whitespace +    D202,D203,D204,D212,D214,D215, +    # Docstring Quotes +    D301,D302, +    # Docstring Content +    D400,D401,D402,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414 +exclude= +    __pycache__,.cache, +    venv,.venv, +    tests  import-order-style=pycharm @@ -1,3 +1,7 @@ +# Editors +.idea/ +.vscode/ +  # Byte-compiled / optimized / DLL files  __pycache__/  *.py[cod] @@ -18,6 +18,7 @@ pytest-cov = "*"  pytest-dependency = "*"  pre-commit = "*"  flake8 = "*" +flake8-docstrings = "*"  flake8-bugbear = "*"  flake8-import-order = "*"  flake8-tidy-imports = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 96627df..a7b5238 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@  {      "_meta": {          "hash": { -            "sha256": "f284c1e82cd45fd2a5352bc656ce21ad4066dd65d3a1ee0915775f1af1c0bd6f" +            "sha256": "5560465709134fb6096835f67c91567819bc6239a2f0182b2fca70ec3d0fb3f5"          },          "pipfile-spec": 6,          "requires": { @@ -32,11 +32,11 @@          },          "docker": {              "hashes": [ -                "sha256:2840ffb9dc3ef6d00876bde476690278ab13fa1f8ba9127ef855ac33d00c3152", -                "sha256:5831256da3477723362bc71a8df07b8cd8493e4a4a60cebd45580483edbe48ae" +                "sha256:0076504c42b6a671c8e7c252913f59852669f5f882522f4d320ec7613b853553", +                "sha256:d2c14d2cc7d54818897cc6f3cf73923c4e7dfe12f08f7bddda9dbea7fa82ea36"              ],              "index": "pypi", -            "version": "==3.7.0" +            "version": "==3.7.1"          },          "docker-pycreds": {              "hashes": [ @@ -83,10 +83,10 @@          },          "websocket-client": {              "hashes": [ -                "sha256:47a3ddf3ee7ecd4e2f81610bcdc7f44d5dd03b602b911d4ce991cd82310d3f3b", -                "sha256:f6029deea21218f2c771848935aa26c15699c831770f4fa66958bdaabff80ca0" +                "sha256:1151d5fb3a62dc129164292e1227655e4bbc5dd5340a5165dfae61128ec50aa9", +                "sha256:1fd5520878b68b84b5748bb30e592b10d0a91529d5383f74f4964e72b297fd3a"              ], -            "version": "==0.55.0" +            "version": "==0.56.0"          }      },      "develop": { @@ -184,6 +184,14 @@              "index": "pypi",              "version": "==18.8.0"          }, +        "flake8-docstrings": { +            "hashes": [ +                "sha256:4e0ce1476b64e6291520e5570cf12b05016dd4e8ae454b8a8a9a48bc5f84e1cd", +                "sha256:8436396b5ecad51a122a2c99ba26e5b4e623bf6e913b0fea0cb6c2c4050f91eb" +            ], +            "index": "pypi", +            "version": "==1.3.0" +        },          "flake8-import-order": {              "hashes": [                  "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543", @@ -192,6 +200,13 @@              "index": "pypi",              "version": "==0.18.1"          }, +        "flake8-polyfill": { +            "hashes": [ +                "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9", +                "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda" +            ], +            "version": "==1.0.2" +        },          "flake8-string-format": {              "hashes": [                  "sha256:68ea72a1a5b75e7018cae44d14f32473c798cf73d75cbaed86c6a9a907b770b2", @@ -417,6 +432,14 @@              ],              "version": "==2.5.0"          }, +        "pydocstyle": { +            "hashes": [ +                "sha256:2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8", +                "sha256:5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4", +                "sha256:ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039" +            ], +            "version": "==3.0.0" +        },          "pyflakes": {              "hashes": [                  "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", @@ -470,6 +493,13 @@              ],              "version": "==1.12.0"          }, +        "snowballstemmer": { +            "hashes": [ +                "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", +                "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" +            ], +            "version": "==1.2.1" +        },          "toml": {              "hashes": [                  "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", @@ -486,10 +516,10 @@          },          "werkzeug": {              "hashes": [ -                "sha256:590abe38f8be026d78457fe3b5200895b3543e58ac3fc1dd792c6333ea11af64", -                "sha256:ee11b0f0640c56fb491b43b38356c4b588b3202b415a1e03eacf1c5561c961cf" +                "sha256:96da23fa8ccecbc3ae832a83df5c722c11547d021637faacb0bec4dd2f4666c8", +                "sha256:ca5c2dcd367d6c0df87185b9082929d255358f5391923269335782b213d52655"              ], -            "version": "==0.15.0" +            "version": "==0.15.1"          },          "zipp": {              "hashes": [ @@ -5,6 +5,8 @@ from docker.errors import NotFound  def autodiscover(): +    """Search for the snekbox container and return its IPv4 address.""" +      container_names = ["rmq", "pdrmq", "snekbox_pdrmq_1"]      client = docker.from_env() @@ -15,13 +17,10 @@ def autodiscover():                  host = list(container.attrs.get('NetworkSettings').get('Networks').values())                  host = host[0]['IPAddress']                  return host -          except NotFound:              continue -          except Exception:              pass -            # print(traceback.format_exc())      return '127.0.0.1' @@ -4,43 +4,28 @@ import traceback  import pika  from pika.exceptions import ConnectionClosed -from config import EXCHANGE -from config import EXCHANGE_TYPE -from config import HOST -from config import PASSWORD -from config import PORT -from config import QUEUE -from config import ROUTING_KEY -from config import USERNAME +from config import EXCHANGE, EXCHANGE_TYPE, HOST, PASSWORD, PORT, QUEUE, ROUTING_KEY, USERNAME  from logs import log -class Rmq(object): +class Rmq: +    """Rabbit MQ (RMQ) implementation used for communication with the bot.""" -    def __init__(self, -                 username=USERNAME, -                 password=PASSWORD, -                 host=HOST, -                 port=PORT, -                 exchange_type=EXCHANGE_TYPE): - -        self.username = USERNAME -        self.password = PASSWORD -        self.host = HOST -        self.port = PORT -        self.exchange_type = EXCHANGE_TYPE -        self.credentials = pika.PlainCredentials(self.username, self.password) -        self.con_params = pika.ConnectionParameters(self.host, self.port, '/', self.credentials) +    def __init__(self): +        self.credentials = pika.PlainCredentials(USERNAME, PASSWORD) +        self.con_params = pika.ConnectionParameters(HOST, PORT, '/', self.credentials)          self.properties = pika.BasicProperties(content_type='text/plain', delivery_mode=1)      def _declare(self, channel, queue):          channel.queue_declare(              queue=queue, -            durable=False,                       # Do not commit messages to disk +            durable=False,  # Do not commit messages to disk              arguments={'x-message-ttl': 5000},  # Delete message automatically after x milliseconds -            auto_delete=True)                    # Delete queue when all connection are closed +            auto_delete=True)  # Delete queue when all connection are closed      def consume(self, queue=QUEUE, callback=None, thread_ws=None, run_once=False): +        """Subscribe to read from a RMQ channel.""" +          while True:              try:                  connection = pika.BlockingConnection(self.con_params) @@ -56,7 +41,7 @@ class Rmq(object):                              callback(ch, method, properties, body, thread_ws=thread_ws),                              queue=queue) -                    log.info(f"Connected to host: {self.host} port: {self.port} queue: {queue}") +                    log.info(f"Connected to host: {HOST} port: {PORT} queue: {queue}")                      if thread_ws:                          if not thread_ws.closed: @@ -77,19 +62,16 @@ class Rmq(object):              except ConnectionClosed:                  if thread_ws:                      if not thread_ws.closed: -                        log.error(f"Connection to {self.host} could not be established") +                        log.error(f"Connection to {HOST} could not be established")                          thread_ws.send('{"service": "disconnected"}')                          exit(1) -                log.error(f"Connection lost, reconnecting to {self.host}") +                log.error(f"Connection lost, reconnecting to {HOST}")              time.sleep(2) -    def publish(self, -                message, -                queue=QUEUE, -                routingkey=ROUTING_KEY, -                exchange=EXCHANGE): +    def publish(self, message, queue=QUEUE, routingkey=ROUTING_KEY, exchange=EXCHANGE): +        """Open a connection to publish (write) to a RMQ channel."""          try:              connection = pika.BlockingConnection(self.con_params) @@ -101,7 +83,7 @@ class Rmq(object):                  channel.exchange_declare(                      exchange=exchange, -                    exchange_type=self.exchange_type) +                    exchange_type=EXCHANGE_TYPE)                  channel.queue_bind(                      exchange=exchange, @@ -121,11 +103,11 @@ class Rmq(object):                      log.error(f"Message '{message}' not delivered")              except ConnectionClosed: -                log.error(f"Could not send message, connection to {self.host} was lost") +                log.error(f"Could not send message, connection to {HOST} was lost")                  exit(1)              finally:                  connection.close()          except ConnectionClosed: -            log.error(f"Could not connect to {self.host}") +            log.error(f"Could not connect to {HOST}") @@ -7,14 +7,15 @@ import sys  from rmq import Rmq -class Snekbox(object): +class Snekbox: +    """Core snekbox functionality, providing safe execution of Python code.""" +      def __init__(self,                   nsjail_binary='nsjail', -                 python_binary=os.path.dirname(sys.executable)+os.sep+'python3.6'): - +                 python_binary=os.path.dirname(sys.executable) + os.sep + 'python3.6'):          self.nsjail_binary = nsjail_binary          self.python_binary = python_binary -        self.nsjail_workaround() +        self._nsjail_workaround()      env = {          'PATH': ( @@ -27,13 +28,24 @@ class Snekbox(object):          'PYTHONDONTWRITEBYTECODE': '1',      } -    def nsjail_workaround(self): +    def _nsjail_workaround(self):          dirs = ['/sys/fs/cgroup/pids/NSJAIL', '/sys/fs/cgroup/memory/NSJAIL']          for d in dirs:              if not os.path.exists(d):                  os.makedirs(d)      def python3(self, cmd): +        """ +        Execute Python 3 code in a isolated environment. + +        The value of ``cmd`` is passed using '-c' to a Python +        interpreter that is started in a ``nsjail``, isolating it +        from the rest of the system. + +        Returns the output of executing the command (stdout) if +        successful, or a error message if the execution failed. +        """ +          args = [self.nsjail_binary, '-Mo',                  '--rlimit_as', '700',                  '--chroot', '/', @@ -87,6 +99,16 @@ class Snekbox(object):          return output      def execute(self, body): +        """ +        Handles execution of a raw JSON-formatted RMQ message, contained in ``body``. + +        The message metadata, including the Python code to be executed, is +        extracted from the message body. The code is then executed in the +        isolated environment, and the results of the execution published +        to RMQ. Once published, the system exits, since the snekboxes +        are created and disposed of per-execution. +        """ +          msg = body.decode('utf-8')          result = ''          snek_msg = json.loads(msg) @@ -102,6 +124,8 @@ class Snekbox(object):          exit(0)      def message_handler(self, ch, method, properties, body, thread_ws=None): +        """Spawns a daemon process that handles RMQ messages.""" +          p = multiprocessing.Process(target=self.execute, args=(body,))          p.daemon = True          p.start() @@ -3,8 +3,7 @@ import logging  import threading  import traceback -from flask import Flask -from flask import render_template +from flask import Flask, render_template  from flask_sockets import Sockets  from rmq import Rmq @@ -22,11 +21,15 @@ log = app.logger  @app.route('/')  def index(): +    """Root path returns standard index.html.""" +      return render_template('index.html')  @sockets.route('/ws/<snekboxid>')  def websocket_route(ws, snekboxid): +    """Opens a websocket that spawns and connects to a snekbox daemon.""" +      localdata = threading.local()      localdata.thread_ws = ws | 
