env.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. #***************************************************************************
  4. # _ _ ____ _
  5. # Project ___| | | | _ \| |
  6. # / __| | | | |_) | |
  7. # | (__| |_| | _ <| |___
  8. # \___|\___/|_| \_\_____|
  9. #
  10. # Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
  11. #
  12. # This software is licensed as described in the file COPYING, which
  13. # you should have received as part of this distribution. The terms
  14. # are also available at https://curl.se/docs/copyright.html.
  15. #
  16. # You may opt to use, copy, modify, merge, publish, distribute and/or sell
  17. # copies of the Software, and permit persons to whom the Software is
  18. # furnished to do so, under the terms of the COPYING file.
  19. #
  20. # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
  21. # KIND, either express or implied.
  22. #
  23. # SPDX-License-Identifier: curl
  24. #
  25. ###########################################################################
  26. #
  27. import logging
  28. import os
  29. import re
  30. import shutil
  31. import socket
  32. import subprocess
  33. import tempfile
  34. from configparser import ConfigParser, ExtendedInterpolation
  35. from typing import Optional
  36. from .certs import CertificateSpec, Credentials, TestCA
  37. from .ports import alloc_ports
  38. log = logging.getLogger(__name__)
  39. def init_config_from(conf_path):
  40. if os.path.isfile(conf_path):
  41. config = ConfigParser(interpolation=ExtendedInterpolation())
  42. config.read(conf_path)
  43. return config
  44. return None
  45. TESTS_HTTPD_PATH = os.path.dirname(os.path.dirname(__file__))
  46. TOP_PATH = os.path.join(os.getcwd(), os.path.pardir)
  47. DEF_CONFIG = init_config_from(os.path.join(TOP_PATH, 'tests', 'http', 'config.ini'))
  48. CURL = os.path.join(TOP_PATH, 'src', 'curl')
  49. class EnvConfig:
  50. def __init__(self):
  51. self.tests_dir = TESTS_HTTPD_PATH
  52. self.gen_dir = os.path.join(self.tests_dir, 'gen')
  53. self.project_dir = os.path.dirname(os.path.dirname(self.tests_dir))
  54. self.build_dir = TOP_PATH
  55. self.config = DEF_CONFIG
  56. # check cur and its features
  57. self.curl = CURL
  58. if 'CURL' in os.environ:
  59. self.curl = os.environ['CURL']
  60. self.curl_props = {
  61. 'version_string': '',
  62. 'version': '',
  63. 'os': '',
  64. 'fullname': '',
  65. 'features_string': '',
  66. 'features': set(),
  67. 'protocols_string': '',
  68. 'protocols': set(),
  69. 'libs': set(),
  70. 'lib_versions': set(),
  71. }
  72. self.curl_is_debug = False
  73. self.curl_protos = []
  74. p = subprocess.run(args=[self.curl, '-V'],
  75. capture_output=True, text=True)
  76. if p.returncode != 0:
  77. raise RuntimeError(f'{self.curl} -V failed with exit code: {p.returncode}')
  78. if p.stderr.startswith('WARNING:'):
  79. self.curl_is_debug = True
  80. for line in p.stdout.splitlines(keepends=False):
  81. if line.startswith('curl '):
  82. self.curl_props['version_string'] = line
  83. m = re.match(r'^curl (?P<version>\S+) (?P<os>\S+) (?P<libs>.*)$', line)
  84. if m:
  85. self.curl_props['fullname'] = m.group(0)
  86. self.curl_props['version'] = m.group('version')
  87. self.curl_props['os'] = m.group('os')
  88. self.curl_props['lib_versions'] = {
  89. lib.lower() for lib in m.group('libs').split(' ')
  90. }
  91. self.curl_props['libs'] = {
  92. re.sub(r'/[a-z0-9.-]*', '', lib) for lib in self.curl_props['lib_versions']
  93. }
  94. if line.startswith('Features: '):
  95. self.curl_props['features_string'] = line[10:]
  96. self.curl_props['features'] = {
  97. feat.lower() for feat in line[10:].split(' ')
  98. }
  99. if line.startswith('Protocols: '):
  100. self.curl_props['protocols_string'] = line[11:]
  101. self.curl_props['protocols'] = {
  102. prot.lower() for prot in line[11:].split(' ')
  103. }
  104. self.ports = alloc_ports(port_specs={
  105. 'ftp': socket.SOCK_STREAM,
  106. 'ftps': socket.SOCK_STREAM,
  107. 'http': socket.SOCK_STREAM,
  108. 'https': socket.SOCK_STREAM,
  109. 'nghttpx_https': socket.SOCK_STREAM,
  110. 'proxy': socket.SOCK_STREAM,
  111. 'proxys': socket.SOCK_STREAM,
  112. 'h2proxys': socket.SOCK_STREAM,
  113. 'caddy': socket.SOCK_STREAM,
  114. 'caddys': socket.SOCK_STREAM,
  115. 'ws': socket.SOCK_STREAM,
  116. })
  117. self.httpd = self.config['httpd']['httpd']
  118. self.apachectl = self.config['httpd']['apachectl']
  119. self.apxs = self.config['httpd']['apxs']
  120. if len(self.apxs) == 0:
  121. self.apxs = None
  122. self._httpd_version = None
  123. self.examples_pem = {
  124. 'key': 'xxx',
  125. 'cert': 'xxx',
  126. }
  127. self.htdocs_dir = os.path.join(self.gen_dir, 'htdocs')
  128. self.tld = 'http.curl.se'
  129. self.domain1 = f"one.{self.tld}"
  130. self.domain1brotli = f"brotli.one.{self.tld}"
  131. self.domain2 = f"two.{self.tld}"
  132. self.ftp_domain = f"ftp.{self.tld}"
  133. self.proxy_domain = f"proxy.{self.tld}"
  134. self.cert_specs = [
  135. CertificateSpec(domains=[self.domain1, self.domain1brotli, 'localhost', '127.0.0.1'], key_type='rsa2048'),
  136. CertificateSpec(domains=[self.domain2], key_type='rsa2048'),
  137. CertificateSpec(domains=[self.ftp_domain], key_type='rsa2048'),
  138. CertificateSpec(domains=[self.proxy_domain, '127.0.0.1'], key_type='rsa2048'),
  139. CertificateSpec(name="clientsX", sub_specs=[
  140. CertificateSpec(name="user1", client=True),
  141. ]),
  142. ]
  143. self.nghttpx = self.config['nghttpx']['nghttpx']
  144. if len(self.nghttpx.strip()) == 0:
  145. self.nghttpx = None
  146. self._nghttpx_version = None
  147. self.nghttpx_with_h3 = False
  148. if self.nghttpx is not None:
  149. p = subprocess.run(args=[self.nghttpx, '-v'],
  150. capture_output=True, text=True)
  151. if p.returncode != 0:
  152. # not a working nghttpx
  153. self.nghttpx = None
  154. else:
  155. self._nghttpx_version = re.sub(r'^nghttpx\s*', '', p.stdout.strip())
  156. self.nghttpx_with_h3 = re.match(r'.* nghttp3/.*', p.stdout.strip()) is not None
  157. log.debug(f'nghttpx -v: {p.stdout}')
  158. self.caddy = self.config['caddy']['caddy']
  159. self._caddy_version = None
  160. if len(self.caddy.strip()) == 0:
  161. self.caddy = None
  162. if self.caddy is not None:
  163. try:
  164. p = subprocess.run(args=[self.caddy, 'version'],
  165. capture_output=True, text=True)
  166. if p.returncode != 0:
  167. # not a working caddy
  168. self.caddy = None
  169. m = re.match(r'v?(\d+\.\d+\.\d+).*', p.stdout)
  170. if m:
  171. self._caddy_version = m.group(1)
  172. else:
  173. raise RuntimeError(f'Unable to determine cadd version from: {p.stdout}')
  174. # TODO: specify specific exceptions here
  175. except: # noqa: E722
  176. self.caddy = None
  177. self.vsftpd = self.config['vsftpd']['vsftpd']
  178. self._vsftpd_version = None
  179. if self.vsftpd is not None:
  180. try:
  181. with tempfile.TemporaryFile('w+') as tmp:
  182. p = subprocess.run(args=[self.vsftpd, '-v'],
  183. capture_output=True, text=True, stdin=tmp)
  184. if p.returncode != 0:
  185. # not a working vsftpd
  186. self.vsftpd = None
  187. if p.stderr:
  188. ver_text = p.stderr
  189. else:
  190. # Oddly, some versions of vsftpd write to stdin (!)
  191. # instead of stderr, which is odd but works. If there
  192. # is nothing on stderr, read the file on stdin and use
  193. # any data there instead.
  194. tmp.seek(0)
  195. ver_text = tmp.read()
  196. m = re.match(r'vsftpd: version (\d+\.\d+\.\d+)', ver_text)
  197. if m:
  198. self._vsftpd_version = m.group(1)
  199. elif len(p.stderr) == 0:
  200. # vsftp does not use stdout or stderr for printing its version... -.-
  201. self._vsftpd_version = 'unknown'
  202. else:
  203. raise Exception(f'Unable to determine VsFTPD version from: {p.stderr}')
  204. except Exception:
  205. self.vsftpd = None
  206. self._tcpdump = shutil.which('tcpdump')
  207. @property
  208. def httpd_version(self):
  209. if self._httpd_version is None and self.apxs is not None:
  210. try:
  211. p = subprocess.run(args=[self.apxs, '-q', 'HTTPD_VERSION'],
  212. capture_output=True, text=True)
  213. if p.returncode != 0:
  214. log.error(f'{self.apxs} failed to query HTTPD_VERSION: {p}')
  215. else:
  216. self._httpd_version = p.stdout.strip()
  217. except Exception:
  218. log.exception(f'{self.apxs} failed to run')
  219. return self._httpd_version
  220. def versiontuple(self, v):
  221. v = re.sub(r'(\d+\.\d+(\.\d+)?)(-\S+)?', r'\1', v)
  222. return tuple(map(int, v.split('.')))
  223. def httpd_is_at_least(self, minv):
  224. if self.httpd_version is None:
  225. return False
  226. hv = self.versiontuple(self.httpd_version)
  227. return hv >= self.versiontuple(minv)
  228. def caddy_is_at_least(self, minv):
  229. if self.caddy_version is None:
  230. return False
  231. hv = self.versiontuple(self.caddy_version)
  232. return hv >= self.versiontuple(minv)
  233. def is_complete(self) -> bool:
  234. return os.path.isfile(self.httpd) and \
  235. os.path.isfile(self.apachectl) and \
  236. self.apxs is not None and \
  237. os.path.isfile(self.apxs)
  238. def get_incomplete_reason(self) -> Optional[str]:
  239. if self.httpd is None or len(self.httpd.strip()) == 0:
  240. return 'httpd not configured, see `--with-test-httpd=<path>`'
  241. if not os.path.isfile(self.httpd):
  242. return f'httpd ({self.httpd}) not found'
  243. if not os.path.isfile(self.apachectl):
  244. return f'apachectl ({self.apachectl}) not found'
  245. if self.apxs is None:
  246. return "command apxs not found (commonly provided in apache2-dev)"
  247. if not os.path.isfile(self.apxs):
  248. return f"apxs ({self.apxs}) not found"
  249. return None
  250. @property
  251. def nghttpx_version(self):
  252. return self._nghttpx_version
  253. @property
  254. def caddy_version(self):
  255. return self._caddy_version
  256. @property
  257. def vsftpd_version(self):
  258. return self._vsftpd_version
  259. @property
  260. def tcpdmp(self) -> Optional[str]:
  261. return self._tcpdump
  262. class Env:
  263. CONFIG = EnvConfig()
  264. @staticmethod
  265. def setup_incomplete() -> bool:
  266. return not Env.CONFIG.is_complete()
  267. @staticmethod
  268. def incomplete_reason() -> Optional[str]:
  269. return Env.CONFIG.get_incomplete_reason()
  270. @staticmethod
  271. def have_nghttpx() -> bool:
  272. return Env.CONFIG.nghttpx is not None
  273. @staticmethod
  274. def have_h3_server() -> bool:
  275. return Env.CONFIG.nghttpx_with_h3
  276. @staticmethod
  277. def have_ssl_curl() -> bool:
  278. return Env.curl_has_feature('ssl') or Env.curl_has_feature('multissl')
  279. @staticmethod
  280. def have_h2_curl() -> bool:
  281. return 'http2' in Env.CONFIG.curl_props['features']
  282. @staticmethod
  283. def have_h3_curl() -> bool:
  284. return 'http3' in Env.CONFIG.curl_props['features']
  285. @staticmethod
  286. def curl_uses_lib(libname: str) -> bool:
  287. return libname.lower() in Env.CONFIG.curl_props['libs']
  288. @staticmethod
  289. def curl_uses_ossl_quic() -> bool:
  290. if Env.have_h3_curl():
  291. return not Env.curl_uses_lib('ngtcp2') and Env.curl_uses_lib('nghttp3')
  292. return False
  293. @staticmethod
  294. def curl_version_string() -> str:
  295. return Env.CONFIG.curl_props['version_string']
  296. @staticmethod
  297. def curl_features_string() -> str:
  298. return Env.CONFIG.curl_props['features_string']
  299. @staticmethod
  300. def curl_has_feature(feature: str) -> bool:
  301. return feature.lower() in Env.CONFIG.curl_props['features']
  302. @staticmethod
  303. def curl_protocols_string() -> str:
  304. return Env.CONFIG.curl_props['protocols_string']
  305. @staticmethod
  306. def curl_has_protocol(protocol: str) -> bool:
  307. return protocol.lower() in Env.CONFIG.curl_props['protocols']
  308. @staticmethod
  309. def curl_lib_version(libname: str) -> str:
  310. prefix = f'{libname.lower()}/'
  311. for lversion in Env.CONFIG.curl_props['lib_versions']:
  312. if lversion.startswith(prefix):
  313. return lversion[len(prefix):]
  314. return 'unknown'
  315. @staticmethod
  316. def curl_lib_version_at_least(libname: str, min_version) -> bool:
  317. lversion = Env.curl_lib_version(libname)
  318. if lversion != 'unknown':
  319. return Env.CONFIG.versiontuple(min_version) <= \
  320. Env.CONFIG.versiontuple(lversion)
  321. return False
  322. @staticmethod
  323. def curl_os() -> str:
  324. return Env.CONFIG.curl_props['os']
  325. @staticmethod
  326. def curl_fullname() -> str:
  327. return Env.CONFIG.curl_props['fullname']
  328. @staticmethod
  329. def curl_version() -> str:
  330. return Env.CONFIG.curl_props['version']
  331. @staticmethod
  332. def curl_is_debug() -> bool:
  333. return Env.CONFIG.curl_is_debug
  334. @staticmethod
  335. def have_h3() -> bool:
  336. return Env.have_h3_curl() and Env.have_h3_server()
  337. @staticmethod
  338. def httpd_version() -> str:
  339. return Env.CONFIG.httpd_version
  340. @staticmethod
  341. def nghttpx_version() -> str:
  342. return Env.CONFIG.nghttpx_version
  343. @staticmethod
  344. def caddy_version() -> str:
  345. return Env.CONFIG.caddy_version
  346. @staticmethod
  347. def caddy_is_at_least(minv) -> bool:
  348. return Env.CONFIG.caddy_is_at_least(minv)
  349. @staticmethod
  350. def httpd_is_at_least(minv) -> bool:
  351. return Env.CONFIG.httpd_is_at_least(minv)
  352. @staticmethod
  353. def has_caddy() -> bool:
  354. return Env.CONFIG.caddy is not None
  355. @staticmethod
  356. def has_vsftpd() -> bool:
  357. return Env.CONFIG.vsftpd is not None
  358. @staticmethod
  359. def vsftpd_version() -> str:
  360. return Env.CONFIG.vsftpd_version
  361. @staticmethod
  362. def tcpdump() -> Optional[str]:
  363. return Env.CONFIG.tcpdmp
  364. def __init__(self, pytestconfig=None):
  365. self._verbose = pytestconfig.option.verbose \
  366. if pytestconfig is not None else 0
  367. self._ca = None
  368. self._test_timeout = 300.0 if self._verbose > 1 else 60.0 # seconds
  369. def issue_certs(self):
  370. if self._ca is None:
  371. ca_dir = os.path.join(self.CONFIG.gen_dir, 'ca')
  372. self._ca = TestCA.create_root(name=self.CONFIG.tld,
  373. store_dir=ca_dir,
  374. key_type="rsa2048")
  375. self._ca.issue_certs(self.CONFIG.cert_specs)
  376. def setup(self):
  377. os.makedirs(self.gen_dir, exist_ok=True)
  378. os.makedirs(self.htdocs_dir, exist_ok=True)
  379. self.issue_certs()
  380. def get_credentials(self, domain) -> Optional[Credentials]:
  381. creds = self.ca.get_credentials_for_name(domain)
  382. if len(creds) > 0:
  383. return creds[0]
  384. return None
  385. @property
  386. def verbose(self) -> int:
  387. return self._verbose
  388. @property
  389. def test_timeout(self) -> Optional[float]:
  390. return self._test_timeout
  391. @test_timeout.setter
  392. def test_timeout(self, val: Optional[float]):
  393. self._test_timeout = val
  394. @property
  395. def gen_dir(self) -> str:
  396. return self.CONFIG.gen_dir
  397. @property
  398. def project_dir(self) -> str:
  399. return self.CONFIG.project_dir
  400. @property
  401. def build_dir(self) -> str:
  402. return self.CONFIG.build_dir
  403. @property
  404. def ca(self):
  405. return self._ca
  406. @property
  407. def htdocs_dir(self) -> str:
  408. return self.CONFIG.htdocs_dir
  409. @property
  410. def tld(self) -> str:
  411. return self.CONFIG.tld
  412. @property
  413. def domain1(self) -> str:
  414. return self.CONFIG.domain1
  415. @property
  416. def domain1brotli(self) -> str:
  417. return self.CONFIG.domain1brotli
  418. @property
  419. def domain2(self) -> str:
  420. return self.CONFIG.domain2
  421. @property
  422. def ftp_domain(self) -> str:
  423. return self.CONFIG.ftp_domain
  424. @property
  425. def proxy_domain(self) -> str:
  426. return self.CONFIG.proxy_domain
  427. @property
  428. def http_port(self) -> int:
  429. return self.CONFIG.ports['http']
  430. @property
  431. def https_port(self) -> int:
  432. return self.CONFIG.ports['https']
  433. @property
  434. def nghttpx_https_port(self) -> int:
  435. return self.CONFIG.ports['nghttpx_https']
  436. @property
  437. def h3_port(self) -> int:
  438. return self.https_port
  439. @property
  440. def proxy_port(self) -> int:
  441. return self.CONFIG.ports['proxy']
  442. @property
  443. def proxys_port(self) -> int:
  444. return self.CONFIG.ports['proxys']
  445. @property
  446. def ftp_port(self) -> int:
  447. return self.CONFIG.ports['ftp']
  448. @property
  449. def ftps_port(self) -> int:
  450. return self.CONFIG.ports['ftps']
  451. @property
  452. def h2proxys_port(self) -> int:
  453. return self.CONFIG.ports['h2proxys']
  454. def pts_port(self, proto: str = 'http/1.1') -> int:
  455. # proxy tunnel port
  456. return self.CONFIG.ports['h2proxys' if proto == 'h2' else 'proxys']
  457. @property
  458. def caddy(self) -> str:
  459. return self.CONFIG.caddy
  460. @property
  461. def caddy_https_port(self) -> int:
  462. return self.CONFIG.ports['caddys']
  463. @property
  464. def caddy_http_port(self) -> int:
  465. return self.CONFIG.ports['caddy']
  466. @property
  467. def vsftpd(self) -> str:
  468. return self.CONFIG.vsftpd
  469. @property
  470. def ws_port(self) -> int:
  471. return self.CONFIG.ports['ws']
  472. @property
  473. def curl(self) -> str:
  474. return self.CONFIG.curl
  475. @property
  476. def httpd(self) -> str:
  477. return self.CONFIG.httpd
  478. @property
  479. def apachectl(self) -> str:
  480. return self.CONFIG.apachectl
  481. @property
  482. def apxs(self) -> str:
  483. return self.CONFIG.apxs
  484. @property
  485. def nghttpx(self) -> Optional[str]:
  486. return self.CONFIG.nghttpx
  487. @property
  488. def slow_network(self) -> bool:
  489. return "CURL_DBG_SOCK_WBLOCK" in os.environ or \
  490. "CURL_DBG_SOCK_WPARTIAL" in os.environ
  491. @property
  492. def ci_run(self) -> bool:
  493. return "CURL_CI" in os.environ
  494. def port_for(self, alpn_proto: Optional[str] = None):
  495. if alpn_proto is None or \
  496. alpn_proto in ['h2', 'http/1.1', 'http/1.0', 'http/0.9']:
  497. return self.https_port
  498. if alpn_proto in ['h3']:
  499. return self.h3_port
  500. return self.http_port
  501. def authority_for(self, domain: str, alpn_proto: Optional[str] = None):
  502. return f'{domain}:{self.port_for(alpn_proto=alpn_proto)}'
  503. def make_data_file(self, indir: str, fname: str, fsize: int,
  504. line_length: int = 1024) -> str:
  505. if line_length < 11:
  506. raise RuntimeError('line_length less than 11 not supported')
  507. fpath = os.path.join(indir, fname)
  508. s10 = "0123456789"
  509. s = round((line_length / 10) + 1) * s10
  510. s = s[0:line_length-11]
  511. with open(fpath, 'w') as fd:
  512. for i in range(int(fsize / line_length)):
  513. fd.write(f"{i:09d}-{s}\n")
  514. remain = int(fsize % line_length)
  515. if remain != 0:
  516. i = int(fsize / line_length) + 1
  517. fd.write(f"{i:09d}-{s}"[0:remain-1] + "\n")
  518. return fpath