server.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. from flask import Flask
  2. from flask_restful import Resource, Api
  3. from flask import jsonify
  4. from flask import request
  5. from threading import Thread
  6. from OpenSSL import crypto
  7. import docker
  8. import uuid
  9. import signal
  10. import time
  11. import os
  12. import re
  13. import sys
  14. import ssl
  15. import socket
  16. docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
  17. app = Flask(__name__)
  18. api = Api(app)
  19. class containers_get(Resource):
  20. def get(self):
  21. containers = {}
  22. try:
  23. for container in docker_client.containers.list(all=True):
  24. containers.update({container.attrs['Id']: container.attrs})
  25. return containers
  26. except Exception as e:
  27. return jsonify(type='danger', msg=str(e))
  28. class container_get(Resource):
  29. def get(self, container_id):
  30. if container_id and container_id.isalnum():
  31. try:
  32. for container in docker_client.containers.list(all=True, filters={"id": container_id}):
  33. return container.attrs
  34. except Exception as e:
  35. return jsonify(type='danger', msg=str(e))
  36. else:
  37. return jsonify(type='danger', msg='no or invalid id defined')
  38. class container_post(Resource):
  39. def post(self, container_id, post_action):
  40. if container_id and container_id.isalnum() and post_action:
  41. if post_action == 'stop':
  42. try:
  43. for container in docker_client.containers.list(all=True, filters={"id": container_id}):
  44. container.stop()
  45. return jsonify(type='success', msg='command completed successfully')
  46. except Exception as e:
  47. return jsonify(type='danger', msg=str(e))
  48. elif post_action == 'start':
  49. try:
  50. for container in docker_client.containers.list(all=True, filters={"id": container_id}):
  51. container.start()
  52. return jsonify(type='success', msg='command completed successfully')
  53. except Exception as e:
  54. return jsonify(type='danger', msg=str(e))
  55. elif post_action == 'restart':
  56. try:
  57. for container in docker_client.containers.list(all=True, filters={"id": container_id}):
  58. container.restart()
  59. return jsonify(type='success', msg='command completed successfully')
  60. except Exception as e:
  61. return jsonify(type='danger', msg=str(e))
  62. elif post_action == 'exec':
  63. if not request.json or not 'cmd' in request.json:
  64. return jsonify(type='danger', msg='cmd is missing')
  65. if request.json['cmd'] == 'df' and request.json['dir']:
  66. try:
  67. for container in docker_client.containers.list(filters={"id": container_id}):
  68. # Should be changed to be able to validate a path
  69. directory = re.sub('[^0-9a-zA-Z/]+', '', request.json['dir'])
  70. df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H " + directory + " | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
  71. if df_return.exit_code == 0:
  72. return df_return.output.rstrip()
  73. else:
  74. return "0,0,0,0,0,0"
  75. except Exception as e:
  76. return jsonify(type='danger', msg=str(e))
  77. elif request.json['cmd'] == 'sieve_list' and request.json['username']:
  78. try:
  79. for container in docker_client.containers.list(filters={"id": container_id}):
  80. sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"], user='vmail')
  81. return sieve_return.output
  82. except Exception as e:
  83. return jsonify(type='danger', msg=str(e))
  84. elif request.json['cmd'] == 'sieve_print' and request.json['script_name'] and request.json['username']:
  85. try:
  86. for container in docker_client.containers.list(filters={"id": container_id}):
  87. sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"], user='vmail')
  88. return sieve_return.output
  89. except Exception as e:
  90. return jsonify(type='danger', msg=str(e))
  91. # not in use...
  92. elif request.json['cmd'] == 'mail_crypt_generate' and request.json['username'] and request.json['old_password'] and request.json['new_password']:
  93. try:
  94. for container in docker_client.containers.list(filters={"id": container_id}):
  95. # create if missing
  96. crypto_generate = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm mailbox cryptokey generate -u '" + request.json['username'].replace("'", "'\\''") + "' -URf"], user='vmail')
  97. if crypto_generate.exit_code == 0:
  98. # open a shell, bind stdin and return socket
  99. cryptokey_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='vmail')
  100. # command to be piped to shell
  101. cryptokey_cmd = "/usr/local/bin/doveadm mailbox cryptokey password -u '" + request.json['username'].replace("'", "'\\''") + "' -n '" + request.json['new_password'].replace("'", "'\\''") + "' -o '" + request.json['old_password'].replace("'", "'\\''") + "'\n"
  102. # socket is .output
  103. cryptokey_socket = cryptokey_shell.output;
  104. try :
  105. # send command utf-8 encoded
  106. cryptokey_socket.sendall(cryptokey_cmd.encode('utf-8'))
  107. # we won't send more data than this
  108. cryptokey_socket.shutdown(socket.SHUT_WR)
  109. except socket.error:
  110. # exit on socket error
  111. return jsonify(type='danger', msg=str('socket error'))
  112. # read response
  113. cryptokey_response = recv_socket_data(cryptokey_socket)
  114. crypto_error = re.search('dcrypt_key_load_private.+failed.+error', cryptokey_response)
  115. if crypto_error is not None:
  116. return jsonify(type='danger', msg=str("dcrypt_key_load_private error"))
  117. return jsonify(type='success', msg=str("key pair generated"))
  118. else:
  119. return jsonify(type='danger', msg=str(crypto_generate.output))
  120. except Exception as e:
  121. return jsonify(type='danger', msg=str(e))
  122. elif request.json['cmd'] == 'maildir_cleanup' and request.json['maildir']:
  123. try:
  124. for container in docker_client.containers.list(filters={"id": container_id}):
  125. sane_name = re.sub(r'\W+', '', request.json['maildir'])
  126. maildir_cleanup = container.exec_run(["/bin/bash", "-c", "/bin/mv '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'"], user='vmail')
  127. if maildir_cleanup.exit_code == 0:
  128. return jsonify(type='success', msg=str("moved to garbage"))
  129. else:
  130. return jsonify(type='danger', msg=str(maildir_cleanup.output))
  131. except Exception as e:
  132. return jsonify(type='danger', msg=str(e))
  133. elif request.json['cmd'] == 'worker_password' and request.json['raw']:
  134. try:
  135. for container in docker_client.containers.list(filters={"id": container_id}):
  136. worker_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='_rspamd')
  137. worker_cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null\n"
  138. worker_socket = worker_shell.output;
  139. try :
  140. worker_socket.sendall(worker_cmd.encode('utf-8'))
  141. worker_socket.shutdown(socket.SHUT_WR)
  142. except socket.error:
  143. return jsonify(type='danger', msg=str('socket error'))
  144. worker_response = recv_socket_data(worker_socket)
  145. matched = False
  146. for line in worker_response.split("\n"):
  147. if '$2$' in line:
  148. matched = True
  149. hash = line.strip()
  150. hash_out = re.search('\$2\$.+$', hash).group(0)
  151. f = open("/access.inc", "w")
  152. f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip()) + '";\n')
  153. f.close()
  154. container.restart()
  155. if matched:
  156. return jsonify(type='success', msg='command completed successfully')
  157. else:
  158. return jsonify(type='danger', msg='command did not complete')
  159. except Exception as e:
  160. return jsonify(type='danger', msg=str(e))
  161. elif request.json['cmd'] == 'mailman_password' and request.json['email'] and request.json['passwd']:
  162. try:
  163. for container in docker_client.containers.list(filters={"id": container_id}):
  164. add_su = container.exec_run(["/bin/bash", "-c", "/opt/mm_web/add_su.py '" + request.json['passwd'].replace("'", "'\\''") + "' '" + request.json['email'].replace("'", "'\\''") + "'"], user='mailman')
  165. if add_su.exit_code == 0:
  166. return jsonify(type='success', msg='command completed successfully')
  167. else:
  168. return jsonify(type='danger', msg='command did not complete, exit code was ' + int(add_su.exit_code))
  169. except Exception as e:
  170. return jsonify(type='danger', msg=str(e))
  171. else:
  172. return jsonify(type='danger', msg='Unknown command')
  173. else:
  174. return jsonify(type='danger', msg='invalid action')
  175. else:
  176. return jsonify(type='danger', msg='invalid container id or missing action')
  177. class GracefulKiller:
  178. kill_now = False
  179. def __init__(self):
  180. signal.signal(signal.SIGINT, self.exit_gracefully)
  181. signal.signal(signal.SIGTERM, self.exit_gracefully)
  182. def exit_gracefully(self, signum, frame):
  183. self.kill_now = True
  184. def startFlaskAPI():
  185. create_self_signed_cert()
  186. try:
  187. ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
  188. ctx.check_hostname = False
  189. ctx.load_cert_chain(certfile='/cert.pem', keyfile='/key.pem')
  190. except:
  191. print "Cannot initialize TLS, retrying in 5s..."
  192. time.sleep(5)
  193. app.run(debug=False, host='0.0.0.0', port=443, threaded=True, ssl_context=ctx)
  194. def recv_socket_data(c_socket, timeout=10):
  195. c_socket.setblocking(0)
  196. total_data=[];
  197. data='';
  198. begin=time.time()
  199. while True:
  200. if total_data and time.time()-begin > timeout:
  201. break
  202. elif time.time()-begin > timeout*2:
  203. break
  204. try:
  205. data = c_socket.recv(8192)
  206. if data:
  207. total_data.append(data)
  208. #change the beginning time for measurement
  209. begin=time.time()
  210. else:
  211. #sleep for sometime to indicate a gap
  212. time.sleep(0.1)
  213. break
  214. except:
  215. pass
  216. return ''.join(total_data)
  217. def create_self_signed_cert():
  218. pkey = crypto.PKey()
  219. pkey.generate_key(crypto.TYPE_RSA, 2048)
  220. cert = crypto.X509()
  221. cert.get_subject().O = "mailcow"
  222. cert.get_subject().CN = "dockerapi"
  223. cert.set_serial_number(int(uuid.uuid4()))
  224. cert.gmtime_adj_notBefore(0)
  225. cert.gmtime_adj_notAfter(10*365*24*60*60)
  226. cert.set_issuer(cert.get_subject())
  227. cert.set_pubkey(pkey)
  228. cert.sign(pkey, 'sha512')
  229. cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
  230. pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
  231. with os.fdopen(os.open('/cert.pem', os.O_WRONLY | os.O_CREAT, 0o644), 'w') as handle:
  232. handle.write(cert)
  233. with os.fdopen(os.open('/key.pem', os.O_WRONLY | os.O_CREAT, 0o600), 'w') as handle:
  234. handle.write(pkey)
  235. api.add_resource(containers_get, '/containers/json')
  236. api.add_resource(container_get, '/containers/<string:container_id>/json')
  237. api.add_resource(container_post, '/containers/<string:container_id>/<string:post_action>')
  238. if __name__ == '__main__':
  239. api_thread = Thread(target=startFlaskAPI)
  240. api_thread.daemon = True
  241. api_thread.start()
  242. killer = GracefulKiller()
  243. while True:
  244. time.sleep(1)
  245. if killer.kill_now:
  246. break
  247. print "Stopping dockerapi-mailcow"