Ticket #492: __init__.py

File __init__.py, 15.9 KB (added by webysther, 17 months ago)
Line 
1# Copyright (c) 2007-2009 The PyAMF Project.
2# See LICENSE for details.
3
4"""
5Remoting client implementation.
6
7@since: 0.1.0
8"""
9
10import httplib, urlparse
11
12import pyamf
13from pyamf import remoting, logging
14
15#: Default AMF client type.
16#: @see: L{ClientTypes<pyamf.ClientTypes>}
17DEFAULT_CLIENT_TYPE = pyamf.ClientTypes.Flash6
18
19#: Default user agent is C{PyAMF/x.x.x}.
20DEFAULT_USER_AGENT = 'PyAMF/%s' % '.'.join(map(lambda x: str(x),
21                                               pyamf.__version__))
22
23HTTP_OK = 200
24
25def convert_args(args):
26    if args == (tuple(),):
27        return []
28    else:
29        return [x for x in args]
30
31class 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
66class 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
120class 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
169class 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            try:
476                bytes = http_response.read(content_length)
477            except:
478                bytes = http_response.read()
479
480        self.logger.debug('Read %d bytes for the response' % len(bytes))
481
482        response = remoting.decode(bytes, strict=self.strict)
483        self.logger.debug('Response: %s' % response)
484
485        if remoting.APPEND_TO_GATEWAY_URL in response.headers:
486            self.original_url += response.headers[remoting.APPEND_TO_GATEWAY_URL]
487
488            self._setUrl(self.original_url)
489        elif remoting.REPLACE_GATEWAY_URL in response.headers:
490            self.original_url = response.headers[remoting.REPLACE_GATEWAY_URL]
491
492            self._setUrl(self.original_url)
493
494        if remoting.REQUEST_PERSISTENT_HEADER in response.headers:
495            data = response.headers[remoting.REQUEST_PERSISTENT_HEADER]
496
497            for k, v in data.iteritems():
498                self.headers[k] = v
499
500        http_response.close()
501
502        return response
503
504    def setCredentials(self, username, password):
505        """
506        Sets authentication credentials for accessing the remote gateway.
507        """
508        self.addHeader('Credentials', dict(userid=unicode(username),
509            password=unicode(password)), True)