mirror of
https://github.com/curl/curl.git
synced 2026-08-24 19:53:46 +03:00
connections: introduce http/3 happy eyeballs
New cfilter HTTP-CONNECT for h3/h2/http1.1 eyeballing.
- filter is installed when `--http3` in the tool is used (or
the equivalent CURLOPT_ done in the library)
- starts a QUIC/HTTP/3 connect right away. Should that not
succeed after 100ms (subject to change), a parallel attempt
is started for HTTP/2 and HTTP/1.1 via TCP
- both attempts are subject to IPv6/IPv4 eyeballing, same
as happens for other connections
- tie timeout to the ip-version HAPPY_EYEBALLS_TIMEOUT
- use a `soft` timeout at half the value. When the soft timeout
expires, the HTTPS-CONNECT filter checks if the QUIC filter
has received any data from the server. If not, it will start
the HTTP/2 attempt.
HTTP/3(ngtcp2) improvements.
- setting call_data in all cfilter calls similar to http/2 and vtls filters
for use in callback where no stream data is available.
- returning CURLE_PARTIAL_FILE for prematurely terminated transfers
- enabling pytest test_05 for h3
- shifting functionality to "connect" UDP sockets from ngtcp2
implementation into the udp socket cfilter. Because unconnected
UDP sockets are weird. For example they error when adding to a
pollset.
HTTP/3(quiche) improvements.
- fixed upload bug in quiche implementation, now passes 251 and pytest
- error codes on stream RESET
- improved debug logs
- handling of DRAIN during connect
- limiting pending event queue
HTTP/2 cfilter improvements.
- use LOG_CF macros for dynamic logging in debug build
- fix CURLcode on RST streams to be CURLE_PARTIAL_FILE
- enable pytest test_05 for h2
- fix upload pytests and improve parallel transfer performance.
GOAWAY handling for ngtcp2/quiche
- during connect, when the remote server refuses to accept new connections
and closes immediately (so the local conn goes into DRAIN phase), the
connection is torn down and a another attempt is made after a short grace
period.
This is the behaviour observed with nghttpx when we tell it to shut
down gracefully. Tested in pytest test_03_02.
TLS improvements
- ALPN selection for SSL/SSL-PROXY filters in one vtls set of functions, replaces
copy of logic in all tls backends.
- standardized the infof logging of offered ALPNs
- ALPN negotiated: have common function for all backends that sets alpn proprty
and connection related things based on the negotiated protocol (or lack thereof).
- new tests/tests-httpd/scorecard.py for testing h3/h2 protocol implementation.
Invoke:
python3 tests/tests-httpd/scorecard.py --help
for usage.
Improvements on gathering connect statistics and socket access.
- new CF_CTRL_CONN_REPORT_STATS cfilter control for having cfilters
report connection statistics. This is triggered when the connection
has completely connected.
- new void Curl_pgrsTimeWas(..) method to report a timer update with
a timestamp of when it happend. This allows for updating timers
"later", e.g. a connect statistic after full connectivity has been
reached.
- in case of HTTP eyeballing, the previous changes will update
statistics only from the filter chain that "won" the eyeballing.
- new cfilter query CF_QUERY_SOCKET for retrieving the socket used
by a filter chain.
Added methods Curl_conn_cf_get_socket() and Curl_conn_get_socket()
for convenient use of this query.
- Change VTLS backend to query their sub-filters for the socket when
checks during the handshake are made.
HTTP/3 documentation on how https eyeballing works.
TLS improvements
- ALPN selection for SSL/SSL-PROXY filters in one vtls set of functions, replaces
copy of logic in all tls backends.
- standardized the infof logging of offered ALPNs
- ALPN negotiated: have common function for all backends that sets alpn proprty
and connection related things based on the negotiated protocol (or lack thereof).
Scorecard with Caddy.
- configure can be run with `--with-test-caddy=path` to specify which caddy to use for testing
- tests/tests-httpd/scorecard.py now measures download speeds with caddy
pytest improvements
- adding Makfile to clean gen dir
- adding nghttpx rundir creation on start
- checking httpd version 2.4.55 for test_05 cases where it is needed. Skipping with message if too old.
- catch exception when checking for caddy existance on system.
Closes #10349
This commit is contained in:
parent
b7aaf074e5
commit
671158242d
61 changed files with 3599 additions and 1249 deletions
|
|
@ -26,6 +26,7 @@
|
|||
#
|
||||
from .env import Env
|
||||
from .certs import TestCA, Credentials
|
||||
from .caddy import Caddy
|
||||
from .httpd import Httpd
|
||||
from .curl import CurlClient, ExecResult
|
||||
from .nghttpx import Nghttpx
|
||||
164
tests/tests-httpd/testenv/caddy.py
Normal file
164
tests/tests-httpd/testenv/caddy.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#***************************************************************************
|
||||
# _ _ ____ _
|
||||
# Project ___| | | | _ \| |
|
||||
# / __| | | | |_) | |
|
||||
# | (__| |_| | _ <| |___
|
||||
# \___|\___/|_| \_\_____|
|
||||
#
|
||||
# Copyright (C) 2008 - 2022, Daniel Stenberg, <daniel@haxx.se>, et al.
|
||||
#
|
||||
# This software is licensed as described in the file COPYING, which
|
||||
# you should have received as part of this distribution. The terms
|
||||
# are also available at https://curl.se/docs/copyright.html.
|
||||
#
|
||||
# You may opt to use, copy, modify, merge, publish, distribute and/or sell
|
||||
# copies of the Software, and permit persons to whom the Software is
|
||||
# furnished to do so, under the terms of the COPYING file.
|
||||
#
|
||||
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
|
||||
# KIND, either express or implied.
|
||||
#
|
||||
# SPDX-License-Identifier: curl
|
||||
#
|
||||
###########################################################################
|
||||
#
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import timedelta, datetime
|
||||
from json import JSONEncoder
|
||||
|
||||
from .curl import CurlClient
|
||||
from .env import Env
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Caddy:
|
||||
|
||||
def __init__(self, env: Env):
|
||||
self.env = env
|
||||
self._caddy = os.environ['CADDY'] if 'CADDY' in os.environ else env.caddy
|
||||
self._caddy_dir = os.path.join(env.gen_dir, 'caddy')
|
||||
self._docs_dir = os.path.join(self._caddy_dir, 'docs')
|
||||
self._conf_file = os.path.join(self._caddy_dir, 'Caddyfile')
|
||||
self._error_log = os.path.join(self._caddy_dir, 'caddy.log')
|
||||
self._tmp_dir = os.path.join(self._caddy_dir, 'tmp')
|
||||
self._process = None
|
||||
self._rmf(self._error_log)
|
||||
|
||||
@property
|
||||
def docs_dir(self):
|
||||
return self._docs_dir
|
||||
|
||||
def clear_logs(self):
|
||||
self._rmf(self._error_log)
|
||||
|
||||
def is_running(self):
|
||||
if self._process:
|
||||
self._process.poll()
|
||||
return self._process.returncode is None
|
||||
return False
|
||||
|
||||
def start_if_needed(self):
|
||||
if not self.is_running():
|
||||
return self.start()
|
||||
return True
|
||||
|
||||
def start(self, wait_live=True):
|
||||
self._mkpath(self._tmp_dir)
|
||||
if self._process:
|
||||
self.stop()
|
||||
self._write_config()
|
||||
args = [
|
||||
self._caddy, 'run'
|
||||
]
|
||||
caddyerr = open(self._error_log, 'a')
|
||||
self._process = subprocess.Popen(args=args, cwd=self._caddy_dir, stderr=caddyerr)
|
||||
if self._process.returncode is not None:
|
||||
return False
|
||||
return not wait_live or self.wait_live(timeout=timedelta(seconds=5))
|
||||
|
||||
def stop_if_running(self):
|
||||
if self.is_running():
|
||||
return self.stop()
|
||||
return True
|
||||
|
||||
def stop(self, wait_dead=True):
|
||||
self._mkpath(self._tmp_dir)
|
||||
if self._process:
|
||||
self._process.terminate()
|
||||
self._process.wait(timeout=2)
|
||||
self._process = None
|
||||
return not wait_dead or self.wait_dead(timeout=timedelta(seconds=5))
|
||||
return True
|
||||
|
||||
def restart(self):
|
||||
self.stop()
|
||||
return self.start()
|
||||
|
||||
def wait_dead(self, timeout: timedelta):
|
||||
curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
|
||||
try_until = datetime.now() + timeout
|
||||
while datetime.now() < try_until:
|
||||
check_url = f'https://{self.env.domain1}:{self.env.caddy_port}/'
|
||||
r = curl.http_get(url=check_url)
|
||||
if r.exit_code != 0:
|
||||
return True
|
||||
log.debug(f'waiting for caddy to stop responding: {r}')
|
||||
time.sleep(.1)
|
||||
log.debug(f"Server still responding after {timeout}")
|
||||
return False
|
||||
|
||||
def wait_live(self, timeout: timedelta):
|
||||
curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
|
||||
try_until = datetime.now() + timeout
|
||||
while datetime.now() < try_until:
|
||||
check_url = f'https://{self.env.domain1}:{self.env.caddy_port}/'
|
||||
r = curl.http_get(url=check_url)
|
||||
if r.exit_code == 0:
|
||||
return True
|
||||
log.error(f'curl: {r}')
|
||||
log.debug(f'waiting for caddy to become responsive: {r}')
|
||||
time.sleep(.1)
|
||||
log.error(f"Server still not responding after {timeout}")
|
||||
return False
|
||||
|
||||
def _rmf(self, path):
|
||||
if os.path.exists(path):
|
||||
return os.remove(path)
|
||||
|
||||
def _mkpath(self, path):
|
||||
if not os.path.exists(path):
|
||||
return os.makedirs(path)
|
||||
|
||||
def _write_config(self):
|
||||
domain1 = self.env.domain1
|
||||
creds1 = self.env.get_credentials(domain1)
|
||||
self._mkpath(self._docs_dir)
|
||||
self._mkpath(self._tmp_dir)
|
||||
with open(os.path.join(self._docs_dir, 'data.json'), 'w') as fd:
|
||||
data = {
|
||||
'server': f'{domain1}',
|
||||
}
|
||||
fd.write(JSONEncoder().encode(data))
|
||||
with open(self._conf_file, 'w') as fd:
|
||||
conf = [ # base server config
|
||||
f'{{',
|
||||
f' https_port {self.env.caddy_port}',
|
||||
f' servers :{self.env.caddy_port} {{',
|
||||
f' protocols h3 h2 h1',
|
||||
f' }}',
|
||||
f'}}',
|
||||
f'{domain1}:{self.env.caddy_port} {{',
|
||||
f' file_server * {{',
|
||||
f' root {self._docs_dir}',
|
||||
f' }}',
|
||||
f' tls {creds1.cert_file} {creds1.pkey_file}',
|
||||
f'}}',
|
||||
]
|
||||
fd.write("\n".join(conf))
|
||||
|
|
@ -155,28 +155,36 @@ class ExecResult:
|
|||
def add_assets(self, assets: List):
|
||||
self._assets.extend(assets)
|
||||
|
||||
def check_responses(self, count: int, exp_status: Optional[int] = None):
|
||||
if len(self.responses) != count:
|
||||
seen_queries = []
|
||||
for idx, resp in enumerate(self.responses):
|
||||
assert resp['status'] == 200, f'response #{idx} status: {resp["status"]}'
|
||||
if 'rquery' not in resp['header']:
|
||||
log.error(f'response #{idx} missing "rquery": {resp["header"]}')
|
||||
seen_queries.append(int(resp['header']['rquery']))
|
||||
for i in range(0, count-1):
|
||||
if i not in seen_queries:
|
||||
log.error(f'response for query {i} missing')
|
||||
if self.with_stats and len(self.stats) == count:
|
||||
log.error(f'got all {count} stats, though')
|
||||
def check_responses(self, count: int, exp_status: Optional[int] = None,
|
||||
exp_exitcode: Optional[int] = None):
|
||||
assert len(self.responses) == count, \
|
||||
f'response count: expected {count}, got {len(self.responses)}'
|
||||
if exp_status is not None:
|
||||
for idx, x in enumerate(self.responses):
|
||||
assert x['status'] == exp_status, \
|
||||
f'response #{idx} unexpectedstatus: {x["status"]}'
|
||||
if exp_exitcode is not None:
|
||||
for idx, x in enumerate(self.responses):
|
||||
if 'exitcode' in x:
|
||||
assert x['exitcode'] == 0, f'response #{idx} exitcode: {x["exitcode"]}'
|
||||
if self.with_stats:
|
||||
assert len(self.stats) == count, f'{self}'
|
||||
|
||||
def check_stats(self, count: int, exp_status: Optional[int] = None,
|
||||
exp_exitcode: Optional[int] = None):
|
||||
assert len(self.stats) == count, \
|
||||
f'stats count: expected {count}, got {len(self.stats)}'
|
||||
if exp_status is not None:
|
||||
for idx, x in enumerate(self.stats):
|
||||
assert 'http_code' in x, \
|
||||
f'status #{idx} reports no http_code'
|
||||
assert x['http_code'] == exp_status, \
|
||||
f'status #{idx} unexpected http_code: {x["http_code"]}'
|
||||
if exp_exitcode is not None:
|
||||
for idx, x in enumerate(self.stats):
|
||||
if 'exitcode' in x:
|
||||
assert x['exitcode'] == 0, f'status #{idx} exitcode: {x["exitcode"]}'
|
||||
|
||||
|
||||
class CurlClient:
|
||||
|
||||
|
|
@ -186,7 +194,7 @@ class CurlClient:
|
|||
'http/1.1': '--http1.1',
|
||||
'h2': '--http2',
|
||||
'h2c': '--http2',
|
||||
'h3': '--http3',
|
||||
'h3': '--http3-only',
|
||||
}
|
||||
|
||||
def __init__(self, env: Env, run_dir: Optional[str] = None):
|
||||
|
|
@ -219,6 +227,7 @@ class CurlClient:
|
|||
def http_download(self, urls: List[str],
|
||||
alpn_proto: Optional[str] = None,
|
||||
with_stats: bool = True,
|
||||
with_headers: bool = False,
|
||||
extra_args: List[str] = None):
|
||||
if extra_args is None:
|
||||
extra_args = []
|
||||
|
|
@ -230,7 +239,41 @@ class CurlClient:
|
|||
'-w', '%{json}\\n'
|
||||
])
|
||||
return self._raw(urls, alpn_proto=alpn_proto, options=extra_args,
|
||||
with_stats=with_stats)
|
||||
with_stats=with_stats,
|
||||
with_headers=with_headers)
|
||||
|
||||
def http_upload(self, urls: List[str], data: str,
|
||||
alpn_proto: Optional[str] = None,
|
||||
with_stats: bool = True,
|
||||
with_headers: bool = False,
|
||||
extra_args: Optional[List[str]] = None):
|
||||
if extra_args is None:
|
||||
extra_args = []
|
||||
extra_args.extend([
|
||||
'--data-binary', data, '-o', 'download_#1.data',
|
||||
])
|
||||
if with_stats:
|
||||
extra_args.extend([
|
||||
'-w', '%{json}\\n'
|
||||
])
|
||||
return self._raw(urls, alpn_proto=alpn_proto, options=extra_args,
|
||||
with_stats=with_stats,
|
||||
with_headers=with_headers)
|
||||
|
||||
def response_file(self, idx: int):
|
||||
return os.path.join(self._run_dir, f'download_{idx}.data')
|
||||
|
||||
def run_direct(self, args, with_stats: bool = False):
|
||||
my_args = [self._curl]
|
||||
if with_stats:
|
||||
my_args.extend([
|
||||
'-w', '%{json}\\n'
|
||||
])
|
||||
my_args.extend([
|
||||
'-o', 'download.data',
|
||||
])
|
||||
my_args.extend(args)
|
||||
return self._run(args=my_args, with_stats=with_stats)
|
||||
|
||||
def _run(self, args, intext='', with_stats: bool = False):
|
||||
self._rmf(self._stdoutfile)
|
||||
|
|
@ -252,12 +295,15 @@ class CurlClient:
|
|||
|
||||
def _raw(self, urls, timeout=10, options=None, insecure=False,
|
||||
alpn_proto: Optional[str] = None,
|
||||
force_resolve=True, with_stats=False):
|
||||
force_resolve=True,
|
||||
with_stats=False,
|
||||
with_headers=True):
|
||||
args = self._complete_args(
|
||||
urls=urls, timeout=timeout, options=options, insecure=insecure,
|
||||
alpn_proto=alpn_proto, force_resolve=force_resolve)
|
||||
alpn_proto=alpn_proto, force_resolve=force_resolve,
|
||||
with_headers=with_headers)
|
||||
r = self._run(args, with_stats=with_stats)
|
||||
if r.exit_code == 0:
|
||||
if r.exit_code == 0 and with_headers:
|
||||
self._parse_headerfile(self._headerfile, r=r)
|
||||
if r.json:
|
||||
r.response["json"] = r.json
|
||||
|
|
@ -265,13 +311,14 @@ class CurlClient:
|
|||
|
||||
def _complete_args(self, urls, timeout=None, options=None,
|
||||
insecure=False, force_resolve=True,
|
||||
alpn_proto: Optional[str] = None):
|
||||
alpn_proto: Optional[str] = None,
|
||||
with_headers: bool = True):
|
||||
if not isinstance(urls, list):
|
||||
urls = [urls]
|
||||
|
||||
args = [
|
||||
self._curl, "-s", "--path-as-is", "-D", self._headerfile,
|
||||
]
|
||||
args = [self._curl, "-s", "--path-as-is"]
|
||||
if with_headers:
|
||||
args.extend(["-D", self._headerfile])
|
||||
if self.env.verbose > 2:
|
||||
args.extend(['--trace', self._tracefile, '--trace-time'])
|
||||
|
||||
|
|
|
|||
|
|
@ -59,19 +59,41 @@ class EnvConfig:
|
|||
self.config = DEF_CONFIG
|
||||
# check cur and its features
|
||||
self.curl = CURL
|
||||
self.curl_features = []
|
||||
self.curl_props = {
|
||||
'version': None,
|
||||
'os': None,
|
||||
'features': [],
|
||||
'protocols': [],
|
||||
'libs': [],
|
||||
'lib_versions': [],
|
||||
}
|
||||
self.curl_protos = []
|
||||
p = subprocess.run(args=[self.curl, '-V'],
|
||||
capture_output=True, text=True)
|
||||
if p.returncode != 0:
|
||||
assert False, f'{self.curl} -V failed with exit code: {p.returncode}'
|
||||
for l in p.stdout.splitlines(keepends=False):
|
||||
if l.startswith('curl '):
|
||||
m = re.match(r'^curl (?P<version>\S+) (?P<os>\S+) (?P<libs>.*)$', l)
|
||||
if m:
|
||||
self.curl_props['version'] = m.group('version')
|
||||
self.curl_props['os'] = m.group('os')
|
||||
self.curl_props['lib_versions'] = [
|
||||
lib.lower() for lib in m.group('libs').split(' ')
|
||||
]
|
||||
self.curl_props['libs'] = [
|
||||
re.sub(r'/.*', '',lib) for lib in self.curl_props['lib_versions']
|
||||
]
|
||||
if l.startswith('Features: '):
|
||||
self.curl_features = [feat.lower() for feat in l[10:].split(' ')]
|
||||
self.curl_props['features'] = [
|
||||
feat.lower() for feat in l[10:].split(' ')
|
||||
]
|
||||
if l.startswith('Protocols: '):
|
||||
self.curl_protos = [prot.lower() for prot in l[11:].split(' ')]
|
||||
self.curl_props['protocols'] = [
|
||||
prot.lower() for prot in l[11:].split(' ')
|
||||
]
|
||||
self.nghttpx_with_h3 = re.match(r'.* nghttp3/.*', p.stdout.strip())
|
||||
log.error(f'nghttpx -v: {p.stdout}')
|
||||
log.debug(f'nghttpx -v: {p.stdout}')
|
||||
|
||||
self.http_port = self.config['test']['http_port']
|
||||
self.https_port = self.config['test']['https_port']
|
||||
|
|
@ -81,6 +103,7 @@ class EnvConfig:
|
|||
self.apxs = self.config['httpd']['apxs']
|
||||
if len(self.apxs) == 0:
|
||||
self.apxs = None
|
||||
self._httpd_version = None
|
||||
|
||||
self.examples_pem = {
|
||||
'key': 'xxx',
|
||||
|
|
@ -110,7 +133,39 @@ class EnvConfig:
|
|||
self.nghttpx = None
|
||||
else:
|
||||
self.nghttpx_with_h3 = re.match(r'.* nghttp3/.*', p.stdout.strip()) is not None
|
||||
log.error(f'nghttpx -v: {p.stdout}')
|
||||
log.debug(f'nghttpx -v: {p.stdout}')
|
||||
|
||||
self.caddy = self.config['caddy']['caddy']
|
||||
if len(self.caddy) == 0:
|
||||
self.caddy = 'caddy'
|
||||
if self.caddy is not None:
|
||||
try:
|
||||
p = subprocess.run(args=[self.caddy, 'version'],
|
||||
capture_output=True, text=True)
|
||||
if p.returncode != 0:
|
||||
# not a working caddy
|
||||
self.caddy = None
|
||||
except:
|
||||
self.caddy = None
|
||||
self.caddy_port = self.config['caddy']['port']
|
||||
|
||||
@property
|
||||
def httpd_version(self):
|
||||
if self._httpd_version is None and self.apxs is not None:
|
||||
p = subprocess.run(args=[self.apxs, '-q', 'HTTPD_VERSION'],
|
||||
capture_output=True, text=True)
|
||||
if p.returncode != 0:
|
||||
raise Exception(f'{self.apxs} failed to query HTTPD_VERSION: {p}')
|
||||
self._httpd_version = p.stdout.strip()
|
||||
return self._httpd_version
|
||||
|
||||
def _versiontuple(self, v):
|
||||
v = re.sub(r'(\d+\.\d+(\.\d+)?)(-\S+)?', r'\1', v)
|
||||
return tuple(map(int, v.split('.')))
|
||||
|
||||
def httpd_is_at_least(self, minv):
|
||||
hv = self._versiontuple(self.httpd_version)
|
||||
return hv >= self._versiontuple(minv)
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
return os.path.isfile(self.httpd) and \
|
||||
|
|
@ -146,14 +201,46 @@ class Env:
|
|||
def have_h3_server() -> bool:
|
||||
return Env.CONFIG.nghttpx_with_h3
|
||||
|
||||
@staticmethod
|
||||
def have_h2_curl() -> bool:
|
||||
return 'http2' in Env.CONFIG.curl_props['features']
|
||||
|
||||
@staticmethod
|
||||
def have_h3_curl() -> bool:
|
||||
return 'http3' in Env.CONFIG.curl_features
|
||||
return 'http3' in Env.CONFIG.curl_props['features']
|
||||
|
||||
@staticmethod
|
||||
def curl_uses_lib(libname: str) -> bool:
|
||||
return libname.lower() in Env.CONFIG.curl_props['libs']
|
||||
|
||||
@staticmethod
|
||||
def curl_lib_version(libname: str) -> str:
|
||||
prefix = f'{libname.lower()}/'
|
||||
for lversion in Env.CONFIG.curl_props['lib_versions']:
|
||||
if lversion.startswith(prefix):
|
||||
return lversion[len(prefix):]
|
||||
return 'unknown'
|
||||
|
||||
@staticmethod
|
||||
def curl_os() -> bool:
|
||||
return Env.CONFIG.curl_props['os']
|
||||
|
||||
@staticmethod
|
||||
def curl_version() -> bool:
|
||||
return Env.CONFIG.curl_props['version']
|
||||
|
||||
@staticmethod
|
||||
def have_h3() -> bool:
|
||||
return Env.have_h3_curl() and Env.have_h3_server()
|
||||
|
||||
@staticmethod
|
||||
def httpd_version() -> str:
|
||||
return Env.CONFIG.httpd_version
|
||||
|
||||
@staticmethod
|
||||
def httpd_is_at_least(minv) -> bool:
|
||||
return Env.CONFIG.httpd_is_at_least(minv)
|
||||
|
||||
def __init__(self, pytestconfig=None):
|
||||
self._verbose = pytestconfig.option.verbose \
|
||||
if pytestconfig is not None else 0
|
||||
|
|
@ -214,6 +301,14 @@ class Env:
|
|||
def h3_port(self) -> str:
|
||||
return self.CONFIG.h3_port
|
||||
|
||||
@property
|
||||
def caddy(self) -> str:
|
||||
return self.CONFIG.caddy
|
||||
|
||||
@property
|
||||
def caddy_port(self) -> str:
|
||||
return self.CONFIG.caddy_port
|
||||
|
||||
@property
|
||||
def curl(self) -> str:
|
||||
return self.CONFIG.curl
|
||||
|
|
|
|||
|
|
@ -69,22 +69,24 @@ class Httpd:
|
|||
self._error_log = os.path.join(self._logs_dir, 'error_log')
|
||||
self._tmp_dir = os.path.join(self._apache_dir, 'tmp')
|
||||
self._mods_dir = None
|
||||
if env.apxs is not None:
|
||||
p = subprocess.run(args=[env.apxs, '-q', 'libexecdir'],
|
||||
capture_output=True, text=True)
|
||||
if p.returncode != 0:
|
||||
raise Exception(f'{env.apxs} failed to query libexecdir: {p}')
|
||||
self._mods_dir = p.stdout.strip()
|
||||
else:
|
||||
for md in self.COMMON_MODULES_DIRS:
|
||||
if os.path.isdir(md):
|
||||
self._mods_dir = md
|
||||
assert env.apxs
|
||||
p = subprocess.run(args=[env.apxs, '-q', 'libexecdir'],
|
||||
capture_output=True, text=True)
|
||||
if p.returncode != 0:
|
||||
raise Exception(f'{env.apxs} failed to query libexecdir: {p}')
|
||||
self._mods_dir = p.stdout.strip()
|
||||
if self._mods_dir is None:
|
||||
raise Exception(f'apache modules dir cannot be found')
|
||||
if not os.path.exists(self._mods_dir):
|
||||
raise Exception(f'apache modules dir does not exist: {self._mods_dir}')
|
||||
self._process = None
|
||||
self._rmf(self._error_log)
|
||||
self._init_curltest()
|
||||
|
||||
@property
|
||||
def docs_dir(self):
|
||||
return self._docs_dir
|
||||
|
||||
def clear_logs(self):
|
||||
self._rmf(self._error_log)
|
||||
|
||||
|
|
@ -213,9 +215,6 @@ class Httpd:
|
|||
f'Listen {self.env.http_port}',
|
||||
f'Listen {self.env.https_port}',
|
||||
f'TypesConfig "{self._conf_dir}/mime.types',
|
||||
# we want the quest string in a response header, so we
|
||||
# can check responses more easily
|
||||
f'Header set rquery "%{{QUERY_STRING}}s"',
|
||||
]
|
||||
conf.extend([ # plain http host for domain1
|
||||
f'<VirtualHost *:{self.env.http_port}>',
|
||||
|
|
|
|||
|
|
@ -24,15 +24,16 @@
|
|||
#
|
||||
###########################################################################
|
||||
#
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from .env import Env
|
||||
from .curl import CurlClient
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -43,12 +44,18 @@ class Nghttpx:
|
|||
def __init__(self, env: Env):
|
||||
self.env = env
|
||||
self._cmd = env.nghttpx
|
||||
self._pid_file = os.path.join(env.gen_dir, 'nghttpx.pid')
|
||||
self._conf_file = os.path.join(env.gen_dir, 'nghttpx.conf')
|
||||
self._error_log = os.path.join(env.gen_dir, 'nghttpx.log')
|
||||
self._stderr = os.path.join(env.gen_dir, 'nghttpx.stderr')
|
||||
self._run_dir = os.path.join(env.gen_dir, 'nghttpx')
|
||||
self._pid_file = os.path.join(self._run_dir, 'nghttpx.pid')
|
||||
self._conf_file = os.path.join(self._run_dir, 'nghttpx.conf')
|
||||
self._error_log = os.path.join(self._run_dir, 'nghttpx.log')
|
||||
self._stderr = os.path.join(self._run_dir, 'nghttpx.stderr')
|
||||
self._tmp_dir = os.path.join(self._run_dir, 'tmp')
|
||||
self._process = None
|
||||
self._process: Optional[subprocess.Popen] = None
|
||||
self._rmf(self._pid_file)
|
||||
self._rmf(self._error_log)
|
||||
self._mkpath(self._run_dir)
|
||||
self._write_config()
|
||||
|
||||
def exists(self):
|
||||
return os.path.exists(self._cmd)
|
||||
|
|
@ -63,10 +70,15 @@ class Nghttpx:
|
|||
return self._process.returncode is None
|
||||
return False
|
||||
|
||||
def start(self):
|
||||
def start_if_needed(self):
|
||||
if not self.is_running():
|
||||
return self.start()
|
||||
return True
|
||||
|
||||
def start(self, wait_live=True):
|
||||
self._mkpath(self._tmp_dir)
|
||||
if self._process:
|
||||
self.stop()
|
||||
self._write_config()
|
||||
args = [
|
||||
self._cmd,
|
||||
f'--frontend=*,{self.env.h3_port};quic',
|
||||
|
|
@ -82,31 +94,78 @@ class Nghttpx:
|
|||
]
|
||||
ngerr = open(self._stderr, 'a')
|
||||
self._process = subprocess.Popen(args=args, stderr=ngerr)
|
||||
return self._process.returncode is None
|
||||
if self._process.returncode is not None:
|
||||
return False
|
||||
return not wait_live or self.wait_live(timeout=timedelta(seconds=5))
|
||||
|
||||
def stop(self):
|
||||
def stop_if_running(self):
|
||||
if self.is_running():
|
||||
return self.stop()
|
||||
return True
|
||||
|
||||
def stop(self, wait_dead=True):
|
||||
self._mkpath(self._tmp_dir)
|
||||
if self._process:
|
||||
self._process.terminate()
|
||||
self._process.wait(timeout=2)
|
||||
self._process = None
|
||||
return not wait_dead or self.wait_dead(timeout=timedelta(seconds=5))
|
||||
return True
|
||||
|
||||
def restart(self):
|
||||
self.stop()
|
||||
return self.start()
|
||||
|
||||
def reload(self, timeout: datetime.timedelta):
|
||||
def reload(self, timeout: timedelta):
|
||||
if self._process:
|
||||
running = self._process
|
||||
self._process = None
|
||||
os.kill(running.pid, signal.SIGQUIT)
|
||||
self.start()
|
||||
try:
|
||||
log.debug(f'waiting for nghttpx({running.pid}) to exit.')
|
||||
running.wait(timeout=timeout.seconds)
|
||||
log.debug(f'nghttpx({running.pid}) terminated -> {running.returncode}')
|
||||
end_wait = datetime.now() + timeout
|
||||
if not self.start(wait_live=False):
|
||||
self._process = running
|
||||
return False
|
||||
while datetime.now() < end_wait:
|
||||
try:
|
||||
log.debug(f'waiting for nghttpx({running.pid}) to exit.')
|
||||
running.wait(2)
|
||||
log.debug(f'nghttpx({running.pid}) terminated -> {running.returncode}')
|
||||
break
|
||||
except subprocess.TimeoutExpired:
|
||||
log.warning(f'nghttpx({running.pid}), not shut down yet.')
|
||||
os.kill(running.pid, signal.SIGQUIT)
|
||||
if datetime.now() >= end_wait:
|
||||
log.error(f'nghttpx({running.pid}), terminate forcefully.')
|
||||
os.kill(running.pid, signal.SIGKILL)
|
||||
running.terminate()
|
||||
running.wait(1)
|
||||
return self.wait_live(timeout=timedelta(seconds=5))
|
||||
return False
|
||||
|
||||
def wait_dead(self, timeout: timedelta):
|
||||
curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
|
||||
try_until = datetime.now() + timeout
|
||||
while datetime.now() < try_until:
|
||||
check_url = f'https://{self.env.domain1}:{self.env.h3_port}/'
|
||||
r = curl.http_get(url=check_url, extra_args=['--http3-only'])
|
||||
if r.exit_code != 0:
|
||||
return True
|
||||
except subprocess.TimeoutExpired:
|
||||
log.error(f'SIGQUIT nghttpx({running.pid}), but did not shut down.')
|
||||
log.debug(f'waiting for nghttpx to stop responding: {r}')
|
||||
time.sleep(.1)
|
||||
log.debug(f"Server still responding after {timeout}")
|
||||
return False
|
||||
|
||||
def wait_live(self, timeout: timedelta):
|
||||
curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
|
||||
try_until = datetime.now() + timeout
|
||||
while datetime.now() < try_until:
|
||||
check_url = f'https://{self.env.domain1}:{self.env.h3_port}/'
|
||||
r = curl.http_get(url=check_url, extra_args=['--http3-only'])
|
||||
if r.exit_code == 0:
|
||||
return True
|
||||
log.debug(f'waiting for nghttpx to become responsive: {r}')
|
||||
time.sleep(.1)
|
||||
log.error(f"Server still not responding after {timeout}")
|
||||
return False
|
||||
|
||||
def _rmf(self, path):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue