| import inspect | |
| import logging | |
| import os | |
| import socket | |
| import sys | |
| from threading import Thread | |
| from pyhttpd.certs import CertificateSpec | |
| from pyhttpd.env import HttpdTestEnv, HttpdTestSetup | |
| log = logging.getLogger(__name__) | |
| class TCPFaker: | |
| # tcp backend for custom responses | |
| def __init__(self, host, port): | |
| self._thread = None | |
| self._socket = None | |
| self._host = host | |
| self._port = port | |
| self._done = False | |
| self._request = None | |
| def start(self): | |
| def process(): | |
| self._socket.listen(1) | |
| self._socket.settimeout(0.5) | |
| self._process() | |
| self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
| self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
| self._socket.bind((self._host, self._port)) | |
| self._thread = Thread(target=process, daemon=True) | |
| self._thread.start() | |
| def stop(self): | |
| self._done = True | |
| self._thread.join(timeout=5) | |
| self._socket.close() | |
| def _make_response(self, data): | |
| return """HTTP/1.1 200 OK | |
| Content-Type: text/html | |
| Content-Length: 5 | |
| Hello""".encode() | |
| def _process(self): | |
| _read_buffer = 8192 if sys.platform == "win32" else 4096 | |
| while not self._done: | |
| try: | |
| c, client_address = self._socket.accept() | |
| try: | |
| data = c.recv(_read_buffer) | |
| # capture request to backend | |
| self._request = data | |
| c.sendall(self._make_response(data)) | |
| # Half-close for an orderly FIN instead of letting the | |
| # OS tear the socket down under us. An abrupt close() | |
| # right after sendall() can, depending on the platform's | |
| # TCP stack and buffering, surface to the peer as a | |
| # reset rather than a clean end of the response, which | |
| # httpd's proxy and cache code treat as an aborted | |
| # connection and a reason to discard the cache entry. | |
| try: | |
| c.shutdown(socket.SHUT_WR) | |
| except OSError: | |
| pass | |
| finally: | |
| c.close() | |
| except socket.timeout: | |
| pass | |
| except ConnectionAbortedError: | |
| self._done = True | |
| class ProxyTestSetup(HttpdTestSetup): | |
| def __init__(self, env: 'HttpdTestEnv'): | |
| super().__init__(env=env) | |
| self.add_source_dir(os.path.dirname(inspect.getfile(ProxyTestSetup))) | |
| self.add_modules(["proxy", "proxy_http", "proxy_ajp", "proxy_balancer", | |
| "proxy_connect", "proxy_uwsgi", "lbmethod_byrequests", | |
| "remoteip"]) | |
| class ProxyTestEnv(HttpdTestEnv): | |
| def __init__(self, pytestconfig=None): | |
| super().__init__(pytestconfig=pytestconfig) | |
| self.add_httpd_conf([ | |
| ]) | |
| self._d_reverse = f"reverse.{self.http_tld}" | |
| self._d_forward = f"forward.{self.http_tld}" | |
| self._d_mixed = f"mixed.{self.http_tld}" | |
| self.add_httpd_log_modules( | |
| ["proxy", "proxy_http", "proxy_balancer", "lbmethod_byrequests", | |
| "ssl"]) | |
| self.add_cert_specs([ | |
| CertificateSpec(domains=[ | |
| self._d_forward, self._d_reverse, self._d_mixed | |
| ]), | |
| CertificateSpec(domains=[f"noh2.{self.http_tld}"], | |
| key_type='rsa2048'), | |
| ]) | |
| def setup_httpd(self, setup: HttpdTestSetup = None): | |
| super().setup_httpd(setup=ProxyTestSetup(env=self)) | |
| @property | |
| def d_forward(self): | |
| return self._d_forward | |
| @property | |
| def d_reverse(self): | |
| return self._d_reverse | |
| @property | |
| def d_mixed(self): | |
| return self._d_mixed |