def test_uses_ssl_context(self, mocker, mock_http_client_constructor): transport = HTTPTransport(hostname=fake_hostname) done = transport.request(fake_method, fake_path, mocker.MagicMock()) done.result() assert mock_http_client_constructor.call_count == 1 assert mock_http_client_constructor.call_args[1]["context"] == transport._ssl_context
def test_calls_http_client_request_with_given_parameters( self, mocker, mock_http_client_constructor, method, path, query_params, body, headers ): transport = HTTPTransport(hostname=fake_hostname) mock_http_client_request = mock_http_client_constructor.return_value.request if query_params: expected_url = "https://{}/{}?{}".format(fake_hostname, path, query_params) else: expected_url = "https://{}/{}".format(fake_hostname, path) cb = mocker.MagicMock() done = transport.request( method, path, cb, body=body, headers=headers, query_params=query_params ) done.result() assert mock_http_client_constructor.call_count == 1 assert mock_http_client_request.call_count == 1 assert mock_http_client_request.call_args[0][0] == method assert mock_http_client_request.call_args[0][1] == expected_url actual_body = mock_http_client_request.call_args[1]["body"] actual_headers = mock_http_client_request.call_args[1]["headers"] if body: assert actual_body == body else: assert not bool(actual_body) if headers: assert actual_headers == headers else: assert not bool(actual_headers)
def test_client_raises_unexpected_error( self, mocker, mock_http_client_constructor, arbitrary_exception ): transport = HTTPTransport(hostname=fake_hostname) mock_http_client_constructor.return_value.connect.side_effect = arbitrary_exception cb = mocker.MagicMock() done = transport.request(fake_method, fake_path, cb) done.result() error = cb.call_args[1]["error"] assert isinstance(error, errors.ProtocolClientError) assert error.__cause__ is arbitrary_exception
def test_returns_response_on_success(self, mocker, mock_http_client_constructor): transport = HTTPTransport(hostname=fake_hostname) cb = mocker.MagicMock() done = transport.request(fake_method, fake_path, cb) done.result() assert mock_http_client_constructor.call_count == 1 assert cb.call_count == 1 assert cb.call_args[1]["response"]["status_code"] == 1234 assert cb.call_args[1]["response"]["reason"] == "__fake_reason__" assert cb.call_args[1]["response"]["resp"] == "__fake_response_read_value__"
def test_configures_tls_context_with_default_certs(self, mocker): mock_ssl_context = mocker.patch.object(ssl, "SSLContext").return_value HTTPTransport(hostname=fake_hostname) assert mock_ssl_context.load_default_certs.call_count == 1 assert mock_ssl_context.load_default_certs.call_args == mocker.call()
def test_formats_http_client_request_with_only_method_and_path( self, mocker, mock_http_client_constructor ): transport = HTTPTransport(hostname=fake_hostname) mock_http_client_request = mock_http_client_constructor.return_value.request fake_method = "__fake_method__" fake_path = "__fake_path__" expected_url = "https://{}/{}".format(fake_hostname, fake_path) done = transport.request(fake_method, fake_path, mocker.MagicMock()) done.result() assert mock_http_client_constructor.call_count == 1 assert mock_http_client_request.call_count == 1 assert mock_http_client_request.call_args == mocker.call( fake_method, expected_url, body="", headers={} )
def test_confgures_tls_context_with_cipher(self, mocker): mock_ssl_context = mocker.patch.object(ssl, "SSLContext").return_value HTTPTransport(hostname=fake_hostname, cipher=fake_cipher) assert mock_ssl_context.set_ciphers.call_count == 1 assert mock_ssl_context.set_ciphers.call_args == mocker.call(fake_cipher)
def test_configures_tls_context_with_ca_certs(self, mocker): mock_ssl_context = mocker.patch.object(ssl, "SSLContext").return_value HTTPTransport(hostname=fake_hostname, ca_cert=fake_ca_cert) assert mock_ssl_context.load_verify_locations.call_count == 1 assert mock_ssl_context.load_verify_locations.call_args == mocker.call(cadata=fake_ca_cert)
def test_proxy_format(self, proxy_options): http_transport_object = HTTPTransport(hostname=fake_hostname, proxy_options=proxy_options) if proxy_options.proxy_username and proxy_options.proxy_password: expected_proxy_string = "{username}:{password}@{address}:{port}".format( username=proxy_options.proxy_username, password=proxy_options.proxy_password, address=proxy_options.proxy_address, port=proxy_options.proxy_port, ) else: expected_proxy_string = "{address}:{port}".format( address=proxy_options.proxy_address, port=proxy_options.proxy_port) if proxy_options.proxy_type == "HTTP": expected_proxy_string = "http://" + expected_proxy_string elif proxy_options.proxy_type == "SOCKS4": expected_proxy_string = "socks4://" + expected_proxy_string else: expected_proxy_string = "socks5://" + expected_proxy_string assert isinstance(http_transport_object._proxies, dict) assert http_transport_object._proxies["http"] == expected_proxy_string assert http_transport_object._proxies["https"] == expected_proxy_string
def test_configures_tls_context(self, mocker): mock_ssl_context_constructor = mocker.patch.object(ssl, "SSLContext") mock_ssl_context = mock_ssl_context_constructor.return_value HTTPTransport(hostname=fake_hostname) # Verify correctness of TLS/SSL Context assert mock_ssl_context_constructor.call_count == 1 assert mock_ssl_context_constructor.call_args == mocker.call(protocol=ssl.PROTOCOL_TLSv1_2) assert mock_ssl_context.check_hostname is True assert mock_ssl_context.verify_mode == ssl.CERT_REQUIRED
def test_sets_required_parameters(self, mocker): mocker.patch.object(ssl, "SSLContext").return_value mocker.patch.object(HTTPTransport, "_create_ssl_context").return_value http_transport_object = HTTPTransport( hostname=fake_hostname, ca_cert=fake_ca_cert, x509_cert=fake_x509_cert ) assert http_transport_object._hostname == fake_hostname assert http_transport_object._ca_cert == fake_ca_cert assert http_transport_object._x509_cert == fake_x509_cert
def test_sets_required_parameters(self, mocker): mocker.patch.object(ssl, "SSLContext").return_value mocker.patch.object(HTTPTransport, "_create_ssl_context").return_value http_transport_object = HTTPTransport( hostname=fake_hostname, server_verification_cert=fake_server_verification_cert, x509_cert=fake_x509_cert, cipher=fake_cipher, ) assert http_transport_object._hostname == fake_hostname
def test_creates_http_connection_object(self, mocker, mock_http_client_constructor): transport = HTTPTransport(hostname=fake_hostname) # We call .result because we need to block for the Future to complete before moving on. transport.request(fake_method, fake_path, mocker.MagicMock()).result() assert mock_http_client_constructor.call_count == 1 transport.request(fake_method, fake_path, mocker.MagicMock()).result() assert mock_http_client_constructor.call_count == 2
def test_http_adapter_pool_manager(self, mocker): # NOTE: This test involves mocking and testing deeper parts of the requests library stack # in order to show that the HTTPAdapter is functioning as intended. This naturally gets a # little messy from a unit testing perspective poolmanager_init_mock = mocker.patch.object(requests.adapters, "PoolManager") proxymanager_init_mock = mocker.patch.object(urllib3.poolmanager, "ProxyManager") socksproxymanager_init_mock = mocker.patch.object( requests.adapters, "SOCKSProxyManager") ssl_context_init_mock = mocker.patch.object(ssl, "SSLContext") mock_ssl_context = ssl_context_init_mock.return_value http_transport_object = HTTPTransport(hostname=fake_hostname) # SSL Context was only created once assert ssl_context_init_mock.call_count == 1 # HTTP Adapter was set on the transport assert isinstance(http_transport_object._http_adapter, requests.adapters.HTTPAdapter) # Reset the poolmanager mock because it's already been called upon instantiation of the adapter # (We will manually test scenarios in which a PoolManager is instantiated) poolmanager_init_mock.reset_mock() # Basic PoolManager init scenario http_transport_object._http_adapter.init_poolmanager( connections=requests.adapters.DEFAULT_POOLSIZE, maxsize=requests.adapters.DEFAULT_POOLSIZE, ) assert poolmanager_init_mock.call_count == 1 assert poolmanager_init_mock.call_args[1][ "ssl_context"] == mock_ssl_context # ProxyManager init scenario http_transport_object._http_adapter.proxy_manager_for( proxy="http://127.0.0.1") assert proxymanager_init_mock.call_count == 1 assert proxymanager_init_mock.call_args[1][ "ssl_context"] == mock_ssl_context # SOCKSProxyManager init scenario http_transport_object._http_adapter.proxy_manager_for( proxy="socks5://127.0.0.1") assert socksproxymanager_init_mock.call_count == 1 assert socksproxymanager_init_mock.call_args[1][ "ssl_context"] == mock_ssl_context # SSL Context was still only ever created once. This proves that the SSL context being # used above is the same one that was configured in a custom way assert ssl_context_init_mock.call_count == 1
def test_configures_tls_context_with_client_provided_certificate_chain(self, mocker): fake_client_cert = X509("fantastic_beasts", "where_to_find_them", "alohomora") mock_ssl_context_constructor = mocker.patch.object(ssl, "SSLContext") mock_ssl_context = mock_ssl_context_constructor.return_value HTTPTransport(hostname=fake_hostname, x509_cert=fake_client_cert) assert mock_ssl_context.load_default_certs.call_count == 1 assert mock_ssl_context.load_cert_chain.call_count == 1 assert mock_ssl_context.load_cert_chain.call_args == mocker.call( fake_client_cert.certificate_file, fake_client_cert.key_file, fake_client_cert.pass_phrase, )
def _run_op(self, op): if isinstance(op, pipeline_ops_base.InitializePipelineOperation): # If there is a gateway hostname, use that as the hostname for connection, # rather than the hostname itself if self.pipeline_root.pipeline_configuration.gateway_hostname: logger.debug( "Gateway Hostname Present. Setting Hostname to: {}".format( self.pipeline_root.pipeline_configuration. gateway_hostname)) hostname = self.pipeline_root.pipeline_configuration.gateway_hostname else: logger.debug( "Gateway Hostname not present. Setting Hostname to: {}". format(self.pipeline_root.pipeline_configuration.hostname)) hostname = self.pipeline_root.pipeline_configuration.hostname # Create HTTP Transport logger.debug("{}({}): got connection args".format( self.name, op.name)) self.transport = HTTPTransport( hostname=hostname, server_verification_cert=self.pipeline_root. pipeline_configuration.server_verification_cert, x509_cert=self.pipeline_root.pipeline_configuration.x509, cipher=self.pipeline_root.pipeline_configuration.cipher, ) self.pipeline_root.transport = self.transport op.complete() elif isinstance(op, pipeline_ops_http.HTTPRequestAndResponseOperation): # This will call down to the HTTP Transport with a request and also created a request callback. Because the HTTP Transport will run on the http transport thread, this call should be non-blocking to the pipline thread. logger.debug( "{}({}): Generating HTTP request and setting callback before completing." .format(self.name, op.name)) @pipeline_thread.invoke_on_pipeline_thread_nowait def on_request_completed(error=None, response=None): if error: logger.error( "{}({}): Error passed to on_request_completed. Error={}" .format(self.name, op.name, error)) op.complete(error=error) else: logger.debug( "{}({}): Request completed. Completing op.".format( self.name, op.name)) logger.debug("HTTP Response Status: {}".format( response["status_code"])) logger.debug("HTTP Response: {}".format( response["resp"].decode("utf-8"))) op.response_body = response["resp"] op.status_code = response["status_code"] op.reason = response["reason"] op.complete() # A deepcopy is necessary here since otherwise the manipulation happening to # http_headers will affect the op.headers, which would be an unintended side effect # and not a good practice. http_headers = copy.deepcopy(op.headers) if self.pipeline_root.pipeline_configuration.sastoken: http_headers["Authorization"] = str( self.pipeline_root.pipeline_configuration.sastoken) self.transport.request( method=op.method, path=op.path, headers=http_headers, query_params=op.query_params, body=op.body, callback=on_request_completed, ) else: self.send_op_down(op)
def _run_op(self, op): if isinstance(op, pipeline_ops_http.SetHTTPConnectionArgsOperation): # pipeline_ops_http.SetHTTPConenctionArgsOperation is used to create the HTTPTransport object and set all of it's properties. logger.debug("{}({}): got connection args".format( self.name, op.name)) self.sas_token = op.sas_token self.transport = HTTPTransport( hostname=op.hostname, server_verification_cert=op.server_verification_cert, x509_cert=op.client_cert, ) self.pipeline_root.transport = self.transport op.complete() elif isinstance(op, pipeline_ops_base.UpdateSasTokenOperation): logger.debug("{}({}): saving sas token and completing".format( self.name, op.name)) self.sas_token = op.sas_token op.complete() elif isinstance(op, pipeline_ops_http.HTTPRequestAndResponseOperation): # This will call down to the HTTP Transport with a request and also created a request callback. Because the HTTP Transport will run on the http transport thread, this call should be non-blocking to the pipline thread. logger.debug( "{}({}): Generating HTTP request and setting callback before completing." .format(self.name, op.name)) @pipeline_thread.invoke_on_pipeline_thread_nowait def on_request_completed(error=None, response=None): if error: logger.error( "{}({}): Error passed to on_request_completed. Error={}" .format(self.name, op.name, error)) op.complete(error=error) else: logger.debug( "{}({}): Request completed. Completing op.".format( self.name, op.name)) logger.debug("HTTP Response Status: {}".format( response["status_code"])) logger.debug("HTTP Response: {}".format( response["resp"].decode("utf-8"))) op.response_body = response["resp"] op.status_code = response["status_code"] op.reason = response["reason"] op.complete() # A deepcopy is necessary here since otherwise the manipulation happening to http_headers will affect the op.headers, which would be an unintended side effect and not a good practice. http_headers = copy.deepcopy(op.headers) if self.sas_token: http_headers["Authorization"] = self.sas_token self.transport.request( method=op.method, path=op.path, headers=http_headers, query_params=op.query_params, body=op.body, callback=on_request_completed, ) else: self.send_op_down(op)
class HTTPTransportStage(PipelineStage): """ PipelineStage object which is responsible for interfacing with the HTTP protocol wrapper object. This stage handles all HTTP operations that are not specific to IoT Hub. """ def __init__(self): super().__init__() # The sas_token will be set when Connection Args are received self.sas_token = None # The transport will be instantiated when Connection Args are received self.transport = None @pipeline_thread.runs_on_pipeline_thread def _run_op(self, op): if isinstance(op, pipeline_ops_base.InitializePipelineOperation): # If there is a gateway hostname, use that as the hostname for connection, # rather than the hostname itself if self.nucleus.pipeline_configuration.gateway_hostname: logger.debug( "Gateway Hostname Present. Setting Hostname to: {}".format( self.nucleus.pipeline_configuration.gateway_hostname)) hostname = self.nucleus.pipeline_configuration.gateway_hostname else: logger.debug( "Gateway Hostname not present. Setting Hostname to: {}". format(self.nucleus.pipeline_configuration.hostname)) hostname = self.nucleus.pipeline_configuration.hostname # Create HTTP Transport logger.debug("{}({}): got connection args".format( self.name, op.name)) self.transport = HTTPTransport( hostname=hostname, server_verification_cert=self.nucleus.pipeline_configuration. server_verification_cert, x509_cert=self.nucleus.pipeline_configuration.x509, cipher=self.nucleus.pipeline_configuration.cipher, proxy_options=self.nucleus.pipeline_configuration. proxy_options, ) self.nucleus.transport = self.transport op.complete() elif isinstance(op, pipeline_ops_http.HTTPRequestAndResponseOperation): # This will call down to the HTTP Transport with a request and also created a request callback. Because the HTTP Transport will run on the http transport thread, this call should be non-blocking to the pipeline thread. logger.debug( "{}({}): Generating HTTP request and setting callback before completing." .format(self.name, op.name)) @pipeline_thread.invoke_on_pipeline_thread_nowait def on_request_completed(error=None, response=None): if error: logger.debug( "{}({}): Error passed to on_request_completed. Error={}" .format(self.name, op.name, error)) op.complete(error=error) else: logger.debug( "{}({}): Request completed. Completing op.".format( self.name, op.name)) logger.debug("HTTP Response Status: {}".format( response["status_code"])) logger.debug("HTTP Response: {}".format(response["resp"])) op.response_body = response["resp"] op.status_code = response["status_code"] op.reason = response["reason"] op.complete() # A deepcopy is necessary here since otherwise the manipulation happening to # http_headers will affect the op.headers, which would be an unintended side effect # and not a good practice. http_headers = copy.deepcopy(op.headers) if self.nucleus.pipeline_configuration.sastoken: http_headers["Authorization"] = str( self.nucleus.pipeline_configuration.sastoken) self.transport.request( method=op.method, path=op.path, headers=http_headers, query_params=op.query_params, body=op.body, callback=on_request_completed, ) else: self.send_op_down(op)
def transport(self): return HTTPTransport(hostname=fake_hostname)