From 07d15cf035dc651e198d78c6b7760fbd169c48bf Mon Sep 17 00:00:00 2001 From: Christian Burmeister Date: Mon, 20 May 2019 21:27:47 +0200 Subject: [PATCH 1/5] Update Dockerfile --- data/Dockerfiles/dockerapi/Dockerfile | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile index 67bb6b07..9f7498cf 100644 --- a/data/Dockerfiles/dockerapi/Dockerfile +++ b/data/Dockerfiles/dockerapi/Dockerfile @@ -1,11 +1,13 @@ FROM alpine:3.9 LABEL maintainer "Andre Peters " -RUN apk add -U --no-cache python2 python-dev py-pip gcc musl-dev tzdata openssl-dev libffi-dev \ - && pip2 install --upgrade pip \ - && pip2 install --upgrade docker==3.0.1 flask flask-restful pyOpenSSL \ - && apk del python-dev py2-pip gcc +WORKDIR /app + +RUN apk add -U --no-cache python3 tzdata python3-dev gcc musl-dev openssl-dev libffi-dev openssl\ + && pip3 install --upgrade pip \ + && pip3 install --upgrade docker flask flask-restful pyOpenSSL \ + && apk del python3-dev gcc musl-dev openssl-dev libffi-dev COPY server.py / -CMD ["python2", "-u", "/server.py"] +CMD ["python3", "-u", "/server.py"] From 9afa2730acb2b9a9e662e45e0227c5bef0dd0ac3 Mon Sep 17 00:00:00 2001 From: Christian Burmeister Date: Mon, 20 May 2019 21:30:40 +0200 Subject: [PATCH 2/5] Update server.py --- data/Dockerfiles/dockerapi/server.py | 641 +++++++++++++-------------- 1 file changed, 320 insertions(+), 321 deletions(-) diff --git a/data/Dockerfiles/dockerapi/server.py b/data/Dockerfiles/dockerapi/server.py index d38775db..85ebe466 100644 --- a/data/Dockerfiles/dockerapi/server.py +++ b/data/Dockerfiles/dockerapi/server.py @@ -1,10 +1,11 @@ +#!/usr/bin/env python3 + from flask import Flask from flask_restful import Resource, Api from flask import jsonify from flask import Response from flask import request from threading import Thread -from OpenSSL import crypto import docker import uuid import signal @@ -14,6 +15,8 @@ import re import sys import ssl import socket +import subprocess +import traceback docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto') app = Flask(__name__) @@ -43,261 +46,316 @@ class container_get(Resource): class container_post(Resource): def post(self, container_id, post_action): if container_id and container_id.isalnum() and post_action: - if post_action == 'stop': - try: - for container in docker_client.containers.list(all=True, filters={"id": container_id}): - container.stop() - return jsonify(type='success', msg='command completed successfully') - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif post_action == 'start': - try: - for container in docker_client.containers.list(all=True, filters={"id": container_id}): - container.start() - return jsonify(type='success', msg='command completed successfully') - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif post_action == 'restart': - try: - for container in docker_client.containers.list(all=True, filters={"id": container_id}): - container.restart() - return jsonify(type='success', msg='command completed successfully') - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif post_action == 'top': - try: - for container in docker_client.containers.list(all=True, filters={"id": container_id}): - return jsonify(type='success', msg=container.top()) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif post_action == 'stats': - try: - for container in docker_client.containers.list(all=True, filters={"id": container_id}): - return jsonify(type='success', msg=container.stats(decode=True, stream=False)) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif post_action == 'exec': - - if not request.json or not 'cmd' in request.json: - return jsonify(type='danger', msg='cmd is missing') - - if request.json['cmd'] == 'mailq': - if 'items' in request.json: - r = re.compile("^[0-9a-fA-F]+$") - filtered_qids = filter(r.match, request.json['items']) - if filtered_qids: - if request.json['task'] == 'delete': - flagged_qids = ['-d %s' % i for i in filtered_qids] - sanitized_string = str(' '.join(flagged_qids)); - try: - for container in docker_client.containers.list(filters={"id": container_id}): - postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) - return exec_run_handler('generic', postsuper_r) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - if request.json['task'] == 'hold': - flagged_qids = ['-h %s' % i for i in filtered_qids] - sanitized_string = str(' '.join(flagged_qids)); - try: - for container in docker_client.containers.list(filters={"id": container_id}): - postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) - return exec_run_handler('generic', postsuper_r) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - if request.json['task'] == 'unhold': - flagged_qids = ['-H %s' % i for i in filtered_qids] - sanitized_string = str(' '.join(flagged_qids)); - try: - for container in docker_client.containers.list(filters={"id": container_id}): - postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) - return exec_run_handler('generic', postsuper_r) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - if request.json['task'] == 'deliver': - flagged_qids = ['-i %s' % i for i in filtered_qids] - try: - for container in docker_client.containers.list(filters={"id": container_id}): - for i in flagged_qids: - postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix') - # todo: check each exit code - return jsonify(type='success', msg=str("Scheduled immediate delivery")) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - elif request.json['task'] == 'list': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix') - return exec_run_handler('utf8_text_only', mailq_return) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - elif request.json['task'] == 'flush': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix') - return exec_run_handler('generic', postqueue_r) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - elif request.json['task'] == 'super_delete': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"]) - return exec_run_handler('generic', postsuper_r) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif request.json['cmd'] == 'system': - if request.json['task'] == 'fts_rescan': - if 'username' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm fts rescan -u '" + request.json['username'].replace("'", "'\\''") + "'"], user='vmail') - if rescan_return.exit_code == 0: - return jsonify(type='success', msg='fts_rescan: rescan triggered') - else: - return jsonify(type='warning', msg='fts_rescan error') - except Exception as e: - return jsonify(type='danger', msg=str(e)) - if 'all' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm fts rescan -A"], user='vmail') - if rescan_return.exit_code == 0: - return jsonify(type='success', msg='fts_rescan: rescan triggered') - else: - return jsonify(type='warning', msg='fts_rescan error') - except Exception as e: - return jsonify(type='danger', msg=str(e)) - elif request.json['task'] == 'df': - if 'dir' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request.json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody') - if df_return.exit_code == 0: - return df_return.output.rstrip() - else: - return "0,0,0,0,0,0" - except Exception as e: - return jsonify(type='danger', msg=str(e)) - elif request.json['task'] == 'mysql_upgrade': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - sql_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='mysql') - upgrade_cmd = "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n" - sql_socket = sql_shell.output; - try : - sql_socket.sendall(upgrade_cmd.encode('utf-8')) - sql_socket.shutdown(socket.SHUT_WR) - except socket.error: - return jsonify(type='danger', msg=str('socket error')) - worker_response = recv_socket_data(sql_socket) - matched = False - for line in worker_response.split("\n"): - if 'is already upgraded to' in line: - matched = True - if matched: - return jsonify(type='success', msg='mysql_upgrade: already upgraded') - else: - container.restart() - return jsonify(type='warning', msg='mysql_upgrade: upgrade was applied') - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif request.json['cmd'] == 'reload': - if request.json['task'] == 'dovecot': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - reload_return = container.exec_run(["/bin/bash", "-c", "/usr/local/sbin/dovecot reload"]) - return exec_run_handler('generic', reload_return) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - if request.json['task'] == 'postfix': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"]) - return exec_run_handler('generic', reload_return) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - if request.json['task'] == 'nginx': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"]) - return exec_run_handler('generic', reload_return) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif request.json['cmd'] == 'sieve': - if request.json['task'] == 'list': - if 'username' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"]) - return exec_run_handler('utf8_text_only', sieve_return) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - elif request.json['task'] == 'print': - if 'username' in request.json and 'script_name' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"]) - return exec_run_handler('utf8_text_only', sieve_return) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif request.json['cmd'] == 'maildir': - if request.json['task'] == 'cleanup': - if 'maildir' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - sane_name = re.sub(r'\W+', '', request.json['maildir']) - maildir_cleanup = container.exec_run(["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"], user='vmail') - return exec_run_handler('generic', maildir_cleanup) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif request.json['cmd'] == 'rspamd': - if request.json['task'] == 'worker_password': - if 'raw' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - worker_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='_rspamd') - worker_cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null\n" - worker_socket = worker_shell.output; - try : - worker_socket.sendall(worker_cmd.encode('utf-8')) - worker_socket.shutdown(socket.SHUT_WR) - except socket.error: - return jsonify(type='danger', msg=str('socket error')) - worker_response = recv_socket_data(worker_socket) - matched = False - for line in worker_response.split("\n"): - if '$2$' in line: - matched = True - hash = line.strip() - hash_out = re.search('\$2\$.+$', hash).group(0) - f = open("/access.inc", "w") - f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip()) + '";\n') - f.close() - container.restart() - if matched: - return jsonify(type='success', msg='command completed successfully') - else: - return jsonify(type='danger', msg='command did not complete') - except Exception as e: - return jsonify(type='danger', msg=str(e)) + try: + """Dispatch container_post api call""" + if post_action == 'exec': + if not request.json or not 'cmd' in request.json: + return jsonify(type='danger', msg='cmd is missing') + if not request.json or not 'task' in request.json: + return jsonify(type='danger', msg='task is missing') + api_call_method_name = '__'.join(['container_post', str(post_action), str(request.json['cmd']), str(request.json['task']) ]) else: - return jsonify(type='danger', msg='Unknown command') + api_call_method_name = '__'.join(['container_post', str(post_action) ]) - else: - return jsonify(type='danger', msg='invalid action') + api_call_method = getattr(self, api_call_method_name, lambda container_id: jsonify(type='danger', msg='container_post - unknown api call')) + + + print("api call: %s, container_id: %s" % (api_call_method_name, container_id)) + return api_call_method(container_id) + except Exception as e: + print("error - container_post: %s" % str(e)) + return jsonify(type='danger', msg=str(e)) else: - return jsonify(type='danger', msg='invalid container id or missing action') + return jsonify(type='danger', msg='invalid container id or missing action') + + + # api call: container_post - post_action: stop + def container_post__stop(self, container_id): + for container in docker_client.containers.list(all=True, filters={"id": container_id}): + container.stop() + return jsonify(type='success', msg='command completed successfully') + + + # api call: container_post - post_action: start + def container_post__start(self, container_id): + for container in docker_client.containers.list(all=True, filters={"id": container_id}): + container.start() + return jsonify(type='success', msg='command completed successfully') + + + # api call: container_post - post_action: restart + def container_post__restart(self, container_id): + for container in docker_client.containers.list(all=True, filters={"id": container_id}): + container.restart() + return jsonify(type='success', msg='command completed successfully') + + + # api call: container_post - post_action: top + def container_post__top(self, container_id): + for container in docker_client.containers.list(all=True, filters={"id": container_id}): + return jsonify(type='success', msg=container.top()) + + + # api call: container_post - post_action: stats + def container_post__stats(self, container_id): + for container in docker_client.containers.list(all=True, filters={"id": container_id}): + for stat in container.stats(decode=True, stream=True): + return jsonify(type='success', msg=stat ) + + + # api call: container_post - post_action: exec - cmd: mailq - task: delete + def container_post__exec__mailq__delete(self, container_id): + if 'items' in request.json: + r = re.compile("^[0-9a-fA-F]+$") + filtered_qids = filter(r.match, request.json['items']) + if filtered_qids: + flagged_qids = ['-d %s' % i for i in filtered_qids] + sanitized_string = str(' '.join(flagged_qids)); + + for container in docker_client.containers.list(filters={"id": container_id}): + postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) + return exec_run_handler('generic', postsuper_r) + + + # api call: container_post - post_action: exec - cmd: mailq - task: hold + def container_post__exec__mailq__hold(self, container_id): + if 'items' in request.json: + r = re.compile("^[0-9a-fA-F]+$") + filtered_qids = filter(r.match, request.json['items']) + if filtered_qids: + flagged_qids = ['-h %s' % i for i in filtered_qids] + sanitized_string = str(' '.join(flagged_qids)); + + for container in docker_client.containers.list(filters={"id": container_id}): + postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) + return exec_run_handler('generic', postsuper_r) + + + # api call: container_post - post_action: exec - cmd: mailq - task: unhold + def container_post__exec__mailq__unhold(self, container_id): + if 'items' in request.json: + r = re.compile("^[0-9a-fA-F]+$") + filtered_qids = filter(r.match, request.json['items']) + if filtered_qids: + flagged_qids = ['-H %s' % i for i in filtered_qids] + sanitized_string = str(' '.join(flagged_qids)); + + for container in docker_client.containers.list(filters={"id": container_id}): + postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) + return exec_run_handler('generic', postsuper_r) + + + # api call: container_post - post_action: exec - cmd: mailq - task: deliver + def container_post__exec__mailq__deliver(self, container_id): + if 'items' in request.json: + r = re.compile("^[0-9a-fA-F]+$") + filtered_qids = filter(r.match, request.json['items']) + if filtered_qids: + flagged_qids = ['-i %s' % i for i in filtered_qids] + + for container in docker_client.containers.list(filters={"id": container_id}): + for i in flagged_qids: + postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix') + # todo: check each exit code + return jsonify(type='success', msg=str("Scheduled immediate delivery")) + + + # api call: container_post - post_action: exec - cmd: mailq - task: list + def container_post__exec__mailq__list(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix') + return exec_run_handler('utf8_text_only', mailq_return) + + + # api call: container_post - post_action: exec - cmd: mailq - task: flush + def container_post__exec__mailq__flush(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix') + return exec_run_handler('generic', postqueue_r) + + + # api call: container_post - post_action: exec - cmd: mailq - task: super_delete + def container_post__exec__mailq__super_delete(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"]) + return exec_run_handler('generic', postsuper_r) + + + # api call: container_post - post_action: exec - cmd: system - task: fts_rescan + def container_post__exec__system__fts_rescan(self, container_id): + if 'username' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm fts rescan -u '" + request.json['username'].replace("'", "'\\''") + "'"], user='vmail') + if rescan_return.exit_code == 0: + return jsonify(type='success', msg='fts_rescan: rescan triggered') + else: + return jsonify(type='warning', msg='fts_rescan error') + + if 'all' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm fts rescan -A"], user='vmail') + if rescan_return.exit_code == 0: + return jsonify(type='success', msg='fts_rescan: rescan triggered') + else: + return jsonify(type='warning', msg='fts_rescan error') + + + # api call: container_post - post_action: exec - cmd: system - task: df + def container_post__exec__system__df(self, container_id): + if 'dir' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request.json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody') + if df_return.exit_code == 0: + return df_return.output.decode('utf-8').rstrip() + else: + return "0,0,0,0,0,0" + + + # api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade + def container_post__exec__system__mysql_upgrade(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + cmd = "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n" + cmd_response = exec_cmd_container(container, cmd, user='mysql') + + matched = False + for line in cmd_response.split("\n"): + if 'is already upgraded to' in line: + matched = True + if matched: + return jsonify(type='success', msg='mysql_upgrade: already upgraded') + else: + container.restart() + return jsonify(type='warning', msg='mysql_upgrade: upgrade was applied') + + + # api call: container_post - post_action: exec - cmd: reload - task: dovecot + def container_post__exec__reload__dovecot(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + reload_return = container.exec_run(["/bin/bash", "-c", "/usr/local/sbin/dovecot reload"]) + return exec_run_handler('generic', reload_return) + + + # api call: container_post - post_action: exec - cmd: reload - task: postfix + def container_post__exec__reload__postfix(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"]) + return exec_run_handler('generic', reload_return) + + + # api call: container_post - post_action: exec - cmd: reload - task: nginx + def container_post__exec__reload__nginx(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"]) + return exec_run_handler('generic', reload_return) + + + # api call: container_post - post_action: exec - cmd: sieve - task: list + def container_post__exec__sieve__list(self, container_id): + if 'username' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"]) + return exec_run_handler('utf8_text_only', sieve_return) + + + # api call: container_post - post_action: exec - cmd: sieve - task: print + def container_post__exec__sieve__print(self, container_id): + if 'username' in request.json and 'script_name' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + cmd = ["/bin/bash", "-c", "/usr/local/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"] + sieve_return = container.exec_run(cmd) + return exec_run_handler('utf8_text_only', sieve_return) + + + # api call: container_post - post_action: exec - cmd: maildir - task: cleanup + def container_post_exec__maildir__cleanup(self, container_id): + if 'maildir' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + sane_name = re.sub(r'\W+', '', request.json['maildir']) + cmd = ["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"] + maildir_cleanup = container.exec_run(cmd, user='vmail') + return exec_run_handler('generic', maildir_cleanup) + + + + # api call: container_post - post_action: exec - cmd: rspamd - task: worker_password + def container_post__exec__rspamd__worker_password(self, container_id): + if 'raw' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null" + cmd_response = exec_cmd_container(container, cmd, user="_rspamd") + + matched = False + for line in cmd_response.split("\n"): + if '$2$' in line: + hash = line.strip() + hash_out = re.search('\$2\$.+$', hash).group(0) + rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip()) + + rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc" + cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename) + cmd_response = exec_cmd_container(container, cmd, user="_rspamd") + + if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response: + container.restart() + matched = True + + if matched: + return jsonify(type='success', msg='command completed successfully') + else: + return jsonify(type='danger', msg='command did not complete') + + +def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"): + + def recv_socket_data(c_socket, timeout): + c_socket.setblocking(0) + total_data=[]; + data=''; + begin=time.time() + while True: + if total_data and time.time()-begin > timeout: + break + elif time.time()-begin > timeout*2: + break + try: + data = c_socket.recv(8192) + if data: + total_data.append(data.decode('utf-8')) + #change the beginning time for measurement + begin=time.time() + else: + #sleep for sometime to indicate a gap + time.sleep(0.1) + break + except: + pass + return ''.join(total_data) + + try : + socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock + if not cmd.endswith("\n"): + cmd = cmd + "\n" + socket.send(cmd.encode('utf-8')) + data = recv_socket_data(socket, timeout) + socket.close() + return data + + except Exception as e: + print("error - exec_cmd_container: %s" % str(e)) + traceback.print_exc(file=sys.stdout) + +def exec_run_handler(type, output): + if type == 'generic': + if output.exit_code == 0: + return jsonify(type='success', msg='command completed successfully') + else: + return jsonify(type='danger', msg='command failed: ' + output.output.decode('utf-8')) + if type == 'utf8_text_only': + r = Response(response=output.output.decode('utf-8'), status=200, mimetype="text/plain") + r.headers["Content-Type"] = "text/plain; charset=utf-8" + return r class GracefulKiller: kill_now = False @@ -308,84 +366,26 @@ class GracefulKiller: def exit_gracefully(self, signum, frame): self.kill_now = True +def create_self_signed_cert(): + process = subprocess.Popen( + "openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout /app/dockerapi_key.pem -out /app/dockerapi_cert.pem -subj /CN=dockerapi/O=mailcow -addext subjectAltName=DNS:dockerapi".split(), + stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell=False + ) + process.wait() + def startFlaskAPI(): create_self_signed_cert() try: ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ctx.check_hostname = False - ctx.load_cert_chain(certfile='/cert.pem', keyfile='/key.pem') + ctx.load_cert_chain(certfile='/app/dockerapi_cert.pem', keyfile='/app/dockerapi_key.pem') except: - print "Cannot initialize TLS, retrying in 5s..." + print ("Cannot initialize TLS, retrying in 5s...") time.sleep(5) app.run(debug=False, host='0.0.0.0', port=443, threaded=True, ssl_context=ctx) -def recv_socket_data(c_socket, timeout=10): - c_socket.setblocking(0) - total_data=[]; - data=''; - begin=time.time() - while True: - if total_data and time.time()-begin > timeout: - break - elif time.time()-begin > timeout*2: - break - try: - data = c_socket.recv(8192) - if data: - total_data.append(data) - #change the beginning time for measurement - begin=time.time() - else: - #sleep for sometime to indicate a gap - time.sleep(0.1) - break - except: - pass - return ''.join(total_data) - -def exec_run_handler(type, output): - if type == 'generic': - if output.exit_code == 0: - return jsonify(type='success', msg='command completed successfully') - else: - return jsonify(type='danger', msg='command failed: ' + output.output) - if type == 'utf8_text_only': - r = Response(response=output.output, status=200, mimetype="text/plain") - r.headers["Content-Type"] = "text/plain; charset=utf-8" - return r - -def create_self_signed_cert(): - success = False - while not success: - try: - pkey = crypto.PKey() - pkey.generate_key(crypto.TYPE_RSA, 2048) - cert = crypto.X509() - cert.get_subject().O = "mailcow" - cert.get_subject().CN = "dockerapi" - cert.set_serial_number(int(uuid.uuid4())) - cert.gmtime_adj_notBefore(0) - cert.gmtime_adj_notAfter(10*365*24*60*60) - cert.set_issuer(cert.get_subject()) - cert.set_pubkey(pkey) - cert.sign(pkey, 'sha512') - cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) - pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey) - with os.fdopen(os.open('/cert.pem', os.O_WRONLY | os.O_CREAT, 0o644), 'w') as handle: - handle.write(cert) - with os.fdopen(os.open('/key.pem', os.O_WRONLY | os.O_CREAT, 0o600), 'w') as handle: - handle.write(pkey) - success = True - except: - time.sleep(1) - try: - os.remove('/cert.pem') - os.remove('/key.pem') - except OSError: - pass - api.add_resource(containers_get, '/containers/json') -api.add_resource(container_get, '/containers//json') +api.add_resource(container_get, '/containers//json') api.add_resource(container_post, '/containers//') if __name__ == '__main__': @@ -397,5 +397,4 @@ if __name__ == '__main__': time.sleep(1) if killer.kill_now: break - print "Stopping dockerapi-mailcow" - + print ("Stopping dockerapi-mailcow") From 7032ea00d0022a93f840cafa0914466cb59e9c8d Mon Sep 17 00:00:00 2001 From: Christian Burmeister Date: Tue, 21 May 2019 19:51:03 +0200 Subject: [PATCH 3/5] add "--virtual .build-dependencies" --- data/Dockerfiles/dockerapi/Dockerfile | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile index 9f7498cf..6d0a0b62 100644 --- a/data/Dockerfiles/dockerapi/Dockerfile +++ b/data/Dockerfiles/dockerapi/Dockerfile @@ -3,11 +3,12 @@ LABEL maintainer "Andre Peters " WORKDIR /app -RUN apk add -U --no-cache python3 tzdata python3-dev gcc musl-dev openssl-dev libffi-dev openssl\ - && pip3 install --upgrade pip \ - && pip3 install --upgrade docker flask flask-restful pyOpenSSL \ - && apk del python3-dev gcc musl-dev openssl-dev libffi-dev +RUN apk add --update --no-cache python3 openssl tzdata \ + && apk add --update --no-cache --virtual .build-dependencies gcc python3-dev musl-dev libffi-dev openssl-dev \ + && pip3 install --upgrade pip \ + && pip3 install --upgrade docker flask flask-restful pyOpenSSL \ + && apk del .build-dependencies -COPY server.py / +COPY server.py /app/ -CMD ["python3", "-u", "/server.py"] +CMD ["python3", "-u", "/app/server.py"] From cd719350018cd75576dafd29ae48aa2c58d17706 Mon Sep 17 00:00:00 2001 From: Christian Burmeister Date: Tue, 21 May 2019 19:58:18 +0200 Subject: [PATCH 4/5] Remove pyOpenSSL --- data/Dockerfiles/dockerapi/Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile index 6d0a0b62..3384e4a4 100644 --- a/data/Dockerfiles/dockerapi/Dockerfile +++ b/data/Dockerfiles/dockerapi/Dockerfile @@ -4,10 +4,8 @@ LABEL maintainer "Andre Peters " WORKDIR /app RUN apk add --update --no-cache python3 openssl tzdata \ - && apk add --update --no-cache --virtual .build-dependencies gcc python3-dev musl-dev libffi-dev openssl-dev \ && pip3 install --upgrade pip \ - && pip3 install --upgrade docker flask flask-restful pyOpenSSL \ - && apk del .build-dependencies + && pip3 install --upgrade docker flask flask-restful COPY server.py /app/ From a8e96c57b156ea55c8a9fa3792744aa268460af4 Mon Sep 17 00:00:00 2001 From: Christian Burmeister Date: Wed, 22 May 2019 23:05:07 +0200 Subject: [PATCH 5/5] fix typo - container_post__exec__maildir__cleanup --- data/Dockerfiles/dockerapi/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/Dockerfiles/dockerapi/server.py b/data/Dockerfiles/dockerapi/server.py index 85ebe466..897ae9da 100644 --- a/data/Dockerfiles/dockerapi/server.py +++ b/data/Dockerfiles/dockerapi/server.py @@ -269,7 +269,7 @@ class container_post(Resource): # api call: container_post - post_action: exec - cmd: maildir - task: cleanup - def container_post_exec__maildir__cleanup(self, container_id): + def container_post__exec__maildir__cleanup(self, container_id): if 'maildir' in request.json: for container in docker_client.containers.list(filters={"id": container_id}): sane_name = re.sub(r'\W+', '', request.json['maildir'])