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