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