# -*- coding: utf-8 -*-
#
# This file is part of RestAuthClient (https://python.restauth.net).
#
# RestAuth is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# RestAuth is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with RestAuth. If not, see <http://www.gnu.org/licenses/>.
"""
Central code for handling connections to a RestAuth service.
.. moduleauthor:: Mathias Ertl <mati@restauth.net>
"""
try:
from http import client
except ImportError:
# this is for python 2.x and earlier
import httplib as client
import os, sys, base64, time
try:
from RestAuthClient.error import *
except ImportError:
from error import *
try:
from urllib.parse import quote, urlencode, urlparse
except ImportError:
# this is for python 2.x and earlier
from urllib import quote, urlencode
from urlparse import urlparse
try:
import RestAuthCommon
from RestAuthCommon import error
except ImportError:
print( "Error: The RestAuthCommon library is not installed." )
sys.exit(1)
if sys.version_info >= (3, 2):
from ssl import SSLContext, CERT_REQUIRED
[docs]class RestAuthConnection:
"""
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.
: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 subclass of
RestAuthCommon.handlers.content_handler.
"""
def __init__( self, host, user, passwd, content_handler='application/json' ):
"""
Initialize a new connection to a RestAuth service.
"""
parseresult = urlparse( host )
if parseresult.scheme == 'https':
self.use_ssl = True
else:
self.use_ssl = False
self.host = parseresult.netloc
self.user = user
self.passwd = passwd
self.set_content_handler( content_handler )
# pre-calculate the auth-header so we only have to do this once:
self.set_credentials( user, passwd )
if sys.version_info >= (3, 2) and self.use_ssl:
self.context = SSLContext()
self.context.verify_mode = CERT_REQUIRED
[docs] def set_credentials( self, user, passwd ):
"""
Set the password for the given user. This method is also
automatically called by the constructor.
: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() )
[docs] def set_content_handler( self, content_handler='application/json' ):
"""
Set the content type used by this connection. The default value
is 'json', which is supported by the reference server
implementation.
:param content_handler: Either a self-implemented handler, which must be a subclass
of :py:class:`~RestAuthCommon:RestAuthCommon.handlers.content_handler` or a
str, which must be one of the MIME types specified in
:py:data:`~RestAuthCommon:RestAuthCommon.CONTENT_HANDLERS`.
:type content_handler: str or :py:class:`~RestAuthCommon:RestAuthCommon.handlers.content_handler`
"""
if isinstance( content_handler, RestAuthCommon.handlers.content_handler ):
self.content_handler = content_handler
elif isinstance( content_handler, str ) or isinstance( content_handler, unicode ):
handler_dict = RestAuthCommon.CONTENT_HANDLERS
try:
cl = handler_dict[content_handler]
except KeyError:
raise RuntimeError( "Unknown content_handler: %s"%(content_handler) )
self.content_handler = cl()
[docs] def send( self, method, url, body=None, headers={} ):
"""
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 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.
"""
headers['Authorization'] = self.auth_header
headers['Accept'] = self.content_handler.mime
if self.use_ssl:
if sys.version_info >= (3, 2):
conn = client.HTTPSConnection( self.host, context=self.context )
else:
conn = client.HTTPSConnection( self.host )
else:
conn = client.HTTPConnection( self.host )
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.NOT_ACCEPTABLE:
raise error.NotAcceptable( response )
elif response.status == client.INTERNAL_SERVER_ERROR:
raise error.InternalServerError( response )
else:
return response
def _sanitize_qs( self, params ):
if sys.version_info < (3, 0):
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' )
def _sanitize_url( self, url ):
# make sure that it starts and ends with /, cut double-slashes:
url = '%s/'%( os.path.normpath( url ) )
if not url.startswith( '/' ):
url = '/%s'%(url)
if sys.version_info < (3, 0) and url.__class__ == unicode:
url = url.encode( 'utf-8' ) # encode utf-8 in python 2.x
url = quote( url )
return url
[docs] def get( self, url, params={}, headers={} ):
"""
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 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.
"""
url = self._sanitize_url( url )
if params:
url = '%s?%s'%( url, self._sanitize_qs( params ) )
return self.send( 'GET', url, headers=headers )
[docs] def post( self, url, params={}, headers={} ):
"""
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.
: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 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.
"""
headers['Content-Type'] = self.content_handler.mime
body = self.content_handler.marshal_dict( params )
url = self._sanitize_url( url )
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
[docs] def put( self, url, params={}, headers={} ):
"""
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.
: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 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.
"""
headers['Content-Type'] = self.content_handler.mime
body = self.content_handler.marshal_dict( params )
url = self._sanitize_url( url )
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
[docs] def delete( self, url, headers={} ):
"""
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 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.
"""
url = self._sanitize_url( url )
return self.send( 'DELETE', url, headers=headers )
def __eq__( self, other ):
return self.host == other.host and self.user == other.user and \
self.passwd == other.passwd
[docs]class RestAuthResource:
"""
Superclass for :py:class:`~.User` and :py:class:`~.Group` objects.
The private methods of this class do nothing but prefix all request URLs
with the prefix of that class (i.e. /users/).
"""
def _get( self, url, params={}, headers={} ):
"""
Internal method that prefixes a GET request with the resource
name and passes the request to :py:meth:`RestAuthConnection.get`.
"""
url = '%s%s'%( self.__class__.prefix, url )
return self.conn.get( url, params, headers )
def _post( self, url, params={}, headers={} ):
"""
Internal method that prefixes a POST request with the resources
name and passes the request to :py:meth:`RestAuthConnection.post`.
"""
url = '%s%s'%( self.__class__.prefix, url )
return self.conn.post( url, params, headers )
def _put( self, url, params={}, headers={} ):
"""
Internal method that prefixes a PUT request with the resources
name and passes the request to :py:meth:`RestAuthConnection.put`.
"""
url = '%s%s'%( self.__class__.prefix, url )
return self.conn.put( url, params, headers )
def _delete( self, url, headers={} ):
"""
Internal method that prefixes a DELETE request with the
resources name and passes the request to :py:meth:`RestAuthConnection.delete`.
"""
url = '%s%s'%( self.__class__.prefix, url )
return self.conn.delete( url, headers )