blob: a19ca37658736eb7aa75147e5fd39462265bac54 [file] [log] [blame]
# Copyright (c) The PyAMF Project.
# See LICENSE.txt for details.
"""
Remoting server implementations.
@since: 0.1.0
"""
import sys
import types
import datetime
import pyamf
from pyamf import remoting, util, python
try:
from platform import python_implementation
impl = python_implementation()
except ImportError:
impl = 'Python'
SERVER_NAME = 'PyAMF/%s %s/%s' % (
pyamf.version, impl,
'.'.join(map(lambda x: str(x), sys.version_info[0:3]))
)
class BaseServiceError(pyamf.BaseError):
"""
Base service error.
"""
class UnknownServiceError(BaseServiceError):
"""
Client made a request for an unknown service.
"""
_amf_code = 'Service.ResourceNotFound'
class UnknownServiceMethodError(BaseServiceError):
"""
Client made a request for an unknown method.
"""
_amf_code = 'Service.MethodNotFound'
class InvalidServiceMethodError(BaseServiceError):
"""
Client made a request for an invalid methodname.
"""
_amf_code = 'Service.MethodInvalid'
class ServiceWrapper(object):
"""
Wraps a supplied service with extra functionality.
@ivar service: The original service.
@type service: C{callable}
@ivar description: A description of the service.
@type description: C{str}
"""
def __init__(self, service, description=None, authenticator=None,
expose_request=None, preprocessor=None):
self.service = service
self.description = description
self.authenticator = authenticator
self.expose_request = expose_request
self.preprocessor = preprocessor
def __cmp__(self, other):
if isinstance(other, ServiceWrapper):
return cmp(self.__dict__, other.__dict__)
return cmp(self.service, other)
def _get_service_func(self, method, params):
"""
@raise InvalidServiceMethodError: Calls to private methods are not
allowed.
@raise UnknownServiceMethodError: Unknown method.
@raise InvalidServiceMethodError: Service method must be callable.
"""
service = None
if isinstance(self.service, (type, types.ClassType)):
service = self.service()
else:
service = self.service
if method is not None:
method = str(method)
if method.startswith('_'):
raise InvalidServiceMethodError(
"Calls to private methods are not allowed")
try:
func = getattr(service, method)
except AttributeError:
raise UnknownServiceMethodError(
"Unknown method %s" % str(method))
if not python.callable(func):
raise InvalidServiceMethodError(
"Service method %s must be callable" % str(method))
return func
if not python.callable(service):
raise UnknownServiceMethodError(
"Unknown method %s" % str(self.service))
return service
def __call__(self, method, params):
"""
Executes the service.
If the service is a class, it will be instantiated.
@param method: The method to call on the service.
@type method: C{None} or C{mixed}
@param params: The params to pass to the service.
@type params: C{list} or C{tuple}
@return: The result of the execution.
@rtype: C{mixed}
"""
func = self._get_service_func(method, params)
return func(*params)
def getMethods(self):
"""
Gets a C{dict} of valid method callables for the underlying service
object.
"""
callables = {}
for name in dir(self.service):
method = getattr(self.service, name)
if name.startswith('_') or not python.callable(method):
continue
callables[name] = method
return callables
def getAuthenticator(self, service_request=None):
if service_request is None:
return self.authenticator
methods = self.getMethods()
if service_request.method is None:
if hasattr(self.service, '_pyamf_authenticator'):
return self.service._pyamf_authenticator
if service_request.method not in methods:
return self.authenticator
method = methods[service_request.method]
if hasattr(method, '_pyamf_authenticator'):
return method._pyamf_authenticator
return self.authenticator
def mustExposeRequest(self, service_request=None):
if service_request is None:
return self.expose_request
methods = self.getMethods()
if service_request.method is None:
if hasattr(self.service, '_pyamf_expose_request'):
return self.service._pyamf_expose_request
return self.expose_request
if service_request.method not in methods:
return self.expose_request
method = methods[service_request.method]
if hasattr(method, '_pyamf_expose_request'):
return method._pyamf_expose_request
return self.expose_request
def getPreprocessor(self, service_request=None):
if service_request is None:
return self.preprocessor
methods = self.getMethods()
if service_request.method is None:
if hasattr(self.service, '_pyamf_preprocessor'):
return self.service._pyamf_preprocessor
if service_request.method not in methods:
return self.preprocessor
method = methods[service_request.method]
if hasattr(method, '_pyamf_preprocessor'):
return method._pyamf_preprocessor
return self.preprocessor
class ServiceRequest(object):
"""
Remoting service request.
@ivar request: The request to service.
@type request: L{Envelope<pyamf.remoting.Envelope>}
@ivar service: Facilitates the request.
@type service: L{ServiceWrapper}
@ivar method: The method to call on the service. A value of C{None}
means that the service will be called directly.
@type method: C{None} or C{str}
"""
def __init__(self, amf_request, service, method):
self.request = amf_request
self.service = service
self.method = method
def __call__(self, *args):
return self.service(self.method, args)
class ServiceCollection(dict):
"""
I hold a collection of services, mapping names to objects.
"""
def __contains__(self, value):
if isinstance(value, basestring):
return value in self.keys()
return value in self.values()
class BaseGateway(object):
"""
Generic Remoting gateway.
@ivar services: A map of service names to callables.
@type services: L{ServiceCollection}
@ivar authenticator: A callable that will check the credentials of
the request before allowing access to the service. Will return a
C{bool} value.
@type authenticator: C{Callable} or C{None}
@ivar preprocessor: Called before the actual service method is invoked.
Useful for setting up sessions etc.
@type preprocessor: C{Callable} or C{None}
@ivar logger: A logging instance.
@ivar strict: Defines whether the gateway should use strict en/decoding.
@type strict: C{bool}
@ivar timezone_offset: A U{datetime.datetime.timedelta<http://
docs.python.org/library/datetime.html#datetime.timedelta} between UTC
and the timezone to be encoded. Most dates should be handled as UTC to
avoid confusion but for older legacy systems this is not an option.
Supplying an int as this will be interpretted in seconds.
@ivar debug: Provides debugging information when an error occurs. Use only
in non production settings.
@type debug: C{bool}
"""
_request_class = ServiceRequest
def __init__(self, services=None, **kwargs):
if services is None:
services = {}
if not hasattr(services, 'iteritems'):
raise TypeError("dict type required for services")
self.services = ServiceCollection()
self.authenticator = kwargs.pop('authenticator', None)
self.preprocessor = kwargs.pop('preprocessor', None)
self.expose_request = kwargs.pop('expose_request', False)
self.strict = kwargs.pop('strict', False)
self.logger = kwargs.pop('logger', None)
self.timezone_offset = kwargs.pop('timezone_offset', None)
self.debug = kwargs.pop('debug', False)
if kwargs:
raise TypeError('Unknown kwargs: %r' % (kwargs,))
for name, service in services.iteritems():
self.addService(service, name)
def addService(self, service, name=None, description=None,
authenticator=None, expose_request=None, preprocessor=None):
"""
Adds a service to the gateway.
@param service: The service to add to the gateway.
@type service: C{callable}, class instance, or a module
@param name: The name of the service.
@type name: C{str}
@raise pyamf.remoting.RemotingError: Service already exists.
@raise TypeError: C{service} cannot be a scalar value.
@raise TypeError: C{service} must be C{callable} or a module.
"""
if isinstance(service, (int, long, float, basestring)):
raise TypeError("Service cannot be a scalar value")
allowed_types = (types.ModuleType, types.FunctionType, types.DictType,
types.MethodType, types.InstanceType, types.ObjectType)
if not python.callable(service) and not isinstance(service, allowed_types):
raise TypeError("Service must be a callable, module, or an object")
if name is None:
# TODO: include the module in the name
if isinstance(service, (type, types.ClassType)):
name = service.__name__
elif isinstance(service, types.FunctionType):
name = service.func_name
elif isinstance(service, types.ModuleType):
name = service.__name__
else:
name = str(service)
if name in self.services:
raise remoting.RemotingError("Service %s already exists" % name)
self.services[name] = ServiceWrapper(service, description,
authenticator, expose_request, preprocessor)
def _get_timezone_offset(self):
if self.timezone_offset is None:
return None
if isinstance(self.timezone_offset, datetime.timedelta):
return self.timezone_offset
return datetime.timedelta(seconds=self.timezone_offset)
def removeService(self, service):
"""
Removes a service from the gateway.
@param service: Either the name or t of the service to remove from the
gateway, or .
@type service: C{callable} or a class instance
@raise NameError: Service not found.
"""
for name, wrapper in self.services.iteritems():
if service in (name, wrapper.service):
del self.services[name]
return
raise NameError("Service %r not found" % (service,))
def getServiceRequest(self, request, target):
"""
Returns a service based on the message.
@raise UnknownServiceError: Unknown service.
@param request: The AMF request.
@type request: L{Request<pyamf.remoting.Request>}
@rtype: L{ServiceRequest}
"""
try:
return self._request_class(
request.envelope, self.services[target], None)
except KeyError:
pass
try:
sp = target.split('.')
name, meth = '.'.join(sp[:-1]), sp[-1]
return self._request_class(
request.envelope, self.services[name], meth)
except (ValueError, KeyError):
pass
raise UnknownServiceError("Unknown service %s" % target)
def getProcessor(self, request):
"""
Returns request processor.
@param request: The AMF message.
@type request: L{Request<remoting.Request>}
"""
if request.target == 'null' or not request.target:
from pyamf.remoting import amf3
return amf3.RequestProcessor(self)
else:
from pyamf.remoting import amf0
return amf0.RequestProcessor(self)
def getResponse(self, amf_request):
"""
Returns the response to the request.
Any implementing gateway must define this function.
@param amf_request: The AMF request.
@type amf_request: L{Envelope<pyamf.remoting.Envelope>}
@return: The AMF response.
@rtype: L{Envelope<pyamf.remoting.Envelope>}
"""
raise NotImplementedError
def mustExposeRequest(self, service_request):
"""
Decides whether the underlying http request should be exposed as the
first argument to the method call. This is granular, looking at the
service method first, then at the service level and finally checking
the gateway.
@rtype: C{bool}
"""
expose_request = service_request.service.mustExposeRequest(service_request)
if expose_request is None:
if self.expose_request is None:
return False
return self.expose_request
return expose_request
def getAuthenticator(self, service_request):
"""
Gets an authenticator callable based on the service_request. This is
granular, looking at the service method first, then at the service
level and finally to see if there is a global authenticator function
for the gateway. Returns C{None} if one could not be found.
"""
auth = service_request.service.getAuthenticator(service_request)
if auth is None:
return self.authenticator
return auth
def authenticateRequest(self, service_request, username, password, **kwargs):
"""
Processes an authentication request. If no authenticator is supplied,
then authentication succeeds.
@return: Returns a C{bool} based on the result of authorization. A
value of C{False} will stop processing the request and return an
error to the client.
@rtype: C{bool}
"""
authenticator = self.getAuthenticator(service_request)
if authenticator is None:
return True
args = (username, password)
if hasattr(authenticator, '_pyamf_expose_request'):
http_request = kwargs.get('http_request', None)
args = (http_request,) + args
return authenticator(*args) == True
def getPreprocessor(self, service_request):
"""
Gets a preprocessor callable based on the service_request. This is
granular, looking at the service method first, then at the service
level and finally to see if there is a global preprocessor function
for the gateway. Returns C{None} if one could not be found.
"""
preproc = service_request.service.getPreprocessor(service_request)
if preproc is None:
return self.preprocessor
return preproc
def preprocessRequest(self, service_request, *args, **kwargs):
"""
Preprocesses a request.
"""
processor = self.getPreprocessor(service_request)
if processor is None:
return
args = (service_request,) + args
if hasattr(processor, '_pyamf_expose_request'):
http_request = kwargs.get('http_request', None)
args = (http_request,) + args
return processor(*args)
def callServiceRequest(self, service_request, *args, **kwargs):
"""
Executes the service_request call
"""
if self.mustExposeRequest(service_request):
http_request = kwargs.get('http_request', None)
args = (http_request,) + args
return service_request(*args)
def authenticate(func, c, expose_request=False):
"""
A decorator that facilitates authentication per method. Setting
C{expose_request} to C{True} will set the underlying request object (if
there is one), usually HTTP and set it to the first argument of the
authenticating callable. If there is no request object, the default is
C{None}.
@raise TypeError: C{func} and authenticator must be callable.
"""
if not python.callable(func):
raise TypeError('func must be callable')
if not python.callable(c):
raise TypeError('Authenticator must be callable')
attr = func
if isinstance(func, types.UnboundMethodType):
attr = func.im_func
if expose_request is True:
c = globals()['expose_request'](c)
setattr(attr, '_pyamf_authenticator', c)
return func
def expose_request(func):
"""
A decorator that adds an expose_request flag to the underlying callable.
@raise TypeError: C{func} must be callable.
"""
if not python.callable(func):
raise TypeError("func must be callable")
if isinstance(func, types.UnboundMethodType):
setattr(func.im_func, '_pyamf_expose_request', True)
else:
setattr(func, '_pyamf_expose_request', True)
return func
def preprocess(func, c, expose_request=False):
"""
A decorator that facilitates preprocessing per method. Setting
C{expose_request} to C{True} will set the underlying request object (if
there is one), usually HTTP and set it to the first argument of the
preprocessing callable. If there is no request object, the default is
C{None}.
@raise TypeError: C{func} and preprocessor must be callable.
"""
if not python.callable(func):
raise TypeError('func must be callable')
if not python.callable(c):
raise TypeError('Preprocessor must be callable')
attr = func
if isinstance(func, types.UnboundMethodType):
attr = func.im_func
if expose_request is True:
c = globals()['expose_request'](c)
setattr(attr, '_pyamf_preprocessor', c)
return func
def format_exception():
import traceback
f = util.BufferedByteStream()
traceback.print_exc(file=f)
return f.getvalue()