• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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            '--frontend-quic-early-data',
209            f'--frontend=*,{self.env.nghttpx_https_port};tls',
210            f'--backend=127.0.0.1,{self.env.https_port};{self.env.domain1};sni={self.env.domain1};proto=h2;tls',
211            f'--backend=127.0.0.1,{self.env.http_port}',
212            '--log-level=INFO',
213            f'--pid-file={self._pid_file}',
214            f'--errorlog-file={self._error_log}',
215            f'--conf={self._conf_file}',
216            f'--cacert={self.env.ca.cert_file}',
217            creds.pkey_file,
218            creds.cert_file,
219            '--frontend-http3-window-size=1M',
220            '--frontend-http3-max-window-size=10M',
221            '--frontend-http3-connection-window-size=10M',
222            '--frontend-http3-max-connection-window-size=100M',
223            # f'--frontend-quic-debug-log',
224        ]
225        ngerr = open(self._stderr, 'a')
226        self._process = subprocess.Popen(args=args, stderr=ngerr)
227        if self._process.returncode is not None:
228            return False
229        return not wait_live or self.wait_live(timeout=timedelta(seconds=5))
230
231
232class NghttpxFwd(Nghttpx):
233
234    def __init__(self, env: Env):
235        super().__init__(env=env, name='nghttpx-fwd', port=env.h2proxys_port,
236                         https_port=0)
237
238    def start(self, wait_live=True):
239        self._mkpath(self._tmp_dir)
240        if self._process:
241            self.stop()
242        creds = self.env.get_credentials(self.env.proxy_domain)
243        assert creds  # convince pytype this isn't None
244        args = [
245            self._cmd,
246            '--http2-proxy',
247            f'--frontend=*,{self.env.h2proxys_port}',
248            f'--backend=127.0.0.1,{self.env.proxy_port}',
249            '--log-level=INFO',
250            f'--pid-file={self._pid_file}',
251            f'--errorlog-file={self._error_log}',
252            f'--conf={self._conf_file}',
253            f'--cacert={self.env.ca.cert_file}',
254            creds.pkey_file,
255            creds.cert_file,
256        ]
257        ngerr = open(self._stderr, 'a')
258        self._process = subprocess.Popen(args=args, stderr=ngerr)
259        if self._process.returncode is not None:
260            return False
261        return not wait_live or self.wait_live(timeout=timedelta(seconds=5))
262
263    def wait_dead(self, timeout: timedelta):
264        curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
265        try_until = datetime.now() + timeout
266        while datetime.now() < try_until:
267            check_url = f'https://{self.env.proxy_domain}:{self.env.h2proxys_port}/'
268            r = curl.http_get(url=check_url)
269            if r.exit_code != 0:
270                return True
271            log.debug(f'waiting for nghttpx-fwd to stop responding: {r}')
272            time.sleep(.1)
273        log.debug(f"Server still responding after {timeout}")
274        return False
275
276    def wait_live(self, timeout: timedelta):
277        curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
278        try_until = datetime.now() + timeout
279        while datetime.now() < try_until:
280            check_url = f'https://{self.env.proxy_domain}:{self.env.h2proxys_port}/'
281            r = curl.http_get(url=check_url, extra_args=[
282                '--trace', 'curl.trace', '--trace-time'
283            ])
284            if r.exit_code == 0:
285                return True
286            log.debug(f'waiting for nghttpx-fwd to become responsive: {r}')
287            time.sleep(.1)
288        log.error(f"Server still not responding after {timeout}")
289        return False
290