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