caddy.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. #***************************************************************************
  4. # _ _ ____ _
  5. # Project ___| | | | _ \| |
  6. # / __| | | | |_) | |
  7. # | (__| |_| | _ <| |___
  8. # \___|\___/|_| \_\_____|
  9. #
  10. # Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
  11. #
  12. # This software is licensed as described in the file COPYING, which
  13. # you should have received as part of this distribution. The terms
  14. # are also available at https://curl.se/docs/copyright.html.
  15. #
  16. # You may opt to use, copy, modify, merge, publish, distribute and/or sell
  17. # copies of the Software, and permit persons to whom the Software is
  18. # furnished to do so, under the terms of the COPYING file.
  19. #
  20. # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
  21. # KIND, either express or implied.
  22. #
  23. # SPDX-License-Identifier: curl
  24. #
  25. ###########################################################################
  26. #
  27. import logging
  28. import os
  29. import subprocess
  30. import time
  31. from datetime import timedelta, datetime
  32. from json import JSONEncoder
  33. from .curl import CurlClient
  34. from .env import Env
  35. log = logging.getLogger(__name__)
  36. class Caddy:
  37. def __init__(self, env: Env):
  38. self.env = env
  39. self._caddy = os.environ['CADDY'] if 'CADDY' in os.environ else env.caddy
  40. self._caddy_dir = os.path.join(env.gen_dir, 'caddy')
  41. self._docs_dir = os.path.join(self._caddy_dir, 'docs')
  42. self._conf_file = os.path.join(self._caddy_dir, 'Caddyfile')
  43. self._error_log = os.path.join(self._caddy_dir, 'caddy.log')
  44. self._tmp_dir = os.path.join(self._caddy_dir, 'tmp')
  45. self._process = None
  46. self._rmf(self._error_log)
  47. @property
  48. def docs_dir(self):
  49. return self._docs_dir
  50. @property
  51. def port(self) -> int:
  52. return self.env.caddy_https_port
  53. def clear_logs(self):
  54. self._rmf(self._error_log)
  55. def is_running(self):
  56. if self._process:
  57. self._process.poll()
  58. return self._process.returncode is None
  59. return False
  60. def start_if_needed(self):
  61. if not self.is_running():
  62. return self.start()
  63. return True
  64. def start(self, wait_live=True):
  65. self._mkpath(self._tmp_dir)
  66. if self._process:
  67. self.stop()
  68. self._write_config()
  69. args = [
  70. self._caddy, 'run'
  71. ]
  72. caddyerr = open(self._error_log, 'a')
  73. self._process = subprocess.Popen(args=args, cwd=self._caddy_dir, stderr=caddyerr)
  74. if self._process.returncode is not None:
  75. return False
  76. return not wait_live or self.wait_live(timeout=timedelta(seconds=5))
  77. def stop_if_running(self):
  78. if self.is_running():
  79. return self.stop()
  80. return True
  81. def stop(self, wait_dead=True):
  82. self._mkpath(self._tmp_dir)
  83. if self._process:
  84. self._process.terminate()
  85. self._process.wait(timeout=2)
  86. self._process = None
  87. return not wait_dead or self.wait_dead(timeout=timedelta(seconds=5))
  88. return True
  89. def restart(self):
  90. self.stop()
  91. return self.start()
  92. def wait_dead(self, timeout: timedelta):
  93. curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
  94. try_until = datetime.now() + timeout
  95. while datetime.now() < try_until:
  96. check_url = f'https://{self.env.domain1}:{self.port}/'
  97. r = curl.http_get(url=check_url)
  98. if r.exit_code != 0:
  99. return True
  100. log.debug(f'waiting for caddy to stop responding: {r}')
  101. time.sleep(.1)
  102. log.debug(f"Server still responding after {timeout}")
  103. return False
  104. def wait_live(self, timeout: timedelta):
  105. curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
  106. try_until = datetime.now() + timeout
  107. while datetime.now() < try_until:
  108. check_url = f'https://{self.env.domain1}:{self.port}/'
  109. r = curl.http_get(url=check_url)
  110. if r.exit_code == 0:
  111. return True
  112. time.sleep(.1)
  113. log.error(f"Caddy still not responding after {timeout}")
  114. return False
  115. def _rmf(self, path):
  116. if os.path.exists(path):
  117. return os.remove(path)
  118. def _mkpath(self, path):
  119. if not os.path.exists(path):
  120. return os.makedirs(path)
  121. def _write_config(self):
  122. domain1 = self.env.domain1
  123. creds1 = self.env.get_credentials(domain1)
  124. assert creds1 # convince pytype this isn't None
  125. domain2 = self.env.domain2
  126. creds2 = self.env.get_credentials(domain2)
  127. assert creds2 # convince pytype this isn't None
  128. self._mkpath(self._docs_dir)
  129. self._mkpath(self._tmp_dir)
  130. with open(os.path.join(self._docs_dir, 'data.json'), 'w') as fd:
  131. data = {
  132. 'server': f'{domain1}',
  133. }
  134. fd.write(JSONEncoder().encode(data))
  135. with open(self._conf_file, 'w') as fd:
  136. conf = [ # base server config
  137. '{',
  138. f' http_port {self.env.caddy_http_port}',
  139. f' https_port {self.env.caddy_https_port}',
  140. f' servers :{self.env.caddy_https_port} {{',
  141. ' protocols h3 h2 h1',
  142. ' }',
  143. '}',
  144. f'{domain1}:{self.env.caddy_https_port} {{',
  145. ' file_server * {',
  146. f' root {self._docs_dir}',
  147. ' }',
  148. f' tls {creds1.cert_file} {creds1.pkey_file}',
  149. '}',
  150. f'{domain2} {{',
  151. f' reverse_proxy /* http://localhost:{self.env.http_port} {{',
  152. ' }',
  153. f' tls {creds2.cert_file} {creds2.pkey_file}',
  154. '}',
  155. ]
  156. fd.write("\n".join(conf))