1
2
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
17
18 DEFAULT_CLIENT_TYPE = pyamf.ClientTypes.Flash6
19
20
21 DEFAULT_USER_AGENT = 'PyAMF/%s' % '.'.join(map(lambda x: str(x),
22 pyamf.__version__))
23
24 HTTP_OK = 200
25
26
28 if args == (tuple(),):
29 return []
30 else:
31 return [x for x in args]
32
33
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
47 self.service = service
48 self.name = name
49
51 """
52 Inform the proxied service that this function has been called.
53 """
54
55 return self.service._call(self, *args)
56
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
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
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
108 return response.body
109
110 return request
111
113 """
114 This allows services to be 'called' without a method name.
115 """
116 return self._call(ServiceMethodProxy(self, None), *args)
117
119 """
120 Returns a string representation of the name of the service.
121 """
122 return self._name
123
124
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
147
158
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
171
172 result = property(_get_result, _set_result)
173
174
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
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
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
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
288 """
289 Adds a header to the underlying HTTP connection.
290 """
291 self.http_headers[name] = value
292
294 """
295 Deletes an HTTP header.
296 """
297 del self.http_headers[name]
298
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
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
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
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
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
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
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
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
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
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