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# 27import difflib 28import filecmp 29import logging 30import os 31import re 32import pytest 33 34from testenv import Env, CurlClient, Caddy, LocalClient 35 36 37log = logging.getLogger(__name__) 38 39 40@pytest.mark.skipif(condition=not Env.has_caddy(), reason="missing caddy") 41@pytest.mark.skipif(condition=not Env.have_ssl_curl(), reason="curl without SSL") 42class TestCaddy: 43 44 @pytest.fixture(autouse=True, scope='class') 45 def caddy(self, env): 46 caddy = Caddy(env=env) 47 assert caddy.start() 48 yield caddy 49 caddy.stop() 50 51 def _make_docs_file(self, docs_dir: str, fname: str, fsize: int): 52 fpath = os.path.join(docs_dir, fname) 53 data1k = 1024*'x' 54 flen = 0 55 with open(fpath, 'w') as fd: 56 while flen < fsize: 57 fd.write(data1k) 58 flen += len(data1k) 59 return flen 60 61 @pytest.fixture(autouse=True, scope='class') 62 def _class_scope(self, env, caddy): 63 self._make_docs_file(docs_dir=caddy.docs_dir, fname='data10k.data', fsize=10*1024) 64 self._make_docs_file(docs_dir=caddy.docs_dir, fname='data1.data', fsize=1024*1024) 65 self._make_docs_file(docs_dir=caddy.docs_dir, fname='data5.data', fsize=5*1024*1024) 66 self._make_docs_file(docs_dir=caddy.docs_dir, fname='data10.data', fsize=10*1024*1024) 67 self._make_docs_file(docs_dir=caddy.docs_dir, fname='data100.data', fsize=100*1024*1024) 68 env.make_data_file(indir=env.gen_dir, fname="data-10m", fsize=10*1024*1024) 69 70 # download 1 file 71 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 72 def test_08_01_download_1(self, env: Env, caddy: Caddy, proto): 73 if proto == 'h3' and not env.have_h3_curl(): 74 pytest.skip("h3 not supported in curl") 75 if proto == 'h3' and env.curl_uses_lib('msh3'): 76 pytest.skip("msh3 itself crashes") 77 curl = CurlClient(env=env) 78 url = f'https://{env.domain1}:{caddy.port}/data.json' 79 r = curl.http_download(urls=[url], alpn_proto=proto) 80 r.check_response(count=1, http_status=200) 81 82 # download 1MB files sequentially 83 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 84 def test_08_02_download_1mb_sequential(self, env: Env, caddy: Caddy, proto): 85 if proto == 'h3' and not env.have_h3_curl(): 86 pytest.skip("h3 not supported in curl") 87 if proto == 'h3' and env.curl_uses_lib('msh3'): 88 pytest.skip("msh3 itself crashes") 89 count = 50 90 curl = CurlClient(env=env) 91 urln = f'https://{env.domain1}:{caddy.port}/data1.data?[0-{count-1}]' 92 r = curl.http_download(urls=[urln], alpn_proto=proto) 93 r.check_response(count=count, http_status=200, connect_count=1) 94 95 # download 1MB files parallel 96 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 97 def test_08_03_download_1mb_parallel(self, env: Env, caddy: Caddy, proto): 98 if proto == 'h3' and not env.have_h3_curl(): 99 pytest.skip("h3 not supported in curl") 100 if proto == 'h3' and env.curl_uses_lib('msh3'): 101 pytest.skip("msh3 itself crashes") 102 count = 20 103 curl = CurlClient(env=env) 104 urln = f'https://{env.domain1}:{caddy.port}/data1.data?[0-{count-1}]' 105 r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[ 106 '--parallel' 107 ]) 108 r.check_response(count=count, http_status=200) 109 if proto == 'http/1.1': 110 # http/1.1 parallel transfers will open multiple connections 111 assert r.total_connects > 1, r.dump_logs() 112 else: 113 assert r.total_connects == 1, r.dump_logs() 114 115 # download 5MB files sequentially 116 @pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests") 117 @pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs") 118 @pytest.mark.parametrize("proto", ['h2', 'h3']) 119 def test_08_04a_download_10mb_sequential(self, env: Env, caddy: Caddy, proto): 120 if proto == 'h3' and not env.have_h3_curl(): 121 pytest.skip("h3 not supported in curl") 122 if proto == 'h3' and env.curl_uses_lib('msh3'): 123 pytest.skip("msh3 itself crashes") 124 count = 40 125 curl = CurlClient(env=env) 126 urln = f'https://{env.domain1}:{caddy.port}/data5.data?[0-{count-1}]' 127 r = curl.http_download(urls=[urln], alpn_proto=proto) 128 r.check_response(count=count, http_status=200, connect_count=1) 129 130 # download 10MB files sequentially 131 @pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests") 132 @pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs") 133 @pytest.mark.parametrize("proto", ['h2', 'h3']) 134 def test_08_04b_download_10mb_sequential(self, env: Env, caddy: Caddy, proto): 135 if proto == 'h3' and not env.have_h3_curl(): 136 pytest.skip("h3 not supported in curl") 137 if proto == 'h3' and env.curl_uses_lib('msh3'): 138 pytest.skip("msh3 itself crashes") 139 count = 20 140 curl = CurlClient(env=env) 141 urln = f'https://{env.domain1}:{caddy.port}/data10.data?[0-{count-1}]' 142 r = curl.http_download(urls=[urln], alpn_proto=proto) 143 r.check_response(count=count, http_status=200, connect_count=1) 144 145 # download 10MB files parallel 146 @pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests") 147 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 148 @pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs") 149 def test_08_05_download_1mb_parallel(self, env: Env, caddy: Caddy, proto): 150 if proto == 'h3' and not env.have_h3_curl(): 151 pytest.skip("h3 not supported in curl") 152 if proto == 'h3' and env.curl_uses_lib('msh3'): 153 pytest.skip("msh3 itself crashes") 154 if proto == 'http/1.1' and env.curl_uses_lib('mbedtls'): 155 pytest.skip("mbedtls 3.6.0 fails on 50 connections with: "\ 156 "ssl_handshake returned: (-0x7F00) SSL - Memory allocation failed") 157 count = 50 158 curl = CurlClient(env=env) 159 urln = f'https://{env.domain1}:{caddy.port}/data10.data?[0-{count-1}]' 160 r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[ 161 '--parallel' 162 ]) 163 r.check_response(count=count, http_status=200) 164 if proto == 'http/1.1': 165 # http/1.1 parallel transfers will open multiple connections 166 assert r.total_connects > 1, r.dump_logs() 167 else: 168 assert r.total_connects == 1, r.dump_logs() 169 170 # post data parallel, check that they were echoed 171 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 172 def test_08_06_post_parallel(self, env: Env, httpd, caddy, proto): 173 if proto == 'h3' and not env.have_h3(): 174 pytest.skip("h3 not supported") 175 if proto == 'h3' and env.curl_uses_lib('msh3'): 176 pytest.skip("msh3 stalls here") 177 # limit since we use a separate connection in h1 178 count = 20 179 data = '0123456789' 180 curl = CurlClient(env=env) 181 url = f'https://{env.domain2}:{caddy.port}/curltest/echo?id=[0-{count-1}]' 182 r = curl.http_upload(urls=[url], data=data, alpn_proto=proto, 183 extra_args=['--parallel']) 184 r.check_stats(count=count, http_status=200, exitcode=0) 185 for i in range(count): 186 respdata = open(curl.response_file(i)).readlines() 187 assert respdata == [data] 188 189 # put large file, check that they length were echoed 190 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 191 def test_08_07_put_large(self, env: Env, httpd, caddy, proto): 192 if proto == 'h3' and not env.have_h3(): 193 pytest.skip("h3 not supported") 194 if proto == 'h3' and env.curl_uses_lib('msh3'): 195 pytest.skip("msh3 stalls here") 196 # limit since we use a separate connection in h1< 197 count = 1 198 fdata = os.path.join(env.gen_dir, 'data-10m') 199 curl = CurlClient(env=env) 200 url = f'https://{env.domain2}:{caddy.port}/curltest/put?id=[0-{count-1}]' 201 r = curl.http_put(urls=[url], fdata=fdata, alpn_proto=proto) 202 exp_data = [f'{os.path.getsize(fdata)}'] 203 r.check_response(count=count, http_status=200) 204 for i in range(count): 205 respdata = open(curl.response_file(i)).readlines() 206 assert respdata == exp_data 207 208 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 209 def test_08_08_earlydata(self, env: Env, httpd, caddy, proto): 210 if not env.curl_uses_lib('gnutls'): 211 pytest.skip('TLS earlydata only implemented in GnuTLS') 212 if proto == 'h3' and not env.have_h3(): 213 pytest.skip("h3 not supported") 214 count = 2 215 docname = 'data10k.data' 216 url = f'https://{env.domain1}:{caddy.port}/{docname}' 217 client = LocalClient(name='hx-download', env=env) 218 if not client.exists(): 219 pytest.skip(f'example client not built: {client.name}') 220 r = client.run(args=[ 221 '-n', f'{count}', 222 '-e', # use TLS earlydata 223 '-f', # forbid reuse of connections 224 '-r', f'{env.domain1}:{caddy.port}:127.0.0.1', 225 '-V', proto, url 226 ]) 227 r.check_exit_code(0) 228 srcfile = os.path.join(caddy.docs_dir, docname) 229 self.check_downloads(client, srcfile, count) 230 earlydata = {} 231 for line in r.trace_lines: 232 m = re.match(r'^\[t-(\d+)] EarlyData: (-?\d+)', line) 233 if m: 234 earlydata[int(m.group(1))] = int(m.group(2)) 235 assert earlydata[0] == 0, f'{earlydata}' 236 if proto == 'h3': 237 assert earlydata[1] == 71, f'{earlydata}' 238 else: 239 # Caddy does not support early data on TCP 240 assert earlydata[1] == 0, f'{earlydata}' 241 242 def check_downloads(self, client, srcfile: str, count: int, 243 complete: bool = True): 244 for i in range(count): 245 dfile = client.download_file(i) 246 assert os.path.exists(dfile) 247 if complete and not filecmp.cmp(srcfile, dfile, shallow=False): 248 diff = "".join(difflib.unified_diff(a=open(srcfile).readlines(), 249 b=open(dfile).readlines(), 250 fromfile=srcfile, 251 tofile=dfile, 252 n=1)) 253 assert False, f'download {dfile} differs:\n{diff}' 254