Package pyamf :: Package remoting :: Package client

[hide private]
[frames] | no frames]

# Source Code for Package pyamf.remoting.client

      1  # Copyright (c) 2007-2009 The PyAMF Project.
      2  # See LICENSE for details.
      3
      4  """
      5  Remoting client implementation.
      6
      7  @since: 0.1.0
      8  """
      9
     10  import httplib, urlparse
     11
     12  import pyamf
     13  from pyamf import remoting, logging
     14
     15  #: Default AMF client type.
     16  #: @see: L{ClientTypes<pyamf.ClientTypes>}
     17  DEFAULT_CLIENT_TYPE = pyamf.ClientTypes.Flash6
     18
     19  #: Default user agent is C{PyAMF/x.x.x}.
     20  DEFAULT_USER_AGENT = 'PyAMF/%s' % '.'.join(map(lambda x: str(x),
     21                                                 pyamf.__version__))
     22
     23  HTTP_OK = 200
     24
     25 -def convert_args(args):
     26      if args == (tuple(),):
     27          return []
     28      else:
     29          return [x for x in args]
     30
     31 -class ServiceMethodProxy(object):
     32      """
     33      Serves as a proxy for calling a service method.
     34
     35      @ivar service: The parent service.
     36      @type service: L{ServiceProxy}
     37      @ivar name: The name of the method.
     38      @type name: C{str} or C{None}
     39
     40      @see: L{ServiceProxy.__getattr__}
     41      """
     42
     43 -    def __init__(self, service, name):
     44          self.service = service
     45          self.name = name
     46
     47 -    def __call__(self, *args):
     48          """
     49          Inform the proxied service that this function has been called.
     50          """
     51
     52          return self.service._call(self, *args)
     53
     54 -    def __str__(self):
     55          """
     56          Returns the full service name, including the method name if there is
     57          one.
     58          """
     59          service_name = str(self.service)
     60
     61          if self.name is not None:
     62              service_name = '%s.%s' % (service_name, self.name)
     63
     64          return service_name
     65
     66 -class ServiceProxy(object):
     67      """
     68      Serves as a service object proxy for RPC calls. Generates
     69      L{ServiceMethodProxy} objects for method calls.
     70
     71      @see: L{RequestWrapper} for more info.
     72
     73      @ivar _gw: The parent gateway
     74      @type _gw: L{RemotingService}
     75      @ivar _name: The name of the service
     76      @type _name: C{str}
     77      @ivar _auto_execute: If set to C{True}, when a service method is called,
     78          the AMF request is immediately sent to the remote gateway and a
     79          response is returned. If set to C{False}, a L{RequestWrapper} is
     80          returned, waiting for the underlying gateway to fire the
     81          L{execute<RemotingService.execute>} method.
     82      """
     83
     84 -    def __init__(self, gw, name, auto_execute=True):
     85          self._gw = gw
     86          self._name = name
     87          self._auto_execute = auto_execute
     88
     89 -    def __getattr__(self, name):
     90          return ServiceMethodProxy(self, name)
     91
     92 -    def _call(self, method_proxy, *args):
     93          """
     94          Executed when a L{ServiceMethodProxy} is called. Adds a request to the
     95          underlying gateway. If C{_auto_execute} is set to C{True}, then the
     96          request is immediately called on the remote gateway.
     97          """
     98          request = self._gw.addRequest(method_proxy, *args)
     99
    100          if self._auto_execute:
    101              response = self._gw.execute_single(request)
    102
    103              # XXX nick: What to do about Fault objects here?
    104              return response.body
    105
    106          return request
    107
    108 -    def __call__(self, *args):
    109          """
    110          This allows services to be 'called' without a method name.
    111          """
    112          return self._call(ServiceMethodProxy(self, None), *args)
    113
    114 -    def __str__(self):
    115          """
    116          Returns a string representation of the name of the service.
    117          """
    118          return self._name
    119
    120 -class RequestWrapper(object):
    121      """
    122      A container object that wraps a service method request.
    123
    124      @ivar gw: The underlying gateway.
    125      @type gw: L{RemotingService}
    126      @ivar id: The id of the request.
    127      @type id: C{str}
    128      @ivar service: The service proxy.
    129      @type service: L{ServiceProxy}
    130      @ivar args: The args used to invoke the call.
    131      @type args: C{list}
    132      """
    133
    134 -    def __init__(self, gw, id_, service, *args):
    135          self.gw = gw
    136          self.id = id_
    137          self.service = service
    138          self.args = args
    139
    140 -    def __str__(self):
    141          return str(self.id)
    142
    143 -    def setResponse(self, response):
    144          """
    145          A response has been received by the gateway
    146          """
    147          # XXX nick: What to do about Fault objects here?
    148          self.response = response
    149          self.result = self.response.body
    150
    151          if isinstance(self.result, remoting.ErrorFault):
    152              self.result.raiseException()
    153
    154 -    def _get_result(self):
    155          """
    156          Returns the result of the called remote request. If the request has not
    157          yet been called, an C{AttributeError} exception is raised.
    158          """
    159          if not hasattr(self, '_result'):
    160              raise AttributeError("'RequestWrapper' object has no attribute 'result'")
    161
    162          return self._result
    163
    164 -    def _set_result(self, result):
    165          self._result = result
    166
    167      result = property(_get_result, _set_result)
    168
    169 -class RemotingService(object):
    170      """
    171      Acts as a client for AMF calls.
    172
    173      @ivar url: The url of the remote gateway. Accepts C{http} or C{https}
    174          as valid schemes.
    175      @type url: C{str}
    176      @ivar requests: The list of pending requests to process.
    177      @type requests: C{list}
    178      @ivar request_number: A unique identifier for tracking the number of
    179          requests.
    180      @ivar amf_version: The AMF version to use.
    181          See L{ENCODING_TYPES<pyamf.ENCODING_TYPES>}.
    182      @type amf_version: C{int}
    183      @ivar referer: The referer, or HTTP referer, identifies the address of the
    184          client. Ignored by default.
    185      @type referer: C{str}
    186      @ivar client_type: The client type. See L{ClientTypes<pyamf.ClientTypes>}.
    187      @type client_type: C{int}
    188      @ivar user_agent: Contains information about the user agent (client)
    189          originating the request. See L{DEFAULT_USER_AGENT}.
    190      @type user_agent: C{str}
    191      @ivar connection: The underlying connection to the remoting server.
    192      @type connection: C{httplib.HTTPConnection} or C{httplib.HTTPSConnection}
    193      @ivar headers: A list of persistent headers to send with each request.
    194      @type headers: L{HeaderCollection<pyamf.remoting.HeaderCollection>}
    195      @ivar http_headers: A dict of HTTP headers to apply to the underlying
    196          HTTP connection.
    197      @type http_headers: L{dict}
    198      @ivar strict: Whether to use strict AMF en/decoding or not.
    199      @type strict: C{bool}
    200      """
    201
    202 -    def __init__(self, url, amf_version=pyamf.AMF0, client_type=DEFAULT_CLIENT_TYPE,
    203                   referer=None, user_agent=DEFAULT_USER_AGENT, strict=False):
    204          self.logger = logging.instance_logger(self)
    205          self.original_url = url
    206          self.requests = []
    207          self.request_number = 1
    208
    209          self.user_agent = user_agent
    210          self.referer = referer
    211          self.amf_version = amf_version
    212          self.client_type = client_type
    213          self.headers = remoting.HeaderCollection()
    214          self.http_headers = {}
    215          self.strict = strict
    216
    217          self._setUrl(url)
    218
    219 -    def _setUrl(self, url):
    220          """
    221          @param url: Gateway URL.
    222          @type url: C{str}
    223          @raise ValueError: Unknown scheme.
    224          """
    225          self.url = urlparse.urlparse(url)
    226          self._root_url = urlparse.urlunparse(['', ''] + list(self.url[2:]))
    227
    228          port = None
    229          hostname = None
    230
    231          if hasattr(self.url, 'port'):
    232              if self.url.port is not None:
    233                  port = self.url.port
    234          else:
    235              if ':' not in self.url[1]:
    236                  hostname = self.url[1]
    237                  port = None
    238              else:
    239                  sp = self.url[1].split(':')
    240
    241                  hostname, port = sp[0], sp[1]
    242                  port = int(port)
    243
    244          if hostname is None:
    245              if hasattr(self.url, 'hostname'):
    246                  hostname = self.url.hostname
    247
    248          if self.url[0] == 'http':
    249              if port is None:
    250                  port = httplib.HTTP_PORT
    251
    252              self.connection = httplib.HTTPConnection(hostname, port)
    253          elif self.url[0] == 'https':
    254              if port is None:
    255                  port = httplib.HTTPS_PORT
    256
    257              self.connection = httplib.HTTPSConnection(hostname, port)
    258          else:
    259              raise ValueError('Unknown scheme')
    260
    261          location = '%s://%s:%s%s' % (self.url[0], hostname, port, self.url[2])
    262
    263          self.logger.info('Connecting to %s' % location)
    264          self.logger.debug('Referer: %s' % self.referer)
    265          self.logger.debug('User-Agent: %s' % self.user_agent)
    266
    267 -    def addHeader(self, name, value, must_understand=False):
    268          """
    269          Sets a persistent header to send with each request.
    270
    271          @param name: Header name.
    272          @type name: C{str}
    273          @param must_understand: Default is C{False}.
    274          @type must_understand: C{bool}
    275          """
    276          self.headers[name] = value
    277          self.headers.set_required(name, must_understand)
    278
    279 -    def addHTTPHeader(self, name, value):
    280          """
    281          Adds a header to the underlying HTTP connection.
    282          """
    283          self.http_headers[name] = value
    284
    285 -    def removeHTTPHeader(self, name):
    286          """
    287          Deletes an HTTP header.
    288          """
    289          del self.http_headers[name]
    290
    291 -    def getService(self, name, auto_execute=True):
    292          """
    293          Returns a L{ServiceProxy} for the supplied name. Sets up an object that
    294          can have method calls made to it that build the AMF requests.
    295
    296          @param auto_execute: Default is C{False}.
    297          @type auto_execute: C{bool}
    298          @raise TypeError: C{string} type required for C{name}.
    299          @rtype: L{ServiceProxy}
    300          """
    301          if not isinstance(name, basestring):
    302              raise TypeError('string type required')
    303
    304          return ServiceProxy(self, name, auto_execute)
    305
    306 -    def getRequest(self, id_):
    307          """
    308          Gets a request based on the id.
    309
    310          @raise LookupError: Request not found.
    311          """
    312          for request in self.requests:
    313              if request.id == id_:
    314                  return request
    315
    316          raise LookupError("Request %s not found" % id_)
    317
    318 -    def addRequest(self, service, *args):
    319          """
    320          Adds a request to be sent to the remoting gateway.
    321          """
    322          wrapper = RequestWrapper(self, '/%d' % self.request_number,
    323              service, *args)
    324
    325          self.request_number += 1
    326          self.requests.append(wrapper)
    327          self.logger.debug('Adding request %s%r' % (wrapper.service, args))
    328
    329          return wrapper
    330
    331 -    def removeRequest(self, service, *args):
    332          """
    333          Removes a request from the pending request list.
    334
    335          @raise LookupError: Request not found.
    336          """
    337          if isinstance(service, RequestWrapper):
    338              self.logger.debug('Removing request: %s' % (
    339                  self.requests[self.requests.index(service)]))
    340              del self.requests[self.requests.index(service)]
    341
    342              return
    343
    344          for request in self.requests:
    345              if request.service == service and request.args == args:
    346                  self.logger.debug('Removing request: %s' % (
    347                      self.requests[self.requests.index(request)]))
    348                  del self.requests[self.requests.index(request)]
    349
    350                  return
    351
    352          raise LookupError("Request not found")
    353
    354 -    def getAMFRequest(self, requests):
    355          """
    356          Builds an AMF request L{Envelope<pyamf.remoting.Envelope>} from a
    357          supplied list of requests.
    358
    359          @param requests: List of requests
    360          @type requests: C{list}
    361          @rtype: L{Envelope<pyamf.remoting.Envelope>}
    362          """
    363          envelope = remoting.Envelope(self.amf_version, self.client_type)
    364
    365          self.logger.debug('AMF version: %s' % self.amf_version)
    366          self.logger.debug('Client type: %s' % self.client_type)
    367
    368          for request in requests:
    369              service = request.service
    370              args = list(request.args)
    371
    372              envelope[request.id] = remoting.Request(str(service), args)
    373
    374          envelope.headers = self.headers
    375
    376          return envelope
    377
    378 -    def _get_execute_headers(self):
    379          headers = self.http_headers.copy()
    380
    381          headers.update({
    382              'Content-Type': remoting.CONTENT_TYPE,
    383              'User-Agent': self.user_agent
    384          })
    385
    386          if self.referer is not None:
    387              headers['Referer'] = self.referer
    388
    389          return headers
    390
    391 -    def execute_single(self, request):
    392          """
    393          Builds, sends and handles the response to a single request, returning
    394          the response.
    395
    396          @param request:
    397          @type request:
    398          @rtype:
    399          """
    400          self.logger.debug('Executing single request: %s' % request)
    401          body = remoting.encode(self.getAMFRequest([request]), strict=self.strict)
    402
    403          self.logger.debug('Sending POST request to %s' % self._root_url)
    404          self.connection.request('POST', self._root_url,
    405              body.getvalue(),
    406              self._get_execute_headers()
    407          )
    408
    409          envelope = self._getResponse()
    410          self.removeRequest(request)
    411
    412          return envelope[request.id]
    413
    414 -    def execute(self):
    415          """
    416          Builds, sends and handles the responses to all requests listed in
    417          C{self.requests}.
    418          """
    419          body = remoting.encode(self.getAMFRequest(self.requests), strict=self.strict)
    420
    421          self.logger.debug('Sending POST request to %s' % self._root_url)
    422          self.connection.request('POST', self._root_url,
    423              body.getvalue(),
    424              self._get_execute_headers()
    425          )
    426
    427          envelope = self._getResponse()
    428
    429          for response in envelope:
    430              request = self.getRequest(response[0])
    431              response = response[1]
    432
    433              request.setResponse(response)
    434
    435              self.removeRequest(request)
    436
    437 -    def _getResponse(self):
    438          """
    439          Gets and handles the HTTP response from the remote gateway.
    440
    441          @raise RemotingError: HTTP Gateway reported error status.
    442          @raise RemotingError: Incorrect MIME type received.
    443          """
    444          self.logger.debug('Waiting for response...')
    445          http_response = self.connection.getresponse()
    446          self.logger.debug('Got response status: %s' % http_response.status)
    447          self.logger.debug('Content-Type: %s' % http_response.getheader('Content-Type'))
    448
    449          if http_response.status != HTTP_OK:
    450              self.logger.debug('Body: %s' % http_response.read())
    451
    452              if hasattr(httplib, 'responses'):
    453                  raise remoting.RemotingError("HTTP Gateway reported status %d %s" % (
    454                      http_response.status, httplib.responses[http_response.status]))
    455
    456              raise remoting.RemotingError("HTTP Gateway reported status %d" % (
    457                  http_response.status,))
    458
    459          content_type = http_response.getheader('Content-Type')
    460
    461          if content_type != remoting.CONTENT_TYPE:
    462              self.logger.debug('Body = %s' % http_response.read())
    463
    464              raise remoting.RemotingError("Incorrect MIME type received. (got: %s)" % content_type)
    465
    466          content_length = http_response.getheader('Content-Length')
    467          bytes = ''
    468
    469          self.logger.debug('Content-Length: %s' % content_length)
    470          self.logger.debug('Server: %s' % http_response.getheader('Server'))
    471
    472          if content_length is None:
    473              bytes = http_response.read()
    474          else:
    475              bytes = http_response.read(content_length)
    476
    477          self.logger.debug('Read %d bytes for the response' % len(bytes))
    478
    479          response = remoting.decode(bytes, strict=self.strict)
    480          self.logger.debug('Response: %s' % response)
    481
    482          if remoting.APPEND_TO_GATEWAY_URL in response.headers:
    483              self.original_url += response.headers[remoting.APPEND_TO_GATEWAY_URL]
    484
    485              self._setUrl(self.original_url)
    486          elif remoting.REPLACE_GATEWAY_URL in response.headers:
    487              self.original_url = response.headers[remoting.REPLACE_GATEWAY_URL]
    488
    489              self._setUrl(self.original_url)
    490
    491          if remoting.REQUEST_PERSISTENT_HEADER in response.headers:
    492              data = response.headers[remoting.REQUEST_PERSISTENT_HEADER]
    493
    494              for k, v in data.iteritems():
    495                  self.headers[k] = v
    496
    497          http_response.close()
    498
    499          return response
    500
    501 -    def setCredentials(self, username, password):
    502          """
    503          Sets authentication credentials for accessing the remote gateway.
    504          """
    505          self.addHeader('Credentials', dict(userid=unicode(username),
    506              password=unicode(password)), True)
    507