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