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