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