aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar scragly <[email protected]>2019-03-25 09:41:47 +1000
committerGravatar GitHub <[email protected]>2019-03-25 09:41:47 +1000
commit5bdf06c2fc86f8d8ab1ead63c30e856d9fead0cd (patch)
treeb71e0889a4ea6ed8d22852b9805bd13a60994f00
parentMerge pull request #11 from python-discord/add-flake8-extensions (diff)
parentMore formatting fixes (diff)
Merge pull request #10 from python-discord/flake8-updates
Flake8 Docstrings Co-authored-by: S. Co1 <[email protected]>
-rw-r--r--.flake816
-rw-r--r--.gitignore4
-rw-r--r--Pipfile1
-rw-r--r--Pipfile.lock50
-rw-r--r--config.py5
-rw-r--r--rmq.py54
-rw-r--r--snekbox.py34
-rw-r--r--snekweb.py7
8 files changed, 113 insertions, 58 deletions
diff --git a/.flake8 b/.flake8
index b2b4bb1..cc5f423 100644
--- a/.flake8
+++ b/.flake8
@@ -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
diff --git a/.gitignore b/.gitignore
index 894a44c..ac702d4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
+# Editors
+.idea/
+.vscode/
+
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
diff --git a/Pipfile b/Pipfile
index 69c0859..e881d52 100644
--- a/Pipfile
+++ b/Pipfile
@@ -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": [
diff --git a/config.py b/config.py
index 138c25c..5e4f648 100644
--- a/config.py
+++ b/config.py
@@ -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'
diff --git a/rmq.py b/rmq.py
index 72e0e9a..919ef19 100644
--- a/rmq.py
+++ b/rmq.py
@@ -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}")
diff --git a/snekbox.py b/snekbox.py
index 59147a8..f8d7c31 100644
--- a/snekbox.py
+++ b/snekbox.py
@@ -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()
diff --git a/snekweb.py b/snekweb.py
index 57d79e2..ff1a72c 100644
--- a/snekweb.py
+++ b/snekweb.py
@@ -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