Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/pkg
Path: blob/main/external/curl/tests/http/testenv/httpd.py
2654 views
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
#***************************************************************************
4
# _ _ ____ _
5
# Project ___| | | | _ \| |
6
# / __| | | | |_) | |
7
# | (__| |_| | _ <| |___
8
# \___|\___/|_| \_\_____|
9
#
10
# Copyright (C) Daniel Stenberg, <[email protected]>, 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
#
27
import inspect
28
import logging
29
import os
30
import shutil
31
import socket
32
import subprocess
33
from datetime import timedelta, datetime
34
from json import JSONEncoder
35
import time
36
from typing import List, Union, Optional, Dict
37
import copy
38
39
from .curl import CurlClient, ExecResult
40
from .env import Env
41
from .ports import alloc_ports_and_do
42
43
log = logging.getLogger(__name__)
44
45
46
class Httpd:
47
48
MODULES = [
49
'log_config', 'logio', 'unixd', 'version', 'watchdog',
50
'authn_core', 'authn_file',
51
'authz_user', 'authz_core', 'authz_host',
52
'auth_basic', 'auth_digest',
53
'alias', 'env', 'filter', 'headers', 'mime', 'setenvif', 'negotiation',
54
'socache_shmcb',
55
'rewrite', 'http2', 'ssl', 'proxy', 'proxy_http', 'proxy_connect',
56
'brotli',
57
'mpm_event',
58
]
59
COMMON_MODULES_DIRS = [
60
'/usr/lib/apache2/modules', # debian
61
'/usr/libexec/apache2/', # macos
62
]
63
64
MOD_CURLTEST = None
65
66
PORT_SPECS = {
67
'http': socket.SOCK_STREAM,
68
'https': socket.SOCK_STREAM,
69
'https-tcp-only': socket.SOCK_STREAM,
70
'proxy': socket.SOCK_STREAM,
71
'proxys': socket.SOCK_STREAM,
72
}
73
74
def __init__(self, env: Env):
75
self.env = env
76
self._apache_dir = os.path.join(env.gen_dir, 'apache')
77
self._run_dir = os.path.join(self._apache_dir, 'run')
78
self._lock_dir = os.path.join(self._apache_dir, 'locks')
79
self._docs_dir = os.path.join(self._apache_dir, 'docs')
80
self._conf_dir = os.path.join(self._apache_dir, 'conf')
81
self._conf_file = os.path.join(self._conf_dir, 'test.conf')
82
self._logs_dir = os.path.join(self._apache_dir, 'logs')
83
self._error_log = os.path.join(self._logs_dir, 'error_log')
84
self._tmp_dir = os.path.join(self._apache_dir, 'tmp')
85
self._basic_passwords = os.path.join(self._conf_dir, 'basic.passwords')
86
self._digest_passwords = os.path.join(self._conf_dir, 'digest.passwords')
87
self._mods_dir = None
88
self._auth_digest = True
89
self._proxy_auth_basic = False
90
# name used to lookup credentials for env.domain1
91
self._domain1_cred_name = env.domain1
92
self._extra_configs = {}
93
self._loaded_extra_configs = None
94
self._loaded_proxy_auth = None
95
self._loaded_domain1_cred_name = None
96
assert env.apxs
97
p = subprocess.run(args=[env.apxs, '-q', 'libexecdir'],
98
capture_output=True, text=True)
99
if p.returncode != 0:
100
raise Exception(f'{env.apxs} failed to query libexecdir: {p}')
101
self._mods_dir = p.stdout.strip()
102
if self._mods_dir is None:
103
raise Exception('apache modules dir cannot be found')
104
if not os.path.exists(self._mods_dir):
105
raise Exception(f'apache modules dir does not exist: {self._mods_dir}')
106
self._maybe_running = False
107
self.ports = {}
108
self._rmf(self._error_log)
109
self._init_curltest()
110
111
@property
112
def docs_dir(self):
113
return self._docs_dir
114
115
def clear_logs(self):
116
self._rmf(self._error_log)
117
118
def exists(self):
119
return os.path.exists(self.env.httpd)
120
121
def set_extra_config(self, domain: str, lines: Optional[Union[str, List[str]]]):
122
if lines is None:
123
self._extra_configs.pop(domain, None)
124
else:
125
self._extra_configs[domain] = lines
126
127
def reset_config(self):
128
self._extra_configs = {}
129
self.set_proxy_auth(False)
130
self._domain1_cred_name = self.env.domain1
131
132
def set_proxy_auth(self, active: bool):
133
self._proxy_auth_basic = active
134
135
def set_domain1_cred_name(self, name):
136
self._domain1_cred_name = name
137
138
def _run(self, args, intext=''):
139
env = os.environ.copy()
140
env['APACHE_RUN_DIR'] = self._run_dir
141
env['APACHE_RUN_USER'] = os.environ['USER']
142
env['APACHE_LOCK_DIR'] = self._lock_dir
143
env['APACHE_CONFDIR'] = self._apache_dir
144
p = subprocess.run(args, stderr=subprocess.PIPE, stdout=subprocess.PIPE,
145
cwd=self.env.gen_dir,
146
input=intext.encode() if intext else None,
147
env=env)
148
start = datetime.now()
149
return ExecResult(args=args, exit_code=p.returncode,
150
stdout=p.stdout.decode().splitlines(),
151
stderr=p.stderr.decode().splitlines(),
152
duration=datetime.now() - start)
153
154
def _cmd_httpd(self, cmd: str):
155
args = [self.env.httpd,
156
"-d", self._apache_dir,
157
"-f", self._conf_file,
158
"-k", cmd]
159
return self._run(args=args)
160
161
def initial_start(self):
162
163
def startup(ports: Dict[str, int]) -> bool:
164
self.ports.update(ports)
165
if self.start():
166
self.env.update_ports(ports)
167
return True
168
self.stop()
169
self.ports.clear()
170
return False
171
172
return alloc_ports_and_do(Httpd.PORT_SPECS, startup,
173
self.env.gen_root, max_tries=3)
174
175
def start(self):
176
# assure ports are allocated
177
for key, _ in Httpd.PORT_SPECS.items():
178
assert self.ports[key] is not None
179
if self._maybe_running:
180
self.stop()
181
self._write_config()
182
with open(self._error_log, 'a') as fd:
183
fd.write('start of server\n')
184
with open(os.path.join(self._apache_dir, 'xxx'), 'a') as fd:
185
fd.write('start of server\n')
186
r = self._cmd_httpd('start')
187
if r.exit_code != 0 or len(r.stderr):
188
log.error(f'failed to start httpd: {r}')
189
self.stop()
190
return False
191
self._loaded_extra_configs = copy.deepcopy(self._extra_configs)
192
self._loaded_proxy_auth = self._proxy_auth_basic
193
return self.wait_live(timeout=timedelta(seconds=Env.SERVER_TIMEOUT))
194
195
def stop(self):
196
r = self._cmd_httpd('stop')
197
self._loaded_extra_configs = None
198
self._loaded_proxy_auth = None
199
if r.exit_code == 0:
200
return self.wait_dead(timeout=timedelta(seconds=Env.SERVER_TIMEOUT))
201
log.fatal(f'stopping httpd failed: {r}')
202
return r.exit_code == 0
203
204
def reload(self):
205
self._write_config()
206
r = self._cmd_httpd("graceful")
207
if r.exit_code != 0:
208
log.error(f'failed to reload httpd: {r}')
209
return False
210
self._loaded_extra_configs = None
211
self._loaded_proxy_auth = None
212
if r.exit_code != 0:
213
log.error(f'failed to reload httpd: {r}')
214
self._loaded_extra_configs = copy.deepcopy(self._extra_configs)
215
self._loaded_proxy_auth = self._proxy_auth_basic
216
return self.wait_live(timeout=timedelta(seconds=Env.SERVER_TIMEOUT))
217
218
def reload_if_config_changed(self):
219
if self._maybe_running and \
220
self._loaded_extra_configs == self._extra_configs and \
221
self._loaded_proxy_auth == self._proxy_auth_basic and \
222
self._loaded_domain1_cred_name == self._domain1_cred_name:
223
return True
224
return self.reload()
225
226
def wait_dead(self, timeout: timedelta):
227
curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
228
try_until = datetime.now() + timeout
229
while datetime.now() < try_until:
230
r = curl.http_get(url=f'http://{self.env.domain1}:{self.ports["http"]}/')
231
if r.exit_code != 0:
232
self._maybe_running = False
233
return True
234
time.sleep(.1)
235
log.debug(f"Server still responding after {timeout}")
236
return False
237
238
def wait_live(self, timeout: timedelta):
239
curl = CurlClient(env=self.env, run_dir=self._tmp_dir,
240
timeout=timeout.total_seconds())
241
try_until = datetime.now() + timeout
242
while datetime.now() < try_until:
243
r = curl.http_get(url=f'http://{self.env.domain1}:{self.ports["http"]}/')
244
if r.exit_code == 0:
245
self._maybe_running = True
246
return True
247
time.sleep(.1)
248
log.error(f"Server still not responding after {timeout}")
249
return False
250
251
def _rmf(self, path):
252
if os.path.exists(path):
253
return os.remove(path)
254
255
def _mkpath(self, path):
256
if not os.path.exists(path):
257
return os.makedirs(path)
258
259
def _write_config(self):
260
domain1 = self.env.domain1
261
domain1brotli = self.env.domain1brotli
262
creds1 = self.env.get_credentials(self._domain1_cred_name)
263
assert creds1 # convince pytype this isn't None
264
self._loaded_domain1_cred_name = self._domain1_cred_name
265
domain2 = self.env.domain2
266
creds2 = self.env.get_credentials(domain2)
267
assert creds2 # convince pytype this isn't None
268
exp_domain = self.env.expired_domain
269
exp_creds = self.env.get_credentials(exp_domain)
270
assert exp_creds # convince pytype this isn't None
271
proxy_domain = self.env.proxy_domain
272
proxy_creds = self.env.get_credentials(proxy_domain)
273
assert proxy_creds # convince pytype this isn't None
274
self._mkpath(self._conf_dir)
275
self._mkpath(self._docs_dir)
276
self._mkpath(self._logs_dir)
277
self._mkpath(self._tmp_dir)
278
self._mkpath(os.path.join(self._docs_dir, 'two'))
279
with open(os.path.join(self._docs_dir, 'data.json'), 'w') as fd:
280
data = {
281
'server': f'{domain1}',
282
}
283
fd.write(JSONEncoder().encode(data))
284
with open(os.path.join(self._docs_dir, 'two/data.json'), 'w') as fd:
285
data = {
286
'server': f'{domain2}',
287
}
288
fd.write(JSONEncoder().encode(data))
289
if self._proxy_auth_basic:
290
with open(self._basic_passwords, 'w') as fd:
291
fd.write('proxy:$apr1$FQfeInbs$WQZbODJlVg60j0ogEIlTW/\n')
292
if self._auth_digest:
293
with open(self._digest_passwords, 'w') as fd:
294
fd.write('test:restricted area:57123e269fd73d71ae0656594e938e2f\n')
295
self._mkpath(os.path.join(self.docs_dir, 'restricted/digest'))
296
with open(os.path.join(self.docs_dir, 'restricted/digest/data.json'), 'w') as fd:
297
fd.write('{"area":"digest"}\n')
298
with open(self._conf_file, 'w') as fd:
299
for m in self.MODULES:
300
if os.path.exists(os.path.join(self._mods_dir, f'mod_{m}.so')):
301
fd.write(f'LoadModule {m}_module "{self._mods_dir}/mod_{m}.so"\n')
302
if Httpd.MOD_CURLTEST is not None:
303
fd.write(f'LoadModule curltest_module "{Httpd.MOD_CURLTEST}"\n')
304
conf = [ # base server config
305
f'ServerRoot "{self._apache_dir}"',
306
'DefaultRuntimeDir logs',
307
'PidFile httpd.pid',
308
f'ServerName {self.env.tld}',
309
f'ErrorLog {self._error_log}',
310
f'LogLevel {self._get_log_level()}',
311
'StartServers 4',
312
'ReadBufferSize 16000',
313
'KeepAliveTimeout 30', # CI may exceed the default of 5 sec
314
'H2MinWorkers 16',
315
'H2MaxWorkers 256',
316
f'TypesConfig "{self._conf_dir}/mime.types',
317
'SSLSessionCache "shmcb:ssl_gcache_data(32000)"',
318
'AddEncoding x-gzip .gz .tgz .gzip',
319
'AddHandler type-map .var',
320
]
321
conf.extend([f'Listen {port}' for _, port in self.ports.items()])
322
323
if 'base' in self._extra_configs:
324
conf.extend(self._extra_configs['base'])
325
conf.extend([ # plain http host for domain1
326
f'<VirtualHost *:{self.ports["http"]}>',
327
f' ServerName {domain1}',
328
' ServerAlias localhost',
329
f' DocumentRoot "{self._docs_dir}"',
330
' Protocols h2c http/1.1',
331
' H2Direct on',
332
])
333
conf.extend(self._curltest_conf(domain1))
334
conf.extend([
335
'</VirtualHost>',
336
'',
337
])
338
conf.extend([ # https host for domain1, h1 + h2
339
f'<VirtualHost *:{self.ports["https"]}>',
340
f' ServerName {domain1}',
341
' ServerAlias localhost',
342
' Protocols h2 http/1.1',
343
' SSLEngine on',
344
f' SSLCertificateFile {creds1.cert_file}',
345
f' SSLCertificateKeyFile {creds1.pkey_file}',
346
f' DocumentRoot "{self._docs_dir}"',
347
])
348
conf.extend(self._curltest_conf(domain1))
349
if domain1 in self._extra_configs:
350
conf.extend(self._extra_configs[domain1])
351
conf.extend([
352
'</VirtualHost>',
353
'',
354
])
355
conf.extend([ # https host for domain1, h1 + h2, tcp only
356
f'<VirtualHost *:{self.ports["https-tcp-only"]}>',
357
f' ServerName {domain1}',
358
' ServerAlias localhost',
359
' Protocols h2 http/1.1',
360
' SSLEngine on',
361
f' SSLCertificateFile {creds1.cert_file}',
362
f' SSLCertificateKeyFile {creds1.pkey_file}',
363
f' DocumentRoot "{self._docs_dir}"',
364
])
365
conf.extend(self._curltest_conf(domain1))
366
if domain1 in self._extra_configs:
367
conf.extend(self._extra_configs[domain1])
368
conf.extend([
369
'</VirtualHost>',
370
'',
371
])
372
# Alternate to domain1 with BROTLI compression
373
conf.extend([ # https host for domain1, h1 + h2
374
f'<VirtualHost *:{self.ports["https"]}>',
375
f' ServerName {domain1brotli}',
376
' Protocols h2 http/1.1',
377
' SSLEngine on',
378
f' SSLCertificateFile {creds1.cert_file}',
379
f' SSLCertificateKeyFile {creds1.pkey_file}',
380
f' DocumentRoot "{self._docs_dir}"',
381
' SetOutputFilter BROTLI_COMPRESS',
382
])
383
conf.extend(self._curltest_conf(domain1))
384
if domain1 in self._extra_configs:
385
conf.extend(self._extra_configs[domain1])
386
conf.extend([
387
'</VirtualHost>',
388
'',
389
])
390
conf.extend([ # plain http host for domain2
391
f'<VirtualHost *:{self.ports["http"]}>',
392
f' ServerName {domain2}',
393
' ServerAlias localhost',
394
f' DocumentRoot "{self._docs_dir}"',
395
' Protocols h2c http/1.1',
396
])
397
conf.extend(self._curltest_conf(domain2))
398
conf.extend([
399
'</VirtualHost>',
400
'',
401
])
402
self._mkpath(os.path.join(self._docs_dir, 'two'))
403
conf.extend([ # https host for domain2, no h2
404
f'<VirtualHost *:{self.ports["https"]}>',
405
f' ServerName {domain2}',
406
' Protocols http/1.1',
407
' SSLEngine on',
408
f' SSLCertificateFile {creds2.cert_file}',
409
f' SSLCertificateKeyFile {creds2.pkey_file}',
410
f' DocumentRoot "{self._docs_dir}/two"',
411
])
412
conf.extend(self._curltest_conf(domain2))
413
if domain2 in self._extra_configs:
414
conf.extend(self._extra_configs[domain2])
415
conf.extend([
416
'</VirtualHost>',
417
'',
418
])
419
conf.extend([ # https host for domain2, no h2, tcp only
420
f'<VirtualHost *:{self.ports["https-tcp-only"]}>',
421
f' ServerName {domain2}',
422
' Protocols http/1.1',
423
' SSLEngine on',
424
f' SSLCertificateFile {creds2.cert_file}',
425
f' SSLCertificateKeyFile {creds2.pkey_file}',
426
f' DocumentRoot "{self._docs_dir}/two"',
427
])
428
conf.extend(self._curltest_conf(domain2))
429
if domain2 in self._extra_configs:
430
conf.extend(self._extra_configs[domain2])
431
conf.extend([
432
'</VirtualHost>',
433
'',
434
])
435
self._mkpath(os.path.join(self._docs_dir, 'expired'))
436
conf.extend([ # https host for expired domain
437
f'<VirtualHost *:{self.ports["https"]}>',
438
f' ServerName {exp_domain}',
439
' Protocols h2 http/1.1',
440
' SSLEngine on',
441
f' SSLCertificateFile {exp_creds.cert_file}',
442
f' SSLCertificateKeyFile {exp_creds.pkey_file}',
443
f' DocumentRoot "{self._docs_dir}/expired"',
444
])
445
conf.extend(self._curltest_conf(exp_domain))
446
if exp_domain in self._extra_configs:
447
conf.extend(self._extra_configs[exp_domain])
448
conf.extend([
449
'</VirtualHost>',
450
'',
451
])
452
conf.extend([ # http forward proxy
453
f'<VirtualHost *:{self.ports["proxy"]}>',
454
f' ServerName {proxy_domain}',
455
' Protocols h2c http/1.1',
456
' ProxyRequests On',
457
' H2ProxyRequests On',
458
' ProxyVia On',
459
f' AllowCONNECT {self.ports["http"]} {self.ports["https"]}',
460
])
461
conf.extend(self._get_proxy_conf())
462
conf.extend([
463
'</VirtualHost>',
464
'',
465
])
466
conf.extend([ # https forward proxy
467
f'<VirtualHost *:{self.ports["proxys"]}>',
468
f' ServerName {proxy_domain}',
469
' Protocols h2 http/1.1',
470
' SSLEngine on',
471
f' SSLCertificateFile {proxy_creds.cert_file}',
472
f' SSLCertificateKeyFile {proxy_creds.pkey_file}',
473
' ProxyRequests On',
474
' H2ProxyRequests On',
475
' ProxyVia On',
476
f' AllowCONNECT {self.ports["http"]} {self.ports["https"]}',
477
])
478
conf.extend(self._get_proxy_conf())
479
conf.extend([
480
'</VirtualHost>',
481
'',
482
])
483
484
fd.write("\n".join(conf))
485
with open(os.path.join(self._conf_dir, 'mime.types'), 'w') as fd:
486
fd.write("\n".join([
487
'text/plain txt',
488
'text/html html',
489
'application/json json',
490
'application/x-gzip gzip',
491
'application/x-gzip gz',
492
''
493
]))
494
495
def _get_proxy_conf(self):
496
if self._proxy_auth_basic:
497
return [
498
' <Proxy "*">',
499
' AuthType Basic',
500
' AuthName "Restricted Proxy"',
501
' AuthBasicProvider file',
502
f' AuthUserFile "{self._basic_passwords}"',
503
' Require user proxy',
504
' </Proxy>',
505
]
506
else:
507
return [
508
' <Proxy "*">',
509
' Require ip 127.0.0.1',
510
' </Proxy>',
511
]
512
513
def _get_log_level(self):
514
if self.env.verbose > 3:
515
return 'trace2'
516
if self.env.verbose > 2:
517
return 'trace1'
518
if self.env.verbose > 1:
519
return 'debug'
520
return 'info'
521
522
def _curltest_conf(self, servername) -> List[str]:
523
lines = []
524
if Httpd.MOD_CURLTEST is not None:
525
lines.extend([
526
' Redirect 302 /data.json.302 /data.json',
527
' Redirect 301 /curltest/echo301 /curltest/echo',
528
' Redirect 302 /curltest/echo302 /curltest/echo',
529
' Redirect 303 /curltest/echo303 /curltest/echo',
530
' Redirect 307 /curltest/echo307 /curltest/echo',
531
' <Location /curltest/sslinfo>',
532
' SSLOptions StdEnvVars',
533
' SetHandler curltest-sslinfo',
534
' </Location>',
535
' <Location /curltest/echo>',
536
' SetHandler curltest-echo',
537
' </Location>',
538
' <Location /curltest/put>',
539
' SetHandler curltest-put',
540
' </Location>',
541
' <Location /curltest/tweak>',
542
' SetHandler curltest-tweak',
543
' </Location>',
544
' Redirect 302 /tweak /curltest/tweak',
545
' <Location /curltest/1_1>',
546
' SetHandler curltest-1_1-required',
547
' </Location>',
548
' <Location /curltest/shutdown_unclean>',
549
' SetHandler curltest-tweak',
550
' SetEnv force-response-1.0 1',
551
' </Location>',
552
' SetEnvIf Request_URI "/shutdown_unclean" ssl-unclean=1',
553
' RewriteEngine on',
554
' RewriteRule "^/curltest/put-redir-301$" "/curltest/put" [R=301]',
555
' RewriteRule "^/curltest/put-redir-302$" "/curltest/put" [R=302]',
556
' RewriteRule "^/curltest/put-redir-307$" "/curltest/put" [R=307]',
557
' RewriteRule "^/curltest/put-redir-308$" "/curltest/put" [R=308]',
558
])
559
if self._auth_digest:
560
lines.extend([
561
f' <Directory {self.docs_dir}/restricted/digest>',
562
' AuthType Digest',
563
' AuthName "restricted area"',
564
f' AuthDigestDomain "https://{servername}"',
565
' AuthBasicProvider file',
566
f' AuthUserFile "{self._digest_passwords}"',
567
' Require valid-user',
568
' </Directory>',
569
570
])
571
return lines
572
573
def _init_curltest(self):
574
if Httpd.MOD_CURLTEST is not None:
575
return
576
local_dir = os.path.dirname(inspect.getfile(Httpd))
577
out_dir = os.path.join(self.env.gen_dir, 'mod_curltest')
578
in_source = os.path.join(local_dir, 'mod_curltest/mod_curltest.c')
579
out_source = os.path.join(out_dir, 'mod_curltest.c')
580
if not os.path.exists(out_dir):
581
os.mkdir(out_dir)
582
if not os.path.exists(out_source) or \
583
os.stat(in_source).st_mtime > os.stat(out_source).st_mtime:
584
shutil.copy(in_source, out_source)
585
p = subprocess.run([
586
self.env.apxs, '-c', out_source
587
], capture_output=True, cwd=out_dir)
588
rv = p.returncode
589
if rv != 0:
590
log.error(f"compiling mod_curltest failed: {p.stderr}")
591
raise Exception(f"compiling mod_curltest failed: {p.stderr}")
592
Httpd.MOD_CURLTEST = os.path.join(out_dir, '.libs/mod_curltest.so')
593
594