def set_content_handler(self, content_handler=None): """Set the content type used by the connection. :param content_handler: The content handler to use. * If None, use :py:class:`~common:RestAuthCommon.handlers.JSONContentHandler`. * If an instance of :py:class:`~common:RestAuthCommon.handlers.ContentHandler`, use that instance unchanged. * If a str, it is asumed to be one of the MIME types specified in :py:data:`~common:RestAuthCommon.handlers.CONTENT_HANDLERS`. :type content_handler: str or :py:class:`~common:RestAuthCommon.handlers.ContentHandler` :raise RestAuthRuntimeException: If an invalid content handler was supplied. """ if content_handler is None: self.content_handler = JSONContentHandler() elif isinstance(content_handler, ContentHandler): self.content_handler = content_handler elif isinstance(content_handler, basestring): try: cl = CONTENT_HANDLERS[content_handler] except KeyError: raise error.RestAuthRuntimeException( "Unknown content_handler: %s" % content_handler) self.content_handler = cl() else: raise error.RestAuthRuntimeException("Unknown content handler defined.") self.mime = self.content_handler.mime
class RestAuthConnection(object): """An instance of this class represents a connection to a RestAuth service. .. NOTE: The constructor does not verify that the connection actually works. Since HTTP is stateless, there is no way of knowing if a connection working now will still work 0.2 seconds from now. .. versionadded:: 0.6.2 The ssl_context, timeout and source_address parameters. :param host: The hostname of the RestAuth service :type host: str :param user: The service name to use for authenticating with RestAuth (passed to :py:meth:`.set_credentials`). :type user: str :param passwd: The password to use for authenticating with RestAuth (passed to :py:meth:`.set_credentials`). :type passwd: str :param content_handler: Directly passed to :py:meth:`.set_content_handler`. :type content_handler: str or :py:class:`~common:RestAuthCommon.handlers.ContentHandler` :param ssl_context: Use a different SSL context for this connection. **This parameter requires Python3.** The default varies depending on the Python version used: * In Python 3.4 or later, the default is created with :py:func:`~ssl.create_default_context`. * In Python 3.2 and 3.3, a context is created with :py:data:`~ssl.PROTOCOL_SSLv23` as protocol, :py:data:`~ssl.CERT_REQUIRED` as :py:attr:`~ssl.SSLContext.verify_mode` and the certificate chain loaded by :py:meth:`~ssl.set_default_verify_paths`. :type ssl_context: :py:class:`~ssl.SSLContext` :param timeout: Timeout for HTTP connections. If omitted, use the systems default. :type timeout: float :param source_address: A tuple of ``(host, port)`` to make connections from. :type source_address: tuple """ context = None _user = RestAuthUser _group = RestAuthGroup def __init__(self, host, user, passwd, content_handler=None, ssl_context=None, timeout=None, source_address=None): """Initialize a new connection to a RestAuth service.""" parseresult = urlparse(host) self._conn_kwargs = { 'host': parseresult.netloc, } if parseresult.scheme == 'https': # pragma: no cover self._conn = client.HTTPSConnection # Add SSLContext in Python3 if ssl_context is not None: self._conn_kwargs['context'] = ssl_context elif PY34: # pragma: py34 self._conn_kwargs['context'] = ssl.create_default_context() elif PY3: # pragma: no branch, py3 context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) context.verify_mode = ssl.CERT_REQUIRED context.set_default_verify_paths() self._conn_kwargs['context'] = context else: self._conn = client.HTTPConnection # Add optional parameters if timeout is not None: self._conn_kwargs['timeout'] = timeout if source_address is not None: self._conn_kwargs['source_address'] = source_address # Set credentials, authentication header self.set_content_handler(content_handler) self.set_credentials(user, passwd) def set_credentials(self, user, passwd): """Set new credentials for the connection. :param user: The user for whom the password should be changed. :type user: str :param passwd: The password to use :type passwd: str """ raw_credentials = '%s:%s' % (user, passwd) enc_credentials = base64.b64encode(raw_credentials.encode()) self.auth_header = "Basic %s" % enc_credentials.decode() def set_content_handler(self, content_handler=None): """Set the content type used by the connection. :param content_handler: The content handler to use. * If None, use :py:class:`~common:RestAuthCommon.handlers.JSONContentHandler`. * If an instance of :py:class:`~common:RestAuthCommon.handlers.ContentHandler`, use that instance unchanged. * If a str, it is asumed to be one of the MIME types specified in :py:data:`~common:RestAuthCommon.handlers.CONTENT_HANDLERS`. :type content_handler: str or :py:class:`~common:RestAuthCommon.handlers.ContentHandler` :raise RestAuthRuntimeException: If an invalid content handler was supplied. """ if content_handler is None: self.content_handler = JSONContentHandler() elif isinstance(content_handler, ContentHandler): self.content_handler = content_handler elif isinstance(content_handler, basestring): try: cl = CONTENT_HANDLERS[content_handler] except KeyError: raise error.RestAuthRuntimeException( "Unknown content_handler: %s" % content_handler) self.content_handler = cl() else: raise error.RestAuthRuntimeException("Unknown content handler defined.") self.mime = self.content_handler.mime def send(self, method, url, body=None, headers=None): """ Send an HTTP request to the RestAuth service. This method is called by the :py:meth:`.get`, :py:meth:`.post`, :py:meth:`.put` and :py:meth:`.delete` methods. This method takes care of service authentication, encryption and sets Content-Type and Accept headers. :param method: The HTTP method to use. Must be either "GET", "POST", "PUT" or "DELETE". :type method: str :param url: The URL path of the request. This does not include the domain, which is configured by the :py:class:`constructor <.RestAuthConnection>`. The path is assumed to be URL escaped. :type url: str :param body: The request body. This (should) only be used by POST and PUT requests. The body is assumed to be URL escaped. :type body: str :param headers: A dictionary of key/value pairs of headers to set. :param headers: dict :return: The response to the request :rtype: :py:class:`~http.client.HTTPResponse` :raise Unauthorized: When the connection uses wrong credentials. :raise Forbidden: When the client is not allowed to perform this action. :raise NotAcceptable: When the server cannot generate a response in the content type used by this connection (see also: :py:meth:`.set_content_handler`). :raise InternalServerError: When the server has some internal error. """ if headers is None: headers = {} headers['Authorization'] = self.auth_header headers['Accept'] = self.mime conn = self._conn(**self._conn_kwargs) try: conn.request(method, url, body, headers) response = conn.getresponse() except Exception as e: raise HttpException(e) if response.status == client.UNAUTHORIZED: raise error.Unauthorized(response) elif response.status == client.FORBIDDEN: raise error.Forbidden(response) elif response.status == client.NOT_ACCEPTABLE: raise error.NotAcceptable(response) elif response.status == client.INTERNAL_SERVER_ERROR: # pragma: no cover raise error.InternalServerError(response) else: return response def get(self, url, params=None, headers=None): """ Perform a GET request on the connection. This method takes care of escaping parameters and assembling the correct URL. This method internally calls the :py:meth:`.send` function to perform service authentication. :param url: The URL to perform the GET request on. The URL must not include a query string. :type url: str :param params: The query parameters for this request. A dictionary of key/value pairs that is passed to :py:func:`urllib.parse.quote`. :type params: dict :param headers: Additional headers to send with this request. :type headers: dict :return: The response to the request :rtype: :py:class:`~http.client.HTTPResponse` :raise Unauthorized: When the connection uses wrong credentials. :raise Forbidden: When the client is not allowed to perform this action. :raise NotAcceptable: When the server cannot generate a response :raise NotAcceptable: When the server cannot generate a response in the content type used by this connection (see also: :py:meth:`.set_content_handler`). :raise InternalServerError: When the server has some internal error. """ if params is None: params = {} if params: url = '%s?%s' % (url, self._sanitize_qs(params)) return self.send('GET', url, headers=headers) def post(self, url, params, headers=None): """ Perform a POST request on the connection. This method takes care of escaping parameters and assembling the correct URL. This method internally calls the :py:meth:`.send` function to perform service authentication. .. versionadded:: 0.6.2 ``params`` is no longer an optional parameter :param url: The URL to perform the GET request on. The URL must not include a query string. :type url: str :param params: A dictionary that will be wrapped into the request body. :type params: dict :param headers: Additional headers to send with this request. :type headers: dict :return: The response to the request :rtype: :py:class:`~http.client.HTTPResponse` :raise BadRequest: If the server was unable to parse the request body. :raise Unauthorized: When the connection uses wrong credentials. :raise Forbidden: When the client is not allowed to perform this action. :raise NotAcceptable: When the server cannot generate a response in the content type used by this connection (see also: :py:meth:`.set_content_handler`). :raise UnsupportedMediaType: The server does not support the content type used by this connection. :raise InternalServerError: When the server has some internal error. """ if headers is None: # pragma: no branch headers = {} headers['Content-Type'] = self.mime body = self.content_handler.marshal_dict(params) response = self.send('POST', url, body, headers) if response.status == client.BAD_REQUEST: raise error.BadRequest(response) elif response.status == client.UNSUPPORTED_MEDIA_TYPE: raise error.UnsupportedMediaType(response) return response def put(self, url, params, headers=None): """ Perform a PUT request on the connection. This method takes care of escaping parameters and assembling the correct URL. This method internally calls the :py:meth:`.send` function to perform service authentication. .. versionadded:: 0.6.2 ``params`` is no longer an optional parameter :param url: The URL to perform the GET request on. The URL must not include a query string. :type url: str :param params: A dictionary that will be wrapped into the request body. :type params: dict :param headers: Additional headers to send with this request. :type headers: dict :return: The response to the request :rtype: :py:class:`~http.client.HTTPResponse` :raise BadRequest: If the server was unable to parse the request body. :raise Unauthorized: When the connection uses wrong credentials. :raise Forbidden: When the client is not allowed to perform this action. :raise NotAcceptable: When the server cannot generate a response in the content type used by this connection (see also: :py:meth:`.set_content_handler`). :raise UnsupportedMediaType: The server does not support the content type used by this connection. :raise InternalServerError: When the server has some internal error. """ if headers is None: # pragma: no branch headers = {} headers['Content-Type'] = self.mime body = self.content_handler.marshal_dict(params) response = self.send('PUT', url, body, headers) if response.status == client.BAD_REQUEST: raise error.BadRequest(response) elif response.status == client.UNSUPPORTED_MEDIA_TYPE: raise error.UnsupportedMediaType(response) return response def delete(self, url, headers=None): """ Perform a DELETE request on the connection. This method internally calls the :py:meth:`.send` function to perform service authentication. :param url: The URL to perform the GET request on. The URL must not include a query string. :type url: str :param headers: Additional headers to send with this request. :type headers: dict :return: The response to the request :rtype: :py:class:`~http.client.HTTPResponse` :raise Unauthorized: When the connection uses wrong credentials. :raise Forbidden: When the client is not allowed to perform this action. :raise NotAcceptable: When the server cannot generate a response in the content type used by this connection (see also: :py:meth:`.set_content_handler`). :raise InternalServerError: When the server has some internal error. """ return self.send('DELETE', url, headers=headers) def __eq__(self, other): return self._conn == other._conn and self._conn_kwargs == other._conn_kwargs and \ self.auth_header == other.auth_header if PY3: # pragma: py3 def quote(self, name): return quote(name, safe='') def _sanitize_qs(self, params): return urlencode(params).replace('+', '%20') else: # pragma: py2 def quote(self, name): if isinstance(name, unicode): name = name.encode('utf-8') return quote(name, safe='') def _sanitize_qs(self, params): for key, value in params.iteritems(): if key.__class__ == unicode: key = key.encode('utf-8') if value.__class__ == unicode: value = value.encode('utf-8') params[key] = value return urlencode(params).replace('+', '%20')