server.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. from flask import Flask
  2. from flask_restful import Resource, Api
  3. from flask import jsonify
  4. from flask import Response
  5. from flask import request
  6. from threading import Thread
  7. from OpenSSL import crypto
  8. import docker
  9. import uuid
  10. import signal
  11. import time
  12. import os
  13. import re
  14. import sys
  15. import ssl
  16. import socket
  17. docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
  18. app = Flask(__name__)
  19. api = Api(app)
  20. class containers_get(Resource):
  21. def get(self):
  22. containers = {}
  23. try:
  24. for container in docker_client.containers.list(all=True):
  25. containers.update({container.attrs['Id']: container.attrs})
  26. return containers
  27. except Exception as e:
  28. return jsonify(type='danger', msg=str(e))
  29. class container_get(Resource):
  30. def get(self, container_id):
  31. if container_id and container_id.isalnum():
  32. try:
  33. for container in docker_client.containers.list(all=True, filters={"id": container_id}):
  34. return container.attrs
  35. except Exception as e:
  36. return jsonify(type='danger', msg=str(e))
  37. else:
  38. return jsonify(type='danger', msg='no or invalid id defined')
  39. class container_post(Resource):
  40. def post(self, container_id, post_action):
  41. if container_id and container_id.isalnum() and post_action:
  42. if post_action == 'stop':
  43. try:
  44. for container in docker_client.containers.list(all=True, filters={"id": container_id}):
  45. container.stop()
  46. return jsonify(type='success', msg='command completed successfully')
  47. except Exception as e:
  48. return jsonify(type='danger', msg=str(e))
  49. elif post_action == 'start':
  50. try:
  51. for container in docker_client.containers.list(all=True, filters={"id": container_id}):
  52. container.start()
  53. return jsonify(type='success', msg='command completed successfully')
  54. except Exception as e:
  55. return jsonify(type='danger', msg=str(e))
  56. elif post_action == 'restart':
  57. try:
  58. for container in docker_client.containers.list(all=True, filters={"id": container_id}):
  59. container.restart()
  60. return jsonify(type='success', msg='command completed successfully')
  61. except Exception as e:
  62. return jsonify(type='danger', msg=str(e))
  63. elif post_action == 'top':
  64. try:
  65. for container in docker_client.containers.list(all=True, filters={"id": container_id}):
  66. return jsonify(type='success', msg=container.top())
  67. except Exception as e:
  68. return jsonify(type='danger', msg=str(e))
  69. elif post_action == 'stats':
  70. try:
  71. for container in docker_client.containers.list(all=True, filters={"id": container_id}):
  72. return jsonify(type='success', msg=container.stats(decode=True, stream=False))
  73. except Exception as e:
  74. return jsonify(type='danger', msg=str(e))
  75. elif post_action == 'exec':
  76. if not request.json or not 'cmd' in request.json:
  77. return jsonify(type='danger', msg='cmd is missing')
  78. if request.json['cmd'] == 'mailq':
  79. if 'items' in request.json:
  80. r = re.compile("^[0-9a-fA-F]+$")
  81. filtered_qids = filter(r.match, request.json['items'])
  82. if filtered_qids:
  83. if request.json['task'] == 'delete':
  84. flagged_qids = ['-d %s' % i for i in filtered_qids]
  85. sanitized_string = str(' '.join(flagged_qids));
  86. try:
  87. for container in docker_client.containers.list(filters={"id": container_id}):
  88. postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
  89. return exec_run_handler('generic', postsuper_r)
  90. except Exception as e:
  91. return jsonify(type='danger', msg=str(e))
  92. if request.json['task'] == 'hold':
  93. flagged_qids = ['-h %s' % i for i in filtered_qids]
  94. sanitized_string = str(' '.join(flagged_qids));
  95. try:
  96. for container in docker_client.containers.list(filters={"id": container_id}):
  97. postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
  98. return exec_run_handler('generic', postsuper_r)
  99. except Exception as e:
  100. return jsonify(type='danger', msg=str(e))
  101. if request.json['task'] == 'unhold':
  102. flagged_qids = ['-H %s' % i for i in filtered_qids]
  103. sanitized_string = str(' '.join(flagged_qids));
  104. try:
  105. for container in docker_client.containers.list(filters={"id": container_id}):
  106. postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
  107. return exec_run_handler('generic', postsuper_r)
  108. except Exception as e:
  109. return jsonify(type='danger', msg=str(e))
  110. if request.json['task'] == 'deliver':
  111. flagged_qids = ['-i %s' % i for i in filtered_qids]
  112. try:
  113. for container in docker_client.containers.list(filters={"id": container_id}):
  114. for i in flagged_qids:
  115. postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
  116. # todo: check each exit code
  117. return jsonify(type='success', msg=str("Scheduled immediate delivery"))
  118. except Exception as e:
  119. return jsonify(type='danger', msg=str(e))
  120. elif request.json['task'] == 'list':
  121. try:
  122. for container in docker_client.containers.list(filters={"id": container_id}):
  123. mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
  124. return exec_run_handler('utf8_text_only', mailq_return)
  125. except Exception as e:
  126. return jsonify(type='danger', msg=str(e))
  127. elif request.json['task'] == 'flush':
  128. try:
  129. for container in docker_client.containers.list(filters={"id": container_id}):
  130. postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
  131. return exec_run_handler('generic', postqueue_r)
  132. except Exception as e:
  133. return jsonify(type='danger', msg=str(e))
  134. elif request.json['task'] == 'super_delete':
  135. try:
  136. for container in docker_client.containers.list(filters={"id": container_id}):
  137. postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
  138. return exec_run_handler('generic', postsuper_r)
  139. except Exception as e:
  140. return jsonify(type='danger', msg=str(e))
  141. elif request.json['cmd'] == 'system':
  142. if request.json['task'] == 'df':
  143. if 'dir' in request.json:
  144. try:
  145. for container in docker_client.containers.list(filters={"id": container_id}):
  146. 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')
  147. if df_return.exit_code == 0:
  148. return df_return.output.rstrip()
  149. else:
  150. return "0,0,0,0,0,0"
  151. except Exception as e:
  152. return jsonify(type='danger', msg=str(e))
  153. elif request.json['task'] == 'mysql_upgrade':
  154. try:
  155. for container in docker_client.containers.list(filters={"id": container_id}):
  156. sql_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='mysql')
  157. upgrade_cmd = "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"
  158. sql_socket = sql_shell.output;
  159. try :
  160. sql_socket.sendall(upgrade_cmd.encode('utf-8'))
  161. sql_socket.shutdown(socket.SHUT_WR)
  162. except socket.error:
  163. return jsonify(type='danger', msg=str('socket error'))
  164. worker_response = recv_socket_data(sql_socket)
  165. matched = False
  166. for line in worker_response.split("\n"):
  167. if 'is already upgraded to' in line:
  168. matched = True
  169. if matched:
  170. return jsonify(type='success', msg='mysql_upgrade: already upgraded')
  171. else:
  172. container.restart()
  173. return jsonify(type='warning', msg='mysql_upgrade: upgrade was applied')
  174. except Exception as e:
  175. return jsonify(type='danger', msg=str(e))
  176. elif request.json['cmd'] == 'reload':
  177. if request.json['task'] == 'dovecot':
  178. try:
  179. for container in docker_client.containers.list(filters={"id": container_id}):
  180. reload_return = container.exec_run(["/bin/bash", "-c", "/usr/local/sbin/dovecot reload"])
  181. return exec_run_handler('generic', reload_return)
  182. except Exception as e:
  183. return jsonify(type='danger', msg=str(e))
  184. if request.json['task'] == 'postfix':
  185. try:
  186. for container in docker_client.containers.list(filters={"id": container_id}):
  187. reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
  188. return exec_run_handler('generic', reload_return)
  189. except Exception as e:
  190. return jsonify(type='danger', msg=str(e))
  191. if request.json['task'] == 'nginx':
  192. try:
  193. for container in docker_client.containers.list(filters={"id": container_id}):
  194. reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
  195. return exec_run_handler('generic', reload_return)
  196. except Exception as e:
  197. return jsonify(type='danger', msg=str(e))
  198. elif request.json['cmd'] == 'sieve':
  199. if request.json['task'] == 'list':
  200. if 'username' in request.json:
  201. try:
  202. for container in docker_client.containers.list(filters={"id": container_id}):
  203. sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"])
  204. return exec_run_handler('utf8_text_only', sieve_return)
  205. except Exception as e:
  206. return jsonify(type='danger', msg=str(e))
  207. elif request.json['task'] == 'print':
  208. if 'username' in request.json and 'script_name' in request.json:
  209. try:
  210. for container in docker_client.containers.list(filters={"id": container_id}):
  211. sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"])
  212. return exec_run_handler('utf8_text_only', sieve_return)
  213. except Exception as e:
  214. return jsonify(type='danger', msg=str(e))
  215. elif request.json['cmd'] == 'maildir':
  216. if request.json['task'] == 'cleanup':
  217. if 'maildir' in request.json:
  218. try:
  219. for container in docker_client.containers.list(filters={"id": container_id}):
  220. sane_name = re.sub(r'\W+', '', request.json['maildir'])
  221. 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')
  222. return exec_run_handler('generic', maildir_cleanup)
  223. except Exception as e:
  224. return jsonify(type='danger', msg=str(e))
  225. elif request.json['cmd'] == 'rspamd':
  226. if request.json['task'] == 'worker_password':
  227. if 'raw' in request.json:
  228. try:
  229. for container in docker_client.containers.list(filters={"id": container_id}):
  230. worker_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='_rspamd')
  231. worker_cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null\n"
  232. worker_socket = worker_shell.output;
  233. try :
  234. worker_socket.sendall(worker_cmd.encode('utf-8'))
  235. worker_socket.shutdown(socket.SHUT_WR)
  236. except socket.error:
  237. return jsonify(type='danger', msg=str('socket error'))
  238. worker_response = recv_socket_data(worker_socket)
  239. matched = False
  240. for line in worker_response.split("\n"):
  241. if '$2$' in line:
  242. matched = True
  243. hash = line.strip()
  244. hash_out = re.search('\$2\$.+$', hash).group(0)
  245. f = open("/access.inc", "w")
  246. f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip()) + '";\n')
  247. f.close()
  248. container.restart()
  249. if matched:
  250. return jsonify(type='success', msg='command completed successfully')
  251. else:
  252. return jsonify(type='danger', msg='command did not complete')
  253. except Exception as e:
  254. return jsonify(type='danger', msg=str(e))
  255. else:
  256. return jsonify(type='danger', msg='Unknown command')
  257. else:
  258. return jsonify(type='danger', msg='invalid action')
  259. else:
  260. return jsonify(type='danger', msg='invalid container id or missing action')
  261. class GracefulKiller:
  262. kill_now = False
  263. def __init__(self):
  264. signal.signal(signal.SIGINT, self.exit_gracefully)
  265. signal.signal(signal.SIGTERM, self.exit_gracefully)
  266. def exit_gracefully(self, signum, frame):
  267. self.kill_now = True
  268. def startFlaskAPI():
  269. create_self_signed_cert()
  270. try:
  271. ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
  272. ctx.check_hostname = False
  273. ctx.load_cert_chain(certfile='/cert.pem', keyfile='/key.pem')
  274. except:
  275. print "Cannot initialize TLS, retrying in 5s..."
  276. time.sleep(5)
  277. app.run(debug=False, host='0.0.0.0', port=443, threaded=True, ssl_context=ctx)
  278. def recv_socket_data(c_socket, timeout=10):
  279. c_socket.setblocking(0)
  280. total_data=[];
  281. data='';
  282. begin=time.time()
  283. while True:
  284. if total_data and time.time()-begin > timeout:
  285. break
  286. elif time.time()-begin > timeout*2:
  287. break
  288. try:
  289. data = c_socket.recv(8192)
  290. if data:
  291. total_data.append(data)
  292. #change the beginning time for measurement
  293. begin=time.time()
  294. else:
  295. #sleep for sometime to indicate a gap
  296. time.sleep(0.1)
  297. break
  298. except:
  299. pass
  300. return ''.join(total_data)
  301. def exec_run_handler(type, output):
  302. if type == 'generic':
  303. if output.exit_code == 0:
  304. return jsonify(type='success', msg='command completed successfully')
  305. else:
  306. return jsonify(type='danger', msg='command failed: ' + output.output)
  307. if type == 'utf8_text_only':
  308. r = Response(response=output.output, status=200, mimetype="text/plain")
  309. r.headers["Content-Type"] = "text/plain; charset=utf-8"
  310. return r
  311. def create_self_signed_cert():
  312. success = False
  313. while not success:
  314. try:
  315. pkey = crypto.PKey()
  316. pkey.generate_key(crypto.TYPE_RSA, 2048)
  317. cert = crypto.X509()
  318. cert.get_subject().O = "mailcow"
  319. cert.get_subject().CN = "dockerapi"
  320. cert.set_serial_number(int(uuid.uuid4()))
  321. cert.gmtime_adj_notBefore(0)
  322. cert.gmtime_adj_notAfter(10*365*24*60*60)
  323. cert.set_issuer(cert.get_subject())
  324. cert.set_pubkey(pkey)
  325. cert.sign(pkey, 'sha512')
  326. cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
  327. pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
  328. with os.fdopen(os.open('/cert.pem', os.O_WRONLY | os.O_CREAT, 0o644), 'w') as handle:
  329. handle.write(cert)
  330. with os.fdopen(os.open('/key.pem', os.O_WRONLY | os.O_CREAT, 0o600), 'w') as handle:
  331. handle.write(pkey)
  332. success = True
  333. except:
  334. time.sleep(1)
  335. try:
  336. os.remove('/cert.pem')
  337. os.remove('/key.pem')
  338. except OSError:
  339. pass
  340. api.add_resource(containers_get, '/containers/json')
  341. api.add_resource(container_get, '/containers/<string:container_id>/json')
  342. api.add_resource(container_post, '/containers/<string:container_id>/<string:post_action>')
  343. if __name__ == '__main__':
  344. api_thread = Thread(target=startFlaskAPI)
  345. api_thread.daemon = True
  346. api_thread.start()
  347. killer = GracefulKiller()
  348. while True:
  349. time.sleep(1)
  350. if killer.kill_now:
  351. break
  352. print "Stopping dockerapi-mailcow"