openssl: test and fix for forward proxy handling (non-tunneling).

- adding pytest test_10 cases for proxy httpd setup tests
- fixing openssl bug in https: proxy hostname verification that
  used the hostname of the request and not the proxy name.

Closes #10498
This commit is contained in:
Stefan Eissing 2023-02-13 16:15:20 +01:00 committed by Daniel Stenberg
parent 17153e173d
commit ce0cad713d
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
4 changed files with 133 additions and 8 deletions

View file

@ -2122,6 +2122,22 @@ static bool subj_alt_hostcheck(struct Curl_easy *data,
return FALSE;
}
static CURLcode
ossl_verifyhost(struct Curl_easy *data, struct connectdata *conn,
X509 *server_cert, const char *hostname,
const char *dispname);
CURLcode Curl_ossl_verifyhost(struct Curl_easy *data, struct connectdata *conn,
X509 *server_cert)
{
const char *hostname, *dispname;
int port;
(void)conn;
Curl_conn_get_host(data, FIRSTSOCKET, &hostname, &dispname, &port);
return ossl_verifyhost(data, conn, server_cert, hostname, dispname);
}
/* Quote from RFC2818 section 3.1 "Server Identity"
If a subjectAltName extension of type dNSName is present, that MUST
@ -2144,8 +2160,10 @@ static bool subj_alt_hostcheck(struct Curl_easy *data,
This function is now used from ngtcp2 (QUIC) as well.
*/
CURLcode Curl_ossl_verifyhost(struct Curl_easy *data, struct connectdata *conn,
X509 *server_cert)
static CURLcode
ossl_verifyhost(struct Curl_easy *data, struct connectdata *conn,
X509 *server_cert, const char *hostname,
const char *dispname)
{
bool matched = FALSE;
int target = GEN_DNS; /* target type, GEN_DNS or GEN_IPADD */
@ -2159,12 +2177,9 @@ CURLcode Curl_ossl_verifyhost(struct Curl_easy *data, struct connectdata *conn,
CURLcode result = CURLE_OK;
bool dNSName = FALSE; /* if a dNSName field exists in the cert */
bool iPAddress = FALSE; /* if a iPAddress field exists in the cert */
const char *hostname, *dispname;
int port;
size_t hostlen;
(void)conn;
Curl_conn_get_host(data, FIRSTSOCKET, &hostname, &dispname, &port);
hostlen = strlen(hostname);
#ifndef ENABLE_IPV6
@ -4129,7 +4144,8 @@ static CURLcode servercert(struct Curl_cfilter *cf,
BIO_free(mem);
if(conn_config->verifyhost) {
result = Curl_ossl_verifyhost(data, conn, backend->server_cert);
result = ossl_verifyhost(data, conn, backend->server_cert,
connssl->hostname, connssl->dispname);
if(result) {
X509_free(backend->server_cert);
backend->server_cert = NULL;

View file

@ -0,0 +1,70 @@
#!/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 pytest
from testenv import Env, CurlClient
log = logging.getLogger(__name__)
@pytest.mark.skipif(condition=Env.setup_incomplete(),
reason=f"missing: {Env.incomplete_reason()}")
class TestProxy:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env, httpd):
push_dir = os.path.join(httpd.docs_dir, 'push')
if not os.path.exists(push_dir):
os.makedirs(push_dir)
# download via http: proxy (no tunnel)
def test_10_01_http_get(self, env: Env, httpd, repeat):
curl = CurlClient(env=env)
url = f'http://localhost:{env.http_port}/data.json'
r = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True,
extra_args=[
'--proxy', f'http://{env.proxy_domain}:{env.http_port}/',
'--resolve', f'{env.proxy_domain}:{env.http_port}:127.0.0.1',
])
assert r.exit_code == 0
r.check_stats(count=1, exp_status=200)
# download via https: proxy (no tunnel)
def test_10_02_http_get(self, env: Env, httpd, repeat):
curl = CurlClient(env=env)
url = f'http://localhost:{env.http_port}/data.json'
r = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True,
extra_args=[
'--proxy', f'https://{env.proxy_domain}:{env.https_port}/',
'--resolve', f'{env.proxy_domain}:{env.https_port}:127.0.0.1',
'--proxy-cacert', env.ca.cert_file,
])
assert r.exit_code == 0
r.check_stats(count=1, exp_status=200)

View file

@ -113,9 +113,11 @@ class EnvConfig:
self.tld = 'tests-httpd.curl.se'
self.domain1 = f"one.{self.tld}"
self.domain2 = f"two.{self.tld}"
self.proxy_domain = f"proxy.{self.tld}"
self.cert_specs = [
CertificateSpec(domains=[self.domain1], key_type='rsa2048'),
CertificateSpec(domains=[self.domain2], key_type='rsa2048'),
CertificateSpec(domains=[self.proxy_domain], key_type='rsa2048'),
CertificateSpec(name="clientsX", sub_specs=[
CertificateSpec(name="user1", client=True),
]),
@ -294,6 +296,11 @@ class Env:
def domain2(self) -> str:
return self.CONFIG.domain2
@property
def proxy_domain(self) -> str:
return self.CONFIG.proxy_domain
@property
def http_port(self) -> str:
return self.CONFIG.http_port

View file

@ -44,9 +44,9 @@ class Httpd:
MODULES = [
'log_config', 'logio', 'unixd', 'version', 'watchdog',
'authn_core', 'authz_user', 'authz_core',
'authn_core', 'authz_user', 'authz_core', 'authz_host',
'env', 'filter', 'headers', 'mime',
'rewrite', 'http2', 'ssl',
'rewrite', 'http2', 'ssl', 'proxy', 'proxy_http', 'proxy_connect',
'mpm_event',
]
COMMON_MODULES_DIRS = [
@ -191,6 +191,8 @@ class Httpd:
creds1 = self.env.get_credentials(domain1)
domain2 = self.env.domain2
creds2 = self.env.get_credentials(domain2)
proxy_domain = self.env.proxy_domain
proxy_creds = self.env.get_credentials(proxy_domain)
self._mkpath(self._conf_dir)
self._mkpath(self._logs_dir)
self._mkpath(self._tmp_dir)
@ -218,6 +220,8 @@ class Httpd:
f'ErrorLog {self._error_log}',
f'LogLevel {self._get_log_level()}',
f'LogLevel http:trace4',
f'LogLevel proxy:trace4',
f'LogLevel proxy_http:trace4',
f'H2MinWorkers 16',
f'H2MaxWorkers 128',
f'Listen {self.env.http_port}',
@ -227,6 +231,7 @@ class Httpd:
conf.extend([ # plain http host for domain1
f'<VirtualHost *:{self.env.http_port}>',
f' ServerName {domain1}',
f' ServerAlias localhost',
f' DocumentRoot "{self._docs_dir}"',
])
conf.extend(self._curltest_conf())
@ -234,6 +239,33 @@ class Httpd:
f'</VirtualHost>',
f'',
])
conf.extend([ # http forward proxy
f'<VirtualHost *:{self.env.http_port}>',
f' ServerName {proxy_domain}',
f' Protocols http/1.1',
f' ProxyRequests On',
f' ProxyVia On',
f' AllowCONNECT {self.env.http_port} {self.env.https_port}',
f' <Proxy "*">',
f' Require ip 127.0.0.1',
f' </Proxy>',
f'</VirtualHost>',
])
conf.extend([ # https forward proxy
f'<VirtualHost *:{self.env.https_port}>',
f' ServerName {proxy_domain}',
f' Protocols http/1.1',
f' SSLEngine on',
f' SSLCertificateFile {proxy_creds.cert_file}',
f' SSLCertificateKeyFile {proxy_creds.pkey_file}',
f' ProxyRequests On',
f' ProxyVia On',
f' AllowCONNECT {self.env.http_port} {self.env.https_port}',
f' <Proxy "*">',
f' Require ip 127.0.0.1',
f' </Proxy>',
f'</VirtualHost>',
])
conf.extend([ # https host for domain1, h1 + h2
f'<VirtualHost *:{self.env.https_port}>',
f' ServerName {domain1}',