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      """
    268
    269      _request_class = ServiceRequest
    270
    271 -    def __init__(self, services={}, **kwargs):
    272          self.services = ServiceCollection()
    273          self.authenticator = kwargs.pop('authenticator', None)
    274          self.preprocessor = kwargs.pop('preprocessor', None)
    275          self.expose_request = kwargs.pop('expose_request', False)
    276          self.strict = kwargs.pop('strict', False)
    277          self.logger = kwargs.pop('logger', None)
    278          self.timezone_offset = kwargs.pop('timezone_offset', None)
    279
    280          debug = kwargs.pop('debug', False)
    281
    282          if kwargs:
    283              raise TypeError('Unknown kwargs: %r' % (kwargs,))
    284
    285          self.debug = debug
    286
    287          if not hasattr(services, 'iteritems'):
    288              raise TypeError("dict type required for services")
    289
    290          for name, service in services.iteritems():
    291              self.addService(service, name)
    292
    293 -    def addService(self, service, name=None, description=None,
    294          authenticator=None, expose_request=None, preprocessor=None):
    295          """
    296          Adds a service to the gateway.
    297
    298          @param service: The service to add to the gateway.
    299          @type service: C{callable}, class instance, or a module
    300          @param name: The name of the service.
    301          @type name: C{str}
    302          @raise pyamf.remoting.RemotingError: Service already exists.
    303          @raise TypeError: C{service} cannot be a scalar value.
    304          @raise TypeError: C{service} must be C{callable} or a module.
    305          """
    306          if isinstance(service, (int, long, float, basestring)):
    307              raise TypeError("Service cannot be a scalar value")
    308
    309          allowed_types = (types.ModuleType, types.FunctionType, types.DictType,
    310              types.MethodType, types.InstanceType, types.ObjectType)
    311
    312          if not callable(service) and not isinstance(service, allowed_types):
    313              raise TypeError("Service must be a callable, module, or an object")
    314
    315          if name is None:
    316              # TODO: include the module in the name
    317              if isinstance(service, (type, types.ClassType)):
    318                  name = service.__name__
    319              elif isinstance(service, types.FunctionType):
    320                  name = service.func_name
    321              elif isinstance(service, types.ModuleType):
    322                  name = service.__name__
    323              else:
    324                  name = str(service)
    325
    326          if name in self.services:
    327              raise remoting.RemotingError("Service %s already exists" % name)
    328
    329          self.services[name] = ServiceWrapper(service, description,
    330              authenticator, expose_request, preprocessor)
    331
    332 -    def _get_timezone_offset(self):
    333          if self.timezone_offset is None:
    334              return None
    335
    336          if isinstance(self.timezone_offset, datetime.timedelta):
    337              return self.timezone_offset
    338
    339          return datetime.timedelta(seconds=self.timezone_offset)
    340
    341 -    def removeService(self, service):
    342          """
    343          Removes a service from the gateway.
    344
    345          @param service: The service to remove from the gateway.
    346          @type service: C{callable} or a class instance
    347          @raise NameError: Service not found.
    348          """
    349          if service not in self.services:
    350              raise NameError("Service %s not found" % str(service))
    351
    352          for name, wrapper in self.services.iteritems():
    353              if isinstance(service, basestring) and service == name:
    354                  del self.services[name]
    355
    356                  return
    357              elif isinstance(service, ServiceWrapper) and wrapper == service:
    358                  del self.services[name]
    359
    360                  return
    361              elif isinstance(service, (type, types.ClassType,
    362                  types.FunctionType)) and wrapper.service == service:
    363                  del self.services[name]
    364
    365                  return
    366
    367          # shouldn't ever get here
    368          raise RuntimeError("Something went wrong ...")
    369
    370 -    def getServiceRequest(self, request, target):
    371          """
    372          Returns a service based on the message.
    373
    374          @raise UnknownServiceError: Unknown service.
    375          @param request: The AMF request.
    376          @type request: L{Request<pyamf.remoting.Request>}
    377          @rtype: L{ServiceRequest}
    378          """
    379          try:
    380              return self._request_class(
    381                  request.envelope, self.services[target], None)
    382          except KeyError:
    383              pass
    384
    385          try:
    386              sp = target.split('.')
    387              name, meth = '.'.join(sp[:-1]), sp[-1]
    388
    389              return self._request_class(
    390                  request.envelope, self.services[name], meth)
    391          except (ValueError, KeyError):
    392              pass
    393
    394          raise UnknownServiceError("Unknown service %s" % target)
    395
    396 -    def getProcessor(self, request):
    397          """
    398          Returns request processor.
    399
    400          @param request: The AMF message.
    401          @type request: L{Request<remoting.Request>}
    402          """
    403          if request.target == 'null':
    404              from pyamf.remoting import amf3
    405
    406              return amf3.RequestProcessor(self)
    407          else:
    408              from pyamf.remoting import amf0
    409
    410              return amf0.RequestProcessor(self)
    411
    412 -    def getResponse(self, amf_request):
    413          """
    414          Returns the response to the request.
    415
    416          Any implementing gateway must define this function.
    417
    418          @param amf_request: The AMF request.
    419          @type amf_request: L{Envelope<pyamf.remoting.Envelope>}
    420
    421          @return: The AMF response.
    422          @rtype: L{Envelope<pyamf.remoting.Envelope>}
    423          """
    424          raise NotImplementedError
    425
    426 -    def mustExposeRequest(self, service_request):
    427          """
    428          Decides whether the underlying http request should be exposed as the
    429          first argument to the method call. This is granular, looking at the
    430          service method first, then at the service level and finally checking
    431          the gateway.
    432
    433          @rtype: C{bool}
    434          """
    435          expose_request = service_request.service.mustExposeRequest(service_request)
    436
    437          if expose_request is None:
    438              if self.expose_request is None:
    439                  return False
    440
    441              return self.expose_request
    442
    443          return expose_request
    444
    445 -    def getAuthenticator(self, service_request):
    446          """
    447          Gets an authenticator callable based on the service_request. This is
    448          granular, looking at the service method first, then at the service
    449          level and finally to see if there is a global authenticator function
    450          for the gateway. Returns C{None} if one could not be found.
    451          """
    452          auth = service_request.service.getAuthenticator(service_request)
    453
    454          if auth is None:
    455              return self.authenticator
    456
    457          return auth
    458
    459 -    def authenticateRequest(self, service_request, username, password, **kwargs):
    460          """
    461          Processes an authentication request. If no authenticator is supplied,
    462          then authentication succeeds.
    463
    464          @return: Returns a C{bool} based on the result of authorization. A
    465              value of C{False} will stop processing the request and return an
    466              error to the client.
    467          @rtype: C{bool}
    468          """
    469          authenticator = self.getAuthenticator(service_request)
    470
    471          if authenticator is None:
    472              return True
    473
    474          args = (username, password)
    475
    476          if hasattr(authenticator, '_pyamf_expose_request'):
    477              http_request = kwargs.get('http_request', None)
    478              args = (http_request,) + args
    479
    480          return authenticator(*args) == True
    481
    482 -    def getPreprocessor(self, service_request):
    483          """
    484          Gets a preprocessor callable based on the service_request. This is
    485          granular, looking at the service method first, then at the service
    486          level and finally to see if there is a global preprocessor function
    487          for the gateway. Returns C{None} if one could not be found.
    488          """
    489          preproc = service_request.service.getPreprocessor(service_request)
    490
    491          if preproc is None:
    492              return self.preprocessor
    493
    494          return preproc
    495
    496 -    def preprocessRequest(self, service_request, *args, **kwargs):
    497          """
    498          Preprocesses a request.
    499          """
    500          processor = self.getPreprocessor(service_request)
    501
    502          if processor is None:
    503              return
    504
    505          args = (service_request,) + args
    506
    507          if hasattr(processor, '_pyamf_expose_request'):
    508              http_request = kwargs.get('http_request', None)
    509              args = (http_request,) + args
    510
    511          return processor(*args)
    512
    513 -    def callServiceRequest(self, service_request, *args, **kwargs):
    514          """
    515          Executes the service_request call
    516          """
    517          if self.mustExposeRequest(service_request):
    518              http_request = kwargs.get('http_request', None)
    519              args = (http_request,) + args
    520
    521          return service_request(*args)
    522
    523
    524 -def authenticate(func, c, expose_request=False):
    525      """
    526      A decorator that facilitates authentication per method. Setting
    527      C{expose_request} to C{True} will set the underlying request object (if
    528      there is one), usually HTTP and set it to the first argument of the
    529      authenticating callable. If there is no request object, the default is
    530      C{None}.
    531
    532      @raise TypeError: C{func} and authenticator must be callable.
    533      """
    534      if not callable(func):
    535          raise TypeError('func must be callable')
    536
    537      if not callable(c):
    538          raise TypeError('Authenticator must be callable')
    539
    540      attr = func
    541
    542      if isinstance(func, types.UnboundMethodType):
    543          attr = func.im_func
    544
    545      if expose_request is True:
    546          c = globals()['expose_request'](c)
    547
    548      setattr(attr, '_pyamf_authenticator', c)
    549
    550      return func
    551
    552
    553 -def expose_request(func):
    554      """
    555      A decorator that adds an expose_request flag to the underlying callable.
    556
    557      @raise TypeError: C{func} must be callable.
    558      """
    559      if not callable(func):
    560          raise TypeError("func must be callable")
    561
    562      if isinstance(func, types.UnboundMethodType):
    563          setattr(func.im_func, '_pyamf_expose_request', True)
    564      else:
    565          setattr(func, '_pyamf_expose_request', True)
    566
    567      return func
    568
    569
    570 -def preprocess(func, c, expose_request=False):
    571      """
    572      A decorator that facilitates preprocessing per method. Setting
    573      C{expose_request} to C{True} will set the underlying request object (if
    574      there is one), usually HTTP and set it to the first argument of the
    575      preprocessing callable. If there is no request object, the default is
    576      C{None}.
    577
    578      @raise TypeError: C{func} and preprocessor must be callable.
    579      """
    580      if not callable(func):
    581          raise TypeError('func must be callable')
    582
    583      if not callable(c):
    584          raise TypeError('Preprocessor must be callable')
    585
    586      attr = func
    587
    588      if isinstance(func, types.UnboundMethodType):
    589          attr = func.im_func
    590
    591      if expose_request is True:
    592          c = globals()['expose_request'](c)
    593
    594      setattr(attr, '_pyamf_preprocessor', c)
    595
    596      return func
    597
    598
    599 -def format_exception():
    600      import traceback
    601
    602      f = util.StringIO()
    603
    604      traceback.print_exc(file=f)
    605
    606      return f.getvalue()
    607