Package pyamf :: Package remoting :: Package gateway

[hide private]
[frames] | no frames]

# Source Code for Package pyamf.remoting.gateway

      1  # Copyright (c) 2007-2009 The PyAMF Project.
      2  # See LICENSE.txt for details.
      3
      4  """
      5  Remoting server implementations.
      6
      7  @since: 0.1.0
      8  """
      9
     10  import sys
     11  import types
     12  import datetime
     13
     14  import pyamf
     15  from pyamf import remoting, util
     16
     17  try:
     18      from platform import python_implementation
     19      impl = python_implementation()
     20  except ImportError:
     21      impl = 'Python'
     22
     23  SERVER_NAME = 'PyAMF/%s %s/%s' % (
     24      '.'.join(map(lambda x: str(x), pyamf.__version__)), impl,
     25      '.'.join(map(lambda x: str(x), sys.version_info[0:3]))
     26  )
     27
     28
     29 -class BaseServiceError(pyamf.BaseError):
     30      """
     31      Base service error.
     32      """
     33
     34
     35 -class UnknownServiceError(BaseServiceError):
     36      """
     37      Client made a request for an unknown service.
     38      """
     39      _amf_code = 'Service.ResourceNotFound'
     40
     41
     42 -class UnknownServiceMethodError(BaseServiceError):
     43      """
     44      Client made a request for an unknown method.
     45      """
     46      _amf_code = 'Service.MethodNotFound'
     47
     48
     49 -class InvalidServiceMethodError(BaseServiceError):
     50      """
     51      Client made a request for an invalid methodname.
     52      """
     53      _amf_code = 'Service.MethodInvalid'
     54
     55
     56 -class ServiceWrapper(object):
     57      """
     58      Wraps a supplied service with extra functionality.
     59
     60      @ivar service: The original service.
     61      @type service: C{callable}
     62      @ivar description: A description of the service.
     63      @type description: C{str}
     64      """
     65 -    def __init__(self, service, description=None, authenticator=None,
     66          expose_request=None, preprocessor=None):
     67          self.service = service
     68          self.description = description
     69          self.authenticator = authenticator
     70          self.expose_request = expose_request
     71          self.preprocessor = preprocessor
     72
     73 -    def __cmp__(self, other):
     74          if isinstance(other, ServiceWrapper):
     75              return cmp(self.__dict__, other.__dict__)
     76
     77          return cmp(self.service, other)
     78
     79 -    def _get_service_func(self, method, params):
     80          """
     81          @raise InvalidServiceMethodError: Calls to private methods are not
     82              allowed.
     83          @raise UnknownServiceMethodError: Unknown method.
     84          @raise InvalidServiceMethodError: Service method must be callable.
     85          """
     86          service = None
     87
     88          if isinstance(self.service, (type, types.ClassType)):
     89              service = self.service()
     90          else:
     91              service = self.service
     92
     93          if method is not None:
     94              method = str(method)
     95
     96              if method.startswith('_'):
     97                  raise InvalidServiceMethodError(
     98                      "Calls to private methods are not allowed")
     99
    100              try:
    101                  func = getattr(service, method)
    102              except AttributeError:
    103                  raise UnknownServiceMethodError(
    104                      "Unknown method %s" % str(method))
    105
    106              if not callable(func):
    107                  raise InvalidServiceMethodError(
    108                      "Service method %s must be callable" % str(method))
    109
    110              return func
    111
    112          if not callable(service):
    113              raise UnknownServiceMethodError(
    114                  "Unknown method %s" % str(self.service))
    115
    116          return service
    117
    118 -    def __call__(self, method, params):
    119          """
    120          Executes the service.
    121
    122          If the service is a class, it will be instantiated.
    123
    124          @param method: The method to call on the service.
    125          @type method: C{None} or C{mixed}
    126          @param params: The params to pass to the service.
    127          @type params: C{list} or C{tuple}
    128          @return: The result of the execution.
    129          @rtype: C{mixed}
    130          """
    131          func = self._get_service_func(method, params)
    132
    133          return func(*params)
    134
    135 -    def getMethods(self):
    136          """
    137          Gets a C{dict} of valid method callables for the underlying service
    138          object.
    139          """
    140          callables = {}
    141
    142          for name in dir(self.service):
    143              method = getattr(self.service, name)
    144
    145              if name.startswith('_') or not callable(method):
    146                  continue
    147
    148              callables[name] = method
    149
    150          return callables
    151
    152 -    def getAuthenticator(self, service_request=None):
    153          if service_request == None:
    154              return self.authenticator
    155
    156          methods = self.getMethods()
    157
    158          if service_request.method is None:
    159              if hasattr(self.service, '_pyamf_authenticator'):
    160                  return self.service._pyamf_authenticator
    161
    162          if service_request.method not in methods:
    163              return self.authenticator
    164
    165          method = methods[service_request.method]
    166
    167          if hasattr(method, '_pyamf_authenticator'):
    168              return method._pyamf_authenticator
    169
    170          return self.authenticator
    171
    172 -    def mustExposeRequest(self, service_request=None):
    173          if service_request == None:
    174              return self.expose_request
    175
    176          methods = self.getMethods()
    177
    178          if service_request.method is None:
    179              if hasattr(self.service, '_pyamf_expose_request'):
    180                  return self.service._pyamf_expose_request
    181
    182              return self.expose_request
    183
    184          if service_request.method not in methods:
    185              return self.expose_request
    186
    187          method = methods[service_request.method]
    188
    189          if hasattr(method, '_pyamf_expose_request'):
    190              return method._pyamf_expose_request
    191
    192          return self.expose_request
    193
    194 -    def getPreprocessor(self, service_request=None):
    195          if service_request == None:
    196              return self.preprocessor
    197
    198          methods = self.getMethods()
    199
    200          if service_request.method is None:
    201              if hasattr(self.service, '_pyamf_preprocessor'):
    202                  return self.service._pyamf_preprocessor
    203
    204          if service_request.method not in methods:
    205              return self.preprocessor
    206
    207          method = methods[service_request.method]
    208
    209          if hasattr(method, '_pyamf_preprocessor'):
    210              return method._pyamf_preprocessor
    211
    212          return self.preprocessor
    213
    214
    215 -class ServiceRequest(object):
    216      """
    217      Remoting service request.
    218
    219      @ivar request: The request to service.
    220      @type request: L{Envelope<pyamf.remoting.Envelope>}
    221      @ivar service: Facilitates the request.
    222      @type service: L{ServiceWrapper}
    223      @ivar method: The method to call on the service. A value of C{None}
    224          means that the service will be called directly.
    225      @type method: C{None} or C{str}
    226      """
    227 -    def __init__(self, amf_request, service, method):
    228          self.request = amf_request
    229          self.service = service
    230          self.method = method
    231
    232 -    def __call__(self, *args):
    233          return self.service(self.method, args)
    234
    235
    236 -class ServiceCollection(dict):
    237      """
    238      I hold a collection of services, mapping names to objects.
    239      """
    240 -    def __contains__(self, value):
    241          if isinstance(value, basestring):
    242              return value in self.keys()
    243
    244          return value in self.values()
    245
    246
    247 -class BaseGateway(object):
    248      """
    249      Generic Remoting gateway.
    250
    251      @ivar services: A map of service names to callables.
    252      @type services: L{ServiceCollection}
    253      @ivar authenticator: A callable that will check the credentials of
    254          the request before allowing access to the service. Will return a
    255          C{bool} value.
    256      @type authenticator: C{Callable} or C{None}
    257      @ivar preprocessor: Called before the actual service method is invoked.
    258          Useful for setting up sessions etc.
    259      @type preprocessor: C{Callable} or C{None}
    260      @ivar logger: A logging instance.
    261      @ivar strict: Defines whether the gateway should use strict en/decoding.
    262      @type strict: C{bool}
    263      @ivar timezone_offset: A L{datetime.timedelta} between UTC and the
    264          timezone to be encoded. Most dates should be handled as UTC to avoid
    265          confusion but for older legacy systems this is not an option. Supplying
    266          an int as this will be interpretted in seconds.
    267      @ivar debug: Provides debugging information when an error occurs. Use only
    268          in non production settings.
    269      @type debug: C{bool}
    270      """
    271
    272      _request_class = ServiceRequest
    273
    274 -    def __init__(self, services={}, **kwargs):
    275          if not hasattr(services, 'iteritems'):
    276              raise TypeError("dict type required for services")
    277
    278          self.services = ServiceCollection()
    279          self.authenticator = kwargs.pop('authenticator', None)
    280          self.preprocessor = kwargs.pop('preprocessor', None)
    281          self.expose_request = kwargs.pop('expose_request', False)
    282          self.strict = kwargs.pop('strict', False)
    283          self.logger = kwargs.pop('logger', None)
    284          self.timezone_offset = kwargs.pop('timezone_offset', None)
    285
    286          self.debug = kwargs.pop('debug', False)
    287
    288          if kwargs:
    289              raise TypeError('Unknown kwargs: %r' % (kwargs,))
    290
    291          for name, service in services.iteritems():
    292              self.addService(service, name)
    293
    294 -    def addService(self, service, name=None, description=None,
    295          authenticator=None, expose_request=None, preprocessor=None):
    296          """
    297          Adds a service to the gateway.
    298
    299          @param service: The service to add to the gateway.
    300          @type service: C{callable}, class instance, or a module
    301          @param name: The name of the service.
    302          @type name: C{str}
    303          @raise pyamf.remoting.RemotingError: Service already exists.
    304          @raise TypeError: C{service} cannot be a scalar value.
    305          @raise TypeError: C{service} must be C{callable} or a module.
    306          """
    307          if isinstance(service, (int, long, float, basestring)):
    308              raise TypeError("Service cannot be a scalar value")
    309
    310          allowed_types = (types.ModuleType, types.FunctionType, types.DictType,
    311              types.MethodType, types.InstanceType, types.ObjectType)
    312
    313          if not callable(service) and not isinstance(service, allowed_types):
    314              raise TypeError("Service must be a callable, module, or an object")
    315
    316          if name is None:
    317              # TODO: include the module in the name
    318              if isinstance(service, (type, types.ClassType)):
    319                  name = service.__name__
    320              elif isinstance(service, types.FunctionType):
    321                  name = service.func_name
    322              elif isinstance(service, types.ModuleType):
    323                  name = service.__name__
    324              else:
    325                  name = str(service)
    326
    327          if name in self.services:
    328              raise remoting.RemotingError("Service %s already exists" % name)
    329
    330          self.services[name] = ServiceWrapper(service, description,
    331              authenticator, expose_request, preprocessor)
    332
    333 -    def _get_timezone_offset(self):
    334          if self.timezone_offset is None:
    335              return None
    336
    337          if isinstance(self.timezone_offset, datetime.timedelta):
    338              return self.timezone_offset
    339
    340          return datetime.timedelta(seconds=self.timezone_offset)
    341
    342 -    def removeService(self, service):
    343          """
    344          Removes a service from the gateway.
    345
    346          @param service: The service to remove from the gateway.
    347          @type service: C{callable} or a class instance
    348          @raise NameError: Service not found.
    349          """
    350          if service not in self.services:
    351              raise NameError("Service %s not found" % str(service))
    352
    353          for name, wrapper in self.services.iteritems():
    354              if isinstance(service, basestring) and service == name:
    355                  del self.services[name]
    356
    357                  return
    358              elif isinstance(service, ServiceWrapper) and wrapper == service:
    359                  del self.services[name]
    360
    361                  return
    362              elif isinstance(service, (type, types.ClassType,
    363                  types.FunctionType)) and wrapper.service == service:
    364                  del self.services[name]
    365
    366                  return
    367
    368          # shouldn't ever get here
    369          raise RuntimeError("Something went wrong ...")
    370
    371 -    def getServiceRequest(self, request, target):
    372          """
    373          Returns a service based on the message.
    374
    375          @raise UnknownServiceError: Unknown service.
    376          @param request: The AMF request.
    377          @type request: L{Request<pyamf.remoting.Request>}
    378          @rtype: L{ServiceRequest}
    379          """
    380          try:
    381              return self._request_class(
    382                  request.envelope, self.services[target], None)
    383          except KeyError:
    384              pass
    385
    386          try:
    387              sp = target.split('.')
    388              name, meth = '.'.join(sp[:-1]), sp[-1]
    389
    390              return self._request_class(
    391                  request.envelope, self.services[name], meth)
    392          except (ValueError, KeyError):
    393              pass
    394
    395          raise UnknownServiceError("Unknown service %s" % target)
    396
    397 -    def getProcessor(self, request):
    398          """
    399          Returns request processor.
    400
    401          @param request: The AMF message.
    402          @type request: L{Request<remoting.Request>}
    403          """
    404          if request.target == 'null':
    405              from pyamf.remoting import amf3
    406
    407              return amf3.RequestProcessor(self)
    408          else:
    409              from pyamf.remoting import amf0
    410
    411              return amf0.RequestProcessor(self)
    412
    413 -    def getResponse(self, amf_request):
    414          """
    415          Returns the response to the request.
    416
    417          Any implementing gateway must define this function.
    418
    419          @param amf_request: The AMF request.
    420          @type amf_request: L{Envelope<pyamf.remoting.Envelope>}
    421
    422          @return: The AMF response.
    423          @rtype: L{Envelope<pyamf.remoting.Envelope>}
    424          """
    425          raise NotImplementedError
    426
    427 -    def mustExposeRequest(self, service_request):
    428          """
    429          Decides whether the underlying http request should be exposed as the
    430          first argument to the method call. This is granular, looking at the
    431          service method first, then at the service level and finally checking
    432          the gateway.
    433
    434          @rtype: C{bool}
    435          """
    436          expose_request = service_request.service.mustExposeRequest(service_request)
    437
    438          if expose_request is None:
    439              if self.expose_request is None:
    440                  return False
    441
    442              return self.expose_request
    443
    444          return expose_request
    445
    446 -    def getAuthenticator(self, service_request):
    447          """
    448          Gets an authenticator callable based on the service_request. This is
    449          granular, looking at the service method first, then at the service
    450          level and finally to see if there is a global authenticator function
    451          for the gateway. Returns C{None} if one could not be found.
    452          """
    453          auth = service_request.service.getAuthenticator(service_request)
    454
    455          if auth is None:
    456              return self.authenticator
    457
    458          return auth
    459
    460 -    def authenticateRequest(self, service_request, username, password, **kwargs):
    461          """
    462          Processes an authentication request. If no authenticator is supplied,
    463          then authentication succeeds.
    464
    465          @return: Returns a C{bool} based on the result of authorization. A
    466              value of C{False} will stop processing the request and return an
    467              error to the client.
    468          @rtype: C{bool}
    469          """
    470          authenticator = self.getAuthenticator(service_request)
    471
    472          if authenticator is None:
    473              return True
    474
    475          args = (username, password)
    476
    477          if hasattr(authenticator, '_pyamf_expose_request'):
    478              http_request = kwargs.get('http_request', None)
    479              args = (http_request,) + args
    480
    481          return authenticator(*args) == True
    482
    483 -    def getPreprocessor(self, service_request):
    484          """
    485          Gets a preprocessor callable based on the service_request. This is
    486          granular, looking at the service method first, then at the service
    487          level and finally to see if there is a global preprocessor function
    488          for the gateway. Returns C{None} if one could not be found.
    489          """
    490          preproc = service_request.service.getPreprocessor(service_request)
    491
    492          if preproc is None:
    493              return self.preprocessor
    494
    495          return preproc
    496
    497 -    def preprocessRequest(self, service_request, *args, **kwargs):
    498          """
    499          Preprocesses a request.
    500          """
    501          processor = self.getPreprocessor(service_request)
    502
    503          if processor is None:
    504              return
    505
    506          args = (service_request,) + args
    507
    508          if hasattr(processor, '_pyamf_expose_request'):
    509              http_request = kwargs.get('http_request', None)
    510              args = (http_request,) + args
    511
    512          return processor(*args)
    513
    514 -    def callServiceRequest(self, service_request, *args, **kwargs):
    515          """
    516          Executes the service_request call
    517          """
    518          if self.mustExposeRequest(service_request):
    519              http_request = kwargs.get('http_request', None)
    520              args = (http_request,) + args
    521
    522          return service_request(*args)
    523
    524
    525 -def authenticate(func, c, expose_request=False):
    526      """
    527      A decorator that facilitates authentication per method. Setting
    528      C{expose_request} to C{True} will set the underlying request object (if
    529      there is one), usually HTTP and set it to the first argument of the
    530      authenticating callable. If there is no request object, the default is
    531      C{None}.
    532
    533      @raise TypeError: C{func} and authenticator must be callable.
    534      """
    535      if not callable(func):
    536          raise TypeError('func must be callable')
    537
    538      if not callable(c):
    539          raise TypeError('Authenticator must be callable')
    540
    541      attr = func
    542
    543      if isinstance(func, types.UnboundMethodType):
    544          attr = func.im_func
    545
    546      if expose_request is True:
    547          c = globals()['expose_request'](c)
    548
    549      setattr(attr, '_pyamf_authenticator', c)
    550
    551      return func
    552
    553
    554 -def expose_request(func):
    555      """
    556      A decorator that adds an expose_request flag to the underlying callable.
    557
    558      @raise TypeError: C{func} must be callable.
    559      """
    560      if not callable(func):
    561          raise TypeError("func must be callable")
    562
    563      if isinstance(func, types.UnboundMethodType):
    564          setattr(func.im_func, '_pyamf_expose_request', True)
    565      else:
    566          setattr(func, '_pyamf_expose_request', True)
    567
    568      return func
    569
    570
    571 -def preprocess(func, c, expose_request=False):
    572      """
    573      A decorator that facilitates preprocessing per method. Setting
    574      C{expose_request} to C{True} will set the underlying request object (if
    575      there is one), usually HTTP and set it to the first argument of the
    576      preprocessing callable. If there is no request object, the default is
    577      C{None}.
    578
    579      @raise TypeError: C{func} and preprocessor must be callable.
    580      """
    581      if not callable(func):
    582          raise TypeError('func must be callable')
    583
    584      if not callable(c):
    585          raise TypeError('Preprocessor must be callable')
    586
    587      attr = func
    588
    589      if isinstance(func, types.UnboundMethodType):
    590          attr = func.im_func
    591
    592      if expose_request is True:
    593          c = globals()['expose_request'](c)
    594
    595      setattr(attr, '_pyamf_preprocessor', c)
    596
    597      return func
    598
    599
    600 -def format_exception():
    601      import traceback
    602
    603      f = util.StringIO()
    604
    605      traceback.print_exc(file=f)
    606
    607      return f.getvalue()
    608