1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
|
import os
import re
import socket
from threading import Thread
import pytest
from pyhttpd.conf import HttpdConf
from pyhttpd.result import ExecResult
class UDSFaker:
def __init__(self, path):
self._uds_path = path
self._done = False
def start(self):
def process(self):
self._socket.listen(1)
self._process()
try:
os.unlink(self._uds_path)
except OSError:
if os.path.exists(self._uds_path):
raise
self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self._socket.bind(self._uds_path)
self._thread = Thread(target=process, daemon=True, args=[self])
self._thread.start()
def stop(self):
self._done = True
self._socket.close()
def _process(self):
while self._done is False:
try:
c, client_address = self._socket.accept()
try:
data = c.recv(16)
c.sendall("""HTTP/1.1 200 Ok
Server: UdsFaker
Content-Type: application/json
Content-Length: 19
{ "host": "faked" }""".encode())
finally:
c.close()
except ConnectionAbortedError:
self._done = True
class TestProxyUds:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
# setup 3 vhosts on https: for reverse, forward and
# mixed proxying to a unix: domain socket
# We setup a UDSFaker running that returns a fixed response
UDS_PATH = f"{env.gen_dir}/proxy_02.sock"
TestProxyUds.UDS_PATH = UDS_PATH
faker = UDSFaker(path=UDS_PATH)
faker.start()
conf = HttpdConf(env)
conf.add("ProxyPreserveHost on")
conf.start_vhost(domains=[env.d_reverse], port=env.https_port)
conf.add([
f"ProxyPass / unix:{UDS_PATH}|http://127.0.0.1:{env.http_port}/"
])
conf.end_vhost()
conf.start_vhost(domains=[env.d_forward], port=env.https_port)
conf.add([
"ProxyRequests on"
])
conf.end_vhost()
conf.start_vhost(domains=[env.d_mixed], port=env.https_port)
conf.add([
f"ProxyPass / unix:{UDS_PATH}|http://127.0.0.1:{env.http_port}/",
"ProxyRequests on"
])
conf.end_vhost()
conf.install()
assert env.apache_restart() == 0
yield
faker.stop()
@pytest.mark.parametrize(["via", "seen"], [
["reverse", "faked"],
["mixed", "faked"],
])
def test_proxy_02_001(self, env, via, seen):
# make requests to a reverse proxy https: vhost to the http: vhost
# check that we see the document we expect there (host matching worked)
r = env.curl_get(f"https://{via}.{env.http_tld}:{env.https_port}/alive.json", 5)
assert r.response["status"] == 200
assert r.json['host'] == seen
@pytest.mark.parametrize(["via", "seen"], [
["forward", "generic"],
["mixed", "faked"],
])
def test_proxy_02_002(self, env, via, seen):
# make requests to a forward proxy https: vhost to the http: vhost
# check that we see the document we expect there (host matching worked)
# we need to explicitly provide a Host: header since mod_proxy cannot
# resolve the name via DNS.
if not env.curl_is_at_least('8.0.0'):
pytest.skip(f'need at least curl v8.0.0 for this')
domain = f"{via}.{env.http_tld}"
r = env.curl_get(f"http://127.0.0.1:{env.http_port}/alive.json", 5, options=[
'-H', f"Host: {domain}",
'--proxy', f"https://{domain}:{env.https_port}/",
'--resolve', f"{domain}:{env.https_port}:127.0.0.1",
'--proxy-cacert', f"{env.get_ca_pem_file(domain)}",
])
assert r.exit_code == 0, f"{r.stdout}{r.stderr}"
assert r.response["status"] == 200
assert r.json['host'] == seen
@pytest.mark.parametrize(["via", "exp_status"], [
["reverse", 400],
["forward", 500],
["mixed", 500],
])
def test_proxy_02_003(self, env, via, exp_status):
# make requests to a forward proxy https: vhost and GET
# a URL which carries the unix: domain socket.
# This needs to fail.
domain = f"{via}.{env.http_tld}"
r = env.run(args=[
'openssl', 's_client', '-connect', f"127.0.0.1:{env.https_port}",
'-servername', domain,
'-crlf', '-ign_eof',
'-CAfile', env.get_ca_pem_file(domain)
], intext=f"""GET unix:{TestProxyUds.UDS_PATH}|http://127.0.0.1:{env.http_port}/alive.json HTTP/1.1
Host: {domain}
""")
assert r.exit_code == 0, f"{r.stdout}{r.stderr}"
lines = r.stdout.split('\n')
rlines = None
for idx, l in enumerate(lines):
if l.startswith('HTTP/'):
rlines = lines[idx:]
assert rlines, f"No response found in: {r.stdout}"
r2 = self.parse_response(rlines)
assert r2.response
assert r2.response['status'] == exp_status
#
env.httpd_error_log.ignore_recent(
lognos = [
"AH01144" # No protocol handler was valid for the URL
]
)
def parse_response(self, lines) -> ExecResult:
exp_body = False
exp_stat = True
r = ExecResult(args=[], exit_code=0, stdout=b'', stderr=b'')
header = {}
body = []
for line in lines:
if exp_stat:
m = re.match(r'^(\S+) (\d+) (.*)$', line)
assert m, f"first line no HTTP status line: {line}"
r.add_response({
"protocol": m.group(1),
"status": int(m.group(2)),
"description": m.group(3),
"body": r.outraw
})
header = {}
exp_stat = False
exp_body = False
elif re.match(r'^\r?$', line):
exp_body = True
elif exp_body:
body.append(line)
else:
m = re.match(r'^([^:]+):\s*(.*)$', line)
assert m, f"not a header line: {line}"
header[m.group(1).lower()] = m.group(2)
if r.response:
r.response["header"] = header
r.response["body"] = body
return r
|