test_proxyagent.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. # Copyright 2019 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import base64
  15. import logging
  16. import os
  17. from typing import Iterable, Optional
  18. from unittest.mock import patch
  19. import treq
  20. from netaddr import IPSet
  21. from parameterized import parameterized
  22. from twisted.internet import interfaces # noqa: F401
  23. from twisted.internet.endpoints import HostnameEndpoint, _WrapperEndpoint
  24. from twisted.internet.interfaces import IProtocol, IProtocolFactory
  25. from twisted.internet.protocol import Factory
  26. from twisted.protocols.tls import TLSMemoryBIOFactory, TLSMemoryBIOProtocol
  27. from twisted.web.http import HTTPChannel
  28. from synapse.http.client import BlacklistingReactorWrapper
  29. from synapse.http.connectproxyclient import ProxyCredentials
  30. from synapse.http.proxyagent import ProxyAgent, parse_proxy
  31. from tests.http import TestServerTLSConnectionFactory, get_test_https_policy
  32. from tests.server import FakeTransport, ThreadedMemoryReactorClock
  33. from tests.unittest import TestCase
  34. logger = logging.getLogger(__name__)
  35. HTTPFactory = Factory.forProtocol(HTTPChannel)
  36. class ProxyParserTests(TestCase):
  37. """
  38. Values for test
  39. [
  40. proxy_string,
  41. expected_scheme,
  42. expected_hostname,
  43. expected_port,
  44. expected_credentials,
  45. ]
  46. """
  47. @parameterized.expand(
  48. [
  49. # host
  50. [b"localhost", b"http", b"localhost", 1080, None],
  51. [b"localhost:9988", b"http", b"localhost", 9988, None],
  52. # host+scheme
  53. [b"https://localhost", b"https", b"localhost", 1080, None],
  54. [b"https://localhost:1234", b"https", b"localhost", 1234, None],
  55. # ipv4
  56. [b"1.2.3.4", b"http", b"1.2.3.4", 1080, None],
  57. [b"1.2.3.4:9988", b"http", b"1.2.3.4", 9988, None],
  58. # ipv4+scheme
  59. [b"https://1.2.3.4", b"https", b"1.2.3.4", 1080, None],
  60. [b"https://1.2.3.4:9988", b"https", b"1.2.3.4", 9988, None],
  61. # ipv6 - without brackets is broken
  62. # [
  63. # b"2001:0db8:85a3:0000:0000:8a2e:0370:effe",
  64. # b"http",
  65. # b"2001:0db8:85a3:0000:0000:8a2e:0370:effe",
  66. # 1080,
  67. # None,
  68. # ],
  69. # [
  70. # b"2001:0db8:85a3:0000:0000:8a2e:0370:1234",
  71. # b"http",
  72. # b"2001:0db8:85a3:0000:0000:8a2e:0370:1234",
  73. # 1080,
  74. # None,
  75. # ],
  76. # [b"::1", b"http", b"::1", 1080, None],
  77. # [b"::ffff:0.0.0.0", b"http", b"::ffff:0.0.0.0", 1080, None],
  78. # ipv6 - with brackets
  79. [
  80. b"[2001:0db8:85a3:0000:0000:8a2e:0370:effe]",
  81. b"http",
  82. b"2001:0db8:85a3:0000:0000:8a2e:0370:effe",
  83. 1080,
  84. None,
  85. ],
  86. [
  87. b"[2001:0db8:85a3:0000:0000:8a2e:0370:1234]",
  88. b"http",
  89. b"2001:0db8:85a3:0000:0000:8a2e:0370:1234",
  90. 1080,
  91. None,
  92. ],
  93. [b"[::1]", b"http", b"::1", 1080, None],
  94. [b"[::ffff:0.0.0.0]", b"http", b"::ffff:0.0.0.0", 1080, None],
  95. # ipv6+port
  96. [
  97. b"[2001:0db8:85a3:0000:0000:8a2e:0370:effe]:9988",
  98. b"http",
  99. b"2001:0db8:85a3:0000:0000:8a2e:0370:effe",
  100. 9988,
  101. None,
  102. ],
  103. [
  104. b"[2001:0db8:85a3:0000:0000:8a2e:0370:1234]:9988",
  105. b"http",
  106. b"2001:0db8:85a3:0000:0000:8a2e:0370:1234",
  107. 9988,
  108. None,
  109. ],
  110. [b"[::1]:9988", b"http", b"::1", 9988, None],
  111. [b"[::ffff:0.0.0.0]:9988", b"http", b"::ffff:0.0.0.0", 9988, None],
  112. # ipv6+scheme
  113. [
  114. b"https://[2001:0db8:85a3:0000:0000:8a2e:0370:effe]",
  115. b"https",
  116. b"2001:0db8:85a3:0000:0000:8a2e:0370:effe",
  117. 1080,
  118. None,
  119. ],
  120. [
  121. b"https://[2001:0db8:85a3:0000:0000:8a2e:0370:1234]",
  122. b"https",
  123. b"2001:0db8:85a3:0000:0000:8a2e:0370:1234",
  124. 1080,
  125. None,
  126. ],
  127. [b"https://[::1]", b"https", b"::1", 1080, None],
  128. [b"https://[::ffff:0.0.0.0]", b"https", b"::ffff:0.0.0.0", 1080, None],
  129. # ipv6+scheme+port
  130. [
  131. b"https://[2001:0db8:85a3:0000:0000:8a2e:0370:effe]:9988",
  132. b"https",
  133. b"2001:0db8:85a3:0000:0000:8a2e:0370:effe",
  134. 9988,
  135. None,
  136. ],
  137. [
  138. b"https://[2001:0db8:85a3:0000:0000:8a2e:0370:1234]:9988",
  139. b"https",
  140. b"2001:0db8:85a3:0000:0000:8a2e:0370:1234",
  141. 9988,
  142. None,
  143. ],
  144. [b"https://[::1]:9988", b"https", b"::1", 9988, None],
  145. # with credentials
  146. [
  147. b"https://user:pass@1.2.3.4:9988",
  148. b"https",
  149. b"1.2.3.4",
  150. 9988,
  151. b"user:pass",
  152. ],
  153. [b"user:pass@1.2.3.4:9988", b"http", b"1.2.3.4", 9988, b"user:pass"],
  154. [
  155. b"https://user:pass@proxy.local:9988",
  156. b"https",
  157. b"proxy.local",
  158. 9988,
  159. b"user:pass",
  160. ],
  161. [
  162. b"user:pass@proxy.local:9988",
  163. b"http",
  164. b"proxy.local",
  165. 9988,
  166. b"user:pass",
  167. ],
  168. ]
  169. )
  170. def test_parse_proxy(
  171. self,
  172. proxy_string: bytes,
  173. expected_scheme: bytes,
  174. expected_hostname: bytes,
  175. expected_port: int,
  176. expected_credentials: Optional[bytes],
  177. ):
  178. """
  179. Tests that a given proxy URL will be broken into the components.
  180. Args:
  181. proxy_string: The proxy connection string.
  182. expected_scheme: Expected value of proxy scheme.
  183. expected_hostname: Expected value of proxy hostname.
  184. expected_port: Expected value of proxy port.
  185. expected_credentials: Expected value of credentials.
  186. Must be in form '<username>:<password>' or None
  187. """
  188. proxy_cred = None
  189. if expected_credentials:
  190. proxy_cred = ProxyCredentials(expected_credentials)
  191. self.assertEqual(
  192. (
  193. expected_scheme,
  194. expected_hostname,
  195. expected_port,
  196. proxy_cred,
  197. ),
  198. parse_proxy(proxy_string),
  199. )
  200. class MatrixFederationAgentTests(TestCase):
  201. def setUp(self):
  202. self.reactor = ThreadedMemoryReactorClock()
  203. def _make_connection(
  204. self,
  205. client_factory: IProtocolFactory,
  206. server_factory: IProtocolFactory,
  207. ssl: bool = False,
  208. expected_sni: Optional[bytes] = None,
  209. tls_sanlist: Optional[Iterable[bytes]] = None,
  210. ) -> IProtocol:
  211. """Builds a test server, and completes the outgoing client connection
  212. Args:
  213. client_factory: the the factory that the
  214. application is trying to use to make the outbound connection. We will
  215. invoke it to build the client Protocol
  216. server_factory: a factory to build the
  217. server-side protocol
  218. ssl: If true, we will expect an ssl connection and wrap
  219. server_factory with a TLSMemoryBIOFactory
  220. expected_sni: the expected SNI value
  221. tls_sanlist: list of SAN entries for the TLS cert presented by the server.
  222. Defaults to [b'DNS:test.com']
  223. Returns:
  224. the server Protocol returned by server_factory
  225. """
  226. if ssl:
  227. server_factory = _wrap_server_factory_for_tls(server_factory, tls_sanlist)
  228. server_protocol = server_factory.buildProtocol(None)
  229. # now, tell the client protocol factory to build the client protocol,
  230. # and wire the output of said protocol up to the server via
  231. # a FakeTransport.
  232. #
  233. # Normally this would be done by the TCP socket code in Twisted, but we are
  234. # stubbing that out here.
  235. client_protocol = client_factory.buildProtocol(None)
  236. client_protocol.makeConnection(
  237. FakeTransport(server_protocol, self.reactor, client_protocol)
  238. )
  239. # tell the server protocol to send its stuff back to the client, too
  240. server_protocol.makeConnection(
  241. FakeTransport(client_protocol, self.reactor, server_protocol)
  242. )
  243. if ssl:
  244. http_protocol = server_protocol.wrappedProtocol
  245. tls_connection = server_protocol._tlsConnection
  246. else:
  247. http_protocol = server_protocol
  248. tls_connection = None
  249. # give the reactor a pump to get the TLS juices flowing (if needed)
  250. self.reactor.advance(0)
  251. if expected_sni is not None:
  252. server_name = tls_connection.get_servername()
  253. self.assertEqual(
  254. server_name,
  255. expected_sni,
  256. f"Expected SNI {expected_sni!s} but got {server_name!s}",
  257. )
  258. return http_protocol
  259. def _test_request_direct_connection(
  260. self,
  261. agent: ProxyAgent,
  262. scheme: bytes,
  263. hostname: bytes,
  264. path: bytes,
  265. ):
  266. """Runs a test case for a direct connection not going through a proxy.
  267. Args:
  268. agent: the proxy agent being tested
  269. scheme: expected to be either "http" or "https"
  270. hostname: the hostname to connect to in the test
  271. path: the path to connect to in the test
  272. """
  273. is_https = scheme == b"https"
  274. self.reactor.lookups[hostname.decode()] = "1.2.3.4"
  275. d = agent.request(b"GET", scheme + b"://" + hostname + b"/" + path)
  276. # there should be a pending TCP connection
  277. clients = self.reactor.tcpClients
  278. self.assertEqual(len(clients), 1)
  279. (host, port, client_factory, _timeout, _bindAddress) = clients[0]
  280. self.assertEqual(host, "1.2.3.4")
  281. self.assertEqual(port, 443 if is_https else 80)
  282. # make a test server, and wire up the client
  283. http_server = self._make_connection(
  284. client_factory,
  285. _get_test_protocol_factory(),
  286. ssl=is_https,
  287. expected_sni=hostname if is_https else None,
  288. )
  289. # the FakeTransport is async, so we need to pump the reactor
  290. self.reactor.advance(0)
  291. # now there should be a pending request
  292. self.assertEqual(len(http_server.requests), 1)
  293. request = http_server.requests[0]
  294. self.assertEqual(request.method, b"GET")
  295. self.assertEqual(request.path, b"/" + path)
  296. self.assertEqual(request.requestHeaders.getRawHeaders(b"host"), [hostname])
  297. request.write(b"result")
  298. request.finish()
  299. self.reactor.advance(0)
  300. resp = self.successResultOf(d)
  301. body = self.successResultOf(treq.content(resp))
  302. self.assertEqual(body, b"result")
  303. def test_http_request(self):
  304. agent = ProxyAgent(self.reactor)
  305. self._test_request_direct_connection(agent, b"http", b"test.com", b"")
  306. def test_https_request(self):
  307. agent = ProxyAgent(self.reactor, contextFactory=get_test_https_policy())
  308. self._test_request_direct_connection(agent, b"https", b"test.com", b"abc")
  309. def test_http_request_use_proxy_empty_environment(self):
  310. agent = ProxyAgent(self.reactor, use_proxy=True)
  311. self._test_request_direct_connection(agent, b"http", b"test.com", b"")
  312. @patch.dict(os.environ, {"http_proxy": "proxy.com:8888", "NO_PROXY": "test.com"})
  313. def test_http_request_via_uppercase_no_proxy(self):
  314. agent = ProxyAgent(self.reactor, use_proxy=True)
  315. self._test_request_direct_connection(agent, b"http", b"test.com", b"")
  316. @patch.dict(
  317. os.environ, {"http_proxy": "proxy.com:8888", "no_proxy": "test.com,unused.com"}
  318. )
  319. def test_http_request_via_no_proxy(self):
  320. agent = ProxyAgent(self.reactor, use_proxy=True)
  321. self._test_request_direct_connection(agent, b"http", b"test.com", b"")
  322. @patch.dict(
  323. os.environ, {"https_proxy": "proxy.com", "no_proxy": "test.com,unused.com"}
  324. )
  325. def test_https_request_via_no_proxy(self):
  326. agent = ProxyAgent(
  327. self.reactor,
  328. contextFactory=get_test_https_policy(),
  329. use_proxy=True,
  330. )
  331. self._test_request_direct_connection(agent, b"https", b"test.com", b"abc")
  332. @patch.dict(os.environ, {"http_proxy": "proxy.com:8888", "no_proxy": "*"})
  333. def test_http_request_via_no_proxy_star(self):
  334. agent = ProxyAgent(self.reactor, use_proxy=True)
  335. self._test_request_direct_connection(agent, b"http", b"test.com", b"")
  336. @patch.dict(os.environ, {"https_proxy": "proxy.com", "no_proxy": "*"})
  337. def test_https_request_via_no_proxy_star(self):
  338. agent = ProxyAgent(
  339. self.reactor,
  340. contextFactory=get_test_https_policy(),
  341. use_proxy=True,
  342. )
  343. self._test_request_direct_connection(agent, b"https", b"test.com", b"abc")
  344. @patch.dict(os.environ, {"http_proxy": "proxy.com:8888", "no_proxy": "unused.com"})
  345. def test_http_request_via_proxy(self):
  346. """
  347. Tests that requests can be made through a proxy.
  348. """
  349. self._do_http_request_via_proxy(
  350. expect_proxy_ssl=False, expected_auth_credentials=None
  351. )
  352. @patch.dict(
  353. os.environ,
  354. {"http_proxy": "bob:pinkponies@proxy.com:8888", "no_proxy": "unused.com"},
  355. )
  356. def test_http_request_via_proxy_with_auth(self):
  357. """
  358. Tests that authenticated requests can be made through a proxy.
  359. """
  360. self._do_http_request_via_proxy(
  361. expect_proxy_ssl=False, expected_auth_credentials=b"bob:pinkponies"
  362. )
  363. @patch.dict(
  364. os.environ, {"http_proxy": "https://proxy.com:8888", "no_proxy": "unused.com"}
  365. )
  366. def test_http_request_via_https_proxy(self):
  367. self._do_http_request_via_proxy(
  368. expect_proxy_ssl=True, expected_auth_credentials=None
  369. )
  370. @patch.dict(
  371. os.environ,
  372. {
  373. "http_proxy": "https://bob:pinkponies@proxy.com:8888",
  374. "no_proxy": "unused.com",
  375. },
  376. )
  377. def test_http_request_via_https_proxy_with_auth(self):
  378. self._do_http_request_via_proxy(
  379. expect_proxy_ssl=True, expected_auth_credentials=b"bob:pinkponies"
  380. )
  381. @patch.dict(os.environ, {"https_proxy": "proxy.com", "no_proxy": "unused.com"})
  382. def test_https_request_via_proxy(self):
  383. """Tests that TLS-encrypted requests can be made through a proxy"""
  384. self._do_https_request_via_proxy(
  385. expect_proxy_ssl=False, expected_auth_credentials=None
  386. )
  387. @patch.dict(
  388. os.environ,
  389. {"https_proxy": "bob:pinkponies@proxy.com", "no_proxy": "unused.com"},
  390. )
  391. def test_https_request_via_proxy_with_auth(self):
  392. """Tests that authenticated, TLS-encrypted requests can be made through a proxy"""
  393. self._do_https_request_via_proxy(
  394. expect_proxy_ssl=False, expected_auth_credentials=b"bob:pinkponies"
  395. )
  396. @patch.dict(
  397. os.environ, {"https_proxy": "https://proxy.com", "no_proxy": "unused.com"}
  398. )
  399. def test_https_request_via_https_proxy(self):
  400. """Tests that TLS-encrypted requests can be made through a proxy"""
  401. self._do_https_request_via_proxy(
  402. expect_proxy_ssl=True, expected_auth_credentials=None
  403. )
  404. @patch.dict(
  405. os.environ,
  406. {"https_proxy": "https://bob:pinkponies@proxy.com", "no_proxy": "unused.com"},
  407. )
  408. def test_https_request_via_https_proxy_with_auth(self):
  409. """Tests that authenticated, TLS-encrypted requests can be made through a proxy"""
  410. self._do_https_request_via_proxy(
  411. expect_proxy_ssl=True, expected_auth_credentials=b"bob:pinkponies"
  412. )
  413. def _do_http_request_via_proxy(
  414. self,
  415. expect_proxy_ssl: bool = False,
  416. expected_auth_credentials: Optional[bytes] = None,
  417. ):
  418. """Send a http request via an agent and check that it is correctly received at
  419. the proxy. The proxy can use either http or https.
  420. Args:
  421. expect_proxy_ssl: True if we expect the request to connect via https to proxy
  422. expected_auth_credentials: credentials to authenticate at proxy
  423. """
  424. if expect_proxy_ssl:
  425. agent = ProxyAgent(
  426. self.reactor, use_proxy=True, contextFactory=get_test_https_policy()
  427. )
  428. else:
  429. agent = ProxyAgent(self.reactor, use_proxy=True)
  430. self.reactor.lookups["proxy.com"] = "1.2.3.5"
  431. d = agent.request(b"GET", b"http://test.com")
  432. # there should be a pending TCP connection
  433. clients = self.reactor.tcpClients
  434. self.assertEqual(len(clients), 1)
  435. (host, port, client_factory, _timeout, _bindAddress) = clients[0]
  436. self.assertEqual(host, "1.2.3.5")
  437. self.assertEqual(port, 8888)
  438. # make a test server, and wire up the client
  439. http_server = self._make_connection(
  440. client_factory,
  441. _get_test_protocol_factory(),
  442. ssl=expect_proxy_ssl,
  443. tls_sanlist=[b"DNS:proxy.com"] if expect_proxy_ssl else None,
  444. expected_sni=b"proxy.com" if expect_proxy_ssl else None,
  445. )
  446. # the FakeTransport is async, so we need to pump the reactor
  447. self.reactor.advance(0)
  448. # now there should be a pending request
  449. self.assertEqual(len(http_server.requests), 1)
  450. request = http_server.requests[0]
  451. # Check whether auth credentials have been supplied to the proxy
  452. proxy_auth_header_values = request.requestHeaders.getRawHeaders(
  453. b"Proxy-Authorization"
  454. )
  455. if expected_auth_credentials is not None:
  456. # Compute the correct header value for Proxy-Authorization
  457. encoded_credentials = base64.b64encode(expected_auth_credentials)
  458. expected_header_value = b"Basic " + encoded_credentials
  459. # Validate the header's value
  460. self.assertIn(expected_header_value, proxy_auth_header_values)
  461. else:
  462. # Check that the Proxy-Authorization header has not been supplied to the proxy
  463. self.assertIsNone(proxy_auth_header_values)
  464. self.assertEqual(request.method, b"GET")
  465. self.assertEqual(request.path, b"http://test.com")
  466. self.assertEqual(request.requestHeaders.getRawHeaders(b"host"), [b"test.com"])
  467. request.write(b"result")
  468. request.finish()
  469. self.reactor.advance(0)
  470. resp = self.successResultOf(d)
  471. body = self.successResultOf(treq.content(resp))
  472. self.assertEqual(body, b"result")
  473. def _do_https_request_via_proxy(
  474. self,
  475. expect_proxy_ssl: bool = False,
  476. expected_auth_credentials: Optional[bytes] = None,
  477. ):
  478. """Send a https request via an agent and check that it is correctly received at
  479. the proxy and client. The proxy can use either http or https.
  480. Args:
  481. expect_proxy_ssl: True if we expect the request to connect via https to proxy
  482. expected_auth_credentials: credentials to authenticate at proxy
  483. """
  484. agent = ProxyAgent(
  485. self.reactor,
  486. contextFactory=get_test_https_policy(),
  487. use_proxy=True,
  488. )
  489. self.reactor.lookups["proxy.com"] = "1.2.3.5"
  490. d = agent.request(b"GET", b"https://test.com/abc")
  491. # there should be a pending TCP connection
  492. clients = self.reactor.tcpClients
  493. self.assertEqual(len(clients), 1)
  494. (host, port, client_factory, _timeout, _bindAddress) = clients[0]
  495. self.assertEqual(host, "1.2.3.5")
  496. self.assertEqual(port, 1080)
  497. # make a test server to act as the proxy, and wire up the client
  498. proxy_server = self._make_connection(
  499. client_factory,
  500. _get_test_protocol_factory(),
  501. ssl=expect_proxy_ssl,
  502. tls_sanlist=[b"DNS:proxy.com"] if expect_proxy_ssl else None,
  503. expected_sni=b"proxy.com" if expect_proxy_ssl else None,
  504. )
  505. assert isinstance(proxy_server, HTTPChannel)
  506. # now there should be a pending CONNECT request
  507. self.assertEqual(len(proxy_server.requests), 1)
  508. request = proxy_server.requests[0]
  509. self.assertEqual(request.method, b"CONNECT")
  510. self.assertEqual(request.path, b"test.com:443")
  511. # Check whether auth credentials have been supplied to the proxy
  512. proxy_auth_header_values = request.requestHeaders.getRawHeaders(
  513. b"Proxy-Authorization"
  514. )
  515. if expected_auth_credentials is not None:
  516. # Compute the correct header value for Proxy-Authorization
  517. encoded_credentials = base64.b64encode(expected_auth_credentials)
  518. expected_header_value = b"Basic " + encoded_credentials
  519. # Validate the header's value
  520. self.assertIn(expected_header_value, proxy_auth_header_values)
  521. else:
  522. # Check that the Proxy-Authorization header has not been supplied to the proxy
  523. self.assertIsNone(proxy_auth_header_values)
  524. # tell the proxy server not to close the connection
  525. proxy_server.persistent = True
  526. request.finish()
  527. # now we make another test server to act as the upstream HTTP server.
  528. server_ssl_protocol = _wrap_server_factory_for_tls(
  529. _get_test_protocol_factory()
  530. ).buildProtocol(None)
  531. # Tell the HTTP server to send outgoing traffic back via the proxy's transport.
  532. proxy_server_transport = proxy_server.transport
  533. server_ssl_protocol.makeConnection(proxy_server_transport)
  534. # ... and replace the protocol on the proxy's transport with the
  535. # TLSMemoryBIOProtocol for the test server, so that incoming traffic
  536. # to the proxy gets sent over to the HTTP(s) server.
  537. #
  538. # This needs a bit of gut-wrenching, which is different depending on whether
  539. # the proxy is using TLS or not.
  540. #
  541. # (an alternative, possibly more elegant, approach would be to use a custom
  542. # Protocol to implement the proxy, which starts out by forwarding to an
  543. # HTTPChannel (to implement the CONNECT command) and can then be switched
  544. # into a mode where it forwards its traffic to another Protocol.)
  545. if expect_proxy_ssl:
  546. assert isinstance(proxy_server_transport, TLSMemoryBIOProtocol)
  547. proxy_server_transport.wrappedProtocol = server_ssl_protocol
  548. else:
  549. assert isinstance(proxy_server_transport, FakeTransport)
  550. client_protocol = proxy_server_transport.other
  551. c2s_transport = client_protocol.transport
  552. c2s_transport.other = server_ssl_protocol
  553. self.reactor.advance(0)
  554. server_name = server_ssl_protocol._tlsConnection.get_servername()
  555. expected_sni = b"test.com"
  556. self.assertEqual(
  557. server_name,
  558. expected_sni,
  559. f"Expected SNI {expected_sni!s} but got {server_name!s}",
  560. )
  561. # now there should be a pending request
  562. http_server = server_ssl_protocol.wrappedProtocol
  563. self.assertEqual(len(http_server.requests), 1)
  564. request = http_server.requests[0]
  565. self.assertEqual(request.method, b"GET")
  566. self.assertEqual(request.path, b"/abc")
  567. self.assertEqual(request.requestHeaders.getRawHeaders(b"host"), [b"test.com"])
  568. # Check that the destination server DID NOT receive proxy credentials
  569. proxy_auth_header_values = request.requestHeaders.getRawHeaders(
  570. b"Proxy-Authorization"
  571. )
  572. self.assertIsNone(proxy_auth_header_values)
  573. request.write(b"result")
  574. request.finish()
  575. self.reactor.advance(0)
  576. resp = self.successResultOf(d)
  577. body = self.successResultOf(treq.content(resp))
  578. self.assertEqual(body, b"result")
  579. @patch.dict(os.environ, {"http_proxy": "proxy.com:8888"})
  580. def test_http_request_via_proxy_with_blacklist(self):
  581. # The blacklist includes the configured proxy IP.
  582. agent = ProxyAgent(
  583. BlacklistingReactorWrapper(
  584. self.reactor, ip_whitelist=None, ip_blacklist=IPSet(["1.0.0.0/8"])
  585. ),
  586. self.reactor,
  587. use_proxy=True,
  588. )
  589. self.reactor.lookups["proxy.com"] = "1.2.3.5"
  590. d = agent.request(b"GET", b"http://test.com")
  591. # there should be a pending TCP connection
  592. clients = self.reactor.tcpClients
  593. self.assertEqual(len(clients), 1)
  594. (host, port, client_factory, _timeout, _bindAddress) = clients[0]
  595. self.assertEqual(host, "1.2.3.5")
  596. self.assertEqual(port, 8888)
  597. # make a test server, and wire up the client
  598. http_server = self._make_connection(
  599. client_factory, _get_test_protocol_factory()
  600. )
  601. # the FakeTransport is async, so we need to pump the reactor
  602. self.reactor.advance(0)
  603. # now there should be a pending request
  604. self.assertEqual(len(http_server.requests), 1)
  605. request = http_server.requests[0]
  606. self.assertEqual(request.method, b"GET")
  607. self.assertEqual(request.path, b"http://test.com")
  608. self.assertEqual(request.requestHeaders.getRawHeaders(b"host"), [b"test.com"])
  609. request.write(b"result")
  610. request.finish()
  611. self.reactor.advance(0)
  612. resp = self.successResultOf(d)
  613. body = self.successResultOf(treq.content(resp))
  614. self.assertEqual(body, b"result")
  615. @patch.dict(os.environ, {"HTTPS_PROXY": "proxy.com"})
  616. def test_https_request_via_uppercase_proxy_with_blacklist(self):
  617. # The blacklist includes the configured proxy IP.
  618. agent = ProxyAgent(
  619. BlacklistingReactorWrapper(
  620. self.reactor, ip_whitelist=None, ip_blacklist=IPSet(["1.0.0.0/8"])
  621. ),
  622. self.reactor,
  623. contextFactory=get_test_https_policy(),
  624. use_proxy=True,
  625. )
  626. self.reactor.lookups["proxy.com"] = "1.2.3.5"
  627. d = agent.request(b"GET", b"https://test.com/abc")
  628. # there should be a pending TCP connection
  629. clients = self.reactor.tcpClients
  630. self.assertEqual(len(clients), 1)
  631. (host, port, client_factory, _timeout, _bindAddress) = clients[0]
  632. self.assertEqual(host, "1.2.3.5")
  633. self.assertEqual(port, 1080)
  634. # make a test HTTP server, and wire up the client
  635. proxy_server = self._make_connection(
  636. client_factory, _get_test_protocol_factory()
  637. )
  638. # fish the transports back out so that we can do the old switcheroo
  639. s2c_transport = proxy_server.transport
  640. client_protocol = s2c_transport.other
  641. c2s_transport = client_protocol.transport
  642. # the FakeTransport is async, so we need to pump the reactor
  643. self.reactor.advance(0)
  644. # now there should be a pending CONNECT request
  645. self.assertEqual(len(proxy_server.requests), 1)
  646. request = proxy_server.requests[0]
  647. self.assertEqual(request.method, b"CONNECT")
  648. self.assertEqual(request.path, b"test.com:443")
  649. # tell the proxy server not to close the connection
  650. proxy_server.persistent = True
  651. # this just stops the http Request trying to do a chunked response
  652. # request.setHeader(b"Content-Length", b"0")
  653. request.finish()
  654. # now we can replace the proxy channel with a new, SSL-wrapped HTTP channel
  655. ssl_factory = _wrap_server_factory_for_tls(_get_test_protocol_factory())
  656. ssl_protocol = ssl_factory.buildProtocol(None)
  657. http_server = ssl_protocol.wrappedProtocol
  658. ssl_protocol.makeConnection(
  659. FakeTransport(client_protocol, self.reactor, ssl_protocol)
  660. )
  661. c2s_transport.other = ssl_protocol
  662. self.reactor.advance(0)
  663. server_name = ssl_protocol._tlsConnection.get_servername()
  664. expected_sni = b"test.com"
  665. self.assertEqual(
  666. server_name,
  667. expected_sni,
  668. f"Expected SNI {expected_sni!s} but got {server_name!s}",
  669. )
  670. # now there should be a pending request
  671. self.assertEqual(len(http_server.requests), 1)
  672. request = http_server.requests[0]
  673. self.assertEqual(request.method, b"GET")
  674. self.assertEqual(request.path, b"/abc")
  675. self.assertEqual(request.requestHeaders.getRawHeaders(b"host"), [b"test.com"])
  676. request.write(b"result")
  677. request.finish()
  678. self.reactor.advance(0)
  679. resp = self.successResultOf(d)
  680. body = self.successResultOf(treq.content(resp))
  681. self.assertEqual(body, b"result")
  682. @patch.dict(os.environ, {"http_proxy": "proxy.com:8888"})
  683. def test_proxy_with_no_scheme(self):
  684. http_proxy_agent = ProxyAgent(self.reactor, use_proxy=True)
  685. self.assertIsInstance(http_proxy_agent.http_proxy_endpoint, HostnameEndpoint)
  686. self.assertEqual(http_proxy_agent.http_proxy_endpoint._hostStr, "proxy.com")
  687. self.assertEqual(http_proxy_agent.http_proxy_endpoint._port, 8888)
  688. @patch.dict(os.environ, {"http_proxy": "socks://proxy.com:8888"})
  689. def test_proxy_with_unsupported_scheme(self):
  690. with self.assertRaises(ValueError):
  691. ProxyAgent(self.reactor, use_proxy=True)
  692. @patch.dict(os.environ, {"http_proxy": "http://proxy.com:8888"})
  693. def test_proxy_with_http_scheme(self):
  694. http_proxy_agent = ProxyAgent(self.reactor, use_proxy=True)
  695. self.assertIsInstance(http_proxy_agent.http_proxy_endpoint, HostnameEndpoint)
  696. self.assertEqual(http_proxy_agent.http_proxy_endpoint._hostStr, "proxy.com")
  697. self.assertEqual(http_proxy_agent.http_proxy_endpoint._port, 8888)
  698. @patch.dict(os.environ, {"http_proxy": "https://proxy.com:8888"})
  699. def test_proxy_with_https_scheme(self):
  700. https_proxy_agent = ProxyAgent(self.reactor, use_proxy=True)
  701. self.assertIsInstance(https_proxy_agent.http_proxy_endpoint, _WrapperEndpoint)
  702. self.assertEqual(
  703. https_proxy_agent.http_proxy_endpoint._wrappedEndpoint._hostStr, "proxy.com"
  704. )
  705. self.assertEqual(
  706. https_proxy_agent.http_proxy_endpoint._wrappedEndpoint._port, 8888
  707. )
  708. def _wrap_server_factory_for_tls(
  709. factory: IProtocolFactory, sanlist: Iterable[bytes] = None
  710. ) -> IProtocolFactory:
  711. """Wrap an existing Protocol Factory with a test TLSMemoryBIOFactory
  712. The resultant factory will create a TLS server which presents a certificate
  713. signed by our test CA, valid for the domains in `sanlist`
  714. Args:
  715. factory: protocol factory to wrap
  716. sanlist: list of domains the cert should be valid for
  717. Returns:
  718. interfaces.IProtocolFactory
  719. """
  720. if sanlist is None:
  721. sanlist = [b"DNS:test.com"]
  722. connection_creator = TestServerTLSConnectionFactory(sanlist=sanlist)
  723. return TLSMemoryBIOFactory(
  724. connection_creator, isClient=False, wrappedFactory=factory
  725. )
  726. def _get_test_protocol_factory() -> IProtocolFactory:
  727. """Get a protocol Factory which will build an HTTPChannel
  728. Returns:
  729. interfaces.IProtocolFactory
  730. """
  731. server_factory = Factory.forProtocol(HTTPChannel)
  732. # Request.finish expects the factory to have a 'log' method.
  733. server_factory.log = _log_request
  734. return server_factory
  735. def _log_request(request: str):
  736. """Implements Factory.log, which is expected by Request.finish"""
  737. logger.info(f"Completed request {request}")