nghttpx.py (12568B)
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 # 27 import logging 28 import os 29 import signal 30 import socket 31 import subprocess 32 import time 33 from typing import Optional, Dict 34 from datetime import datetime, timedelta 35 36 from .env import Env, NghttpxUtil 37 from .curl import CurlClient 38 from .ports import alloc_ports_and_do 39 40 log = logging.getLogger(__name__) 41 42 43 class Nghttpx: 44 45 def __init__(self, env: Env, name: str, domain: str, cred_name: str): 46 self.env = env 47 self._name = name 48 self._domain = domain 49 self._port = 0 50 self._https_port = 0 51 self._cmd = env.nghttpx 52 self._run_dir = os.path.join(env.gen_dir, name) 53 self._pid_file = os.path.join(self._run_dir, 'nghttpx.pid') 54 self._conf_file = os.path.join(self._run_dir, 'nghttpx.conf') 55 self._error_log = os.path.join(self._run_dir, 'nghttpx.log') 56 self._stderr = os.path.join(self._run_dir, 'nghttpx.stderr') 57 self._tmp_dir = os.path.join(self._run_dir, 'tmp') 58 self._process: Optional[subprocess.Popen] = None 59 self._cred_name = self._def_cred_name = cred_name 60 self._loaded_cred_name = '' 61 self._version = NghttpxUtil.version(self._cmd) 62 63 def supports_h3(self): 64 return NghttpxUtil.version_with_h3(self._version) 65 66 def set_cred_name(self, name: str): 67 self._cred_name = name 68 69 def reset_config(self): 70 self._cred_name = self._def_cred_name 71 72 def reload_if_config_changed(self): 73 if self._process and self._port > 0 and \ 74 self._loaded_cred_name == self._cred_name: 75 return True 76 return self.reload() 77 78 @property 79 def https_port(self): 80 return self._https_port 81 82 def exists(self): 83 return self._cmd and os.path.exists(self._cmd) 84 85 def clear_logs(self): 86 self._rmf(self._error_log) 87 self._rmf(self._stderr) 88 89 def is_running(self): 90 if self._process: 91 self._process.poll() 92 return self._process.returncode is None 93 return False 94 95 def start_if_needed(self): 96 if not self.is_running(): 97 return self.start() 98 return True 99 100 def initial_start(self): 101 self._rmf(self._pid_file) 102 self._rmf(self._error_log) 103 self._mkpath(self._run_dir) 104 self._write_config() 105 106 def start(self, wait_live=True): 107 pass 108 109 def stop(self, wait_dead=True): 110 self._mkpath(self._tmp_dir) 111 if self._process: 112 self._process.terminate() 113 self._process.wait(timeout=2) 114 self._process = None 115 return not wait_dead or self.wait_dead(timeout=timedelta(seconds=5)) 116 return True 117 118 def restart(self): 119 self.stop() 120 return self.start() 121 122 def reload(self, timeout: timedelta = timedelta(seconds=Env.SERVER_TIMEOUT)): 123 if self._process: 124 running = self._process 125 self._process = None 126 os.kill(running.pid, signal.SIGQUIT) 127 end_wait = datetime.now() + timeout 128 if not self.start(wait_live=False): 129 self._process = running 130 return False 131 while datetime.now() < end_wait: 132 try: 133 log.debug(f'waiting for nghttpx({running.pid}) to exit.') 134 running.wait(2) 135 log.debug(f'nghttpx({running.pid}) terminated -> {running.returncode}') 136 break 137 except subprocess.TimeoutExpired: 138 log.warning(f'nghttpx({running.pid}), not shut down yet.') 139 os.kill(running.pid, signal.SIGQUIT) 140 if datetime.now() >= end_wait: 141 log.error(f'nghttpx({running.pid}), terminate forcefully.') 142 os.kill(running.pid, signal.SIGKILL) 143 running.terminate() 144 running.wait(1) 145 return self.wait_live(timeout=timedelta(seconds=Env.SERVER_TIMEOUT)) 146 return False 147 148 def wait_dead(self, timeout: timedelta): 149 curl = CurlClient(env=self.env, run_dir=self._tmp_dir) 150 try_until = datetime.now() + timeout 151 while datetime.now() < try_until: 152 if self._https_port > 0: 153 check_url = f'https://{self._domain}:{self._port}/' 154 r = curl.http_get(url=check_url, extra_args=[ 155 '--trace', 'curl.trace', '--trace-time', 156 '--connect-timeout', '1' 157 ]) 158 else: 159 check_url = f'https://{self._domain}:{self._port}/' 160 r = curl.http_get(url=check_url, extra_args=[ 161 '--trace', 'curl.trace', '--trace-time', 162 '--http3-only', '--connect-timeout', '1' 163 ]) 164 if r.exit_code != 0: 165 return True 166 log.debug(f'waiting for nghttpx to stop responding: {r}') 167 time.sleep(.1) 168 log.debug(f"Server still responding after {timeout}") 169 return False 170 171 def wait_live(self, timeout: timedelta): 172 curl = CurlClient(env=self.env, run_dir=self._tmp_dir) 173 try_until = datetime.now() + timeout 174 while datetime.now() < try_until: 175 if self._https_port > 0: 176 check_url = f'https://{self._domain}:{self._port}/' 177 r = curl.http_get(url=check_url, extra_args=[ 178 '--trace', 'curl.trace', '--trace-time', 179 '--connect-timeout', '1' 180 ]) 181 else: 182 check_url = f'https://{self._domain}:{self._port}/' 183 r = curl.http_get(url=check_url, extra_args=[ 184 '--http3-only', '--trace', 'curl.trace', '--trace-time', 185 '--connect-timeout', '1' 186 ]) 187 if r.exit_code == 0: 188 return True 189 time.sleep(.1) 190 log.error(f"Server still not responding after {timeout}") 191 return False 192 193 def _rmf(self, path): 194 if os.path.exists(path): 195 return os.remove(path) 196 197 def _mkpath(self, path): 198 if not os.path.exists(path): 199 return os.makedirs(path) 200 201 def _write_config(self): 202 with open(self._conf_file, 'w') as fd: 203 fd.write('# nghttpx test config') 204 fd.write("\n".join([ 205 '# do we need something here?' 206 ])) 207 208 209 class NghttpxQuic(Nghttpx): 210 211 PORT_SPECS = { 212 'nghttpx_https': socket.SOCK_STREAM, 213 } 214 215 def __init__(self, env: Env): 216 super().__init__(env=env, name='nghttpx-quic', 217 domain=env.domain1, cred_name=env.domain1) 218 self._https_port = env.https_port 219 220 def initial_start(self): 221 super().initial_start() 222 223 def startup(ports: Dict[str, int]) -> bool: 224 self._port = ports['nghttpx_https'] 225 if self.start(): 226 self.env.update_ports(ports) 227 return True 228 self.stop() 229 self._port = 0 230 return False 231 232 return alloc_ports_and_do(NghttpxQuic.PORT_SPECS, startup, 233 self.env.gen_root, max_tries=3) 234 235 def start(self, wait_live=True): 236 self._mkpath(self._tmp_dir) 237 if self._process: 238 self.stop() 239 creds = self.env.get_credentials(self._cred_name) 240 assert creds # convince pytype this isn't None 241 self._loaded_cred_name = self._cred_name 242 args = [self._cmd, f'--frontend=*,{self._port};tls'] 243 if self.supports_h3(): 244 args.extend([ 245 f'--frontend=*,{self.env.h3_port};quic', 246 '--frontend-quic-early-data', 247 ]) 248 args.extend([ 249 f'--backend=127.0.0.1,{self.env.https_port};{self._domain};sni={self._domain};proto=h2;tls', 250 f'--backend=127.0.0.1,{self.env.http_port}', 251 '--log-level=ERROR', 252 f'--pid-file={self._pid_file}', 253 f'--errorlog-file={self._error_log}', 254 f'--conf={self._conf_file}', 255 f'--cacert={self.env.ca.cert_file}', 256 creds.pkey_file, 257 creds.cert_file, 258 '--frontend-http3-window-size=1M', 259 '--frontend-http3-max-window-size=10M', 260 '--frontend-http3-connection-window-size=10M', 261 '--frontend-http3-max-connection-window-size=100M', 262 # f'--frontend-quic-debug-log', 263 ]) 264 ngerr = open(self._stderr, 'a') 265 self._process = subprocess.Popen(args=args, stderr=ngerr) 266 if self._process.returncode is not None: 267 return False 268 return not wait_live or self.wait_live(timeout=timedelta(seconds=Env.SERVER_TIMEOUT)) 269 270 271 class NghttpxFwd(Nghttpx): 272 273 def __init__(self, env: Env): 274 super().__init__(env=env, name='nghttpx-fwd', 275 domain=env.proxy_domain, 276 cred_name=env.proxy_domain) 277 278 def initial_start(self): 279 super().initial_start() 280 281 def startup(ports: Dict[str, int]) -> bool: 282 self._port = ports['h2proxys'] 283 if self.start(): 284 self.env.update_ports(ports) 285 return True 286 self.stop() 287 self._port = 0 288 return False 289 290 return alloc_ports_and_do({'h2proxys': socket.SOCK_STREAM}, 291 startup, self.env.gen_root, max_tries=3) 292 293 def start(self, wait_live=True): 294 assert self._port > 0 295 self._mkpath(self._tmp_dir) 296 if self._process: 297 self.stop() 298 creds = self.env.get_credentials(self._cred_name) 299 assert creds # convince pytype this isn't None 300 self._loaded_cred_name = self._cred_name 301 args = [ 302 self._cmd, 303 '--http2-proxy', 304 f'--frontend=*,{self._port}', 305 f'--backend=127.0.0.1,{self.env.proxy_port}', 306 '--log-level=ERROR', 307 f'--pid-file={self._pid_file}', 308 f'--errorlog-file={self._error_log}', 309 f'--conf={self._conf_file}', 310 f'--cacert={self.env.ca.cert_file}', 311 creds.pkey_file, 312 creds.cert_file, 313 ] 314 ngerr = open(self._stderr, 'a') 315 self._process = subprocess.Popen(args=args, stderr=ngerr) 316 if self._process.returncode is not None: 317 return False 318 return not wait_live or self.wait_live(timeout=timedelta(seconds=Env.SERVER_TIMEOUT)) 319 320 def wait_dead(self, timeout: timedelta): 321 curl = CurlClient(env=self.env, run_dir=self._tmp_dir) 322 try_until = datetime.now() + timeout 323 while datetime.now() < try_until: 324 check_url = f'https://{self.env.proxy_domain}:{self._port}/' 325 r = curl.http_get(url=check_url) 326 if r.exit_code != 0: 327 return True 328 log.debug(f'waiting for nghttpx-fwd to stop responding: {r}') 329 time.sleep(.1) 330 log.debug(f"Server still responding after {timeout}") 331 return False 332 333 def wait_live(self, timeout: timedelta): 334 curl = CurlClient(env=self.env, run_dir=self._tmp_dir) 335 try_until = datetime.now() + timeout 336 while datetime.now() < try_until: 337 check_url = f'https://{self.env.proxy_domain}:{self._port}/' 338 r = curl.http_get(url=check_url, extra_args=[ 339 '--trace', 'curl.trace', '--trace-time' 340 ]) 341 if r.exit_code == 0: 342 return True 343 time.sleep(.1) 344 log.error(f"Server still not responding after {timeout}") 345 return False