xref: /aosp_15_r20/external/curl/tests/http/testenv/curl.py (revision 6236dae45794135f37c4eb022389c904c8b0090d)
1*6236dae4SAndroid Build Coastguard Worker#!/usr/bin/env python3
2*6236dae4SAndroid Build Coastguard Worker# -*- coding: utf-8 -*-
3*6236dae4SAndroid Build Coastguard Worker#***************************************************************************
4*6236dae4SAndroid Build Coastguard Worker#                                  _   _ ____  _
5*6236dae4SAndroid Build Coastguard Worker#  Project                     ___| | | |  _ \| |
6*6236dae4SAndroid Build Coastguard Worker#                             / __| | | | |_) | |
7*6236dae4SAndroid Build Coastguard Worker#                            | (__| |_| |  _ <| |___
8*6236dae4SAndroid Build Coastguard Worker#                             \___|\___/|_| \_\_____|
9*6236dae4SAndroid Build Coastguard Worker#
10*6236dae4SAndroid Build Coastguard Worker# Copyright (C) Daniel Stenberg, <[email protected]>, et al.
11*6236dae4SAndroid Build Coastguard Worker#
12*6236dae4SAndroid Build Coastguard Worker# This software is licensed as described in the file COPYING, which
13*6236dae4SAndroid Build Coastguard Worker# you should have received as part of this distribution. The terms
14*6236dae4SAndroid Build Coastguard Worker# are also available at https://curl.se/docs/copyright.html.
15*6236dae4SAndroid Build Coastguard Worker#
16*6236dae4SAndroid Build Coastguard Worker# You may opt to use, copy, modify, merge, publish, distribute and/or sell
17*6236dae4SAndroid Build Coastguard Worker# copies of the Software, and permit persons to whom the Software is
18*6236dae4SAndroid Build Coastguard Worker# furnished to do so, under the terms of the COPYING file.
19*6236dae4SAndroid Build Coastguard Worker#
20*6236dae4SAndroid Build Coastguard Worker# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
21*6236dae4SAndroid Build Coastguard Worker# KIND, either express or implied.
22*6236dae4SAndroid Build Coastguard Worker#
23*6236dae4SAndroid Build Coastguard Worker# SPDX-License-Identifier: curl
24*6236dae4SAndroid Build Coastguard Worker#
25*6236dae4SAndroid Build Coastguard Worker###########################################################################
26*6236dae4SAndroid Build Coastguard Worker#
27*6236dae4SAndroid Build Coastguard Workerimport json
28*6236dae4SAndroid Build Coastguard Workerimport logging
29*6236dae4SAndroid Build Coastguard Workerimport os
30*6236dae4SAndroid Build Coastguard Workerimport sys
31*6236dae4SAndroid Build Coastguard Workerimport time
32*6236dae4SAndroid Build Coastguard Workerfrom threading import Thread
33*6236dae4SAndroid Build Coastguard Worker
34*6236dae4SAndroid Build Coastguard Workerimport psutil
35*6236dae4SAndroid Build Coastguard Workerimport re
36*6236dae4SAndroid Build Coastguard Workerimport shutil
37*6236dae4SAndroid Build Coastguard Workerimport subprocess
38*6236dae4SAndroid Build Coastguard Workerfrom statistics import mean, fmean
39*6236dae4SAndroid Build Coastguard Workerfrom datetime import timedelta, datetime
40*6236dae4SAndroid Build Coastguard Workerfrom typing import List, Optional, Dict, Union, Any
41*6236dae4SAndroid Build Coastguard Workerfrom urllib.parse import urlparse
42*6236dae4SAndroid Build Coastguard Worker
43*6236dae4SAndroid Build Coastguard Workerfrom .env import Env
44*6236dae4SAndroid Build Coastguard Worker
45*6236dae4SAndroid Build Coastguard Worker
46*6236dae4SAndroid Build Coastguard Workerlog = logging.getLogger(__name__)
47*6236dae4SAndroid Build Coastguard Worker
48*6236dae4SAndroid Build Coastguard Worker
49*6236dae4SAndroid Build Coastguard Workerclass RunProfile:
50*6236dae4SAndroid Build Coastguard Worker
51*6236dae4SAndroid Build Coastguard Worker    STAT_KEYS = ['cpu', 'rss', 'vsz']
52*6236dae4SAndroid Build Coastguard Worker
53*6236dae4SAndroid Build Coastguard Worker    @classmethod
54*6236dae4SAndroid Build Coastguard Worker    def AverageStats(cls, profiles: List['RunProfile']):
55*6236dae4SAndroid Build Coastguard Worker        avg = {}
56*6236dae4SAndroid Build Coastguard Worker        stats = [p.stats for p in profiles]
57*6236dae4SAndroid Build Coastguard Worker        for key in cls.STAT_KEYS:
58*6236dae4SAndroid Build Coastguard Worker            vals = [s[key] for s in stats]
59*6236dae4SAndroid Build Coastguard Worker            avg[key] = mean(vals) if len(vals) else 0.0
60*6236dae4SAndroid Build Coastguard Worker        return avg
61*6236dae4SAndroid Build Coastguard Worker
62*6236dae4SAndroid Build Coastguard Worker    def __init__(self, pid: int, started_at: datetime, run_dir):
63*6236dae4SAndroid Build Coastguard Worker        self._pid = pid
64*6236dae4SAndroid Build Coastguard Worker        self._started_at = started_at
65*6236dae4SAndroid Build Coastguard Worker        self._duration = timedelta(seconds=0)
66*6236dae4SAndroid Build Coastguard Worker        self._run_dir = run_dir
67*6236dae4SAndroid Build Coastguard Worker        self._samples = []
68*6236dae4SAndroid Build Coastguard Worker        self._psu = None
69*6236dae4SAndroid Build Coastguard Worker        self._stats = None
70*6236dae4SAndroid Build Coastguard Worker
71*6236dae4SAndroid Build Coastguard Worker    @property
72*6236dae4SAndroid Build Coastguard Worker    def duration(self) -> timedelta:
73*6236dae4SAndroid Build Coastguard Worker        return self._duration
74*6236dae4SAndroid Build Coastguard Worker
75*6236dae4SAndroid Build Coastguard Worker    @property
76*6236dae4SAndroid Build Coastguard Worker    def stats(self) -> Optional[Dict[str,Any]]:
77*6236dae4SAndroid Build Coastguard Worker        return self._stats
78*6236dae4SAndroid Build Coastguard Worker
79*6236dae4SAndroid Build Coastguard Worker    def sample(self):
80*6236dae4SAndroid Build Coastguard Worker        elapsed = datetime.now() - self._started_at
81*6236dae4SAndroid Build Coastguard Worker        try:
82*6236dae4SAndroid Build Coastguard Worker            if self._psu is None:
83*6236dae4SAndroid Build Coastguard Worker                self._psu = psutil.Process(pid=self._pid)
84*6236dae4SAndroid Build Coastguard Worker            mem = self._psu.memory_info()
85*6236dae4SAndroid Build Coastguard Worker            self._samples.append({
86*6236dae4SAndroid Build Coastguard Worker                'time': elapsed,
87*6236dae4SAndroid Build Coastguard Worker                'cpu': self._psu.cpu_percent(),
88*6236dae4SAndroid Build Coastguard Worker                'vsz': mem.vms,
89*6236dae4SAndroid Build Coastguard Worker                'rss': mem.rss,
90*6236dae4SAndroid Build Coastguard Worker            })
91*6236dae4SAndroid Build Coastguard Worker        except psutil.NoSuchProcess:
92*6236dae4SAndroid Build Coastguard Worker            pass
93*6236dae4SAndroid Build Coastguard Worker
94*6236dae4SAndroid Build Coastguard Worker    def finish(self):
95*6236dae4SAndroid Build Coastguard Worker        self._duration = datetime.now() - self._started_at
96*6236dae4SAndroid Build Coastguard Worker        if len(self._samples) > 0:
97*6236dae4SAndroid Build Coastguard Worker            weights = [s['time'].total_seconds() for s in self._samples]
98*6236dae4SAndroid Build Coastguard Worker            self._stats = {}
99*6236dae4SAndroid Build Coastguard Worker            for key in self.STAT_KEYS:
100*6236dae4SAndroid Build Coastguard Worker                self._stats[key] = fmean([s[key] for s in self._samples], weights)
101*6236dae4SAndroid Build Coastguard Worker        else:
102*6236dae4SAndroid Build Coastguard Worker            self._stats = None
103*6236dae4SAndroid Build Coastguard Worker        self._psu = None
104*6236dae4SAndroid Build Coastguard Worker
105*6236dae4SAndroid Build Coastguard Worker    def __repr__(self):
106*6236dae4SAndroid Build Coastguard Worker        return f'RunProfile[pid={self._pid}, '\
107*6236dae4SAndroid Build Coastguard Worker               f'duration={self.duration.total_seconds():.3f}s, '\
108*6236dae4SAndroid Build Coastguard Worker               f'stats={self.stats}]'
109*6236dae4SAndroid Build Coastguard Worker
110*6236dae4SAndroid Build Coastguard Worker
111*6236dae4SAndroid Build Coastguard Workerclass RunTcpDump:
112*6236dae4SAndroid Build Coastguard Worker
113*6236dae4SAndroid Build Coastguard Worker    def __init__(self, env, run_dir):
114*6236dae4SAndroid Build Coastguard Worker        self._env = env
115*6236dae4SAndroid Build Coastguard Worker        self._run_dir = run_dir
116*6236dae4SAndroid Build Coastguard Worker        self._proc = None
117*6236dae4SAndroid Build Coastguard Worker        self._stdoutfile = os.path.join(self._run_dir, 'tcpdump.out')
118*6236dae4SAndroid Build Coastguard Worker        self._stderrfile = os.path.join(self._run_dir, 'tcpdump.err')
119*6236dae4SAndroid Build Coastguard Worker
120*6236dae4SAndroid Build Coastguard Worker    @property
121*6236dae4SAndroid Build Coastguard Worker    def stats(self) -> Optional[List[str]]:
122*6236dae4SAndroid Build Coastguard Worker        if self._proc:
123*6236dae4SAndroid Build Coastguard Worker            raise Exception('tcpdump still running')
124*6236dae4SAndroid Build Coastguard Worker        return [line
125*6236dae4SAndroid Build Coastguard Worker                for line in open(self._stdoutfile)
126*6236dae4SAndroid Build Coastguard Worker                if re.match(r'.* IP 127\.0\.0\.1\.\d+ [<>] 127\.0\.0\.1\.\d+:.*', line)]
127*6236dae4SAndroid Build Coastguard Worker
128*6236dae4SAndroid Build Coastguard Worker    def stats_excluding(self, src_port) -> Optional[List[str]]:
129*6236dae4SAndroid Build Coastguard Worker        if self._proc:
130*6236dae4SAndroid Build Coastguard Worker            raise Exception('tcpdump still running')
131*6236dae4SAndroid Build Coastguard Worker        return [line
132*6236dae4SAndroid Build Coastguard Worker                for line in self.stats
133*6236dae4SAndroid Build Coastguard Worker                if not re.match(r'.* IP 127\.0\.0\.1\.' + str(src_port) + ' >.*', line)]
134*6236dae4SAndroid Build Coastguard Worker
135*6236dae4SAndroid Build Coastguard Worker    @property
136*6236dae4SAndroid Build Coastguard Worker    def stderr(self) -> List[str]:
137*6236dae4SAndroid Build Coastguard Worker        if self._proc:
138*6236dae4SAndroid Build Coastguard Worker            raise Exception('tcpdump still running')
139*6236dae4SAndroid Build Coastguard Worker        return open(self._stderrfile).readlines()
140*6236dae4SAndroid Build Coastguard Worker
141*6236dae4SAndroid Build Coastguard Worker    def sample(self):
142*6236dae4SAndroid Build Coastguard Worker        # not sure how to make that detection reliable for all platforms
143*6236dae4SAndroid Build Coastguard Worker        local_if = 'lo0' if sys.platform.startswith('darwin') else 'lo'
144*6236dae4SAndroid Build Coastguard Worker        try:
145*6236dae4SAndroid Build Coastguard Worker            tcpdump = self._env.tcpdump()
146*6236dae4SAndroid Build Coastguard Worker            if tcpdump is None:
147*6236dae4SAndroid Build Coastguard Worker                raise Exception('tcpdump not available')
148*6236dae4SAndroid Build Coastguard Worker            # look with tcpdump for TCP RST packets which indicate
149*6236dae4SAndroid Build Coastguard Worker            # we did not shut down connections cleanly
150*6236dae4SAndroid Build Coastguard Worker            args = []
151*6236dae4SAndroid Build Coastguard Worker            # at least on Linux, we need root permissions to run tcpdump
152*6236dae4SAndroid Build Coastguard Worker            if sys.platform.startswith('linux'):
153*6236dae4SAndroid Build Coastguard Worker                args.append('sudo')
154*6236dae4SAndroid Build Coastguard Worker            args.extend([
155*6236dae4SAndroid Build Coastguard Worker                tcpdump, '-i', local_if, '-n', 'tcp[tcpflags] & (tcp-rst)!=0'
156*6236dae4SAndroid Build Coastguard Worker            ])
157*6236dae4SAndroid Build Coastguard Worker            with open(self._stdoutfile, 'w') as cout, open(self._stderrfile, 'w') as cerr:
158*6236dae4SAndroid Build Coastguard Worker                self._proc = subprocess.Popen(args, stdout=cout, stderr=cerr,
159*6236dae4SAndroid Build Coastguard Worker                                              text=True, cwd=self._run_dir,
160*6236dae4SAndroid Build Coastguard Worker                                              shell=False)
161*6236dae4SAndroid Build Coastguard Worker                assert self._proc
162*6236dae4SAndroid Build Coastguard Worker                assert self._proc.returncode is None
163*6236dae4SAndroid Build Coastguard Worker                while self._proc:
164*6236dae4SAndroid Build Coastguard Worker                    try:
165*6236dae4SAndroid Build Coastguard Worker                        self._proc.wait(timeout=1)
166*6236dae4SAndroid Build Coastguard Worker                    except subprocess.TimeoutExpired:
167*6236dae4SAndroid Build Coastguard Worker                        pass
168*6236dae4SAndroid Build Coastguard Worker        except Exception:
169*6236dae4SAndroid Build Coastguard Worker            log.exception('Tcpdump')
170*6236dae4SAndroid Build Coastguard Worker
171*6236dae4SAndroid Build Coastguard Worker    def start(self):
172*6236dae4SAndroid Build Coastguard Worker        def do_sample():
173*6236dae4SAndroid Build Coastguard Worker            self.sample()
174*6236dae4SAndroid Build Coastguard Worker        t = Thread(target=do_sample)
175*6236dae4SAndroid Build Coastguard Worker        t.start()
176*6236dae4SAndroid Build Coastguard Worker
177*6236dae4SAndroid Build Coastguard Worker    def finish(self):
178*6236dae4SAndroid Build Coastguard Worker        if self._proc:
179*6236dae4SAndroid Build Coastguard Worker            time.sleep(1)
180*6236dae4SAndroid Build Coastguard Worker            self._proc.terminate()
181*6236dae4SAndroid Build Coastguard Worker            self._proc = None
182*6236dae4SAndroid Build Coastguard Worker
183*6236dae4SAndroid Build Coastguard Worker
184*6236dae4SAndroid Build Coastguard Workerclass ExecResult:
185*6236dae4SAndroid Build Coastguard Worker
186*6236dae4SAndroid Build Coastguard Worker    def __init__(self, args: List[str], exit_code: int,
187*6236dae4SAndroid Build Coastguard Worker                 stdout: List[str], stderr: List[str],
188*6236dae4SAndroid Build Coastguard Worker                 duration: Optional[timedelta] = None,
189*6236dae4SAndroid Build Coastguard Worker                 with_stats: bool = False,
190*6236dae4SAndroid Build Coastguard Worker                 exception: Optional[str] = None,
191*6236dae4SAndroid Build Coastguard Worker                 profile: Optional[RunProfile] = None,
192*6236dae4SAndroid Build Coastguard Worker                 tcpdump: Optional[RunTcpDump] = None):
193*6236dae4SAndroid Build Coastguard Worker        self._args = args
194*6236dae4SAndroid Build Coastguard Worker        self._exit_code = exit_code
195*6236dae4SAndroid Build Coastguard Worker        self._exception = exception
196*6236dae4SAndroid Build Coastguard Worker        self._stdout = stdout
197*6236dae4SAndroid Build Coastguard Worker        self._stderr = stderr
198*6236dae4SAndroid Build Coastguard Worker        self._profile = profile
199*6236dae4SAndroid Build Coastguard Worker        self._tcpdump = tcpdump
200*6236dae4SAndroid Build Coastguard Worker        self._duration = duration if duration is not None else timedelta()
201*6236dae4SAndroid Build Coastguard Worker        self._response = None
202*6236dae4SAndroid Build Coastguard Worker        self._responses = []
203*6236dae4SAndroid Build Coastguard Worker        self._results = {}
204*6236dae4SAndroid Build Coastguard Worker        self._assets = []
205*6236dae4SAndroid Build Coastguard Worker        self._stats = []
206*6236dae4SAndroid Build Coastguard Worker        self._json_out = None
207*6236dae4SAndroid Build Coastguard Worker        self._with_stats = with_stats
208*6236dae4SAndroid Build Coastguard Worker        if with_stats:
209*6236dae4SAndroid Build Coastguard Worker            self._parse_stats()
210*6236dae4SAndroid Build Coastguard Worker        else:
211*6236dae4SAndroid Build Coastguard Worker            # noinspection PyBroadException
212*6236dae4SAndroid Build Coastguard Worker            try:
213*6236dae4SAndroid Build Coastguard Worker                out = ''.join(self._stdout)
214*6236dae4SAndroid Build Coastguard Worker                self._json_out = json.loads(out)
215*6236dae4SAndroid Build Coastguard Worker            except:  # noqa: E722
216*6236dae4SAndroid Build Coastguard Worker                pass
217*6236dae4SAndroid Build Coastguard Worker
218*6236dae4SAndroid Build Coastguard Worker    def __repr__(self):
219*6236dae4SAndroid Build Coastguard Worker        return f"ExecResult[code={self.exit_code}, exception={self._exception}, "\
220*6236dae4SAndroid Build Coastguard Worker               f"args={self._args}, stdout={self._stdout}, stderr={self._stderr}]"
221*6236dae4SAndroid Build Coastguard Worker
222*6236dae4SAndroid Build Coastguard Worker    def _parse_stats(self):
223*6236dae4SAndroid Build Coastguard Worker        self._stats = []
224*6236dae4SAndroid Build Coastguard Worker        for line in self._stdout:
225*6236dae4SAndroid Build Coastguard Worker            try:
226*6236dae4SAndroid Build Coastguard Worker                self._stats.append(json.loads(line))
227*6236dae4SAndroid Build Coastguard Worker            # TODO: specify specific exceptions here
228*6236dae4SAndroid Build Coastguard Worker            except:  # noqa: E722
229*6236dae4SAndroid Build Coastguard Worker                log.exception(f'not a JSON stat: {line}')
230*6236dae4SAndroid Build Coastguard Worker                break
231*6236dae4SAndroid Build Coastguard Worker
232*6236dae4SAndroid Build Coastguard Worker    @property
233*6236dae4SAndroid Build Coastguard Worker    def exit_code(self) -> int:
234*6236dae4SAndroid Build Coastguard Worker        return self._exit_code
235*6236dae4SAndroid Build Coastguard Worker
236*6236dae4SAndroid Build Coastguard Worker    @property
237*6236dae4SAndroid Build Coastguard Worker    def args(self) -> List[str]:
238*6236dae4SAndroid Build Coastguard Worker        return self._args
239*6236dae4SAndroid Build Coastguard Worker
240*6236dae4SAndroid Build Coastguard Worker    @property
241*6236dae4SAndroid Build Coastguard Worker    def outraw(self) -> bytes:
242*6236dae4SAndroid Build Coastguard Worker        return ''.join(self._stdout).encode()
243*6236dae4SAndroid Build Coastguard Worker
244*6236dae4SAndroid Build Coastguard Worker    @property
245*6236dae4SAndroid Build Coastguard Worker    def stdout(self) -> str:
246*6236dae4SAndroid Build Coastguard Worker        return ''.join(self._stdout)
247*6236dae4SAndroid Build Coastguard Worker
248*6236dae4SAndroid Build Coastguard Worker    @property
249*6236dae4SAndroid Build Coastguard Worker    def json(self) -> Optional[Dict]:
250*6236dae4SAndroid Build Coastguard Worker        """Output as JSON dictionary or None if not parseable."""
251*6236dae4SAndroid Build Coastguard Worker        return self._json_out
252*6236dae4SAndroid Build Coastguard Worker
253*6236dae4SAndroid Build Coastguard Worker    @property
254*6236dae4SAndroid Build Coastguard Worker    def stderr(self) -> str:
255*6236dae4SAndroid Build Coastguard Worker        return ''.join(self._stderr)
256*6236dae4SAndroid Build Coastguard Worker
257*6236dae4SAndroid Build Coastguard Worker    @property
258*6236dae4SAndroid Build Coastguard Worker    def trace_lines(self) -> List[str]:
259*6236dae4SAndroid Build Coastguard Worker        return self._stderr
260*6236dae4SAndroid Build Coastguard Worker
261*6236dae4SAndroid Build Coastguard Worker    @property
262*6236dae4SAndroid Build Coastguard Worker    def duration(self) -> timedelta:
263*6236dae4SAndroid Build Coastguard Worker        return self._duration
264*6236dae4SAndroid Build Coastguard Worker
265*6236dae4SAndroid Build Coastguard Worker    @property
266*6236dae4SAndroid Build Coastguard Worker    def profile(self) -> Optional[RunProfile]:
267*6236dae4SAndroid Build Coastguard Worker        return self._profile
268*6236dae4SAndroid Build Coastguard Worker
269*6236dae4SAndroid Build Coastguard Worker    @property
270*6236dae4SAndroid Build Coastguard Worker    def tcpdump(self) -> Optional[RunTcpDump]:
271*6236dae4SAndroid Build Coastguard Worker        return self._tcpdump
272*6236dae4SAndroid Build Coastguard Worker
273*6236dae4SAndroid Build Coastguard Worker    @property
274*6236dae4SAndroid Build Coastguard Worker    def response(self) -> Optional[Dict]:
275*6236dae4SAndroid Build Coastguard Worker        return self._response
276*6236dae4SAndroid Build Coastguard Worker
277*6236dae4SAndroid Build Coastguard Worker    @property
278*6236dae4SAndroid Build Coastguard Worker    def responses(self) -> List[Dict]:
279*6236dae4SAndroid Build Coastguard Worker        return self._responses
280*6236dae4SAndroid Build Coastguard Worker
281*6236dae4SAndroid Build Coastguard Worker    @property
282*6236dae4SAndroid Build Coastguard Worker    def results(self) -> Dict:
283*6236dae4SAndroid Build Coastguard Worker        return self._results
284*6236dae4SAndroid Build Coastguard Worker
285*6236dae4SAndroid Build Coastguard Worker    @property
286*6236dae4SAndroid Build Coastguard Worker    def assets(self) -> List:
287*6236dae4SAndroid Build Coastguard Worker        return self._assets
288*6236dae4SAndroid Build Coastguard Worker
289*6236dae4SAndroid Build Coastguard Worker    @property
290*6236dae4SAndroid Build Coastguard Worker    def with_stats(self) -> bool:
291*6236dae4SAndroid Build Coastguard Worker        return self._with_stats
292*6236dae4SAndroid Build Coastguard Worker
293*6236dae4SAndroid Build Coastguard Worker    @property
294*6236dae4SAndroid Build Coastguard Worker    def stats(self) -> List:
295*6236dae4SAndroid Build Coastguard Worker        return self._stats
296*6236dae4SAndroid Build Coastguard Worker
297*6236dae4SAndroid Build Coastguard Worker    @property
298*6236dae4SAndroid Build Coastguard Worker    def total_connects(self) -> Optional[int]:
299*6236dae4SAndroid Build Coastguard Worker        if len(self.stats):
300*6236dae4SAndroid Build Coastguard Worker            n = 0
301*6236dae4SAndroid Build Coastguard Worker            for stat in self.stats:
302*6236dae4SAndroid Build Coastguard Worker                n += stat['num_connects']
303*6236dae4SAndroid Build Coastguard Worker            return n
304*6236dae4SAndroid Build Coastguard Worker        return None
305*6236dae4SAndroid Build Coastguard Worker
306*6236dae4SAndroid Build Coastguard Worker    def add_response(self, resp: Dict):
307*6236dae4SAndroid Build Coastguard Worker        self._response = resp
308*6236dae4SAndroid Build Coastguard Worker        self._responses.append(resp)
309*6236dae4SAndroid Build Coastguard Worker
310*6236dae4SAndroid Build Coastguard Worker    def add_results(self, results: Dict):
311*6236dae4SAndroid Build Coastguard Worker        self._results.update(results)
312*6236dae4SAndroid Build Coastguard Worker        if 'response' in results:
313*6236dae4SAndroid Build Coastguard Worker            self.add_response(results['response'])
314*6236dae4SAndroid Build Coastguard Worker
315*6236dae4SAndroid Build Coastguard Worker    def add_assets(self, assets: List):
316*6236dae4SAndroid Build Coastguard Worker        self._assets.extend(assets)
317*6236dae4SAndroid Build Coastguard Worker
318*6236dae4SAndroid Build Coastguard Worker    def check_exit_code(self, code: Union[int, bool]):
319*6236dae4SAndroid Build Coastguard Worker        if code is True:
320*6236dae4SAndroid Build Coastguard Worker            assert self.exit_code == 0, f'expected exit code {code}, '\
321*6236dae4SAndroid Build Coastguard Worker                                        f'got {self.exit_code}\n{self.dump_logs()}'
322*6236dae4SAndroid Build Coastguard Worker        elif code is False:
323*6236dae4SAndroid Build Coastguard Worker            assert self.exit_code != 0, f'expected exit code {code}, '\
324*6236dae4SAndroid Build Coastguard Worker                                                f'got {self.exit_code}\n{self.dump_logs()}'
325*6236dae4SAndroid Build Coastguard Worker        else:
326*6236dae4SAndroid Build Coastguard Worker            assert self.exit_code == code, f'expected exit code {code}, '\
327*6236dae4SAndroid Build Coastguard Worker                                           f'got {self.exit_code}\n{self.dump_logs()}'
328*6236dae4SAndroid Build Coastguard Worker
329*6236dae4SAndroid Build Coastguard Worker    def check_response(self, http_status: Optional[int] = 200,
330*6236dae4SAndroid Build Coastguard Worker                       count: Optional[int] = 1,
331*6236dae4SAndroid Build Coastguard Worker                       protocol: Optional[str] = None,
332*6236dae4SAndroid Build Coastguard Worker                       exitcode: Optional[int] = 0,
333*6236dae4SAndroid Build Coastguard Worker                       connect_count: Optional[int] = None):
334*6236dae4SAndroid Build Coastguard Worker        if exitcode:
335*6236dae4SAndroid Build Coastguard Worker            self.check_exit_code(exitcode)
336*6236dae4SAndroid Build Coastguard Worker            if self.with_stats and isinstance(exitcode, int):
337*6236dae4SAndroid Build Coastguard Worker                for idx, x in enumerate(self.stats):
338*6236dae4SAndroid Build Coastguard Worker                    if 'exitcode' in x:
339*6236dae4SAndroid Build Coastguard Worker                        assert int(x['exitcode']) == exitcode, \
340*6236dae4SAndroid Build Coastguard Worker                            f'response #{idx} exitcode: expected {exitcode}, '\
341*6236dae4SAndroid Build Coastguard Worker                            f'got {x["exitcode"]}\n{self.dump_logs()}'
342*6236dae4SAndroid Build Coastguard Worker
343*6236dae4SAndroid Build Coastguard Worker        if self.with_stats:
344*6236dae4SAndroid Build Coastguard Worker            assert len(self.stats) == count, \
345*6236dae4SAndroid Build Coastguard Worker                f'response count: expected {count}, ' \
346*6236dae4SAndroid Build Coastguard Worker                f'got {len(self.stats)}\n{self.dump_logs()}'
347*6236dae4SAndroid Build Coastguard Worker        else:
348*6236dae4SAndroid Build Coastguard Worker            assert len(self.responses) == count, \
349*6236dae4SAndroid Build Coastguard Worker                f'response count: expected {count}, ' \
350*6236dae4SAndroid Build Coastguard Worker                f'got {len(self.responses)}\n{self.dump_logs()}'
351*6236dae4SAndroid Build Coastguard Worker        if http_status is not None:
352*6236dae4SAndroid Build Coastguard Worker            if self.with_stats:
353*6236dae4SAndroid Build Coastguard Worker                for idx, x in enumerate(self.stats):
354*6236dae4SAndroid Build Coastguard Worker                    assert 'http_code' in x, \
355*6236dae4SAndroid Build Coastguard Worker                        f'response #{idx} reports no http_code\n{self.dump_stat(x)}'
356*6236dae4SAndroid Build Coastguard Worker                    assert x['http_code'] == http_status, \
357*6236dae4SAndroid Build Coastguard Worker                        f'response #{idx} http_code: expected {http_status}, '\
358*6236dae4SAndroid Build Coastguard Worker                        f'got {x["http_code"]}\n{self.dump_stat(x)}'
359*6236dae4SAndroid Build Coastguard Worker            else:
360*6236dae4SAndroid Build Coastguard Worker                for idx, x in enumerate(self.responses):
361*6236dae4SAndroid Build Coastguard Worker                    assert x['status'] == http_status, \
362*6236dae4SAndroid Build Coastguard Worker                        f'response #{idx} status: expected {http_status},'\
363*6236dae4SAndroid Build Coastguard Worker                        f'got {x["status"]}\n{self.dump_stat(x)}'
364*6236dae4SAndroid Build Coastguard Worker        if protocol is not None:
365*6236dae4SAndroid Build Coastguard Worker            if self.with_stats:
366*6236dae4SAndroid Build Coastguard Worker                http_version = None
367*6236dae4SAndroid Build Coastguard Worker                if protocol == 'HTTP/1.1':
368*6236dae4SAndroid Build Coastguard Worker                    http_version = '1.1'
369*6236dae4SAndroid Build Coastguard Worker                elif protocol == 'HTTP/2':
370*6236dae4SAndroid Build Coastguard Worker                    http_version = '2'
371*6236dae4SAndroid Build Coastguard Worker                elif protocol == 'HTTP/3':
372*6236dae4SAndroid Build Coastguard Worker                    http_version = '3'
373*6236dae4SAndroid Build Coastguard Worker                if http_version is not None:
374*6236dae4SAndroid Build Coastguard Worker                    for idx, x in enumerate(self.stats):
375*6236dae4SAndroid Build Coastguard Worker                        assert x['http_version'] == http_version, \
376*6236dae4SAndroid Build Coastguard Worker                            f'response #{idx} protocol: expected http/{http_version},' \
377*6236dae4SAndroid Build Coastguard Worker                            f'got version {x["http_version"]}\n{self.dump_stat(x)}'
378*6236dae4SAndroid Build Coastguard Worker            else:
379*6236dae4SAndroid Build Coastguard Worker                for idx, x in enumerate(self.responses):
380*6236dae4SAndroid Build Coastguard Worker                    assert x['protocol'] == protocol, \
381*6236dae4SAndroid Build Coastguard Worker                        f'response #{idx} protocol: expected {protocol},'\
382*6236dae4SAndroid Build Coastguard Worker                        f'got {x["protocol"]}\n{self.dump_logs()}'
383*6236dae4SAndroid Build Coastguard Worker        if connect_count is not None:
384*6236dae4SAndroid Build Coastguard Worker            assert self.total_connects == connect_count, \
385*6236dae4SAndroid Build Coastguard Worker                f'expected {connect_count}, but {self.total_connects} '\
386*6236dae4SAndroid Build Coastguard Worker                f'were made\n{self.dump_logs()}'
387*6236dae4SAndroid Build Coastguard Worker
388*6236dae4SAndroid Build Coastguard Worker    def check_stats(self, count: int, http_status: Optional[int] = None,
389*6236dae4SAndroid Build Coastguard Worker                    exitcode: Optional[int] = None,
390*6236dae4SAndroid Build Coastguard Worker                    remote_port: Optional[int] = None,
391*6236dae4SAndroid Build Coastguard Worker                    remote_ip: Optional[str] = None):
392*6236dae4SAndroid Build Coastguard Worker        if exitcode is None:
393*6236dae4SAndroid Build Coastguard Worker            self.check_exit_code(0)
394*6236dae4SAndroid Build Coastguard Worker        assert len(self.stats) == count, \
395*6236dae4SAndroid Build Coastguard Worker            f'stats count: expected {count}, got {len(self.stats)}\n{self.dump_logs()}'
396*6236dae4SAndroid Build Coastguard Worker        if http_status is not None:
397*6236dae4SAndroid Build Coastguard Worker            for idx, x in enumerate(self.stats):
398*6236dae4SAndroid Build Coastguard Worker                assert 'http_code' in x, \
399*6236dae4SAndroid Build Coastguard Worker                    f'status #{idx} reports no http_code\n{self.dump_stat(x)}'
400*6236dae4SAndroid Build Coastguard Worker                assert x['http_code'] == http_status, \
401*6236dae4SAndroid Build Coastguard Worker                    f'status #{idx} http_code: expected {http_status}, '\
402*6236dae4SAndroid Build Coastguard Worker                    f'got {x["http_code"]}\n{self.dump_stat(x)}'
403*6236dae4SAndroid Build Coastguard Worker        if exitcode is not None:
404*6236dae4SAndroid Build Coastguard Worker            for idx, x in enumerate(self.stats):
405*6236dae4SAndroid Build Coastguard Worker                if 'exitcode' in x:
406*6236dae4SAndroid Build Coastguard Worker                    assert x['exitcode'] == exitcode, \
407*6236dae4SAndroid Build Coastguard Worker                        f'status #{idx} exitcode: expected {exitcode}, '\
408*6236dae4SAndroid Build Coastguard Worker                        f'got {x["exitcode"]}\n{self.dump_stat(x)}'
409*6236dae4SAndroid Build Coastguard Worker        if remote_port is not None:
410*6236dae4SAndroid Build Coastguard Worker            for idx, x in enumerate(self.stats):
411*6236dae4SAndroid Build Coastguard Worker                assert 'remote_port' in x, f'remote_port missing\n{self.dump_stat(x)}'
412*6236dae4SAndroid Build Coastguard Worker                assert x['remote_port'] == remote_port, \
413*6236dae4SAndroid Build Coastguard Worker                        f'status #{idx} remote_port: expected {remote_port}, '\
414*6236dae4SAndroid Build Coastguard Worker                        f'got {x["remote_port"]}\n{self.dump_stat(x)}'
415*6236dae4SAndroid Build Coastguard Worker        if remote_ip is not None:
416*6236dae4SAndroid Build Coastguard Worker            for idx, x in enumerate(self.stats):
417*6236dae4SAndroid Build Coastguard Worker                assert 'remote_ip' in x, f'remote_ip missing\n{self.dump_stat(x)}'
418*6236dae4SAndroid Build Coastguard Worker                assert x['remote_ip'] == remote_ip, \
419*6236dae4SAndroid Build Coastguard Worker                        f'status #{idx} remote_ip: expected {remote_ip}, '\
420*6236dae4SAndroid Build Coastguard Worker                        f'got {x["remote_ip"]}\n{self.dump_stat(x)}'
421*6236dae4SAndroid Build Coastguard Worker
422*6236dae4SAndroid Build Coastguard Worker    def dump_logs(self):
423*6236dae4SAndroid Build Coastguard Worker        lines = ['>>--stdout ----------------------------------------------\n']
424*6236dae4SAndroid Build Coastguard Worker        lines.extend(self._stdout)
425*6236dae4SAndroid Build Coastguard Worker        lines.append('>>--stderr ----------------------------------------------\n')
426*6236dae4SAndroid Build Coastguard Worker        lines.extend(self._stderr)
427*6236dae4SAndroid Build Coastguard Worker        lines.append('<<-------------------------------------------------------\n')
428*6236dae4SAndroid Build Coastguard Worker        return ''.join(lines)
429*6236dae4SAndroid Build Coastguard Worker
430*6236dae4SAndroid Build Coastguard Worker    def dump_stat(self, x):
431*6236dae4SAndroid Build Coastguard Worker        lines = [
432*6236dae4SAndroid Build Coastguard Worker            'json stat from curl:',
433*6236dae4SAndroid Build Coastguard Worker            json.JSONEncoder(indent=2).encode(x),
434*6236dae4SAndroid Build Coastguard Worker        ]
435*6236dae4SAndroid Build Coastguard Worker        if 'xfer_id' in x:
436*6236dae4SAndroid Build Coastguard Worker            xfer_id = x['xfer_id']
437*6236dae4SAndroid Build Coastguard Worker            lines.append(f'>>--xfer {xfer_id} trace:\n')
438*6236dae4SAndroid Build Coastguard Worker            lines.extend(self.xfer_trace_for(xfer_id))
439*6236dae4SAndroid Build Coastguard Worker        else:
440*6236dae4SAndroid Build Coastguard Worker            lines.append('>>--full trace-------------------------------------------\n')
441*6236dae4SAndroid Build Coastguard Worker            lines.extend(self._stderr)
442*6236dae4SAndroid Build Coastguard Worker            lines.append('<<-------------------------------------------------------\n')
443*6236dae4SAndroid Build Coastguard Worker        return ''.join(lines)
444*6236dae4SAndroid Build Coastguard Worker
445*6236dae4SAndroid Build Coastguard Worker    def xfer_trace_for(self, xfer_id) -> List[str]:
446*6236dae4SAndroid Build Coastguard Worker            pat = re.compile(f'^[^[]* \\[{xfer_id}-.*$')
447*6236dae4SAndroid Build Coastguard Worker            return [line for line in self._stderr if pat.match(line)]
448*6236dae4SAndroid Build Coastguard Worker
449*6236dae4SAndroid Build Coastguard Worker
450*6236dae4SAndroid Build Coastguard Workerclass CurlClient:
451*6236dae4SAndroid Build Coastguard Worker
452*6236dae4SAndroid Build Coastguard Worker    ALPN_ARG = {
453*6236dae4SAndroid Build Coastguard Worker        'http/0.9': '--http0.9',
454*6236dae4SAndroid Build Coastguard Worker        'http/1.0': '--http1.0',
455*6236dae4SAndroid Build Coastguard Worker        'http/1.1': '--http1.1',
456*6236dae4SAndroid Build Coastguard Worker        'h2': '--http2',
457*6236dae4SAndroid Build Coastguard Worker        'h2c': '--http2',
458*6236dae4SAndroid Build Coastguard Worker        'h3': '--http3-only',
459*6236dae4SAndroid Build Coastguard Worker    }
460*6236dae4SAndroid Build Coastguard Worker
461*6236dae4SAndroid Build Coastguard Worker    def __init__(self, env: Env,
462*6236dae4SAndroid Build Coastguard Worker                 run_dir: Optional[str] = None,
463*6236dae4SAndroid Build Coastguard Worker                 timeout: Optional[float] = None,
464*6236dae4SAndroid Build Coastguard Worker                 silent: bool = False,
465*6236dae4SAndroid Build Coastguard Worker                 run_env: Optional[Dict[str, str]] = None,
466*6236dae4SAndroid Build Coastguard Worker                 server_addr: Optional[str] = None):
467*6236dae4SAndroid Build Coastguard Worker        self.env = env
468*6236dae4SAndroid Build Coastguard Worker        self._timeout = timeout if timeout else env.test_timeout
469*6236dae4SAndroid Build Coastguard Worker        self._curl = os.environ['CURL'] if 'CURL' in os.environ else env.curl
470*6236dae4SAndroid Build Coastguard Worker        self._run_dir = run_dir if run_dir else os.path.join(env.gen_dir, 'curl')
471*6236dae4SAndroid Build Coastguard Worker        self._stdoutfile = f'{self._run_dir}/curl.stdout'
472*6236dae4SAndroid Build Coastguard Worker        self._stderrfile = f'{self._run_dir}/curl.stderr'
473*6236dae4SAndroid Build Coastguard Worker        self._headerfile = f'{self._run_dir}/curl.headers'
474*6236dae4SAndroid Build Coastguard Worker        self._log_path = f'{self._run_dir}/curl.log'
475*6236dae4SAndroid Build Coastguard Worker        self._silent = silent
476*6236dae4SAndroid Build Coastguard Worker        self._run_env = run_env
477*6236dae4SAndroid Build Coastguard Worker        self._server_addr = server_addr if server_addr else '127.0.0.1'
478*6236dae4SAndroid Build Coastguard Worker        self._rmrf(self._run_dir)
479*6236dae4SAndroid Build Coastguard Worker        self._mkpath(self._run_dir)
480*6236dae4SAndroid Build Coastguard Worker
481*6236dae4SAndroid Build Coastguard Worker    @property
482*6236dae4SAndroid Build Coastguard Worker    def run_dir(self) -> str:
483*6236dae4SAndroid Build Coastguard Worker        return self._run_dir
484*6236dae4SAndroid Build Coastguard Worker
485*6236dae4SAndroid Build Coastguard Worker    def download_file(self, i: int) -> str:
486*6236dae4SAndroid Build Coastguard Worker        return os.path.join(self.run_dir, f'download_{i}.data')
487*6236dae4SAndroid Build Coastguard Worker
488*6236dae4SAndroid Build Coastguard Worker    def _rmf(self, path):
489*6236dae4SAndroid Build Coastguard Worker        if os.path.exists(path):
490*6236dae4SAndroid Build Coastguard Worker            return os.remove(path)
491*6236dae4SAndroid Build Coastguard Worker
492*6236dae4SAndroid Build Coastguard Worker    def _rmrf(self, path):
493*6236dae4SAndroid Build Coastguard Worker        if os.path.exists(path):
494*6236dae4SAndroid Build Coastguard Worker            return shutil.rmtree(path)
495*6236dae4SAndroid Build Coastguard Worker
496*6236dae4SAndroid Build Coastguard Worker    def _mkpath(self, path):
497*6236dae4SAndroid Build Coastguard Worker        if not os.path.exists(path):
498*6236dae4SAndroid Build Coastguard Worker            return os.makedirs(path)
499*6236dae4SAndroid Build Coastguard Worker
500*6236dae4SAndroid Build Coastguard Worker    def get_proxy_args(self, proto: str = 'http/1.1',
501*6236dae4SAndroid Build Coastguard Worker                       proxys: bool = True, tunnel: bool = False,
502*6236dae4SAndroid Build Coastguard Worker                       use_ip: bool = False):
503*6236dae4SAndroid Build Coastguard Worker        proxy_name = self._server_addr if use_ip else self.env.proxy_domain
504*6236dae4SAndroid Build Coastguard Worker        if proxys:
505*6236dae4SAndroid Build Coastguard Worker            pport = self.env.pts_port(proto) if tunnel else self.env.proxys_port
506*6236dae4SAndroid Build Coastguard Worker            xargs = [
507*6236dae4SAndroid Build Coastguard Worker                '--proxy', f'https://{proxy_name}:{pport}/',
508*6236dae4SAndroid Build Coastguard Worker                '--resolve', f'{proxy_name}:{pport}:{self._server_addr}',
509*6236dae4SAndroid Build Coastguard Worker                '--proxy-cacert', self.env.ca.cert_file,
510*6236dae4SAndroid Build Coastguard Worker            ]
511*6236dae4SAndroid Build Coastguard Worker            if proto == 'h2':
512*6236dae4SAndroid Build Coastguard Worker                xargs.append('--proxy-http2')
513*6236dae4SAndroid Build Coastguard Worker        else:
514*6236dae4SAndroid Build Coastguard Worker            xargs = [
515*6236dae4SAndroid Build Coastguard Worker                '--proxy', f'http://{proxy_name}:{self.env.proxy_port}/',
516*6236dae4SAndroid Build Coastguard Worker                '--resolve', f'{proxy_name}:{self.env.proxy_port}:{self._server_addr}',
517*6236dae4SAndroid Build Coastguard Worker            ]
518*6236dae4SAndroid Build Coastguard Worker        if tunnel:
519*6236dae4SAndroid Build Coastguard Worker            xargs.append('--proxytunnel')
520*6236dae4SAndroid Build Coastguard Worker        return xargs
521*6236dae4SAndroid Build Coastguard Worker
522*6236dae4SAndroid Build Coastguard Worker    def http_get(self, url: str, extra_args: Optional[List[str]] = None,
523*6236dae4SAndroid Build Coastguard Worker                 alpn_proto: Optional[str] = None,
524*6236dae4SAndroid Build Coastguard Worker                 def_tracing: bool = True,
525*6236dae4SAndroid Build Coastguard Worker                 with_stats: bool = False,
526*6236dae4SAndroid Build Coastguard Worker                 with_profile: bool = False,
527*6236dae4SAndroid Build Coastguard Worker                 with_tcpdump: bool = False):
528*6236dae4SAndroid Build Coastguard Worker        return self._raw(url, options=extra_args,
529*6236dae4SAndroid Build Coastguard Worker                         with_stats=with_stats,
530*6236dae4SAndroid Build Coastguard Worker                         alpn_proto=alpn_proto,
531*6236dae4SAndroid Build Coastguard Worker                         def_tracing=def_tracing,
532*6236dae4SAndroid Build Coastguard Worker                         with_profile=with_profile,
533*6236dae4SAndroid Build Coastguard Worker                         with_tcpdump=with_tcpdump)
534*6236dae4SAndroid Build Coastguard Worker
535*6236dae4SAndroid Build Coastguard Worker    def http_download(self, urls: List[str],
536*6236dae4SAndroid Build Coastguard Worker                      alpn_proto: Optional[str] = None,
537*6236dae4SAndroid Build Coastguard Worker                      with_stats: bool = True,
538*6236dae4SAndroid Build Coastguard Worker                      with_headers: bool = False,
539*6236dae4SAndroid Build Coastguard Worker                      with_profile: bool = False,
540*6236dae4SAndroid Build Coastguard Worker                      with_tcpdump: bool = False,
541*6236dae4SAndroid Build Coastguard Worker                      no_save: bool = False,
542*6236dae4SAndroid Build Coastguard Worker                      extra_args: Optional[List[str]] = None):
543*6236dae4SAndroid Build Coastguard Worker        if extra_args is None:
544*6236dae4SAndroid Build Coastguard Worker            extra_args = []
545*6236dae4SAndroid Build Coastguard Worker        if no_save:
546*6236dae4SAndroid Build Coastguard Worker            extra_args.extend([
547*6236dae4SAndroid Build Coastguard Worker                '-o', '/dev/null',
548*6236dae4SAndroid Build Coastguard Worker            ])
549*6236dae4SAndroid Build Coastguard Worker        else:
550*6236dae4SAndroid Build Coastguard Worker            extra_args.extend([
551*6236dae4SAndroid Build Coastguard Worker                '-o', 'download_#1.data',
552*6236dae4SAndroid Build Coastguard Worker            ])
553*6236dae4SAndroid Build Coastguard Worker        # remove any existing ones
554*6236dae4SAndroid Build Coastguard Worker        for i in range(100):
555*6236dae4SAndroid Build Coastguard Worker            self._rmf(self.download_file(i))
556*6236dae4SAndroid Build Coastguard Worker        if with_stats:
557*6236dae4SAndroid Build Coastguard Worker            extra_args.extend([
558*6236dae4SAndroid Build Coastguard Worker                '-w', '%{json}\\n'
559*6236dae4SAndroid Build Coastguard Worker            ])
560*6236dae4SAndroid Build Coastguard Worker        return self._raw(urls, alpn_proto=alpn_proto, options=extra_args,
561*6236dae4SAndroid Build Coastguard Worker                         with_stats=with_stats,
562*6236dae4SAndroid Build Coastguard Worker                         with_headers=with_headers,
563*6236dae4SAndroid Build Coastguard Worker                         with_profile=with_profile,
564*6236dae4SAndroid Build Coastguard Worker                         with_tcpdump=with_tcpdump)
565*6236dae4SAndroid Build Coastguard Worker
566*6236dae4SAndroid Build Coastguard Worker    def http_upload(self, urls: List[str], data: str,
567*6236dae4SAndroid Build Coastguard Worker                    alpn_proto: Optional[str] = None,
568*6236dae4SAndroid Build Coastguard Worker                    with_stats: bool = True,
569*6236dae4SAndroid Build Coastguard Worker                    with_headers: bool = False,
570*6236dae4SAndroid Build Coastguard Worker                    with_profile: bool = False,
571*6236dae4SAndroid Build Coastguard Worker                    with_tcpdump: bool = False,
572*6236dae4SAndroid Build Coastguard Worker                    extra_args: Optional[List[str]] = None):
573*6236dae4SAndroid Build Coastguard Worker        if extra_args is None:
574*6236dae4SAndroid Build Coastguard Worker            extra_args = []
575*6236dae4SAndroid Build Coastguard Worker        extra_args.extend([
576*6236dae4SAndroid Build Coastguard Worker            '--data-binary', data, '-o', 'download_#1.data',
577*6236dae4SAndroid Build Coastguard Worker        ])
578*6236dae4SAndroid Build Coastguard Worker        if with_stats:
579*6236dae4SAndroid Build Coastguard Worker            extra_args.extend([
580*6236dae4SAndroid Build Coastguard Worker                '-w', '%{json}\\n'
581*6236dae4SAndroid Build Coastguard Worker            ])
582*6236dae4SAndroid Build Coastguard Worker        return self._raw(urls, alpn_proto=alpn_proto, options=extra_args,
583*6236dae4SAndroid Build Coastguard Worker                         with_stats=with_stats,
584*6236dae4SAndroid Build Coastguard Worker                         with_headers=with_headers,
585*6236dae4SAndroid Build Coastguard Worker                         with_profile=with_profile,
586*6236dae4SAndroid Build Coastguard Worker                         with_tcpdump=with_tcpdump)
587*6236dae4SAndroid Build Coastguard Worker
588*6236dae4SAndroid Build Coastguard Worker    def http_delete(self, urls: List[str],
589*6236dae4SAndroid Build Coastguard Worker                    alpn_proto: Optional[str] = None,
590*6236dae4SAndroid Build Coastguard Worker                    with_stats: bool = True,
591*6236dae4SAndroid Build Coastguard Worker                    with_profile: bool = False,
592*6236dae4SAndroid Build Coastguard Worker                    extra_args: Optional[List[str]] = None):
593*6236dae4SAndroid Build Coastguard Worker        if extra_args is None:
594*6236dae4SAndroid Build Coastguard Worker            extra_args = []
595*6236dae4SAndroid Build Coastguard Worker        extra_args.extend([
596*6236dae4SAndroid Build Coastguard Worker            '-X', 'DELETE', '-o', '/dev/null',
597*6236dae4SAndroid Build Coastguard Worker        ])
598*6236dae4SAndroid Build Coastguard Worker        if with_stats:
599*6236dae4SAndroid Build Coastguard Worker            extra_args.extend([
600*6236dae4SAndroid Build Coastguard Worker                '-w', '%{json}\\n'
601*6236dae4SAndroid Build Coastguard Worker            ])
602*6236dae4SAndroid Build Coastguard Worker        return self._raw(urls, alpn_proto=alpn_proto, options=extra_args,
603*6236dae4SAndroid Build Coastguard Worker                         with_stats=with_stats,
604*6236dae4SAndroid Build Coastguard Worker                         with_headers=False,
605*6236dae4SAndroid Build Coastguard Worker                         with_profile=with_profile)
606*6236dae4SAndroid Build Coastguard Worker
607*6236dae4SAndroid Build Coastguard Worker    def http_put(self, urls: List[str], data=None, fdata=None,
608*6236dae4SAndroid Build Coastguard Worker                 alpn_proto: Optional[str] = None,
609*6236dae4SAndroid Build Coastguard Worker                 with_stats: bool = True,
610*6236dae4SAndroid Build Coastguard Worker                 with_headers: bool = False,
611*6236dae4SAndroid Build Coastguard Worker                 with_profile: bool = False,
612*6236dae4SAndroid Build Coastguard Worker                 extra_args: Optional[List[str]] = None):
613*6236dae4SAndroid Build Coastguard Worker        if extra_args is None:
614*6236dae4SAndroid Build Coastguard Worker            extra_args = []
615*6236dae4SAndroid Build Coastguard Worker        if fdata is not None:
616*6236dae4SAndroid Build Coastguard Worker            extra_args.extend(['-T', fdata])
617*6236dae4SAndroid Build Coastguard Worker        elif data is not None:
618*6236dae4SAndroid Build Coastguard Worker            extra_args.extend(['-T', '-'])
619*6236dae4SAndroid Build Coastguard Worker        extra_args.extend([
620*6236dae4SAndroid Build Coastguard Worker            '-o', 'download_#1.data',
621*6236dae4SAndroid Build Coastguard Worker        ])
622*6236dae4SAndroid Build Coastguard Worker        if with_stats:
623*6236dae4SAndroid Build Coastguard Worker            extra_args.extend([
624*6236dae4SAndroid Build Coastguard Worker                '-w', '%{json}\\n'
625*6236dae4SAndroid Build Coastguard Worker            ])
626*6236dae4SAndroid Build Coastguard Worker        return self._raw(urls, intext=data,
627*6236dae4SAndroid Build Coastguard Worker                         alpn_proto=alpn_proto, options=extra_args,
628*6236dae4SAndroid Build Coastguard Worker                         with_stats=with_stats,
629*6236dae4SAndroid Build Coastguard Worker                         with_headers=with_headers,
630*6236dae4SAndroid Build Coastguard Worker                         with_profile=with_profile)
631*6236dae4SAndroid Build Coastguard Worker
632*6236dae4SAndroid Build Coastguard Worker    def http_form(self, urls: List[str], form: Dict[str, str],
633*6236dae4SAndroid Build Coastguard Worker                  alpn_proto: Optional[str] = None,
634*6236dae4SAndroid Build Coastguard Worker                  with_stats: bool = True,
635*6236dae4SAndroid Build Coastguard Worker                  with_headers: bool = False,
636*6236dae4SAndroid Build Coastguard Worker                  extra_args: Optional[List[str]] = None):
637*6236dae4SAndroid Build Coastguard Worker        if extra_args is None:
638*6236dae4SAndroid Build Coastguard Worker            extra_args = []
639*6236dae4SAndroid Build Coastguard Worker        for key, val in form.items():
640*6236dae4SAndroid Build Coastguard Worker            extra_args.extend(['-F', f'{key}={val}'])
641*6236dae4SAndroid Build Coastguard Worker        extra_args.extend([
642*6236dae4SAndroid Build Coastguard Worker            '-o', 'download_#1.data',
643*6236dae4SAndroid Build Coastguard Worker        ])
644*6236dae4SAndroid Build Coastguard Worker        if with_stats:
645*6236dae4SAndroid Build Coastguard Worker            extra_args.extend([
646*6236dae4SAndroid Build Coastguard Worker                '-w', '%{json}\\n'
647*6236dae4SAndroid Build Coastguard Worker            ])
648*6236dae4SAndroid Build Coastguard Worker        return self._raw(urls, alpn_proto=alpn_proto, options=extra_args,
649*6236dae4SAndroid Build Coastguard Worker                         with_stats=with_stats,
650*6236dae4SAndroid Build Coastguard Worker                         with_headers=with_headers)
651*6236dae4SAndroid Build Coastguard Worker
652*6236dae4SAndroid Build Coastguard Worker    def ftp_get(self, urls: List[str],
653*6236dae4SAndroid Build Coastguard Worker                      with_stats: bool = True,
654*6236dae4SAndroid Build Coastguard Worker                      with_profile: bool = False,
655*6236dae4SAndroid Build Coastguard Worker                      with_tcpdump: bool = False,
656*6236dae4SAndroid Build Coastguard Worker                      no_save: bool = False,
657*6236dae4SAndroid Build Coastguard Worker                      extra_args: Optional[List[str]] = None):
658*6236dae4SAndroid Build Coastguard Worker        if extra_args is None:
659*6236dae4SAndroid Build Coastguard Worker            extra_args = []
660*6236dae4SAndroid Build Coastguard Worker        if no_save:
661*6236dae4SAndroid Build Coastguard Worker            extra_args.extend([
662*6236dae4SAndroid Build Coastguard Worker                '-o', '/dev/null',
663*6236dae4SAndroid Build Coastguard Worker            ])
664*6236dae4SAndroid Build Coastguard Worker        else:
665*6236dae4SAndroid Build Coastguard Worker            extra_args.extend([
666*6236dae4SAndroid Build Coastguard Worker                '-o', 'download_#1.data',
667*6236dae4SAndroid Build Coastguard Worker            ])
668*6236dae4SAndroid Build Coastguard Worker        # remove any existing ones
669*6236dae4SAndroid Build Coastguard Worker        for i in range(100):
670*6236dae4SAndroid Build Coastguard Worker            self._rmf(self.download_file(i))
671*6236dae4SAndroid Build Coastguard Worker        if with_stats:
672*6236dae4SAndroid Build Coastguard Worker            extra_args.extend([
673*6236dae4SAndroid Build Coastguard Worker                '-w', '%{json}\\n'
674*6236dae4SAndroid Build Coastguard Worker            ])
675*6236dae4SAndroid Build Coastguard Worker        return self._raw(urls, options=extra_args,
676*6236dae4SAndroid Build Coastguard Worker                         with_stats=with_stats,
677*6236dae4SAndroid Build Coastguard Worker                         with_headers=False,
678*6236dae4SAndroid Build Coastguard Worker                         with_profile=with_profile,
679*6236dae4SAndroid Build Coastguard Worker                         with_tcpdump=with_tcpdump)
680*6236dae4SAndroid Build Coastguard Worker
681*6236dae4SAndroid Build Coastguard Worker    def ftp_ssl_get(self, urls: List[str],
682*6236dae4SAndroid Build Coastguard Worker                      with_stats: bool = True,
683*6236dae4SAndroid Build Coastguard Worker                      with_profile: bool = False,
684*6236dae4SAndroid Build Coastguard Worker                      with_tcpdump: bool = False,
685*6236dae4SAndroid Build Coastguard Worker                      no_save: bool = False,
686*6236dae4SAndroid Build Coastguard Worker                      extra_args: Optional[List[str]] = None):
687*6236dae4SAndroid Build Coastguard Worker        if extra_args is None:
688*6236dae4SAndroid Build Coastguard Worker            extra_args = []
689*6236dae4SAndroid Build Coastguard Worker        extra_args.extend([
690*6236dae4SAndroid Build Coastguard Worker            '--ssl-reqd',
691*6236dae4SAndroid Build Coastguard Worker        ])
692*6236dae4SAndroid Build Coastguard Worker        return self.ftp_get(urls=urls, with_stats=with_stats,
693*6236dae4SAndroid Build Coastguard Worker                            with_profile=with_profile, no_save=no_save,
694*6236dae4SAndroid Build Coastguard Worker                            with_tcpdump=with_tcpdump,
695*6236dae4SAndroid Build Coastguard Worker                            extra_args=extra_args)
696*6236dae4SAndroid Build Coastguard Worker
697*6236dae4SAndroid Build Coastguard Worker    def ftp_upload(self, urls: List[str],
698*6236dae4SAndroid Build Coastguard Worker                   fupload: Optional[Any] = None,
699*6236dae4SAndroid Build Coastguard Worker                   updata: Optional[str] = None,
700*6236dae4SAndroid Build Coastguard Worker                   with_stats: bool = True,
701*6236dae4SAndroid Build Coastguard Worker                   with_profile: bool = False,
702*6236dae4SAndroid Build Coastguard Worker                   with_tcpdump: bool = False,
703*6236dae4SAndroid Build Coastguard Worker                   extra_args: Optional[List[str]] = None):
704*6236dae4SAndroid Build Coastguard Worker        if extra_args is None:
705*6236dae4SAndroid Build Coastguard Worker            extra_args = []
706*6236dae4SAndroid Build Coastguard Worker        if fupload is not None:
707*6236dae4SAndroid Build Coastguard Worker            extra_args.extend([
708*6236dae4SAndroid Build Coastguard Worker                '--upload-file', fupload
709*6236dae4SAndroid Build Coastguard Worker            ])
710*6236dae4SAndroid Build Coastguard Worker        elif updata is not None:
711*6236dae4SAndroid Build Coastguard Worker            extra_args.extend([
712*6236dae4SAndroid Build Coastguard Worker                '--upload-file', '-'
713*6236dae4SAndroid Build Coastguard Worker            ])
714*6236dae4SAndroid Build Coastguard Worker        else:
715*6236dae4SAndroid Build Coastguard Worker            raise Exception('need either file or data to upload')
716*6236dae4SAndroid Build Coastguard Worker        if with_stats:
717*6236dae4SAndroid Build Coastguard Worker            extra_args.extend([
718*6236dae4SAndroid Build Coastguard Worker                '-w', '%{json}\\n'
719*6236dae4SAndroid Build Coastguard Worker            ])
720*6236dae4SAndroid Build Coastguard Worker        return self._raw(urls, options=extra_args,
721*6236dae4SAndroid Build Coastguard Worker                         intext=updata,
722*6236dae4SAndroid Build Coastguard Worker                         with_stats=with_stats,
723*6236dae4SAndroid Build Coastguard Worker                         with_headers=False,
724*6236dae4SAndroid Build Coastguard Worker                         with_profile=with_profile,
725*6236dae4SAndroid Build Coastguard Worker                         with_tcpdump=with_tcpdump)
726*6236dae4SAndroid Build Coastguard Worker
727*6236dae4SAndroid Build Coastguard Worker    def ftp_ssl_upload(self, urls: List[str],
728*6236dae4SAndroid Build Coastguard Worker                       fupload: Optional[Any] = None,
729*6236dae4SAndroid Build Coastguard Worker                       updata: Optional[str] = None,
730*6236dae4SAndroid Build Coastguard Worker                       with_stats: bool = True,
731*6236dae4SAndroid Build Coastguard Worker                       with_profile: bool = False,
732*6236dae4SAndroid Build Coastguard Worker                       with_tcpdump: bool = False,
733*6236dae4SAndroid Build Coastguard Worker                       extra_args: Optional[List[str]] = None):
734*6236dae4SAndroid Build Coastguard Worker        if extra_args is None:
735*6236dae4SAndroid Build Coastguard Worker            extra_args = []
736*6236dae4SAndroid Build Coastguard Worker        extra_args.extend([
737*6236dae4SAndroid Build Coastguard Worker            '--ssl-reqd',
738*6236dae4SAndroid Build Coastguard Worker        ])
739*6236dae4SAndroid Build Coastguard Worker        return self.ftp_upload(urls=urls, fupload=fupload, updata=updata,
740*6236dae4SAndroid Build Coastguard Worker                               with_stats=with_stats, with_profile=with_profile,
741*6236dae4SAndroid Build Coastguard Worker                               with_tcpdump=with_tcpdump,
742*6236dae4SAndroid Build Coastguard Worker                               extra_args=extra_args)
743*6236dae4SAndroid Build Coastguard Worker
744*6236dae4SAndroid Build Coastguard Worker    def response_file(self, idx: int):
745*6236dae4SAndroid Build Coastguard Worker        return os.path.join(self._run_dir, f'download_{idx}.data')
746*6236dae4SAndroid Build Coastguard Worker
747*6236dae4SAndroid Build Coastguard Worker    def run_direct(self, args, with_stats: bool = False, with_profile: bool = False):
748*6236dae4SAndroid Build Coastguard Worker        my_args = [self._curl]
749*6236dae4SAndroid Build Coastguard Worker        if with_stats:
750*6236dae4SAndroid Build Coastguard Worker            my_args.extend([
751*6236dae4SAndroid Build Coastguard Worker                '-w', '%{json}\\n'
752*6236dae4SAndroid Build Coastguard Worker            ])
753*6236dae4SAndroid Build Coastguard Worker        my_args.extend([
754*6236dae4SAndroid Build Coastguard Worker            '-o', 'download.data',
755*6236dae4SAndroid Build Coastguard Worker        ])
756*6236dae4SAndroid Build Coastguard Worker        my_args.extend(args)
757*6236dae4SAndroid Build Coastguard Worker        return self._run(args=my_args, with_stats=with_stats, with_profile=with_profile)
758*6236dae4SAndroid Build Coastguard Worker
759*6236dae4SAndroid Build Coastguard Worker    def _run(self, args, intext='', with_stats: bool = False,
760*6236dae4SAndroid Build Coastguard Worker             with_profile: bool = True, with_tcpdump: bool = False):
761*6236dae4SAndroid Build Coastguard Worker        self._rmf(self._stdoutfile)
762*6236dae4SAndroid Build Coastguard Worker        self._rmf(self._stderrfile)
763*6236dae4SAndroid Build Coastguard Worker        self._rmf(self._headerfile)
764*6236dae4SAndroid Build Coastguard Worker        exception = None
765*6236dae4SAndroid Build Coastguard Worker        profile = None
766*6236dae4SAndroid Build Coastguard Worker        tcpdump = None
767*6236dae4SAndroid Build Coastguard Worker        started_at = datetime.now()
768*6236dae4SAndroid Build Coastguard Worker        if with_tcpdump:
769*6236dae4SAndroid Build Coastguard Worker            tcpdump = RunTcpDump(self.env, self._run_dir)
770*6236dae4SAndroid Build Coastguard Worker            tcpdump.start()
771*6236dae4SAndroid Build Coastguard Worker        try:
772*6236dae4SAndroid Build Coastguard Worker            with open(self._stdoutfile, 'w') as cout, open(self._stderrfile, 'w') as cerr:
773*6236dae4SAndroid Build Coastguard Worker                if with_profile:
774*6236dae4SAndroid Build Coastguard Worker                    end_at = started_at + timedelta(seconds=self._timeout) \
775*6236dae4SAndroid Build Coastguard Worker                        if self._timeout else None
776*6236dae4SAndroid Build Coastguard Worker                    log.info(f'starting: {args}')
777*6236dae4SAndroid Build Coastguard Worker                    p = subprocess.Popen(args, stderr=cerr, stdout=cout,
778*6236dae4SAndroid Build Coastguard Worker                                         cwd=self._run_dir, shell=False,
779*6236dae4SAndroid Build Coastguard Worker                                         env=self._run_env)
780*6236dae4SAndroid Build Coastguard Worker                    profile = RunProfile(p.pid, started_at, self._run_dir)
781*6236dae4SAndroid Build Coastguard Worker                    if intext is not None and False:
782*6236dae4SAndroid Build Coastguard Worker                        p.communicate(input=intext.encode(), timeout=1)
783*6236dae4SAndroid Build Coastguard Worker                    ptimeout = 0.0
784*6236dae4SAndroid Build Coastguard Worker                    while True:
785*6236dae4SAndroid Build Coastguard Worker                        try:
786*6236dae4SAndroid Build Coastguard Worker                            p.wait(timeout=ptimeout)
787*6236dae4SAndroid Build Coastguard Worker                            break
788*6236dae4SAndroid Build Coastguard Worker                        except subprocess.TimeoutExpired:
789*6236dae4SAndroid Build Coastguard Worker                            if end_at and datetime.now() >= end_at:
790*6236dae4SAndroid Build Coastguard Worker                                p.kill()
791*6236dae4SAndroid Build Coastguard Worker                                raise subprocess.TimeoutExpired(cmd=args, timeout=self._timeout)
792*6236dae4SAndroid Build Coastguard Worker                            profile.sample()
793*6236dae4SAndroid Build Coastguard Worker                            ptimeout = 0.01
794*6236dae4SAndroid Build Coastguard Worker                    exitcode = p.returncode
795*6236dae4SAndroid Build Coastguard Worker                    profile.finish()
796*6236dae4SAndroid Build Coastguard Worker                    log.info(f'done: exit={exitcode}, profile={profile}')
797*6236dae4SAndroid Build Coastguard Worker                else:
798*6236dae4SAndroid Build Coastguard Worker                    p = subprocess.run(args, stderr=cerr, stdout=cout,
799*6236dae4SAndroid Build Coastguard Worker                                       cwd=self._run_dir, shell=False,
800*6236dae4SAndroid Build Coastguard Worker                                       input=intext.encode() if intext else None,
801*6236dae4SAndroid Build Coastguard Worker                                       timeout=self._timeout,
802*6236dae4SAndroid Build Coastguard Worker                                       env=self._run_env)
803*6236dae4SAndroid Build Coastguard Worker                    exitcode = p.returncode
804*6236dae4SAndroid Build Coastguard Worker        except subprocess.TimeoutExpired:
805*6236dae4SAndroid Build Coastguard Worker            now = datetime.now()
806*6236dae4SAndroid Build Coastguard Worker            duration = now - started_at
807*6236dae4SAndroid Build Coastguard Worker            log.warning(f'Timeout at {now} after {duration.total_seconds()}s '
808*6236dae4SAndroid Build Coastguard Worker                        f'(configured {self._timeout}s): {args}')
809*6236dae4SAndroid Build Coastguard Worker            exitcode = -1
810*6236dae4SAndroid Build Coastguard Worker            exception = 'TimeoutExpired'
811*6236dae4SAndroid Build Coastguard Worker        if tcpdump:
812*6236dae4SAndroid Build Coastguard Worker            tcpdump.finish()
813*6236dae4SAndroid Build Coastguard Worker        coutput = open(self._stdoutfile).readlines()
814*6236dae4SAndroid Build Coastguard Worker        cerrput = open(self._stderrfile).readlines()
815*6236dae4SAndroid Build Coastguard Worker        return ExecResult(args=args, exit_code=exitcode, exception=exception,
816*6236dae4SAndroid Build Coastguard Worker                          stdout=coutput, stderr=cerrput,
817*6236dae4SAndroid Build Coastguard Worker                          duration=datetime.now() - started_at,
818*6236dae4SAndroid Build Coastguard Worker                          with_stats=with_stats,
819*6236dae4SAndroid Build Coastguard Worker                          profile=profile, tcpdump=tcpdump)
820*6236dae4SAndroid Build Coastguard Worker
821*6236dae4SAndroid Build Coastguard Worker    def _raw(self, urls, intext='', timeout=None, options=None, insecure=False,
822*6236dae4SAndroid Build Coastguard Worker             alpn_proto: Optional[str] = None,
823*6236dae4SAndroid Build Coastguard Worker             force_resolve=True,
824*6236dae4SAndroid Build Coastguard Worker             with_stats=False,
825*6236dae4SAndroid Build Coastguard Worker             with_headers=True,
826*6236dae4SAndroid Build Coastguard Worker             def_tracing=True,
827*6236dae4SAndroid Build Coastguard Worker             with_profile=False,
828*6236dae4SAndroid Build Coastguard Worker             with_tcpdump=False):
829*6236dae4SAndroid Build Coastguard Worker        args = self._complete_args(
830*6236dae4SAndroid Build Coastguard Worker            urls=urls, timeout=timeout, options=options, insecure=insecure,
831*6236dae4SAndroid Build Coastguard Worker            alpn_proto=alpn_proto, force_resolve=force_resolve,
832*6236dae4SAndroid Build Coastguard Worker            with_headers=with_headers, def_tracing=def_tracing)
833*6236dae4SAndroid Build Coastguard Worker        r = self._run(args, intext=intext, with_stats=with_stats,
834*6236dae4SAndroid Build Coastguard Worker                      with_profile=with_profile, with_tcpdump=with_tcpdump)
835*6236dae4SAndroid Build Coastguard Worker        if r.exit_code == 0 and with_headers:
836*6236dae4SAndroid Build Coastguard Worker            self._parse_headerfile(self._headerfile, r=r)
837*6236dae4SAndroid Build Coastguard Worker        return r
838*6236dae4SAndroid Build Coastguard Worker
839*6236dae4SAndroid Build Coastguard Worker    def _complete_args(self, urls, timeout=None, options=None,
840*6236dae4SAndroid Build Coastguard Worker                       insecure=False, force_resolve=True,
841*6236dae4SAndroid Build Coastguard Worker                       alpn_proto: Optional[str] = None,
842*6236dae4SAndroid Build Coastguard Worker                       with_headers: bool = True,
843*6236dae4SAndroid Build Coastguard Worker                       def_tracing: bool = True):
844*6236dae4SAndroid Build Coastguard Worker        if not isinstance(urls, list):
845*6236dae4SAndroid Build Coastguard Worker            urls = [urls]
846*6236dae4SAndroid Build Coastguard Worker
847*6236dae4SAndroid Build Coastguard Worker        args = [self._curl, "-s", "--path-as-is"]
848*6236dae4SAndroid Build Coastguard Worker        if 'CURL_TEST_EVENT' in os.environ:
849*6236dae4SAndroid Build Coastguard Worker            args.append('--test-event')
850*6236dae4SAndroid Build Coastguard Worker
851*6236dae4SAndroid Build Coastguard Worker        if with_headers:
852*6236dae4SAndroid Build Coastguard Worker            args.extend(["-D", self._headerfile])
853*6236dae4SAndroid Build Coastguard Worker        if def_tracing is not False and not self._silent:
854*6236dae4SAndroid Build Coastguard Worker            args.extend(['-v', '--trace-ids', '--trace-time'])
855*6236dae4SAndroid Build Coastguard Worker            if self.env.verbose > 1:
856*6236dae4SAndroid Build Coastguard Worker                args.extend(['--trace-config', 'http/2,http/3,h2-proxy,h1-proxy'])
857*6236dae4SAndroid Build Coastguard Worker
858*6236dae4SAndroid Build Coastguard Worker        active_options = options
859*6236dae4SAndroid Build Coastguard Worker        if options is not None and '--next' in options:
860*6236dae4SAndroid Build Coastguard Worker            active_options = options[options.index('--next') + 1:]
861*6236dae4SAndroid Build Coastguard Worker
862*6236dae4SAndroid Build Coastguard Worker        for url in urls:
863*6236dae4SAndroid Build Coastguard Worker            u = urlparse(urls[0])
864*6236dae4SAndroid Build Coastguard Worker            if options:
865*6236dae4SAndroid Build Coastguard Worker                args.extend(options)
866*6236dae4SAndroid Build Coastguard Worker            if alpn_proto is not None:
867*6236dae4SAndroid Build Coastguard Worker                if alpn_proto not in self.ALPN_ARG:
868*6236dae4SAndroid Build Coastguard Worker                    raise Exception(f'unknown ALPN protocol: "{alpn_proto}"')
869*6236dae4SAndroid Build Coastguard Worker                args.append(self.ALPN_ARG[alpn_proto])
870*6236dae4SAndroid Build Coastguard Worker
871*6236dae4SAndroid Build Coastguard Worker            if u.scheme == 'http':
872*6236dae4SAndroid Build Coastguard Worker                pass
873*6236dae4SAndroid Build Coastguard Worker            elif insecure:
874*6236dae4SAndroid Build Coastguard Worker                args.append('--insecure')
875*6236dae4SAndroid Build Coastguard Worker            elif active_options and "--cacert" in active_options:
876*6236dae4SAndroid Build Coastguard Worker                pass
877*6236dae4SAndroid Build Coastguard Worker            elif u.hostname:
878*6236dae4SAndroid Build Coastguard Worker                args.extend(["--cacert", self.env.ca.cert_file])
879*6236dae4SAndroid Build Coastguard Worker
880*6236dae4SAndroid Build Coastguard Worker            if force_resolve and u.hostname and u.hostname != 'localhost' \
881*6236dae4SAndroid Build Coastguard Worker                    and not re.match(r'^(\d+|\[|:).*', u.hostname):
882*6236dae4SAndroid Build Coastguard Worker                port = u.port if u.port else 443
883*6236dae4SAndroid Build Coastguard Worker                args.extend([
884*6236dae4SAndroid Build Coastguard Worker                    '--resolve', f'{u.hostname}:{port}:{self._server_addr}',
885*6236dae4SAndroid Build Coastguard Worker                ])
886*6236dae4SAndroid Build Coastguard Worker            if timeout is not None and int(timeout) > 0:
887*6236dae4SAndroid Build Coastguard Worker                args.extend(["--connect-timeout", str(int(timeout))])
888*6236dae4SAndroid Build Coastguard Worker            args.append(url)
889*6236dae4SAndroid Build Coastguard Worker        return args
890*6236dae4SAndroid Build Coastguard Worker
891*6236dae4SAndroid Build Coastguard Worker    def _parse_headerfile(self, headerfile: str, r: Optional[ExecResult] = None) -> ExecResult:
892*6236dae4SAndroid Build Coastguard Worker        lines = open(headerfile).readlines()
893*6236dae4SAndroid Build Coastguard Worker        if r is None:
894*6236dae4SAndroid Build Coastguard Worker            r = ExecResult(args=[], exit_code=0, stdout=[], stderr=[])
895*6236dae4SAndroid Build Coastguard Worker
896*6236dae4SAndroid Build Coastguard Worker        response = None
897*6236dae4SAndroid Build Coastguard Worker
898*6236dae4SAndroid Build Coastguard Worker        def fin_response(resp):
899*6236dae4SAndroid Build Coastguard Worker            if resp:
900*6236dae4SAndroid Build Coastguard Worker                r.add_response(resp)
901*6236dae4SAndroid Build Coastguard Worker
902*6236dae4SAndroid Build Coastguard Worker        expected = ['status']
903*6236dae4SAndroid Build Coastguard Worker        for line in lines:
904*6236dae4SAndroid Build Coastguard Worker            line = line.strip()
905*6236dae4SAndroid Build Coastguard Worker            if re.match(r'^$', line):
906*6236dae4SAndroid Build Coastguard Worker                if 'trailer' in expected:
907*6236dae4SAndroid Build Coastguard Worker                    # end of trailers
908*6236dae4SAndroid Build Coastguard Worker                    fin_response(response)
909*6236dae4SAndroid Build Coastguard Worker                    response = None
910*6236dae4SAndroid Build Coastguard Worker                    expected = ['status']
911*6236dae4SAndroid Build Coastguard Worker                elif 'header' in expected:
912*6236dae4SAndroid Build Coastguard Worker                    # end of header, another status or trailers might follow
913*6236dae4SAndroid Build Coastguard Worker                    expected = ['status', 'trailer']
914*6236dae4SAndroid Build Coastguard Worker                else:
915*6236dae4SAndroid Build Coastguard Worker                    assert False, f"unexpected line: '{line}'"
916*6236dae4SAndroid Build Coastguard Worker                continue
917*6236dae4SAndroid Build Coastguard Worker            if 'status' in expected:
918*6236dae4SAndroid Build Coastguard Worker                # log.debug("reading 1st response line: %s", line)
919*6236dae4SAndroid Build Coastguard Worker                m = re.match(r'^(\S+) (\d+)( .*)?$', line)
920*6236dae4SAndroid Build Coastguard Worker                if m:
921*6236dae4SAndroid Build Coastguard Worker                    fin_response(response)
922*6236dae4SAndroid Build Coastguard Worker                    response = {
923*6236dae4SAndroid Build Coastguard Worker                        "protocol": m.group(1),
924*6236dae4SAndroid Build Coastguard Worker                        "status": int(m.group(2)),
925*6236dae4SAndroid Build Coastguard Worker                        "description": m.group(3),
926*6236dae4SAndroid Build Coastguard Worker                        "header": {},
927*6236dae4SAndroid Build Coastguard Worker                        "trailer": {},
928*6236dae4SAndroid Build Coastguard Worker                        "body": r.outraw
929*6236dae4SAndroid Build Coastguard Worker                    }
930*6236dae4SAndroid Build Coastguard Worker                    expected = ['header']
931*6236dae4SAndroid Build Coastguard Worker                    continue
932*6236dae4SAndroid Build Coastguard Worker            if 'trailer' in expected:
933*6236dae4SAndroid Build Coastguard Worker                m = re.match(r'^([^:]+):\s*(.*)$', line)
934*6236dae4SAndroid Build Coastguard Worker                if m:
935*6236dae4SAndroid Build Coastguard Worker                    response['trailer'][m.group(1).lower()] = m.group(2)
936*6236dae4SAndroid Build Coastguard Worker                    continue
937*6236dae4SAndroid Build Coastguard Worker            if 'header' in expected:
938*6236dae4SAndroid Build Coastguard Worker                m = re.match(r'^([^:]+):\s*(.*)$', line)
939*6236dae4SAndroid Build Coastguard Worker                if m:
940*6236dae4SAndroid Build Coastguard Worker                    response['header'][m.group(1).lower()] = m.group(2)
941*6236dae4SAndroid Build Coastguard Worker                    continue
942*6236dae4SAndroid Build Coastguard Worker            assert False, f"unexpected line: '{line}, expected: {expected}'"
943*6236dae4SAndroid Build Coastguard Worker
944*6236dae4SAndroid Build Coastguard Worker        fin_response(response)
945*6236dae4SAndroid Build Coastguard Worker        return r
946