xref: /curl/tests/http/testenv/nghttpx.py (revision 0d475da1)
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 signal
30import subprocess
31import time
32from typing import Optional
33from datetime import datetime, timedelta
34
35from .env import Env
36from .curl import CurlClient
37
38
39log = logging.getLogger(__name__)
40
41
42class Nghttpx:
43
44    def __init__(self, env: Env, port: int, https_port: int, name: str):
45        self.env = env
46        self._name = name
47        self._port = port
48        self._https_port = https_port
49        self._cmd = env.nghttpx
50        self._run_dir = os.path.join(env.gen_dir, name)
51        self._pid_file = os.path.join(self._run_dir, 'nghttpx.pid')
52        self._conf_file = os.path.join(self._run_dir, 'nghttpx.conf')
53        self._error_log = os.path.join(self._run_dir, 'nghttpx.log')
54        self._stderr = os.path.join(self._run_dir, 'nghttpx.stderr')
55        self._tmp_dir = os.path.join(self._run_dir, 'tmp')
56        self._process: Optional[subprocess.Popen] = None
57        self._rmf(self._pid_file)
58        self._rmf(self._error_log)
59        self._mkpath(self._run_dir)
60        self._write_config()
61
62    @property
63    def https_port(self):
64        return self._https_port
65
66    def exists(self):
67        return self._cmd and os.path.exists(self._cmd)
68
69    def clear_logs(self):
70        self._rmf(self._error_log)
71        self._rmf(self._stderr)
72
73    def is_running(self):
74        if self._process:
75            self._process.poll()
76            return self._process.returncode is None
77        return False
78
79    def start_if_needed(self):
80        if not self.is_running():
81            return self.start()
82        return True
83
84    def start(self, wait_live=True):
85        pass
86
87    def stop_if_running(self):
88        if self.is_running():
89            return self.stop()
90        return True
91
92    def stop(self, wait_dead=True):
93        self._mkpath(self._tmp_dir)
94        if self._process:
95            self._process.terminate()
96            self._process.wait(timeout=2)
97            self._process = None
98            return not wait_dead or self.wait_dead(timeout=timedelta(seconds=5))
99        return True
100
101    def restart(self):
102        self.stop()
103        return self.start()
104
105    def reload(self, timeout: timedelta):
106        if self._process:
107            running = self._process
108            self._process = None
109            os.kill(running.pid, signal.SIGQUIT)
110            end_wait = datetime.now() + timeout
111            if not self.start(wait_live=False):
112                self._process = running
113                return False
114            while datetime.now() < end_wait:
115                try:
116                    log.debug(f'waiting for nghttpx({running.pid}) to exit.')
117                    running.wait(2)
118                    log.debug(f'nghttpx({running.pid}) terminated -> {running.returncode}')
119                    break
120                except subprocess.TimeoutExpired:
121                    log.warning(f'nghttpx({running.pid}), not shut down yet.')
122                    os.kill(running.pid, signal.SIGQUIT)
123            if datetime.now() >= end_wait:
124                log.error(f'nghttpx({running.pid}), terminate forcefully.')
125                os.kill(running.pid, signal.SIGKILL)
126                running.terminate()
127                running.wait(1)
128            return self.wait_live(timeout=timedelta(seconds=5))
129        return False
130
131    def wait_dead(self, timeout: timedelta):
132        curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
133        try_until = datetime.now() + timeout
134        while datetime.now() < try_until:
135            if self._https_port > 0:
136                check_url = f'https://{self.env.domain1}:{self._https_port}/'
137                r = curl.http_get(url=check_url, extra_args=[
138                    '--trace', 'curl.trace', '--trace-time',
139                    '--connect-timeout', '1'
140                ])
141            else:
142                check_url = f'https://{self.env.domain1}:{self._port}/'
143                r = curl.http_get(url=check_url, extra_args=[
144                    '--trace', 'curl.trace', '--trace-time',
145                    '--http3-only', '--connect-timeout', '1'
146                ])
147            if r.exit_code != 0:
148                return True
149            log.debug(f'waiting for nghttpx to stop responding: {r}')
150            time.sleep(.1)
151        log.debug(f"Server still responding after {timeout}")
152        return False
153
154    def wait_live(self, timeout: timedelta):
155        curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
156        try_until = datetime.now() + timeout
157        while datetime.now() < try_until:
158            if self._https_port > 0:
159                check_url = f'https://{self.env.domain1}:{self._https_port}/'
160                r = curl.http_get(url=check_url, extra_args=[
161                    '--trace', 'curl.trace', '--trace-time',
162                    '--connect-timeout', '1'
163                ])
164            else:
165                check_url = f'https://{self.env.domain1}:{self._port}/'
166                r = curl.http_get(url=check_url, extra_args=[
167                    '--http3-only', '--trace', 'curl.trace', '--trace-time',
168                    '--connect-timeout', '1'
169                ])
170            if r.exit_code == 0:
171                return True
172            log.debug(f'waiting for nghttpx to become responsive: {r}')
173            time.sleep(.1)
174        log.error(f"Server still not responding after {timeout}")
175        return False
176
177    def _rmf(self, path):
178        if os.path.exists(path):
179            return os.remove(path)
180
181    def _mkpath(self, path):
182        if not os.path.exists(path):
183            return os.makedirs(path)
184
185    def _write_config(self):
186        with open(self._conf_file, 'w') as fd:
187            fd.write('# nghttpx test config')
188            fd.write("\n".join([
189                '# do we need something here?'
190            ]))
191
192
193class NghttpxQuic(Nghttpx):
194
195    def __init__(self, env: Env):
196        super().__init__(env=env, name='nghttpx-quic', port=env.h3_port,
197                         https_port=env.nghttpx_https_port)
198
199    def start(self, wait_live=True):
200        self._mkpath(self._tmp_dir)
201        if self._process:
202            self.stop()
203        creds = self.env.get_credentials(self.env.domain1)
204        assert creds  # convince pytype this isn't None
205        args = [
206            self._cmd,
207            f'--frontend=*,{self.env.h3_port};quic',
208            f'--frontend=*,{self.env.nghttpx_https_port};tls',
209            f'--backend=127.0.0.1,{self.env.https_port};{self.env.domain1};sni={self.env.domain1};proto=h2;tls',
210            f'--backend=127.0.0.1,{self.env.http_port}',
211            '--log-level=INFO',
212            f'--pid-file={self._pid_file}',
213            f'--errorlog-file={self._error_log}',
214            f'--conf={self._conf_file}',
215            f'--cacert={self.env.ca.cert_file}',
216            creds.pkey_file,
217            creds.cert_file,
218            '--frontend-http3-window-size=1M',
219            '--frontend-http3-max-window-size=10M',
220            '--frontend-http3-connection-window-size=10M',
221            '--frontend-http3-max-connection-window-size=100M',
222            # f'--frontend-quic-debug-log',
223        ]
224        ngerr = open(self._stderr, 'a')
225        self._process = subprocess.Popen(args=args, stderr=ngerr)
226        if self._process.returncode is not None:
227            return False
228        return not wait_live or self.wait_live(timeout=timedelta(seconds=5))
229
230
231class NghttpxFwd(Nghttpx):
232
233    def __init__(self, env: Env):
234        super().__init__(env=env, name='nghttpx-fwd', port=env.h2proxys_port,
235                         https_port=0)
236
237    def start(self, wait_live=True):
238        self._mkpath(self._tmp_dir)
239        if self._process:
240            self.stop()
241        creds = self.env.get_credentials(self.env.proxy_domain)
242        assert creds  # convince pytype this isn't None
243        args = [
244            self._cmd,
245            '--http2-proxy',
246            f'--frontend=*,{self.env.h2proxys_port}',
247            f'--backend=127.0.0.1,{self.env.proxy_port}',
248            '--log-level=INFO',
249            f'--pid-file={self._pid_file}',
250            f'--errorlog-file={self._error_log}',
251            f'--conf={self._conf_file}',
252            f'--cacert={self.env.ca.cert_file}',
253            creds.pkey_file,
254            creds.cert_file,
255        ]
256        ngerr = open(self._stderr, 'a')
257        self._process = subprocess.Popen(args=args, stderr=ngerr)
258        if self._process.returncode is not None:
259            return False
260        return not wait_live or self.wait_live(timeout=timedelta(seconds=5))
261
262    def wait_dead(self, timeout: timedelta):
263        curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
264        try_until = datetime.now() + timeout
265        while datetime.now() < try_until:
266            check_url = f'https://{self.env.proxy_domain}:{self.env.h2proxys_port}/'
267            r = curl.http_get(url=check_url)
268            if r.exit_code != 0:
269                return True
270            log.debug(f'waiting for nghttpx-fwd to stop responding: {r}')
271            time.sleep(.1)
272        log.debug(f"Server still responding after {timeout}")
273        return False
274
275    def wait_live(self, timeout: timedelta):
276        curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
277        try_until = datetime.now() + timeout
278        while datetime.now() < try_until:
279            check_url = f'https://{self.env.proxy_domain}:{self.env.h2proxys_port}/'
280            r = curl.http_get(url=check_url, extra_args=[
281                '--trace', 'curl.trace', '--trace-time'
282            ])
283            if r.exit_code == 0:
284                return True
285            log.debug(f'waiting for nghttpx-fwd to become responsive: {r}')
286            time.sleep(.1)
287        log.error(f"Server still not responding after {timeout}")
288        return False
289