xref: /curl/tests/http/testenv/caddy.py (revision 0f7ba5c5)
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