| # |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you 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. |
| # |
| |
| from __future__ import absolute_import |
| |
| from cproton import PN_EOS, PN_OK, PN_SASL_AUTH, PN_SASL_NONE, PN_SASL_OK, PN_SASL_PERM, PN_SASL_SYS, PN_SASL_TEMP, \ |
| PN_SSL_ANONYMOUS_PEER, PN_SSL_CERT_SUBJECT_CITY_OR_LOCALITY, PN_SSL_CERT_SUBJECT_COMMON_NAME, \ |
| PN_SSL_CERT_SUBJECT_COUNTRY_NAME, PN_SSL_CERT_SUBJECT_ORGANIZATION_NAME, PN_SSL_CERT_SUBJECT_ORGANIZATION_UNIT, \ |
| PN_SSL_CERT_SUBJECT_STATE_OR_PROVINCE, PN_SSL_MD5, PN_SSL_MODE_CLIENT, PN_SSL_MODE_SERVER, PN_SSL_RESUME_NEW, \ |
| PN_SSL_RESUME_REUSED, PN_SSL_RESUME_UNKNOWN, PN_SSL_SHA1, PN_SSL_SHA256, PN_SSL_SHA512, PN_SSL_VERIFY_PEER, \ |
| PN_SSL_VERIFY_PEER_NAME, PN_TRACE_DRV, PN_TRACE_FRM, PN_TRACE_OFF, PN_TRACE_RAW, pn_error_text, pn_sasl, \ |
| pn_sasl_allowed_mechs, pn_sasl_config_name, pn_sasl_config_path, pn_sasl_done, pn_sasl_extended, \ |
| pn_sasl_get_allow_insecure_mechs, pn_sasl_get_mech, pn_sasl_get_user, pn_sasl_outcome, \ |
| pn_sasl_set_allow_insecure_mechs, pn_ssl, pn_ssl_domain, pn_ssl_domain_allow_unsecured_client, pn_ssl_domain_free, \ |
| pn_ssl_domain_set_credentials, pn_ssl_domain_set_peer_authentication, pn_ssl_domain_set_trusted_ca_db, \ |
| pn_ssl_get_cert_fingerprint, pn_ssl_get_cipher_name, pn_ssl_get_peer_hostname, pn_ssl_get_protocol_name, \ |
| pn_ssl_get_remote_subject, pn_ssl_get_remote_subject_subfield, pn_ssl_init, pn_ssl_present, pn_ssl_resume_status, \ |
| pn_ssl_set_peer_hostname, pn_transport, pn_transport_attachments, pn_transport_bind, pn_transport_capacity, \ |
| pn_transport_close_head, pn_transport_close_tail, pn_transport_closed, pn_transport_condition, \ |
| pn_transport_connection, pn_transport_error, pn_transport_get_channel_max, pn_transport_get_frames_input, \ |
| pn_transport_get_frames_output, pn_transport_get_idle_timeout, pn_transport_get_max_frame, \ |
| pn_transport_get_pytracer, pn_transport_get_remote_idle_timeout, pn_transport_get_remote_max_frame, \ |
| pn_transport_get_user, pn_transport_is_authenticated, pn_transport_is_encrypted, pn_transport_log, \ |
| pn_transport_peek, pn_transport_pending, pn_transport_pop, pn_transport_push, pn_transport_remote_channel_max, \ |
| pn_transport_require_auth, pn_transport_require_encryption, pn_transport_set_channel_max, \ |
| pn_transport_set_idle_timeout, pn_transport_set_max_frame, pn_transport_set_pytracer, pn_transport_set_server, \ |
| pn_transport_tick, pn_transport_trace, pn_transport_unbind |
| |
| from ._common import millis2secs, secs2millis, unicode2utf8, utf82unicode |
| from ._condition import cond2obj, obj2cond |
| from ._exceptions import EXCEPTIONS, SSLException, SSLUnavailable, SessionException, TransportException |
| from ._wrapper import Wrapper |
| |
| |
| class TraceAdapter: |
| |
| def __init__(self, tracer): |
| self.tracer = tracer |
| |
| def __call__(self, trans_impl, message): |
| self.tracer(Transport.wrap(trans_impl), message) |
| |
| |
| class Transport(Wrapper): |
| """ |
| A network channel supporting an AMQP connection. |
| """ |
| |
| TRACE_OFF = PN_TRACE_OFF |
| """ Turn logging off entirely. """ |
| |
| TRACE_DRV = PN_TRACE_DRV |
| """ Log driver-related events. """ |
| |
| TRACE_FRM = PN_TRACE_FRM |
| """ Log protocol frames going in and out of the transport. """ |
| |
| TRACE_RAW = PN_TRACE_RAW |
| """ Log raw binary data going in and out of the transport. """ |
| |
| |
| CLIENT = 1 |
| """ Transport mode is as a client. """ |
| |
| SERVER = 2 |
| """ Transport mode is as a server. """ |
| |
| |
| @staticmethod |
| def wrap(impl): |
| if impl is None: |
| return None |
| else: |
| return Transport(_impl=impl) |
| |
| def __init__(self, mode=None, _impl=pn_transport): |
| Wrapper.__init__(self, _impl, pn_transport_attachments) |
| if mode == Transport.SERVER: |
| pn_transport_set_server(self._impl) |
| elif mode is None or mode == Transport.CLIENT: |
| pass |
| else: |
| raise TransportException("Cannot initialise Transport from mode: %s" % str(mode)) |
| |
| def _init(self): |
| self._sasl = None |
| self._ssl = None |
| self._reactor = None |
| self._connect_selectable = None |
| |
| def _check(self, err): |
| if err < 0: |
| exc = EXCEPTIONS.get(err, TransportException) |
| raise exc("[%s]: %s" % (err, pn_error_text(pn_transport_error(self._impl)))) |
| else: |
| return err |
| |
| def _set_tracer(self, tracer): |
| pn_transport_set_pytracer(self._impl, TraceAdapter(tracer)) |
| |
| def _get_tracer(self): |
| adapter = pn_transport_get_pytracer(self._impl) |
| if adapter: |
| return adapter.tracer |
| else: |
| return None |
| |
| tracer = property(_get_tracer, _set_tracer, doc=""" |
| A callback for trace logging. The callback is passed the transport |
| and log message. For no tracer callback, value is ``None``. |
| |
| :type: Tracer callback function |
| """) |
| |
| def log(self, message): |
| """ |
| Log a message using a transport's logging mechanism. |
| |
| This can be useful in a debugging context as the log message will |
| be prefixed with the transport's identifier. |
| |
| :param message: The message to be logged. |
| :type message: ``str`` |
| """ |
| pn_transport_log(self._impl, message) |
| |
| def require_auth(self, bool): |
| """ |
| Set whether a non-authenticated transport connection is allowed. |
| |
| There are several ways within the AMQP protocol suite to get |
| unauthenticated connections: |
| |
| - Use no SASL layer (with either no TLS or TLS without client certificates) |
| - Use a SASL layer but the ANONYMOUS mechanism |
| |
| The default if this option is not set is to allow unauthenticated connections. |
| |
| :param bool: ``True`` when authenticated connections are required. |
| :type bool: ``bool`` |
| """ |
| pn_transport_require_auth(self._impl, bool) |
| |
| @property |
| def authenticated(self): |
| """ |
| Indicate whether the transport connection is authenticated. |
| |
| .. note:: This property may not be stable until the :const:`Event.CONNECTION_REMOTE_OPEN` |
| event is received. |
| |
| :type: ``bool`` |
| """ |
| return pn_transport_is_authenticated(self._impl) |
| |
| def require_encryption(self, bool): |
| """ |
| Set whether a non encrypted transport connection is allowed |
| |
| There are several ways within the AMQP protocol suite to get encrypted connections: |
| |
| - Use TLS |
| - Use a SASL with a mechanism that supports security layers |
| |
| The default if this option is not set is to allow unencrypted connections. |
| |
| :param bool: ``True`` if encryption is required on this transport, ``False`` otherwise. |
| :type bool: ``bool`` |
| """ |
| pn_transport_require_encryption(self._impl, bool) |
| |
| @property |
| def encrypted(self): |
| """ |
| Indicate whether the transport connection is encrypted. |
| |
| .. note:: This property may not be stable until the :const:`Event.CONNECTION_REMOTE_OPEN` |
| event is received. |
| |
| :type: ``bool`` |
| """ |
| return pn_transport_is_encrypted(self._impl) |
| |
| @property |
| def user(self): |
| """ |
| The authenticated user. |
| |
| On the client it will return whatever user was passed in to the |
| :attr:`Connection.user` attribute of the bound connection. |
| |
| The returned value is only reliable after the ``PN_TRANSPORT_AUTHENTICATED`` |
| event has been received. |
| |
| :type: ``str`` |
| """ |
| return pn_transport_get_user(self._impl) |
| |
| def bind(self, connection): |
| """ |
| Assign a connection to the transport. |
| |
| :param connection: Connection to which to bind. |
| :type connection: :class:`Connection` |
| :raise: :exc:`TransportException` if there is any Proton error. |
| """ |
| self._check(pn_transport_bind(self._impl, connection._impl)) |
| |
| def bind_nothrow(self, connection): |
| """ |
| Assign a connection to the transport. Any failure is |
| ignored rather than thrown. |
| |
| :param connection: Connection to which to bind. |
| :type connection: :class:`Connection` |
| """ |
| pn_transport_bind(self._impl, connection._impl) |
| |
| def unbind(self): |
| """ |
| Unbinds a transport from its AMQP connection. |
| |
| :raise: :exc:`TransportException` if there is any Proton error. |
| """ |
| self._check(pn_transport_unbind(self._impl)) |
| |
| def trace(self, n): |
| """ |
| Update a transports trace flags. |
| |
| The trace flags for a transport control what sort of information is |
| logged. The value may be :const:`TRACE_OFF` or any combination of |
| :const:`TRACE_DRV`, :const:`TRACE_FRM`, :const:`TRACE_RAW` using |
| a bitwise or operation. |
| |
| :param n: Trace flags |
| :type n: ``int`` |
| """ |
| pn_transport_trace(self._impl, n) |
| |
| def tick(self, now): |
| """ |
| Process any pending transport timer events (like heartbeat generation). |
| |
| This method should be called after all pending input has been |
| processed by the transport and before generating output. It returns |
| the deadline for the next pending timer event, if any are present. |
| |
| .. note:: This function does nothing until the first data is read |
| from or written to the transport. |
| |
| :param now: seconds since epoch. |
| :type now: ``float`` |
| :return: If non-zero, then the monotonic expiration time of the next |
| pending timer event for the transport. The caller must invoke |
| :meth:`tick` again at least once at or before this deadline |
| occurs. If ``0.0``, then there are no pending events. |
| :rtype: ``float`` |
| """ |
| return millis2secs(pn_transport_tick(self._impl, secs2millis(now))) |
| |
| def capacity(self): |
| """ |
| Get the amount of free space for input following the transport's |
| tail pointer. |
| |
| :return: Available space for input in bytes. |
| :rtype: ``int`` |
| :raise: :exc:`TransportException` if there is any Proton error. |
| """ |
| c = pn_transport_capacity(self._impl) |
| if c >= PN_EOS: |
| return c |
| else: |
| return self._check(c) |
| |
| def push(self, binary): |
| """ |
| Pushes the supplied bytes into the tail of the transport. |
| Only some of the bytes will be copied if there is insufficient |
| capacity available. Use :meth:`capacity` to determine how much |
| capacity the transport has. |
| |
| :param binary: Data to be pushed onto the transport tail. |
| :type binary: ``bytes`` |
| :raise: - :exc:`TransportException` if there is any Proton error. |
| - ``OverflowError`` if the size of the data exceeds the |
| transport capacity. |
| """ |
| n = self._check(pn_transport_push(self._impl, binary)) |
| if n != len(binary): |
| raise OverflowError("unable to process all bytes: %s, %s" % (n, len(binary))) |
| |
| def close_tail(self): |
| """ |
| Indicate that the input has reached End Of Stream (EOS). |
| |
| This tells the transport that no more input will be forthcoming. |
| |
| :raise: :exc:`TransportException` if there is any Proton error. |
| """ |
| self._check(pn_transport_close_tail(self._impl)) |
| |
| def pending(self): |
| """ |
| Get the number of pending output bytes following the transport's |
| head pointer. |
| |
| :return: The number of pending output bytes. |
| :rtype: ``int`` |
| :raise: :exc:`TransportException` if there is any Proton error. |
| """ |
| p = pn_transport_pending(self._impl) |
| if p >= PN_EOS: |
| return p |
| else: |
| return self._check(p) |
| |
| def peek(self, size): |
| """ |
| Returns ``size`` bytes from the head of the transport. |
| |
| It is an error to call this with a value of ``size`` that |
| is greater than the value reported by :meth:`pending`. |
| |
| :param size: Number of bytes to return. |
| :type size: ``int`` |
| :return: ``size`` bytes from the head of the transport, or ``None`` |
| if none are available. |
| :rtype: ``bytes`` |
| :raise: :exc:`TransportException` if there is any Proton error. |
| """ |
| cd, out = pn_transport_peek(self._impl, size) |
| if cd == PN_EOS: |
| return None |
| else: |
| self._check(cd) |
| return out |
| |
| def pop(self, size): |
| """ |
| Removes ``size`` bytes of output from the pending output queue |
| following the transport's head pointer. |
| |
| Calls to this function may alter the transport's head pointer as |
| well as the number of pending bytes reported by |
| :meth:`pending`. |
| |
| :param size: Number of bytes to return. |
| :type size: ``int`` |
| """ |
| pn_transport_pop(self._impl, size) |
| |
| def close_head(self): |
| """ |
| Indicate that the output has closed. |
| |
| This tells the transport that no more output will be popped. |
| |
| :raise: :exc:`TransportException` if there is any Proton error. |
| """ |
| self._check(pn_transport_close_head(self._impl)) |
| |
| @property |
| def closed(self): |
| """ |
| ``True`` iff both the transport head and transport tail are closed |
| using :meth:`close_head` and :meth:`close_tail` respectively. |
| |
| :type: ``bool`` |
| """ |
| return pn_transport_closed(self._impl) |
| |
| # AMQP 1.0 max-frame-size |
| def _get_max_frame_size(self): |
| return pn_transport_get_max_frame(self._impl) |
| |
| def _set_max_frame_size(self, value): |
| pn_transport_set_max_frame(self._impl, value) |
| |
| max_frame_size = property(_get_max_frame_size, _set_max_frame_size, doc=""" |
| The maximum size for transport frames (in bytes). |
| |
| :type: ``int`` |
| """) |
| |
| @property |
| def remote_max_frame_size(self): |
| """ |
| The maximum frame size of a transport's remote peer (in bytes). |
| |
| :type: ``int`` |
| """ |
| return pn_transport_get_remote_max_frame(self._impl) |
| |
| def _get_channel_max(self): |
| return pn_transport_get_channel_max(self._impl) |
| |
| def _set_channel_max(self, value): |
| if pn_transport_set_channel_max(self._impl, value): |
| raise SessionException("Too late to change channel max.") |
| |
| channel_max = property(_get_channel_max, _set_channel_max, doc=""" |
| The maximum channel number that may be used on this transport. |
| |
| .. note:: This is the maximum channel number allowed, giving a |
| valid channel number range of ``[0 .. channel_max]``. Therefore the |
| maximum number of simultaneously active channels will be |
| channel_max plus 1. |
| |
| You can set this more than once to raise and lower |
| the limit your application imposes on max channels for this |
| transport. However, smaller limits may be imposed by Proton, |
| or by the remote peer. |
| |
| After the ``OPEN`` frame has been sent to the remote peer, |
| further calls to this function will have no effect. |
| |
| :type: ``int`` |
| :raise: :exc:`SessionException` if the ``OPEN`` frame has already |
| been sent. |
| """) |
| |
| @property |
| def remote_channel_max(self): |
| """ |
| The maximum allowed channel number of a transport's remote peer. |
| |
| :type: ``int`` |
| """ |
| return pn_transport_remote_channel_max(self._impl) |
| |
| # AMQP 1.0 idle-time-out |
| def _get_idle_timeout(self): |
| return millis2secs(pn_transport_get_idle_timeout(self._impl)) |
| |
| def _set_idle_timeout(self, sec): |
| pn_transport_set_idle_timeout(self._impl, secs2millis(sec)) |
| |
| idle_timeout = property(_get_idle_timeout, _set_idle_timeout, doc=""" |
| The idle timeout of the connection in seconds. A zero idle |
| timeout means heartbeats are disabled. |
| |
| :type: ``float`` |
| """) |
| |
| @property |
| def remote_idle_timeout(self): |
| """ |
| Get the idle timeout for a transport's remote peer in |
| seconds. A zero idle timeout means heartbeats are disabled. |
| |
| :type: ``float`` |
| """ |
| return millis2secs(pn_transport_get_remote_idle_timeout(self._impl)) |
| |
| @property |
| def frames_output(self): |
| """ |
| Get the number of frames output by a transport. |
| |
| :type: ``int`` |
| """ |
| return pn_transport_get_frames_output(self._impl) |
| |
| @property |
| def frames_input(self): |
| """ |
| Get the number of frames input by a transport. |
| |
| :type: ``int`` |
| """ |
| return pn_transport_get_frames_input(self._impl) |
| |
| def sasl(self): |
| """ |
| Get the :class:`SASL` object associated with this transport. |
| |
| :return: SASL object associated with this transport. |
| :rtype: :class:`SASL` |
| """ |
| return SASL(self) |
| |
| def ssl(self, domain=None, session_details=None): |
| """ |
| Get the :class:`SSL` session associated with this transport. If |
| not set, then a new session will be created using ``domain`` and |
| ``session_details``. |
| |
| :param domain: An SSL domain configuration object |
| :type domain: :class:`SSLDomain` |
| :param session_details: A unique identifier for the SSL session. |
| :type session_details: :class:`SSLSessionDetails` |
| :return: SSL session associated with this transport. |
| :rtype: :class:`SSL` |
| """ |
| # SSL factory (singleton for this transport) |
| if not self._ssl: |
| self._ssl = SSL(self, domain, session_details) |
| return self._ssl |
| |
| def _get_condition(self): |
| return cond2obj(pn_transport_condition(self._impl)) |
| |
| def _set_condition(self, cond): |
| pn_cond = pn_transport_condition(self._impl) |
| obj2cond(cond, pn_cond) |
| |
| condition = property(_get_condition, _set_condition, doc=""" |
| Get additional information about the condition of the transport. |
| |
| When a :const:`Event.TRANSPORT_ERROR` event occurs, this operation |
| can be used to access the details of the error condition. |
| |
| See :class:`Condition` for more information. |
| |
| :type: :class:`Condition` |
| """) |
| |
| @property |
| def connection(self): |
| """ |
| The connection bound to this transport. |
| |
| :type: :class:`Connection` |
| """ |
| from . import _endpoints |
| return _endpoints.Connection.wrap(pn_transport_connection(self._impl)) |
| |
| |
| class SASLException(TransportException): |
| pass |
| |
| |
| class SASL(Wrapper): |
| """ |
| The SASL layer is responsible for establishing an authenticated |
| and/or encrypted tunnel over which AMQP frames are passed between |
| peers. The peer acting as the SASL Client must provide |
| authentication credentials. The peer acting as the SASL Server must |
| provide authentication against the received credentials. |
| """ |
| OK = PN_SASL_OK |
| AUTH = PN_SASL_AUTH |
| SYS = PN_SASL_SYS |
| PERM = PN_SASL_PERM |
| TEMP = PN_SASL_TEMP |
| |
| @staticmethod |
| def extended(): |
| """ |
| Check for support of extended SASL negotiation. |
| |
| All implementations of Proton support ``ANONYMOUS`` and ``EXTERNAL`` on both |
| client and server sides and ``PLAIN`` on the client side. |
| |
| Extended SASL implementations use an external library (Cyrus SASL) |
| to support other mechanisms beyond these basic ones. |
| |
| :rtype: ``True`` if we support extended SASL negotiation, ``False`` if |
| we only support basic negotiation. |
| """ |
| return pn_sasl_extended() |
| |
| def __init__(self, transport): |
| Wrapper.__init__(self, transport._impl, pn_transport_attachments) |
| self._sasl = pn_sasl(transport._impl) |
| |
| def _check(self, err): |
| if err < 0: |
| exc = EXCEPTIONS.get(err, SASLException) |
| raise exc("[%s]" % (err)) |
| else: |
| return err |
| |
| @property |
| def user(self): |
| """ |
| Retrieve the authenticated user. This is usually used at the the |
| server end to find the name of the authenticated user. |
| |
| If :meth:`outcome` returns a value other than :const:`OK`, then |
| there will be no user to return. The returned value is only reliable |
| after the ``PN_TRANSPORT_AUTHENTICATED`` event has been received. |
| |
| :rtype: * If the SASL layer was not negotiated then ``0`` is returned. |
| * If the ``ANONYMOUS`` mechanism is used then the user will be |
| ``"anonymous"``. |
| * Otherwise a string containing the user is |
| returned. |
| """ |
| return pn_sasl_get_user(self._sasl) |
| |
| @property |
| def mech(self): |
| """ |
| Return the selected SASL mechanism. |
| |
| The returned value is only reliable after the ``PN_TRANSPORT_AUTHENTICATED`` |
| event has been received. |
| |
| :rtype: The authentication mechanism selected by the SASL layer. |
| """ |
| return pn_sasl_get_mech(self._sasl) |
| |
| @property |
| def outcome(self): |
| """ |
| Retrieve the outcome of SASL negotiation. |
| |
| :rtype: * ``None`` if no negotiation has taken place. |
| * Otherwise the outcome of the negotiation. |
| """ |
| outcome = pn_sasl_outcome(self._sasl) |
| if outcome == PN_SASL_NONE: |
| return None |
| else: |
| return outcome |
| |
| def allowed_mechs(self, mechs): |
| """ |
| SASL mechanisms that are to be considered for authentication. |
| |
| This can be used on either the client or the server to restrict |
| the SASL mechanisms that may be used to the mechanisms on the list. |
| |
| **NOTE:** By default the ``GSSAPI`` and ``GSS-SPNEGO`` mechanisms |
| are not enabled for clients. This is because these mechanisms have |
| the problematic behaviour of 'capturing' the client whenever they |
| are installed so that they will be used by the client if offered by |
| the server even if the client can't successfully authenticate this |
| way. This can lead to some very hard to debug failures. |
| |
| **NOTE:** The ``GSSAPI`` or ``GSS-SPNEGO`` mechanisms need to be |
| explicitly enabled if they are required (together with any other |
| required mechanisms). |
| |
| :param mechs: A list of mechanisms that are allowed for authentication, |
| either a string containing a space-separated list of mechs |
| ``"mech1 mech2 ..."``, or a Python list of strings |
| ``["mech1", "mech2", ...]``. |
| :type mechs: string |
| """ |
| if isinstance(mechs, list): |
| mechs = " ".join(mechs) |
| pn_sasl_allowed_mechs(self._sasl, unicode2utf8(mechs)) |
| |
| def _get_allow_insecure_mechs(self): |
| return pn_sasl_get_allow_insecure_mechs(self._sasl) |
| |
| def _set_allow_insecure_mechs(self, insecure): |
| pn_sasl_set_allow_insecure_mechs(self._sasl, insecure) |
| |
| allow_insecure_mechs = property(_get_allow_insecure_mechs, _set_allow_insecure_mechs, |
| doc=""" |
| Allow unencrypted cleartext passwords (PLAIN mech) |
| """) |
| |
| def done(self, outcome): |
| """ |
| Set the outcome of SASL negotiation. Used by the server to set the |
| result of the negotiation process. |
| """ |
| pn_sasl_done(self._sasl, outcome) |
| |
| def config_name(self, name): |
| """ |
| Set the SASL configuration name. This is used to construct the SASL |
| configuration filename. In the current implementation ``".conf"`` is |
| added to the name and the file is looked for in the configuration |
| directory. |
| |
| If not set it will default to ``"proton-server"`` for a sasl server |
| and ``"proton-client"`` for a client. |
| |
| :param name: The configuration name. |
| :type name: string |
| """ |
| pn_sasl_config_name(self._sasl, name) |
| |
| def config_path(self, path): |
| """ |
| Set the SASL configuration path. This is used to tell SASL where |
| to look for the configuration file. In the current implementation |
| it can be a colon separated list of directories. |
| |
| The environment variable ``PN_SASL_CONFIG_PATH`` can also be used |
| to set this path, but if both methods are used then this |
| :meth:`config_path` will take precedence. |
| |
| If not set, the underlying implementation default will be used. |
| |
| :param path: The configuration path, may contain colon-separated list |
| if more than one path is specified. |
| :type path: string |
| """ |
| pn_sasl_config_path(self._sasl, path) |
| |
| |
| class SSLDomain(object): |
| """ |
| An SSL configuration domain, used to hold the SSL configuration |
| for one or more SSL sessions. |
| """ |
| |
| MODE_CLIENT = PN_SSL_MODE_CLIENT |
| """Local connection endpoint is an SSL client.""" |
| |
| MODE_SERVER = PN_SSL_MODE_SERVER |
| """Local connection endpoint is an SSL server.""" |
| |
| VERIFY_PEER = PN_SSL_VERIFY_PEER |
| """Require peer to provide a valid identifying certificate.""" |
| |
| VERIFY_PEER_NAME = PN_SSL_VERIFY_PEER_NAME |
| """Require valid certificate and matching name.""" |
| |
| ANONYMOUS_PEER = PN_SSL_ANONYMOUS_PEER |
| """Do not require a certificate nor cipher authorization.""" |
| |
| |
| def __init__(self, mode): |
| self._domain = pn_ssl_domain(mode) |
| if self._domain is None: |
| raise SSLUnavailable() |
| |
| def _check(self, err): |
| if err < 0: |
| exc = EXCEPTIONS.get(err, SSLException) |
| raise exc("SSL failure.") |
| else: |
| return err |
| |
| def set_credentials(self, cert_file, key_file, password): |
| """ |
| Set the certificate that identifies the local node to the remote. |
| |
| This certificate establishes the identity for the local node for all :class:`SSL` sessions |
| created from this domain. It will be sent to the remote if the remote needs to verify |
| the identity of this node. This may be used for both SSL servers and SSL clients (if |
| client authentication is required by the server). |
| |
| .. note:: This setting effects only those :class:`SSL` objects created after this call |
| returns. :class:`SSL` objects created before invoking this method will use the domain's |
| previous setting. |
| |
| :param cert_file: Specifier for the file/database containing the identifying |
| certificate. For Openssl users, this is a PEM file. For Windows SChannel |
| users, this is the PKCS#12 file or system store. |
| :type cert_file: ``str`` |
| :param key_file: An optional key to access the identifying certificate. For |
| Openssl users, this is an optional PEM file containing the private key |
| used to sign the certificate. For Windows SChannel users, this is the |
| friendly name of the self-identifying certificate if there are multiple |
| certificates in the store. |
| :type key_file: ``str`` |
| :param password: The password used to sign the key, else ``None`` if key is not |
| protected. |
| :type password: ``str`` or ``None`` |
| :return: 0 on success |
| :rtype: ``int`` |
| :raise: :exc:`SSLException` if there is any Proton error |
| """ |
| return self._check(pn_ssl_domain_set_credentials(self._domain, |
| cert_file, key_file, |
| password)) |
| |
| def set_trusted_ca_db(self, certificate_db): |
| """ |
| Configure the set of trusted CA certificates used by this domain to verify peers. |
| |
| If the local SSL client/server needs to verify the identity of the remote, it must |
| validate the signature of the remote's certificate. This function sets the database of |
| trusted CAs that will be used to verify the signature of the remote's certificate. |
| |
| .. note:: This setting effects only those :class:`SSL` objects created after this call |
| returns. :class:`SSL` objects created before invoking this method will use the domain's |
| previous setting. |
| |
| .. note:: By default the list of trusted CA certificates will be set to the system default. |
| What this is is depends on the OS and the SSL implementation used: For OpenSSL the default |
| will depend on how the OS is set up. When using the Windows SChannel implementation the default |
| will be the users default trusted certificate store. |
| |
| :param certificate_db: Database of trusted CAs, used to authenticate the peer. |
| :type certificate_db: ``str`` |
| :return: 0 on success |
| :rtype: ``int`` |
| :raise: :exc:`SSLException` if there is any Proton error |
| """ |
| return self._check(pn_ssl_domain_set_trusted_ca_db(self._domain, |
| certificate_db)) |
| |
| def set_peer_authentication(self, verify_mode, trusted_CAs=None): |
| """ |
| This method controls how the peer's certificate is validated, if at all. By default, |
| neither servers nor clients attempt to verify their peers (PN_SSL_ANONYMOUS_PEER). |
| Once certificates and trusted CAs are configured, peer verification can be enabled. |
| |
| .. note:: In order to verify a peer, a trusted CA must be configured. See |
| :meth:`set_trusted_ca_db`. |
| |
| .. note:: Servers must provide their own certificate when verifying a peer. See |
| :meth:`set_credentials`. |
| |
| .. note:: This setting effects only those :class:`SSL` objects created after this call |
| returns. :class:`SSL` objects created before invoking this method will use the domain's |
| previous setting. |
| |
| :param verify_mode: The level of validation to apply to the peer, one of :const:`VERIFY_PEER`, |
| :const:`VERIFY_PEER_NAME`, :const:`ANONYMOUS_PEER`, |
| :type verify_mode: ``int`` |
| :param trusted_CAs: Path to a database of trusted CAs that the server will advertise. |
| :type trusted_CAs: ``str`` |
| :return: 0 on success |
| :rtype: ``int`` |
| :raise: :exc:`SSLException` if there is any Proton error |
| """ |
| return self._check(pn_ssl_domain_set_peer_authentication(self._domain, |
| verify_mode, |
| trusted_CAs)) |
| |
| def allow_unsecured_client(self): |
| """ |
| Permit a server to accept connection requests from non-SSL clients. |
| |
| This configures the server to "sniff" the incoming client data stream, |
| and dynamically determine whether SSL/TLS is being used. This option |
| is disabled by default: only clients using SSL/TLS are accepted. |
| |
| :raise: :exc:`SSLException` if there is any Proton error |
| """ |
| return self._check(pn_ssl_domain_allow_unsecured_client(self._domain)) |
| |
| def __del__(self): |
| pn_ssl_domain_free(self._domain) |
| |
| |
| class SSL(object): |
| """ |
| An SSL session associated with a transport. A transport must have |
| an SSL object in order to "speak" SSL over its connection. |
| """ |
| |
| @staticmethod |
| def present(): |
| """ |
| Tests for an SSL implementation being present. |
| |
| :return: ``True`` if we support SSL, ``False`` if not. |
| :rtype: ``bool`` |
| """ |
| return pn_ssl_present() |
| |
| def _check(self, err): |
| if err < 0: |
| exc = EXCEPTIONS.get(err, SSLException) |
| raise exc("SSL failure.") |
| else: |
| return err |
| |
| def __new__(cls, transport, domain, session_details=None): |
| """Enforce a singleton SSL object per Transport""" |
| if transport._ssl: |
| # unfortunately, we've combined the allocation and the configuration in a |
| # single step. So catch any attempt by the application to provide what |
| # may be a different configuration than the original (hack) |
| ssl = transport._ssl |
| if (domain and (ssl._domain is not domain) or |
| session_details and (ssl._session_details is not session_details)): |
| raise SSLException("Cannot re-configure existing SSL object!") |
| else: |
| obj = super(SSL, cls).__new__(cls) |
| obj._domain = domain |
| obj._session_details = session_details |
| session_id = None |
| if session_details: |
| session_id = session_details.get_session_id() |
| obj._ssl = pn_ssl(transport._impl) |
| if obj._ssl is None: |
| raise SSLUnavailable() |
| if domain: |
| pn_ssl_init(obj._ssl, domain._domain, session_id) |
| transport._ssl = obj |
| return transport._ssl |
| |
| def cipher_name(self): |
| """ |
| Get the name of the Cipher that is currently in use. |
| |
| Gets a text description of the cipher that is currently active, or |
| returns ``None`` if SSL is not active (no cipher). |
| |
| .. note:: The cipher in use may change over time due to renegotiation |
| or other changes to the SSL state. |
| |
| :return: The cypher name, or ``None`` if no cipher in use. |
| :rtype: ``str`` or ``None`` |
| """ |
| rc, name = pn_ssl_get_cipher_name(self._ssl, 128) |
| if rc: |
| return name |
| return None |
| |
| def protocol_name(self): |
| """ |
| Get the name of the SSL protocol that is currently in use. |
| |
| Gets a text description of the SSL protocol that is currently active, |
| or returns ``None`` if SSL is not active. |
| |
| .. note:: The protocol may change over time due to renegotiation. |
| |
| :return: The protocol name if SSL is active, or ``None`` if SSL connection |
| is not ready or active. |
| :rtype: ``str`` or ``None`` |
| """ |
| rc, name = pn_ssl_get_protocol_name(self._ssl, 128) |
| if rc: |
| return name |
| return None |
| |
| SHA1 = PN_SSL_SHA1 |
| """Produces hash that is 20 bytes long using SHA-1""" |
| |
| SHA256 = PN_SSL_SHA256 |
| """Produces hash that is 32 bytes long using SHA-256""" |
| |
| SHA512 = PN_SSL_SHA512 |
| """Produces hash that is 64 bytes long using SHA-512""" |
| |
| MD5 = PN_SSL_MD5 |
| """Produces hash that is 16 bytes long using MD5""" |
| |
| CERT_COUNTRY_NAME = PN_SSL_CERT_SUBJECT_COUNTRY_NAME |
| """Certificate country name 2-char ISO code""" |
| |
| CERT_STATE_OR_PROVINCE = PN_SSL_CERT_SUBJECT_STATE_OR_PROVINCE |
| """Certificate state or province, not abbreviated""" |
| |
| CERT_CITY_OR_LOCALITY = PN_SSL_CERT_SUBJECT_CITY_OR_LOCALITY |
| """Certificate city or place name, not abbreviated""" |
| |
| CERT_ORGANIZATION_NAME = PN_SSL_CERT_SUBJECT_ORGANIZATION_NAME |
| """Certificate organization name""" |
| |
| CERT_ORGANIZATION_UNIT = PN_SSL_CERT_SUBJECT_ORGANIZATION_UNIT |
| """Certificate organization unit or division within organization""" |
| |
| CERT_COMMON_NAME = PN_SSL_CERT_SUBJECT_COMMON_NAME |
| """Certificate common name or URL""" |
| |
| def get_cert_subject_subfield(self, subfield_name): |
| """ |
| Returns a string that contains the value of the sub field of |
| the subject field in the ssl certificate. The subject field |
| usually contains the following values: |
| |
| * :const:`CERT_COUNTRY_NAME` |
| * :const:`CERT_STATE_OR_PROVINCE` |
| * :const:`CERT_CITY_OR_LOCALITY` |
| * :const:`CERT_ORGANIZATION_NAME` |
| * :const:`CERT_ORGANIZATION_UNIT` |
| * :const:`CERT_COMMON_NAME` |
| |
| :param subfield_name: The enumeration representing the required |
| sub field listed above |
| :type subfield_name: ``int`` |
| :return: A string which contains the requested sub field value which |
| is valid until the ssl object is destroyed. |
| :rtype: ``str`` |
| """ |
| subfield_value = pn_ssl_get_remote_subject_subfield(self._ssl, subfield_name) |
| return subfield_value |
| |
| def get_cert_subject(self): |
| """ |
| Get the subject from the peer's certificate. |
| |
| :return: A string containing the full subject. |
| :rtype: ``str`` |
| """ |
| subject = pn_ssl_get_remote_subject(self._ssl) |
| return subject |
| |
| def _get_cert_subject_unknown_subfield(self): |
| # Pass in an unhandled enum |
| return self.get_cert_subject_subfield(10) |
| |
| # Convenience functions for obtaining the subfields of the subject field. |
| def get_cert_common_name(self): |
| """ |
| A convenience method to get a string that contains the :const:`CERT_COMMON_NAME` |
| sub field of the subject field in the ssl certificate. |
| |
| :return: A string containing the :const:`CERT_COMMON_NAME` sub field. |
| :rtype: ``str`` |
| """ |
| return self.get_cert_subject_subfield(SSL.CERT_COMMON_NAME) |
| |
| def get_cert_organization(self): |
| """ |
| A convenience method to get a string that contains the :const:`CERT_ORGANIZATION_NAME` |
| sub field of the subject field in the ssl certificate. |
| |
| :return: A string containing the :const:`CERT_ORGANIZATION_NAME` sub field. |
| :rtype: ``str`` |
| """ |
| return self.get_cert_subject_subfield(SSL.CERT_ORGANIZATION_NAME) |
| |
| def get_cert_organization_unit(self): |
| """ |
| A convenience method to get a string that contains the :const:`CERT_ORGANIZATION_UNIT` |
| sub field of the subject field in the ssl certificate. |
| |
| :return: A string containing the :const:`CERT_ORGANIZATION_UNIT` sub field. |
| :rtype: ``str`` |
| """ |
| return self.get_cert_subject_subfield(SSL.CERT_ORGANIZATION_UNIT) |
| |
| def get_cert_locality_or_city(self): |
| """ |
| A convenience method to get a string that contains the :const:`CERT_CITY_OR_LOCALITY` |
| sub field of the subject field in the ssl certificate. |
| |
| :return: A string containing the :const:`CERT_CITY_OR_LOCALITY` sub field. |
| :rtype: ``str`` |
| """ |
| return self.get_cert_subject_subfield(SSL.CERT_CITY_OR_LOCALITY) |
| |
| def get_cert_country(self): |
| """ |
| A convenience method to get a string that contains the :const:`CERT_COUNTRY_NAME` |
| sub field of the subject field in the ssl certificate. |
| |
| :return: A string containing the :const:`CERT_COUNTRY_NAME` sub field. |
| :rtype: ``str`` |
| """ |
| return self.get_cert_subject_subfield(SSL.CERT_COUNTRY_NAME) |
| |
| def get_cert_state_or_province(self): |
| """ |
| A convenience method to get a string that contains the :const:`CERT_STATE_OR_PROVINCE` |
| sub field of the subject field in the ssl certificate. |
| |
| :return: A string containing the :const:`CERT_STATE_OR_PROVINCE` sub field. |
| :rtype: ``str`` |
| """ |
| return self.get_cert_subject_subfield(SSL.CERT_STATE_OR_PROVINCE) |
| |
| def get_cert_fingerprint(self, fingerprint_length, digest_name): |
| """ |
| Get the fingerprint of the certificate. The certificate fingerprint |
| (as displayed in the Fingerprints section when looking at a certificate |
| with say the Firefox browser) is the hexadecimal hash of the entire |
| certificate. The fingerprint is not part of the certificate, rather |
| it is computed from the certificate and can be used to uniquely identify |
| a certificate. |
| |
| :param fingerprint_length: Must be :math:`>= 33` for md5, :math:`>= 41` |
| for sha1, :math:`>= 65` for sha256 and :math:`>= 129` |
| for sha512. |
| :type fingerprint_length: ``int`` |
| :param digest_name: The hash algorithm to use. Must be one of :const:`SHA1`, |
| :const:`SHA256`, :const:`SHA512`, :const:`MD5`. |
| :type digest_name: ``str`` |
| :return: Hex fingerprint in a string, or ``None`` if an error occurred. |
| :rtype: ``str`` or ``None`` |
| """ |
| rc, fingerprint_str = pn_ssl_get_cert_fingerprint(self._ssl, fingerprint_length, digest_name) |
| if rc == PN_OK: |
| return fingerprint_str |
| return None |
| |
| # Convenience functions for obtaining fingerprint for specific hashing algorithms |
| def _get_cert_fingerprint_unknown_hash_alg(self): |
| return self.get_cert_fingerprint(41, 10) |
| |
| def get_cert_fingerprint_sha1(self): |
| """ |
| A convenience method to get the :const:`SHA1` fingerprint of the |
| certificate. |
| |
| :return: Hex fingerprint in a string, or ``None`` if an error occurred. |
| :rtype: ``str`` or ``None`` |
| """ |
| return self.get_cert_fingerprint(41, SSL.SHA1) |
| |
| def get_cert_fingerprint_sha256(self): |
| """ |
| A convenience method to get the :const:`SHA256` fingerprint of the |
| certificate. |
| |
| :return: Hex fingerprint in a string, or ``None`` if an error occurred. |
| :rtype: ``str`` or ``None`` |
| """ |
| # sha256 produces a fingerprint that is 64 characters long |
| return self.get_cert_fingerprint(65, SSL.SHA256) |
| |
| def get_cert_fingerprint_sha512(self): |
| """ |
| A convenience method to get the :const:`SHA512` fingerprint of the |
| certificate. |
| |
| :return: Hex fingerprint in a string, or ``None`` if an error occurred. |
| :rtype: ``str`` or ``None`` |
| """ |
| # sha512 produces a fingerprint that is 128 characters long |
| return self.get_cert_fingerprint(129, SSL.SHA512) |
| |
| def get_cert_fingerprint_md5(self): |
| """ |
| A convenience method to get the :const:`MD5` fingerprint of the |
| certificate. |
| |
| :return: Hex fingerprint in a string, or ``None`` if an error occurred. |
| :rtype: ``str`` or ``None`` |
| """ |
| return self.get_cert_fingerprint(33, SSL.MD5) |
| |
| @property |
| def remote_subject(self): |
| """ |
| The subject from the peers certificate. |
| |
| :type: ``str`` |
| """ |
| return pn_ssl_get_remote_subject(self._ssl) |
| |
| RESUME_UNKNOWN = PN_SSL_RESUME_UNKNOWN |
| """Session resume state unknown/not supported.""" |
| |
| RESUME_NEW = PN_SSL_RESUME_NEW |
| """Session renegotiated - not resumed.""" |
| |
| RESUME_REUSED = PN_SSL_RESUME_REUSED |
| """Session resumed from previous session.""" |
| |
| def resume_status(self): |
| """ |
| Check whether the state has been resumed. |
| |
| Used for client session resume. When called on an active session, |
| indicates whether the state has been resumed from a previous session. |
| |
| .. note:: This is a best-effort service - there is no guarantee that |
| the remote server will accept the resumed parameters. The remote |
| server may choose to ignore these parameters, and request a |
| re-negotiation instead. |
| |
| :return: Status code indicating whether or not the session has been |
| resumed. One of: |
| * :const:`RESUME_UNKNOWN` |
| * :const:`RESUME_NEW` |
| * :const:`RESUME_REUSED` |
| :rtype: ``int`` |
| """ |
| return pn_ssl_resume_status(self._ssl) |
| |
| def _set_peer_hostname(self, hostname): |
| self._check(pn_ssl_set_peer_hostname(self._ssl, unicode2utf8(hostname))) |
| |
| def _get_peer_hostname(self): |
| err, name = pn_ssl_get_peer_hostname(self._ssl, 1024) |
| self._check(err) |
| return utf82unicode(name) |
| |
| peer_hostname = property(_get_peer_hostname, _set_peer_hostname, doc=""" |
| Manage the expected name of the remote peer. |
| |
| The hostname is used for two purposes: |
| |
| 1. when set on an SSL client, it is sent to the server during the |
| handshake (if Server Name Indication is supported) |
| 2. it is used to check against the identifying name provided in the |
| peer's certificate. If the supplied name does not exactly match a |
| SubjectAltName (type DNS name), or the CommonName entry in the |
| peer's certificate, the peer is considered unauthenticated |
| (potential imposter), and the SSL connection is aborted. |
| |
| .. note:: Verification of the hostname is only done if |
| :const:`SSLDomain.VERIFY_PEER_NAME` is set using |
| :meth:`SSLDomain.set_peer_authentication`. |
| |
| :type: ``str`` |
| """) |
| |
| |
| class SSLSessionDetails(object): |
| """ |
| Unique identifier for the SSL session. Used to resume previous |
| session on a new SSL connection. |
| """ |
| |
| def __init__(self, session_id): |
| self._session_id = session_id |
| |
| def get_session_id(self): |
| """ |
| Get the unique identifier for this SSL session |
| |
| :return: Session identifier |
| :rtype: ``str`` |
| """ |
| return self._session_id |