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)
Ejemplo n.º 9
0
    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
Ejemplo n.º 12
0
    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
Ejemplo n.º 14
0
    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)
Ejemplo n.º 19
0
 def transport(self):
     return HTTPTransport(hostname=fake_hostname)