blob: f925d3f9eace27d26fe6eb3c8de439dce17b2bfa [file] [log] [blame]
# Copyright (c) The PyAMF Project.
# See LICENSE.txt for details.
"""
Remoting client implementation.
@since: 0.1.0
"""
import urllib2
import urlparse
import pyamf
from pyamf import remoting
try:
from gzip import GzipFile
except ImportError:
GzipFile = False
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
#: Default user agent is `PyAMF/x.x(.x)`.
DEFAULT_USER_AGENT = 'PyAMF/%s' % (pyamf.version,)
class ServiceMethodProxy(object):
"""
Serves as a proxy for calling a service method.
@ivar service: The parent service.
@type service: L{ServiceProxy}
@ivar name: The name of the method.
@type name: C{str} or C{None}
@see: L{ServiceProxy.__getattr__}
"""
def __init__(self, service, name):
self.service = service
self.name = name
def __call__(self, *args):
"""
Inform the proxied service that this function has been called.
"""
return self.service._call(self, *args)
def __str__(self):
"""
Returns the full service name, including the method name if there is
one.
"""
service_name = str(self.service)
if self.name is not None:
service_name = '%s.%s' % (service_name, self.name)
return service_name
class ServiceProxy(object):
"""
Serves as a service object proxy for RPC calls. Generates
L{ServiceMethodProxy} objects for method calls.
@see: L{RequestWrapper} for more info.
@ivar _gw: The parent gateway
@type _gw: L{RemotingService}
@ivar _name: The name of the service
@type _name: L{str}
@ivar _auto_execute: If set to C{True}, when a service method is called,
the AMF request is immediately sent to the remote gateway and a
response is returned. If set to C{False}, a C{RequestWrapper}
is returned, waiting for the underlying gateway to fire the
L{execute <RemotingService.execute>} method.
"""
def __init__(self, gw, name, auto_execute=True):
self._gw = gw
self._name = name
self._auto_execute = auto_execute
def __getattr__(self, name):
return ServiceMethodProxy(self, name)
def _call(self, method_proxy, *args):
"""
Executed when a L{ServiceMethodProxy} is called. Adds a request to the
underlying gateway.
"""
request = self._gw.addRequest(method_proxy, *args)
if self._auto_execute:
response = self._gw.execute_single(request)
if response.status == remoting.STATUS_ERROR:
if hasattr(response.body, 'raiseException'):
try:
response.body.raiseException()
except:
raise
else:
raise remoting.RemotingError
return response.body
return request
def __call__(self, *args):
"""
This allows services to be 'called' without a method name.
"""
return self._call(ServiceMethodProxy(self, None), *args)
def __str__(self):
"""
Returns a string representation of the name of the service.
"""
return self._name
class RequestWrapper(object):
"""
A container object that wraps a service method request.
@ivar gw: The underlying gateway.
@type gw: L{RemotingService}
@ivar id: The id of the request.
@type id: C{str}
@ivar service: The service proxy.
@type service: L{ServiceProxy}
@ivar args: The args used to invoke the call.
@type args: C{list}
"""
def __init__(self, gw, id_, service, *args):
self.gw = gw
self.id = id_
self.service = service
self.args = args
def __str__(self):
return str(self.id)
def setResponse(self, response):
"""
A response has been received by the gateway
"""
self.response = response
self.result = self.response.body
if isinstance(self.result, remoting.ErrorFault):
self.result.raiseException()
def _get_result(self):
"""
Returns the result of the called remote request. If the request has not
yet been called, an C{AttributeError} exception is raised.
"""
if not hasattr(self, '_result'):
raise AttributeError(
"'RequestWrapper' object has no attribute 'result'")
return self._result
def _set_result(self, result):
self._result = result
result = property(_get_result, _set_result)
class RemotingService(object):
"""
Acts as a client for AMF calls.
@ivar url: The url of the remote gateway. Accepts C{http} or C{https} as
valid schemes.
@type url: C{string}
@ivar requests: The list of pending requests to process.
@type requests: C{list}
@ivar request_number: A unique identifier for tracking the number of
requests.
@ivar amf_version: The AMF version to use. See
L{ENCODING_TYPES<pyamf.ENCODING_TYPES>}.
@ivar referer: The referer, or HTTP referer, identifies the address of the
client. Ignored by default.
@type referer: C{string}
@ivar user_agent: Contains information about the user agent (client)
originating the request. See L{DEFAULT_USER_AGENT}.
@type user_agent: C{string}
@ivar headers: A list of persistent headers to send with each request.
@type headers: L{HeaderCollection<pyamf.remoting.HeaderCollection>}
@ivar http_headers: A dict of HTTP headers to apply to the underlying HTTP
connection.
@type http_headers: L{dict}
@ivar strict: Whether to use strict AMF en/decoding or not.
@ivar opener: The function used to power the connection to the remote
server. Defaults to U{urllib2.urlopen<http://
docs.python.org/library/urllib2.html#urllib2.urlopen}.
"""
def __init__(self, url, amf_version=pyamf.AMF0, **kwargs):
self.original_url = url
self.amf_version = amf_version
self.requests = []
self.request_number = 1
self.headers = remoting.HeaderCollection()
self.http_headers = {}
self.proxy_args = None
self.user_agent = kwargs.pop('user_agent', DEFAULT_USER_AGENT)
self.referer = kwargs.pop('referer', None)
self.strict = kwargs.pop('strict', False)
self.logger = kwargs.pop('logger', None)
self.opener = kwargs.pop('opener', urllib2.urlopen)
if kwargs:
raise TypeError('Unexpected keyword arguments %r' % (kwargs,))
self._setUrl(url)
def _setUrl(self, url):
"""
"""
self.url = urlparse.urlparse(url)
self._root_url = url
if not self.url[0] in ('http', 'https'):
raise ValueError('Unknown scheme %r' % (self.url[0],))
if self.logger:
self.logger.info('Connecting to %r', self._root_url)
self.logger.debug('Referer: %r', self.referer)
self.logger.debug('User-Agent: %r', self.user_agent)
def setProxy(self, host, type='http'):
"""
Set the proxy for all requests to use.
@see: U{The Python Docs<http://docs.python.org/library/urllib2.html#
urllib2.Request.set_proxy}
"""
self.proxy_args = (host, type)
def addHeader(self, name, value, must_understand=False):
"""
Sets a persistent header to send with each request.
@param name: Header name.
"""
self.headers[name] = value
self.headers.set_required(name, must_understand)
def addHTTPHeader(self, name, value):
"""
Adds a header to the underlying HTTP connection.
"""
self.http_headers[name] = value
def removeHTTPHeader(self, name):
"""
Deletes an HTTP header.
"""
del self.http_headers[name]
def getService(self, name, auto_execute=True):
"""
Returns a L{ServiceProxy} for the supplied name. Sets up an object that
can have method calls made to it that build the AMF requests.
@rtype: L{ServiceProxy}
"""
if not isinstance(name, basestring):
raise TypeError('string type required')
return ServiceProxy(self, name, auto_execute)
def getRequest(self, id_):
"""
Gets a request based on the id.
:raise LookupError: Request not found.
"""
for request in self.requests:
if request.id == id_:
return request
raise LookupError("Request %r not found" % (id_,))
def addRequest(self, service, *args):
"""
Adds a request to be sent to the remoting gateway.
"""
wrapper = RequestWrapper(self, '/%d' % self.request_number,
service, *args)
self.request_number += 1
self.requests.append(wrapper)
if self.logger:
self.logger.debug('Adding request %s%r', wrapper.service, args)
return wrapper
def removeRequest(self, service, *args):
"""
Removes a request from the pending request list.
"""
if isinstance(service, RequestWrapper):
if self.logger:
self.logger.debug('Removing request: %s',
self.requests[self.requests.index(service)])
del self.requests[self.requests.index(service)]
return
for request in self.requests:
if request.service == service and request.args == args:
if self.logger:
self.logger.debug('Removing request: %s',
self.requests[self.requests.index(request)])
del self.requests[self.requests.index(request)]
return
raise LookupError("Request not found")
def getAMFRequest(self, requests):
"""
Builds an AMF request {LEnvelope<pyamf.remoting.Envelope>} from a
supplied list of requests.
"""
envelope = remoting.Envelope(self.amf_version)
if self.logger:
self.logger.debug('AMF version: %s' % self.amf_version)
for request in requests:
service = request.service
args = list(request.args)
envelope[request.id] = remoting.Request(str(service), args)
envelope.headers = self.headers
return envelope
def _get_execute_headers(self):
headers = self.http_headers.copy()
headers.update({
'Content-Type': remoting.CONTENT_TYPE,
'User-Agent': self.user_agent
})
if self.referer is not None:
headers['Referer'] = self.referer
return headers
def execute_single(self, request):
"""
Builds, sends and handles the response to a single request, returning
the response.
"""
if self.logger:
self.logger.debug('Executing single request: %s', request)
self.removeRequest(request)
body = remoting.encode(self.getAMFRequest([request]), strict=self.strict)
http_request = urllib2.Request(self._root_url, body.getvalue(),
self._get_execute_headers())
if self.proxy_args:
http_request.set_proxy(*self.proxy_args)
envelope = self._getResponse(http_request)
return envelope[request.id]
def execute(self):
"""
Builds, sends and handles the responses to all requests listed in
C{self.requests}.
"""
requests = self.requests[:]
for r in requests:
self.removeRequest(r)
body = remoting.encode(self.getAMFRequest(requests),
strict=self.strict)
http_request = urllib2.Request(self._root_url, body.getvalue(),
self._get_execute_headers())
if self.proxy_args:
http_request.set_proxy(*self.proxy_args)
envelope = self._getResponse(http_request)
return envelope
def _getResponse(self, http_request):
"""
Gets and handles the HTTP response from the remote gateway.
"""
if self.logger:
self.logger.debug('Sending POST request to %s', self._root_url)
try:
fbh = self.opener(http_request)
except urllib2.URLError, e:
if self.logger:
self.logger.exception('Failed request for %s',
self._root_url)
raise remoting.RemotingError(str(e))
http_message = fbh.info()
content_encoding = http_message.getheader('Content-Encoding')
content_length = http_message.getheader('Content-Length') or -1
content_type = http_message.getheader('Content-Type')
server = http_message.getheader('Server')
if self.logger:
self.logger.debug('Content-Type: %r', content_type)
self.logger.debug('Content-Encoding: %r', content_encoding)
self.logger.debug('Content-Length: %r', content_length)
self.logger.debug('Server: %r', server)
if content_type != remoting.CONTENT_TYPE:
if self.logger:
self.logger.debug('Body = %s', fbh.read())
raise remoting.RemotingError('Incorrect MIME type received. '
'(got: %s)' % (content_type,))
bytes = fbh.read(int(content_length))
if self.logger:
self.logger.debug('Read %d bytes for the response', len(bytes))
if content_encoding and content_encoding.strip().lower() == 'gzip':
if not GzipFile:
raise remoting.RemotingError(
'Decompression of Content-Encoding: %s not available.' % (
content_encoding,))
compressedstream = StringIO(bytes)
gzipper = GzipFile(fileobj=compressedstream)
bytes = gzipper.read()
gzipper.close()
response = remoting.decode(bytes, strict=self.strict)
if self.logger:
self.logger.debug('Response: %s', response)
if remoting.APPEND_TO_GATEWAY_URL in response.headers:
self.original_url += response.headers[remoting.APPEND_TO_GATEWAY_URL]
self._setUrl(self.original_url)
elif remoting.REPLACE_GATEWAY_URL in response.headers:
self.original_url = response.headers[remoting.REPLACE_GATEWAY_URL]
self._setUrl(self.original_url)
if remoting.REQUEST_PERSISTENT_HEADER in response.headers:
data = response.headers[remoting.REQUEST_PERSISTENT_HEADER]
for k, v in data.iteritems():
self.headers[k] = v
return response
def setCredentials(self, username, password):
"""
Sets authentication credentials for accessing the remote gateway.
"""
self.addHeader('Credentials', dict(userid=username.decode('utf-8'),
password=password.decode('utf-8')), True)