env.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  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 socket
  31. import subprocess
  32. import sys
  33. from configparser import ConfigParser, ExtendedInterpolation
  34. from typing import Optional
  35. import pytest
  36. from .certs import CertificateSpec, TestCA, Credentials
  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. DEF_CONFIG = init_config_from(os.path.join(TESTS_HTTPD_PATH, 'config.ini'))
  47. TOP_PATH = os.path.dirname(os.path.dirname(TESTS_HTTPD_PATH))
  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.config = DEF_CONFIG
  55. # check cur and its features
  56. self.curl = CURL
  57. if 'CURL' in os.environ:
  58. self.curl = os.environ['CURL']
  59. self.curl_props = {
  60. 'version': None,
  61. 'os': None,
  62. 'fullname': None,
  63. 'features': [],
  64. 'protocols': [],
  65. 'libs': [],
  66. 'lib_versions': [],
  67. }
  68. self.curl_protos = []
  69. p = subprocess.run(args=[self.curl, '-V'],
  70. capture_output=True, text=True)
  71. if p.returncode != 0:
  72. assert False, f'{self.curl} -V failed with exit code: {p.returncode}'
  73. for l in p.stdout.splitlines(keepends=False):
  74. if l.startswith('curl '):
  75. m = re.match(r'^curl (?P<version>\S+) (?P<os>\S+) (?P<libs>.*)$', l)
  76. if m:
  77. self.curl_props['fullname'] = m.group(0)
  78. self.curl_props['version'] = m.group('version')
  79. self.curl_props['os'] = m.group('os')
  80. self.curl_props['lib_versions'] = [
  81. lib.lower() for lib in m.group('libs').split(' ')
  82. ]
  83. self.curl_props['libs'] = [
  84. re.sub(r'/.*', '', lib) for lib in self.curl_props['lib_versions']
  85. ]
  86. if l.startswith('Features: '):
  87. self.curl_props['features'] = [
  88. feat.lower() for feat in l[10:].split(' ')
  89. ]
  90. if l.startswith('Protocols: '):
  91. self.curl_props['protocols'] = [
  92. prot.lower() for prot in l[11:].split(' ')
  93. ]
  94. self.ports = alloc_ports(port_specs={
  95. 'http': socket.SOCK_STREAM,
  96. 'https': socket.SOCK_STREAM,
  97. 'proxy': socket.SOCK_STREAM,
  98. 'proxys': socket.SOCK_STREAM,
  99. 'h2proxys': socket.SOCK_STREAM,
  100. 'caddy': socket.SOCK_STREAM,
  101. 'caddys': socket.SOCK_STREAM,
  102. 'ws': socket.SOCK_STREAM,
  103. })
  104. self.httpd = self.config['httpd']['httpd']
  105. self.apachectl = self.config['httpd']['apachectl']
  106. self.apxs = self.config['httpd']['apxs']
  107. if len(self.apxs) == 0:
  108. self.apxs = None
  109. self._httpd_version = None
  110. self.examples_pem = {
  111. 'key': 'xxx',
  112. 'cert': 'xxx',
  113. }
  114. self.htdocs_dir = os.path.join(self.gen_dir, 'htdocs')
  115. self.tld = 'http.curl.se'
  116. self.domain1 = f"one.{self.tld}"
  117. self.domain2 = f"two.{self.tld}"
  118. self.proxy_domain = f"proxy.{self.tld}"
  119. self.cert_specs = [
  120. CertificateSpec(domains=[self.domain1, 'localhost'], key_type='rsa2048'),
  121. CertificateSpec(domains=[self.domain2], key_type='rsa2048'),
  122. CertificateSpec(domains=[self.proxy_domain], key_type='rsa2048'),
  123. CertificateSpec(name="clientsX", sub_specs=[
  124. CertificateSpec(name="user1", client=True),
  125. ]),
  126. ]
  127. self.nghttpx = self.config['nghttpx']['nghttpx']
  128. if len(self.nghttpx.strip()) == 0:
  129. self.nghttpx = None
  130. self._nghttpx_version = None
  131. self.nghttpx_with_h3 = False
  132. if self.nghttpx is not None:
  133. p = subprocess.run(args=[self.nghttpx, '-v'],
  134. capture_output=True, text=True)
  135. if p.returncode != 0:
  136. # not a working nghttpx
  137. self.nghttpx = None
  138. else:
  139. self._nghttpx_version = re.sub(r'^nghttpx\s*', '', p.stdout.strip())
  140. self.nghttpx_with_h3 = re.match(r'.* nghttp3/.*', p.stdout.strip()) is not None
  141. log.debug(f'nghttpx -v: {p.stdout}')
  142. self.caddy = self.config['caddy']['caddy']
  143. self._caddy_version = None
  144. if len(self.caddy.strip()) == 0:
  145. self.caddy = None
  146. if self.caddy is not None:
  147. try:
  148. p = subprocess.run(args=[self.caddy, 'version'],
  149. capture_output=True, text=True)
  150. if p.returncode != 0:
  151. # not a working caddy
  152. self.caddy = None
  153. self._caddy_version = re.sub(r' .*', '', p.stdout.strip())
  154. except:
  155. self.caddy = None
  156. @property
  157. def httpd_version(self):
  158. if self._httpd_version is None and self.apxs is not None:
  159. try:
  160. p = subprocess.run(args=[self.apxs, '-q', 'HTTPD_VERSION'],
  161. capture_output=True, text=True)
  162. if p.returncode != 0:
  163. log.error(f'{self.apxs} failed to query HTTPD_VERSION: {p}')
  164. else:
  165. self._httpd_version = p.stdout.strip()
  166. except Exception as e:
  167. log.error(f'{self.apxs} failed to run: {e}')
  168. return self._httpd_version
  169. def _versiontuple(self, v):
  170. v = re.sub(r'(\d+\.\d+(\.\d+)?)(-\S+)?', r'\1', v)
  171. return tuple(map(int, v.split('.')))
  172. def httpd_is_at_least(self, minv):
  173. if self.httpd_version is None:
  174. return False
  175. hv = self._versiontuple(self.httpd_version)
  176. return hv >= self._versiontuple(minv)
  177. def is_complete(self) -> bool:
  178. return os.path.isfile(self.httpd) and \
  179. os.path.isfile(self.apachectl) and \
  180. self.apxs is not None and \
  181. os.path.isfile(self.apxs)
  182. def get_incomplete_reason(self) -> Optional[str]:
  183. if self.httpd is None or len(self.httpd.strip()) == 0:
  184. return f'httpd not configured, see `--with-test-httpd=<path>`'
  185. if not os.path.isfile(self.httpd):
  186. return f'httpd ({self.httpd}) not found'
  187. if not os.path.isfile(self.apachectl):
  188. return f'apachectl ({self.apachectl}) not found'
  189. if self.apxs is None:
  190. return f"command apxs not found (commonly provided in apache2-dev)"
  191. if not os.path.isfile(self.apxs):
  192. return f"apxs ({self.apxs}) not found"
  193. return None
  194. @property
  195. def nghttpx_version(self):
  196. return self._nghttpx_version
  197. @property
  198. def caddy_version(self):
  199. return self._caddy_version
  200. class Env:
  201. CONFIG = EnvConfig()
  202. @staticmethod
  203. def setup_incomplete() -> bool:
  204. return not Env.CONFIG.is_complete()
  205. @staticmethod
  206. def incomplete_reason() -> Optional[str]:
  207. return Env.CONFIG.get_incomplete_reason()
  208. @staticmethod
  209. def have_nghttpx() -> bool:
  210. return Env.CONFIG.nghttpx is not None
  211. @staticmethod
  212. def have_h3_server() -> bool:
  213. return Env.CONFIG.nghttpx_with_h3
  214. @staticmethod
  215. def have_ssl_curl() -> bool:
  216. return 'ssl' in Env.CONFIG.curl_props['features']
  217. @staticmethod
  218. def have_h2_curl() -> bool:
  219. return 'http2' in Env.CONFIG.curl_props['features']
  220. @staticmethod
  221. def have_h3_curl() -> bool:
  222. return 'http3' in Env.CONFIG.curl_props['features']
  223. @staticmethod
  224. def curl_uses_lib(libname: str) -> bool:
  225. return libname.lower() in Env.CONFIG.curl_props['libs']
  226. @staticmethod
  227. def curl_has_feature(feature: str) -> bool:
  228. return feature.lower() in Env.CONFIG.curl_props['features']
  229. @staticmethod
  230. def curl_has_protocol(protocol: str) -> bool:
  231. return protocol.lower() in Env.CONFIG.curl_props['protocols']
  232. @staticmethod
  233. def curl_lib_version(libname: str) -> str:
  234. prefix = f'{libname.lower()}/'
  235. for lversion in Env.CONFIG.curl_props['lib_versions']:
  236. if lversion.startswith(prefix):
  237. return lversion[len(prefix):]
  238. return 'unknown'
  239. @staticmethod
  240. def curl_os() -> str:
  241. return Env.CONFIG.curl_props['os']
  242. @staticmethod
  243. def curl_fullname() -> str:
  244. return Env.CONFIG.curl_props['fullname']
  245. @staticmethod
  246. def curl_version() -> str:
  247. return Env.CONFIG.curl_props['version']
  248. @staticmethod
  249. def have_h3() -> bool:
  250. return Env.have_h3_curl() and Env.have_h3_server()
  251. @staticmethod
  252. def httpd_version() -> str:
  253. return Env.CONFIG.httpd_version
  254. @staticmethod
  255. def nghttpx_version() -> str:
  256. return Env.CONFIG.nghttpx_version
  257. @staticmethod
  258. def caddy_version() -> str:
  259. return Env.CONFIG.caddy_version
  260. @staticmethod
  261. def httpd_is_at_least(minv) -> bool:
  262. return Env.CONFIG.httpd_is_at_least(minv)
  263. @staticmethod
  264. def has_caddy() -> bool:
  265. return Env.CONFIG.caddy is not None
  266. def __init__(self, pytestconfig=None):
  267. self._verbose = pytestconfig.option.verbose \
  268. if pytestconfig is not None else 0
  269. self._ca = None
  270. self._test_timeout = 300.0 if self._verbose > 1 else 60.0 # seconds
  271. def issue_certs(self):
  272. if self._ca is None:
  273. ca_dir = os.path.join(self.CONFIG.gen_dir, 'ca')
  274. self._ca = TestCA.create_root(name=self.CONFIG.tld,
  275. store_dir=ca_dir,
  276. key_type="rsa2048")
  277. self._ca.issue_certs(self.CONFIG.cert_specs)
  278. def setup(self):
  279. os.makedirs(self.gen_dir, exist_ok=True)
  280. os.makedirs(self.htdocs_dir, exist_ok=True)
  281. self.issue_certs()
  282. def get_credentials(self, domain) -> Optional[Credentials]:
  283. creds = self.ca.get_credentials_for_name(domain)
  284. if len(creds) > 0:
  285. return creds[0]
  286. return None
  287. @property
  288. def verbose(self) -> int:
  289. return self._verbose
  290. @property
  291. def test_timeout(self) -> Optional[float]:
  292. return self._test_timeout
  293. @test_timeout.setter
  294. def test_timeout(self, val: Optional[float]):
  295. self._test_timeout = val
  296. @property
  297. def gen_dir(self) -> str:
  298. return self.CONFIG.gen_dir
  299. @property
  300. def project_dir(self) -> str:
  301. return self.CONFIG.project_dir
  302. @property
  303. def ca(self):
  304. return self._ca
  305. @property
  306. def htdocs_dir(self) -> str:
  307. return self.CONFIG.htdocs_dir
  308. @property
  309. def domain1(self) -> str:
  310. return self.CONFIG.domain1
  311. @property
  312. def domain2(self) -> str:
  313. return self.CONFIG.domain2
  314. @property
  315. def proxy_domain(self) -> str:
  316. return self.CONFIG.proxy_domain
  317. @property
  318. def http_port(self) -> int:
  319. return self.CONFIG.ports['http']
  320. @property
  321. def https_port(self) -> int:
  322. return self.CONFIG.ports['https']
  323. @property
  324. def h3_port(self) -> int:
  325. return self.https_port
  326. @property
  327. def proxy_port(self) -> int:
  328. return self.CONFIG.ports['proxy']
  329. @property
  330. def proxys_port(self) -> int:
  331. return self.CONFIG.ports['proxys']
  332. @property
  333. def h2proxys_port(self) -> int:
  334. return self.CONFIG.ports['h2proxys']
  335. def pts_port(self, proto: str = 'http/1.1') -> int:
  336. # proxy tunnel port
  337. return self.CONFIG.ports['h2proxys' if proto == 'h2' else 'proxys']
  338. @property
  339. def caddy(self) -> str:
  340. return self.CONFIG.caddy
  341. @property
  342. def caddy_https_port(self) -> int:
  343. return self.CONFIG.ports['caddys']
  344. @property
  345. def caddy_http_port(self) -> int:
  346. return self.CONFIG.ports['caddy']
  347. @property
  348. def ws_port(self) -> int:
  349. return self.CONFIG.ports['ws']
  350. @property
  351. def curl(self) -> str:
  352. return self.CONFIG.curl
  353. @property
  354. def httpd(self) -> str:
  355. return self.CONFIG.httpd
  356. @property
  357. def apachectl(self) -> str:
  358. return self.CONFIG.apachectl
  359. @property
  360. def apxs(self) -> str:
  361. return self.CONFIG.apxs
  362. @property
  363. def nghttpx(self) -> Optional[str]:
  364. return self.CONFIG.nghttpx
  365. def authority_for(self, domain: str, alpn_proto: Optional[str] = None):
  366. if alpn_proto is None or \
  367. alpn_proto in ['h2', 'http/1.1', 'http/1.0', 'http/0.9']:
  368. return f'{domain}:{self.https_port}'
  369. if alpn_proto in ['h3']:
  370. return f'{domain}:{self.h3_port}'
  371. return f'{domain}:{self.http_port}'
  372. def make_data_file(self, indir: str, fname: str, fsize: int) -> str:
  373. fpath = os.path.join(indir, fname)
  374. s10 = "0123456789"
  375. s = (101 * s10) + s10[0:3]
  376. with open(fpath, 'w') as fd:
  377. for i in range(int(fsize / 1024)):
  378. fd.write(f"{i:09d}-{s}\n")
  379. remain = int(fsize % 1024)
  380. if remain != 0:
  381. i = int(fsize / 1024) + 1
  382. s = f"{i:09d}-{s}\n"
  383. fd.write(s[0:remain])
  384. return fpath
  385. def make_clients(self):
  386. client_dir = os.path.join(self.project_dir, 'tests/http/clients')
  387. p = subprocess.run(['make'], capture_output=True, text=True,
  388. cwd=client_dir)
  389. if p.returncode != 0:
  390. pytest.exit(f"`make`in {client_dir} failed:\n{p.stderr}")
  391. return False
  392. return True