url: fix logic in connection reuse to deny reuse on "unclean" connections

- add parameter to `conn_is_alive()` cfilter method that returns
  if there is input data waiting on the connection
- refrain from re-using connnection from the cache that have
  input pending
- adapt http/2 and http/3 alive checks to digest pending input
  to check the connection state
- remove check_cxn method from openssl as that was just doing
  what the socket filter now does.
- add tests for connection reuse with special server configs

Closes #10690
This commit is contained in:
Stefan Eissing 2023-03-06 12:44:45 +01:00 committed by Daniel Stenberg
parent 6466071e8e
commit 7c5637b8b4
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
17 changed files with 264 additions and 190 deletions

View File

@ -325,20 +325,6 @@ int Curl_socket_close(struct Curl_easy *data, struct connectdata *conn,
return socket_close(data, conn, FALSE, sock);
}
bool Curl_socket_is_dead(curl_socket_t sock)
{
int sval;
bool ret_val = TRUE;
sval = SOCKET_READABLE(sock, 0);
if(sval == 0)
/* timeout */
ret_val = FALSE;
return ret_val;
}
#ifdef USE_WINSOCK
/* When you run a program that uses the Windows Sockets API, you may
experience slow performance when you copy data to a TCP server.
@ -1449,12 +1435,14 @@ static CURLcode cf_socket_cntrl(struct Curl_cfilter *cf,
}
static bool cf_socket_conn_is_alive(struct Curl_cfilter *cf,
struct Curl_easy *data)
struct Curl_easy *data,
bool *input_pending)
{
struct cf_socket_ctx *ctx = cf->ctx;
struct pollfd pfd[1];
int r;
*input_pending = FALSE;
(void)data;
if(!ctx || ctx->sock == CURL_SOCKET_BAD)
return FALSE;
@ -1479,6 +1467,7 @@ static bool cf_socket_conn_is_alive(struct Curl_cfilter *cf,
}
DEBUGF(LOG_CF(data, cf, "is_alive: valid events, looks alive"));
*input_pending = TRUE;
return TRUE;
}

View File

@ -70,13 +70,6 @@ CURLcode Curl_socket_open(struct Curl_easy *data,
int Curl_socket_close(struct Curl_easy *data, struct connectdata *conn,
curl_socket_t sock);
/*
* This function should return TRUE if the socket is to be assumed to
* be dead. Most commonly this happens when the server has closed the
* connection due to inactivity.
*/
bool Curl_socket_is_dead(curl_socket_t sock);
/**
* Determine the curl code for a socket connect() == -1 with errno.
*/

View File

@ -124,10 +124,11 @@ ssize_t Curl_cf_def_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
}
bool Curl_cf_def_conn_is_alive(struct Curl_cfilter *cf,
struct Curl_easy *data)
struct Curl_easy *data,
bool *input_pending)
{
return cf->next?
cf->next->cft->is_alive(cf->next, data) :
cf->next->cft->is_alive(cf->next, data, input_pending) :
FALSE; /* pessimistic in absence of data */
}
@ -631,10 +632,12 @@ void Curl_conn_report_connect_stats(struct Curl_easy *data,
}
}
bool Curl_conn_is_alive(struct Curl_easy *data, struct connectdata *conn)
bool Curl_conn_is_alive(struct Curl_easy *data, struct connectdata *conn,
bool *input_pending)
{
struct Curl_cfilter *cf = conn->cfilter[FIRSTSOCKET];
return cf && !cf->conn->bits.close && cf->cft->is_alive(cf, data);
return cf && !cf->conn->bits.close &&
cf->cft->is_alive(cf, data, input_pending);
}
CURLcode Curl_conn_keep_alive(struct Curl_easy *data,

View File

@ -85,7 +85,8 @@ typedef ssize_t Curl_cft_recv(struct Curl_cfilter *cf,
CURLcode *err); /* error to return */
typedef bool Curl_cft_conn_is_alive(struct Curl_cfilter *cf,
struct Curl_easy *data);
struct Curl_easy *data,
bool *input_pending);
typedef CURLcode Curl_cft_conn_keep_alive(struct Curl_cfilter *cf,
struct Curl_easy *data);
@ -216,7 +217,8 @@ CURLcode Curl_cf_def_cntrl(struct Curl_cfilter *cf,
struct Curl_easy *data,
int event, int arg1, void *arg2);
bool Curl_cf_def_conn_is_alive(struct Curl_cfilter *cf,
struct Curl_easy *data);
struct Curl_easy *data,
bool *input_pending);
CURLcode Curl_cf_def_conn_keep_alive(struct Curl_cfilter *cf,
struct Curl_easy *data);
CURLcode Curl_cf_def_query(struct Curl_cfilter *cf,
@ -443,7 +445,8 @@ void Curl_conn_report_connect_stats(struct Curl_easy *data,
/**
* Check if FIRSTSOCKET's cfilter chain deems connection alive.
*/
bool Curl_conn_is_alive(struct Curl_easy *data, struct connectdata *conn);
bool Curl_conn_is_alive(struct Curl_easy *data, struct connectdata *conn,
bool *input_pending);
/**
* Try to upkeep the connection filters at sockindex.

View File

@ -366,6 +366,15 @@ static void http2_stream_free(struct HTTP *stream)
}
}
/*
* Returns nonzero if current HTTP/2 session should be closed.
*/
static int should_close_session(struct cf_h2_ctx *ctx)
{
return ctx->drain_total == 0 && !nghttp2_session_want_read(ctx->h2) &&
!nghttp2_session_want_write(ctx->h2);
}
/*
* The server may send us data at any point (e.g. PING frames). Therefore,
* we cannot assume that an HTTP/2 socket is dead just because it is readable.
@ -373,35 +382,27 @@ static void http2_stream_free(struct HTTP *stream)
* Check the lower filters first and, if successful, peek at the socket
* and distinguish between closed and data.
*/
static bool http2_connisdead(struct Curl_cfilter *cf, struct Curl_easy *data)
static bool http2_connisalive(struct Curl_cfilter *cf, struct Curl_easy *data,
bool *input_pending)
{
struct cf_h2_ctx *ctx = cf->ctx;
int sval;
bool dead = TRUE;
bool alive = TRUE;
if(!cf->next || !cf->next->cft->is_alive(cf->next, data))
return TRUE;
*input_pending = FALSE;
if(!cf->next || !cf->next->cft->is_alive(cf->next, data, input_pending))
return FALSE;
sval = SOCKET_READABLE(Curl_conn_cf_get_socket(cf, data), 0);
if(sval == 0) {
/* timeout */
dead = FALSE;
}
else if(sval & CURL_CSELECT_ERR) {
/* socket is in an error state */
dead = TRUE;
}
else if(sval & CURL_CSELECT_IN) {
if(*input_pending) {
/* This happens before we've sent off a request and the connection is
not in use by any other transfer, there shouldn't be any data here,
only "protocol frames" */
CURLcode result;
ssize_t nread = -1;
*input_pending = FALSE;
Curl_attach_connection(data, cf->conn);
nread = Curl_conn_cf_recv(cf->next, data,
ctx->inbuf, H2_BUFSIZE, &result);
dead = FALSE;
if(nread != -1) {
DEBUGF(LOG_CF(data, cf, "%d bytes stray data read before trying "
"h2 connection", (int)nread));
@ -409,15 +410,19 @@ static bool http2_connisdead(struct Curl_cfilter *cf, struct Curl_easy *data)
ctx->inbuflen = nread;
if(h2_process_pending_input(cf, data, &result) < 0)
/* immediate error, considered dead */
dead = TRUE;
alive = FALSE;
else {
alive = !should_close_session(ctx);
}
}
else
else {
/* the read failed so let's say this is dead anyway */
dead = TRUE;
alive = FALSE;
}
Curl_detach_connection(data);
}
return dead;
return alive;
}
static CURLcode http2_send_ping(struct Curl_cfilter *cf,
@ -1451,15 +1456,6 @@ CURLcode Curl_http2_request_upgrade(struct dynbuf *req,
return result;
}
/*
* Returns nonzero if current HTTP/2 session should be closed.
*/
static int should_close_session(struct cf_h2_ctx *ctx)
{
return ctx->drain_total == 0 && !nghttp2_session_want_read(ctx->h2) &&
!nghttp2_session_want_write(ctx->h2);
}
/*
* h2_process_pending_input() processes pending input left in
* httpc->inbuf. Then, call h2_session_send() to send pending data.
@ -2359,14 +2355,17 @@ static bool cf_h2_data_pending(struct Curl_cfilter *cf,
}
static bool cf_h2_is_alive(struct Curl_cfilter *cf,
struct Curl_easy *data)
struct Curl_easy *data,
bool *input_pending)
{
struct cf_h2_ctx *ctx = cf->ctx;
CURLcode result;
struct cf_call_data save;
CF_DATA_SAVE(save, cf, data);
result = (ctx && ctx->h2 && !http2_connisdead(cf, data));
result = (ctx && ctx->h2 && http2_connisalive(cf, data, input_pending));
DEBUGF(LOG_CF(data, cf, "conn alive -> %d, input_pending=%d",
result, *input_pending));
CF_DATA_RESTORE(cf, save);
return result;
}

View File

@ -145,7 +145,8 @@ static unsigned int rtsp_conncheck(struct Curl_easy *data,
(void)data;
if(checks_to_perform & CONNCHECK_ISDEAD) {
if(!Curl_conn_is_alive(data, conn))
bool input_pending;
if(!Curl_conn_is_alive(data, conn, &input_pending))
ret_val |= CONNRESULT_DEAD;
}

View File

@ -965,7 +965,20 @@ static bool extract_if_dead(struct connectdata *conn,
}
else {
dead = !Curl_conn_is_alive(data, conn);
bool input_pending;
dead = !Curl_conn_is_alive(data, conn, &input_pending);
if(input_pending) {
/* For reuse, we want a "clean" connection state. The includes
* that we expect - in general - no waiting input data. Input
* waiting might be a TLS Notify Close, for example. We reject
* that.
* For protocols where data from other other end may arrive at
* any time (HTTP/2 PING for example), the protocol handler needs
* to install its own `connection_check` callback.
*/
dead = TRUE;
}
}
if(dead) {

View File

@ -769,11 +769,13 @@ static CURLcode cf_msh3_query(struct Curl_cfilter *cf,
}
static bool cf_msh3_conn_is_alive(struct Curl_cfilter *cf,
struct Curl_easy *data)
struct Curl_easy *data,
bool *input_pending)
{
struct cf_msh3_ctx *ctx = cf->ctx;
(void)data;
*input_pending = FALSE;
return ctx && ctx->sock[SP_LOCAL] != CURL_SOCKET_BAD && ctx->qconn &&
ctx->connected;
}

View File

@ -2444,6 +2444,32 @@ static CURLcode cf_ngtcp2_query(struct Curl_cfilter *cf,
CURLE_UNKNOWN_OPTION;
}
static bool cf_ngtcp2_conn_is_alive(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *input_pending)
{
bool alive = TRUE;
*input_pending = FALSE;
if(!cf->next || !cf->next->cft->is_alive(cf->next, data, input_pending))
return FALSE;
if(*input_pending) {
/* This happens before we've sent off a request and the connection is
not in use by any other transfer, there shouldn't be any data here,
only "protocol frames" */
*input_pending = FALSE;
Curl_attach_connection(data, cf->conn);
if(cf_process_ingress(cf, data))
alive = FALSE;
else {
alive = TRUE;
}
Curl_detach_connection(data);
}
return alive;
}
struct Curl_cftype Curl_cft_http3 = {
"HTTP/3",
@ -2458,7 +2484,7 @@ struct Curl_cftype Curl_cft_http3 = {
cf_ngtcp2_send,
cf_ngtcp2_recv,
cf_ngtcp2_data_event,
Curl_cf_def_conn_is_alive,
cf_ngtcp2_conn_is_alive,
Curl_cf_def_conn_keep_alive,
cf_ngtcp2_query,
};

View File

@ -1359,6 +1359,32 @@ static CURLcode cf_quiche_query(struct Curl_cfilter *cf,
CURLE_UNKNOWN_OPTION;
}
static bool cf_quiche_conn_is_alive(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *input_pending)
{
bool alive = TRUE;
*input_pending = FALSE;
if(!cf->next || !cf->next->cft->is_alive(cf->next, data, input_pending))
return FALSE;
if(*input_pending) {
/* This happens before we've sent off a request and the connection is
not in use by any other transfer, there shouldn't be any data here,
only "protocol frames" */
*input_pending = FALSE;
Curl_attach_connection(data, cf->conn);
if(cf_process_ingress(cf, data))
alive = FALSE;
else {
alive = TRUE;
}
Curl_detach_connection(data);
}
return alive;
}
struct Curl_cftype Curl_cft_http3 = {
"HTTP/3",
@ -1373,7 +1399,7 @@ struct Curl_cftype Curl_cft_http3 = {
cf_quiche_send,
cf_quiche_recv,
cf_quiche_data_event,
Curl_cf_def_conn_is_alive,
cf_quiche_conn_is_alive,
Curl_cf_def_conn_keep_alive,
cf_quiche_query,
};

View File

@ -1536,36 +1536,6 @@ static void nss_cleanup(void)
initialized = 0;
}
/*
* This function uses SSL_peek to determine connection status.
*
* Return codes:
* 1 means the connection is still in place
* 0 means the connection has been closed
* -1 means the connection status is unknown
*/
static int nss_check_cxn(struct Curl_cfilter *cf, struct Curl_easy *data)
{
struct ssl_connect_data *connssl = cf->ctx;
struct ssl_backend_data *backend = connssl->backend;
int rc;
char buf;
(void)data;
DEBUGASSERT(backend);
rc =
PR_Recv(backend->handle, (void *)&buf, 1, PR_MSG_PEEK,
PR_SecondsToInterval(1));
if(rc > 0)
return 1; /* connection still in place */
if(rc == 0)
return 0; /* connection has been closed */
return -1; /* connection status unknown */
}
static void close_one(struct ssl_connect_data *connssl)
{
/* before the cleanup, check whether we are using a client certificate */
@ -2524,7 +2494,7 @@ const struct Curl_ssl Curl_ssl_nss = {
nss_init, /* init */
nss_cleanup, /* cleanup */
nss_version, /* version */
nss_check_cxn, /* check_cxn */
Curl_none_check_cxn, /* check_cxn */
/* NSS has no shutdown function provided and thus always fail */
Curl_none_shutdown, /* shutdown */
nss_data_pending, /* data_pending */

View File

@ -1780,63 +1780,6 @@ static void ossl_cleanup(void)
Curl_tls_keylog_close();
}
/*
* This function is used to determine connection status.
*
* Return codes:
* 1 means the connection is still in place
* 0 means the connection has been closed
* -1 means the connection status is unknown
*/
static int ossl_check_cxn(struct Curl_cfilter *cf, struct Curl_easy *data)
{
/* SSL_peek takes data out of the raw recv buffer without peeking so we use
recv MSG_PEEK instead. Bug #795 */
#ifdef MSG_PEEK
char buf;
ssize_t nread;
curl_socket_t sock = Curl_conn_cf_get_socket(cf, data);
if(sock == CURL_SOCKET_BAD)
return 0; /* no socket, consider closed */
nread = recv((RECV_TYPE_ARG1)sock,
(RECV_TYPE_ARG2)&buf, (RECV_TYPE_ARG3)1,
(RECV_TYPE_ARG4)MSG_PEEK);
if(nread == 0)
return 0; /* connection has been closed */
if(nread == 1)
return 1; /* connection still in place */
else if(nread == -1) {
int err = SOCKERRNO;
if(err == EINPROGRESS ||
#if defined(EAGAIN) && (EAGAIN != EWOULDBLOCK)
err == EAGAIN ||
#endif
err == EWOULDBLOCK)
return 1; /* connection still in place */
if(err == ECONNRESET ||
#ifdef ECONNABORTED
err == ECONNABORTED ||
#endif
#ifdef ENETDOWN
err == ENETDOWN ||
#endif
#ifdef ENETRESET
err == ENETRESET ||
#endif
#ifdef ESHUTDOWN
err == ESHUTDOWN ||
#endif
#ifdef ETIMEDOUT
err == ETIMEDOUT ||
#endif
err == ENOTCONN)
return 0; /* connection has been closed */
}
#endif
(void)data;
return -1; /* connection status unknown */
}
/* Selects an OpenSSL crypto engine
*/
static CURLcode ossl_set_engine(struct Curl_easy *data, const char *engine)
@ -4820,7 +4763,7 @@ const struct Curl_ssl Curl_ssl_openssl = {
ossl_init, /* init */
ossl_cleanup, /* cleanup */
ossl_version, /* version */
ossl_check_cxn, /* check_cxn */
Curl_none_check_cxn, /* check_cxn */
ossl_shutdown, /* shutdown */
ossl_data_pending, /* data_pending */
ossl_random, /* random */

View File

@ -3233,35 +3233,6 @@ static size_t sectransp_version(char *buffer, size_t size)
return msnprintf(buffer, size, "SecureTransport");
}
/*
* This function uses SSLGetSessionState to determine connection status.
*
* Return codes:
* 1 means the connection is still in place
* 0 means the connection has been closed
* -1 means the connection status is unknown
*/
static int sectransp_check_cxn(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct ssl_connect_data *connssl = cf->ctx;
struct ssl_backend_data *backend = connssl->backend;
OSStatus err;
SSLSessionState state;
(void)data;
DEBUGASSERT(backend);
if(backend->ssl_ctx) {
DEBUGF(LOG_CF(data, cf, "check connection"));
err = SSLGetSessionState(backend->ssl_ctx, &state);
if(err == noErr)
return state == kSSLConnected || state == kSSLHandshake;
return -1;
}
return 0;
}
static bool sectransp_data_pending(struct Curl_cfilter *cf,
const struct Curl_easy *data)
{
@ -3473,7 +3444,7 @@ const struct Curl_ssl Curl_ssl_sectransp = {
Curl_none_init, /* init */
Curl_none_cleanup, /* cleanup */
sectransp_version, /* version */
sectransp_check_cxn, /* check_cxn */
Curl_none_check_cxn, /* check_cxn */
sectransp_shutdown, /* shutdown */
sectransp_data_pending, /* data_pending */
sectransp_random, /* random */

View File

@ -1650,10 +1650,11 @@ static CURLcode ssl_cf_query(struct Curl_cfilter *cf,
CURLE_UNKNOWN_OPTION;
}
static bool cf_ssl_is_alive(struct Curl_cfilter *cf, struct Curl_easy *data)
static bool cf_ssl_is_alive(struct Curl_cfilter *cf, struct Curl_easy *data,
bool *input_pending)
{
struct cf_call_data save;
bool result;
int result;
/*
* This function tries to determine connection status.
*
@ -1663,15 +1664,19 @@ static bool cf_ssl_is_alive(struct Curl_cfilter *cf, struct Curl_easy *data)
* -1 means the connection status is unknown
*/
CF_DATA_SAVE(save, cf, data);
result = Curl_ssl->check_cxn(cf, data) != 0;
result = Curl_ssl->check_cxn(cf, data);
CF_DATA_RESTORE(cf, save);
if(result > 0)
if(result > 0) {
*input_pending = TRUE;
return TRUE;
if(result == 0)
}
if(result == 0) {
*input_pending = FALSE;
return FALSE;
}
/* ssl backend does not know */
return cf->next?
cf->next->cft->is_alive(cf->next, data) :
cf->next->cft->is_alive(cf->next, data, input_pending) :
FALSE; /* pessimistic in absence of data */
}

View File

@ -110,4 +110,36 @@ class TestGoAway:
assert r.duration >= timedelta(seconds=count)
r.check_stats(count=count, exp_status=200, exp_exitcode=0)
# download files sequentially with delay, reload server for GOAWAY
def test_03_03_h1_goaway(self, env: Env, httpd, nghttpx, repeat):
proto = 'http/1.1'
count = 3
self.r = None
def long_run():
curl = CurlClient(env=env)
# send 10 chunks of 1024 bytes in a response body with 100ms delay in between
urln = f'https://{env.authority_for(env.domain1, proto)}' \
f'/curltest/tweak?id=[0-{count - 1}]'\
'&chunks=10&chunk_size=1024&chunk_delay=100ms'
self.r = curl.http_download(urls=[urln], alpn_proto=proto)
t = Thread(target=long_run)
t.start()
# each request will take a second, reload the server in the middle
# of the first one.
time.sleep(1.5)
assert httpd.reload()
t.join()
r: ExecResult = self.r
assert r.exit_code == 0, f'{r}'
r.check_stats(count=count, exp_status=200)
# reload will shut down the connection gracefully with GOAWAY
# we expect to see a second connection opened afterwards
assert r.total_connects == 2
for idx, s in enumerate(r.stats):
if s['num_connects'] > 0:
log.debug(f'request {idx} connected')
# this should take `count` seconds to retrieve
assert r.duration >= timedelta(seconds=count)

View File

@ -0,0 +1,93 @@
#!/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 difflib
import filecmp
import logging
import os
import pytest
from testenv import Env, CurlClient
log = logging.getLogger(__name__)
@pytest.mark.skipif(condition=Env.setup_incomplete(),
reason=f"missing: {Env.incomplete_reason()}")
class TestReuse:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env, httpd, nghttpx):
env.make_data_file(indir=httpd.docs_dir, fname="data-100k", fsize=100*1024)
env.make_data_file(indir=httpd.docs_dir, fname="data-1m", fsize=1024*1024)
env.make_data_file(indir=httpd.docs_dir, fname="data-10m", fsize=10*1024*1024)
yield
# restore default config
httpd.clear_extra_configs()
httpd.reload()
# check if HTTP/1.1 handles 'Connection: close' correctly
@pytest.mark.parametrize("proto", ['http/1.1'])
def test_12_01_h1_conn_close(self, env: Env,
httpd, nghttpx, repeat, proto):
httpd.clear_extra_configs()
httpd.set_extra_config('base', [
f'MaxKeepAliveRequests 1',
])
httpd.reload()
count = 100
curl = CurlClient(env=env)
urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
r = curl.http_download(urls=[urln], alpn_proto=proto)
assert r.exit_code == 0
r.check_stats(count=count, exp_status=200)
# Server sends `Connection: close` on every 2nd request, requiring
# a new connection
assert r.total_connects == count/2
@pytest.mark.parametrize("proto", ['http/1.1'])
def test_12_02_h1_conn_timeout(self, env: Env,
httpd, nghttpx, repeat, proto):
httpd.clear_extra_configs()
httpd.set_extra_config('base', [
f'KeepAliveTimeout 1',
])
httpd.reload()
count = 5
curl = CurlClient(env=env)
urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
'--rate', '30/m',
])
assert r.exit_code == 0
r.check_stats(count=count, exp_status=200)
# Connections time out on server before we send another request,
assert r.total_connects == count
# we do not see how often a request was retried in the stats, so
# we cannot check that connection reuse attempted a connection that
# was later detected to be "dead". We would like to
# assert stat['retry_count'] == 0

View File

@ -100,6 +100,9 @@ class Httpd:
else:
self._extra_configs[domain] = lines
def clear_extra_configs(self):
self._extra_configs = {}
def _run(self, args, intext=''):
env = {}
for key, val in os.environ.items():
@ -231,6 +234,8 @@ class Httpd:
f'Listen {self.env.proxys_port}',
f'TypesConfig "{self._conf_dir}/mime.types',
]
if 'base' in self._extra_configs:
conf.extend(self._extra_configs['base'])
conf.extend([ # plain http host for domain1
f'<VirtualHost *:{self.env.http_port}>',
f' ServerName {domain1}',