#!/usr/bin/env python # Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import httparchive import httplib import httpproxy import threading import unittest import util class MockCustomResponseHandler(object): def __init__(self, response): """ Args: response: An instance of ArchivedHttpResponse that is returned for each request. """ self._response = response def handle(self, request): del request return self._response class MockHttpArchiveFetch(object): def __init__(self, response): """ Args: response: An instance of ArchivedHttpResponse that is returned for each request. """ self.is_record_mode = False self._response = response def __call__(self, request): del request # unused return self._response class MockHttpArchiveHandler(httpproxy.HttpArchiveHandler): def handle_one_request(self): httpproxy.HttpArchiveHandler.handle_one_request(self) HttpProxyTest.HANDLED_REQUEST_COUNT += 1 class MockRules(object): def Find(self, unused_rule_type_name): # pylint: disable=unused-argument return lambda unused_request, unused_response: None class HttpProxyTest(unittest.TestCase): def setUp(self): self.has_proxy_server_bound_port = False self.has_proxy_server_started = False self.allow_generate_304 = False self.serve_response_by_http_archive = False def set_up_proxy_server(self, response): """ Args: response: An instance of ArchivedHttpResponse that is returned for each request. """ HttpProxyTest.HANDLED_REQUEST_COUNT = 0 self.host = 'localhost' self.port = 8889 custom_handlers = MockCustomResponseHandler( response if not self.serve_response_by_http_archive else None) rules = MockRules() http_archive_fetch = MockHttpArchiveFetch( response if self.serve_response_by_http_archive else None) self.proxy_server = httpproxy.HttpProxyServer( http_archive_fetch, custom_handlers, rules, host=self.host, port=self.port, allow_generate_304=self.allow_generate_304) self.proxy_server.RequestHandlerClass = MockHttpArchiveHandler self.has_proxy_server_bound_port = True def tear_down_proxy_server(self): if self.has_proxy_server_started: self.proxy_server.shutdown() if self.has_proxy_server_bound_port: self.proxy_server.server_close() def tearDown(self): self.tear_down_proxy_server() def serve_requests_forever(self): self.has_proxy_server_started = True self.proxy_server.serve_forever(poll_interval=0.01) # Tests that handle_one_request does not leak threads, and does not try to # re-handle connections that are finished. def test_handle_one_request_closes_connection(self): # By default, BaseHTTPServer.py treats all HTTP 1.1 requests as keep-alive. # Intentionally use HTTP 1.0 to prevent this behavior. response = httparchive.ArchivedHttpResponse( version=10, status=200, reason="OK", headers=[], response_data=["bat1"]) self.set_up_proxy_server(response) t = threading.Thread( target=HttpProxyTest.serve_requests_forever, args=(self,)) t.start() initial_thread_count = threading.activeCount() # Make a bunch of requests. request_count = 10 for _ in range(request_count): conn = httplib.HTTPConnection('localhost', 8889, timeout=10) conn.request("GET", "/index.html") res = conn.getresponse().read() self.assertEqual(res, "bat1") conn.close() # Check to make sure that there is no leaked thread. util.WaitFor(lambda: threading.activeCount() == initial_thread_count, 2) self.assertEqual(request_count, HttpProxyTest.HANDLED_REQUEST_COUNT) # Tests that keep-alive header works. def test_keep_alive_header(self): response = httparchive.ArchivedHttpResponse( version=11, status=200, reason="OK", headers=[("Connection", "keep-alive")], response_data=["bat1"]) self.set_up_proxy_server(response) t = threading.Thread( target=HttpProxyTest.serve_requests_forever, args=(self,)) t.start() initial_thread_count = threading.activeCount() # Make a bunch of requests. request_count = 10 connections = [] for _ in range(request_count): conn = httplib.HTTPConnection('localhost', 8889, timeout=10) conn.request("GET", "/index.html", headers={"Connection": "keep-alive"}) res = conn.getresponse().read() self.assertEqual(res, "bat1") connections.append(conn) # Repeat the same requests. for conn in connections: conn.request("GET", "/index.html", headers={"Connection": "keep-alive"}) res = conn.getresponse().read() self.assertEqual(res, "bat1") # Check that the right number of requests have been handled. self.assertEqual(2 * request_count, HttpProxyTest.HANDLED_REQUEST_COUNT) # Check to make sure that exactly "request_count" new threads are active. self.assertEqual( threading.activeCount(), initial_thread_count + request_count) for conn in connections: conn.close() util.WaitFor(lambda: threading.activeCount() == initial_thread_count, 1) # Test that opening 400 simultaneous connections does not cause httpproxy to # hit a process fd limit. The default limit is 256 fds. def test_max_fd(self): response = httparchive.ArchivedHttpResponse( version=11, status=200, reason="OK", headers=[("Connection", "keep-alive")], response_data=["bat1"]) self.set_up_proxy_server(response) t = threading.Thread( target=HttpProxyTest.serve_requests_forever, args=(self,)) t.start() # Make a bunch of requests. request_count = 400 connections = [] for _ in range(request_count): conn = httplib.HTTPConnection('localhost', 8889, timeout=10) conn.request("GET", "/index.html", headers={"Connection": "keep-alive"}) res = conn.getresponse().read() self.assertEqual(res, "bat1") connections.append(conn) # Check that the right number of requests have been handled. self.assertEqual(request_count, HttpProxyTest.HANDLED_REQUEST_COUNT) for conn in connections: conn.close() # Tests that conditional requests return 304. def test_generate_304(self): REQUEST_HEADERS = [ {}, {'If-Modified-Since': 'whatever'}, {'If-None-Match': 'whatever yet again'}] RESPONSE_STATUSES = [200, 204, 304, 404] for allow_generate_304 in [False, True]: self.allow_generate_304 = allow_generate_304 for serve_response_by_http_archive in [False, True]: self.serve_response_by_http_archive = serve_response_by_http_archive for response_status in RESPONSE_STATUSES: response = None if response_status != 404: response = httparchive.ArchivedHttpResponse( version=11, status=response_status, reason="OK", headers=[], response_data=["some content"]) self.set_up_proxy_server(response) t = threading.Thread( target=HttpProxyTest.serve_requests_forever, args=(self,)) t.start() for method in ['GET', 'HEAD', 'POST']: for headers in REQUEST_HEADERS: connection = httplib.HTTPConnection('localhost', 8889, timeout=10) connection.request(method, "/index.html", headers=headers) response = connection.getresponse() connection.close() if (allow_generate_304 and serve_response_by_http_archive and method in ['GET', 'HEAD'] and headers and response_status == 200): self.assertEqual(304, response.status) self.assertEqual('', response.read()) else: self.assertEqual(response_status, response.status) self.tear_down_proxy_server() if __name__ == '__main__': unittest.main()