1
2
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
16
17 DEFAULT_CLIENT_TYPE = pyamf.ClientTypes.Flash6
18
19
20 DEFAULT_USER_AGENT = 'PyAMF/%s' % '.'.join(map(lambda x: str(x),
21 pyamf.__version__))
22
23 HTTP_OK = 200
24
26 if args == (tuple(),):
27 return []
28 else:
29 return [x for x in args]
30
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
44 self.service = service
45 self.name = name
46
48 """
49 Inform the proxied service that this function has been called.
50 """
51
52 return self.service._call(self, *args)
53
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
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
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
104 return response.body
105
106 return request
107
109 """
110 This allows services to be 'called' without a method name.
111 """
112 return self._call(ServiceMethodProxy(self, None), *args)
113
115 """
116 Returns a string representation of the name of the service.
117 """
118 return self._name
119
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
142
153
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
166
167 result = property(_get_result, _set_result)
168
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
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
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
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
280 """
281 Adds a header to the underlying HTTP connection.
282 """
283 self.http_headers[name] = value
284
286 """
287 Deletes an HTTP header.
288 """
289 del self.http_headers[name]
290
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
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
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
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
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
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
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
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
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
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