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# 27import logging 28import os 29import subprocess 30import time 31from datetime import timedelta, datetime 32from json import JSONEncoder 33 34from .curl import CurlClient 35from .env import Env 36 37 38log = logging.getLogger(__name__) 39 40 41class Caddy: 42 43 def __init__(self, env: Env): 44 self.env = env 45 self._caddy = os.environ['CADDY'] if 'CADDY' in os.environ else env.caddy 46 self._caddy_dir = os.path.join(env.gen_dir, 'caddy') 47 self._docs_dir = os.path.join(self._caddy_dir, 'docs') 48 self._conf_file = os.path.join(self._caddy_dir, 'Caddyfile') 49 self._error_log = os.path.join(self._caddy_dir, 'caddy.log') 50 self._tmp_dir = os.path.join(self._caddy_dir, 'tmp') 51 self._process = None 52 self._rmf(self._error_log) 53 54 @property 55 def docs_dir(self): 56 return self._docs_dir 57 58 @property 59 def port(self) -> int: 60 return self.env.caddy_https_port 61 62 def clear_logs(self): 63 self._rmf(self._error_log) 64 65 def is_running(self): 66 if self._process: 67 self._process.poll() 68 return self._process.returncode is None 69 return False 70 71 def start_if_needed(self): 72 if not self.is_running(): 73 return self.start() 74 return True 75 76 def start(self, wait_live=True): 77 self._mkpath(self._tmp_dir) 78 if self._process: 79 self.stop() 80 self._write_config() 81 args = [ 82 self._caddy, 'run' 83 ] 84 caddyerr = open(self._error_log, 'a') 85 self._process = subprocess.Popen(args=args, cwd=self._caddy_dir, stderr=caddyerr) 86 if self._process.returncode is not None: 87 return False 88 return not wait_live or self.wait_live(timeout=timedelta(seconds=5)) 89 90 def stop_if_running(self): 91 if self.is_running(): 92 return self.stop() 93 return True 94 95 def stop(self, wait_dead=True): 96 self._mkpath(self._tmp_dir) 97 if self._process: 98 self._process.terminate() 99 self._process.wait(timeout=2) 100 self._process = None 101 return not wait_dead or self.wait_dead(timeout=timedelta(seconds=5)) 102 return True 103 104 def restart(self): 105 self.stop() 106 return self.start() 107 108 def wait_dead(self, timeout: timedelta): 109 curl = CurlClient(env=self.env, run_dir=self._tmp_dir) 110 try_until = datetime.now() + timeout 111 while datetime.now() < try_until: 112 check_url = f'https://{self.env.domain1}:{self.port}/' 113 r = curl.http_get(url=check_url) 114 if r.exit_code != 0: 115 return True 116 log.debug(f'waiting for caddy to stop responding: {r}') 117 time.sleep(.1) 118 log.debug(f"Server still responding after {timeout}") 119 return False 120 121 def wait_live(self, timeout: timedelta): 122 curl = CurlClient(env=self.env, run_dir=self._tmp_dir) 123 try_until = datetime.now() + timeout 124 while datetime.now() < try_until: 125 check_url = f'https://{self.env.domain1}:{self.port}/' 126 r = curl.http_get(url=check_url) 127 if r.exit_code == 0: 128 return True 129 time.sleep(.1) 130 log.error(f"Caddy still not responding after {timeout}") 131 return False 132 133 def _rmf(self, path): 134 if os.path.exists(path): 135 return os.remove(path) 136 137 def _mkpath(self, path): 138 if not os.path.exists(path): 139 return os.makedirs(path) 140 141 def _write_config(self): 142 domain1 = self.env.domain1 143 creds1 = self.env.get_credentials(domain1) 144 assert creds1 # convince pytype this isn't None 145 domain2 = self.env.domain2 146 creds2 = self.env.get_credentials(domain2) 147 assert creds2 # convince pytype this isn't None 148 self._mkpath(self._docs_dir) 149 self._mkpath(self._tmp_dir) 150 with open(os.path.join(self._docs_dir, 'data.json'), 'w') as fd: 151 data = { 152 'server': f'{domain1}', 153 } 154 fd.write(JSONEncoder().encode(data)) 155 with open(self._conf_file, 'w') as fd: 156 conf = [ # base server config 157 '{', 158 f' http_port {self.env.caddy_http_port}', 159 f' https_port {self.env.caddy_https_port}', 160 f' servers :{self.env.caddy_https_port} {{', 161 ' protocols h3 h2 h1', 162 ' }', 163 '}', 164 f'{domain1}:{self.env.caddy_https_port} {{', 165 ' file_server * {', 166 f' root {self._docs_dir}', 167 ' }', 168 f' tls {creds1.cert_file} {creds1.pkey_file}', 169 '}', 170 f'{domain2} {{', 171 f' reverse_proxy /* http://localhost:{self.env.http_port} {{', 172 ' }', 173 f' tls {creds2.cert_file} {creds2.pkey_file}', 174 '}', 175 ] 176 fd.write("\n".join(conf)) 177