quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

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