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 for details. 
  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  #: Default AMF client type. 
 16  #: @see: L{ClientTypes<pyamf.ClientTypes>} 
 17  DEFAULT_CLIENT_TYPE = pyamf.ClientTypes.Flash6 
 18   
 19  #: Default user agent is C{PyAMF/x.x.x}. 
 20  DEFAULT_USER_AGENT = 'PyAMF/%s' % '.'.join(map(lambda x: str(x), 
 21                                                 pyamf.__version__)) 
 22   
 23  HTTP_OK = 200 
 24   
25 -def convert_args(args):
26 if args == (tuple(),): 27 return [] 28 else: 29 return [x for x in args]
30
31 -class 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
66 -class 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
120 -class 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 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
169 -class 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 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 self.logger.info('Creating connection to %s://%s:%s' % (self.url[0], 262 hostname, port)) 263 self.logger.debug('Referer: %s' % self.referer) 264 self.logger.debug('User-Agent: %s' % self.user_agent)
265
266 - def addHeader(self, name, value, must_understand=False):
267 """ 268 Sets a persistent header to send with each request. 269 270 @param name: Header name. 271 @type name: C{str} 272 @param must_understand: Default is C{False}. 273 @type must_understand: C{bool} 274 """ 275 self.headers[name] = value 276 self.headers.set_required(name, must_understand)
277
278 - def addHTTPHeader(self, name, value):
279 """ 280 Adds a header to the underlying HTTP connection. 281 """ 282 self.http_headers[name] = value
283
284 - def removeHTTPHeader(self, name):
285 """ 286 Deletes an HTTP header. 287 """ 288 del self.http_headers[name]
289
290 - def getService(self, name, auto_execute=True):
291 """ 292 Returns a L{ServiceProxy} for the supplied name. Sets up an object that 293 can have method calls made to it that build the AMF requests. 294 295 @param auto_execute: Default is C{False}. 296 @type auto_execute: C{bool} 297 @raise TypeError: C{string} type required for C{name}. 298 @rtype: L{ServiceProxy} 299 """ 300 if not isinstance(name, basestring): 301 raise TypeError('string type required') 302 303 return ServiceProxy(self, name, auto_execute)
304
305 - def getRequest(self, id_):
306 """ 307 Gets a request based on the id. 308 309 @raise LookupError: Request not found. 310 """ 311 for request in self.requests: 312 if request.id == id_: 313 return request 314 315 raise LookupError("Request %s not found" % id_)
316
317 - def addRequest(self, service, *args):
318 """ 319 Adds a request to be sent to the remoting gateway. 320 """ 321 wrapper = RequestWrapper(self, '/%d' % self.request_number, 322 service, *args) 323 324 self.request_number += 1 325 self.requests.append(wrapper) 326 self.logger.debug('Adding request %s%r' % (wrapper.service, args)) 327 328 return wrapper
329
330 - def removeRequest(self, service, *args):
331 """ 332 Removes a request from the pending request list. 333 334 @raise LookupError: Request not found. 335 """ 336 if isinstance(service, RequestWrapper): 337 self.logger.debug('Removing request: %s' % ( 338 self.requests[self.requests.index(service)])) 339 del self.requests[self.requests.index(service)] 340 341 return 342 343 for request in self.requests: 344 if request.service == service and request.args == args: 345 self.logger.debug('Removing request: %s' % ( 346 self.requests[self.requests.index(request)])) 347 del self.requests[self.requests.index(request)] 348 349 return 350 351 raise LookupError("Request not found")
352
353 - def getAMFRequest(self, requests):
354 """ 355 Builds an AMF request L{Envelope<pyamf.remoting.Envelope>} from a 356 supplied list of requests. 357 358 @param requests: List of requests 359 @type requests: C{list} 360 @rtype: L{Envelope<pyamf.remoting.Envelope>} 361 """ 362 envelope = remoting.Envelope(self.amf_version, self.client_type) 363 364 self.logger.debug('AMF version: %s' % self.amf_version) 365 self.logger.debug('Client type: %s' % self.client_type) 366 367 for request in requests: 368 service = request.service 369 args = list(request.args) 370 371 envelope[request.id] = remoting.Request(str(service), args) 372 373 envelope.headers = self.headers 374 375 return envelope
376
377 - def _get_execute_headers(self):
378 headers = self.http_headers.copy() 379 380 headers.update({ 381 'Content-Type': remoting.CONTENT_TYPE, 382 'User-Agent': self.user_agent 383 }) 384 385 if self.referer is not None: 386 headers['Referer'] = self.referer 387 388 return headers
389
390 - def execute_single(self, request):
391 """ 392 Builds, sends and handles the response to a single request, returning 393 the response. 394 395 @param request: 396 @type request: 397 @rtype: 398 """ 399 self.logger.debug('Executing single request: %s' % request) 400 body = remoting.encode(self.getAMFRequest([request]), strict=self.strict) 401 402 self.logger.debug('Sending POST request to %s' % self._root_url) 403 self.connection.request('POST', self._root_url, 404 body.getvalue(), 405 self._get_execute_headers() 406 ) 407 408 envelope = self._getResponse() 409 self.removeRequest(request) 410 411 return envelope[request.id]
412
413 - def execute(self):
414 """ 415 Builds, sends and handles the responses to all requests listed in 416 C{self.requests}. 417 """ 418 body = remoting.encode(self.getAMFRequest(self.requests), strict=self.strict) 419 420 self.logger.debug('Sending POST request to %s' % self._root_url) 421 self.connection.request('POST', self._root_url, 422 body.getvalue(), 423 self._get_execute_headers() 424 ) 425 426 envelope = self._getResponse() 427 428 for response in envelope: 429 request = self.getRequest(response[0]) 430 response = response[1] 431 432 request.setResponse(response) 433 434 self.removeRequest(request)
435
436 - def _getResponse(self):
437 """ 438 Gets and handles the HTTP response from the remote gateway. 439 """ 440 self.logger.debug('Waiting for response...') 441 http_response = self.connection.getresponse() 442 self.logger.debug('Got response status: %s' % http_response.status) 443 self.logger.debug('Content-Type: %s' % http_response.getheader('Content-Type')) 444 445 if http_response.status != HTTP_OK: 446 self.logger.debug('Body: %s' % http_response.read()) 447 448 if hasattr(httplib, 'responses'): 449 raise remoting.RemotingError("HTTP Gateway reported status %d %s" % ( 450 http_response.status, httplib.responses[http_response.status])) 451 452 raise remoting.RemotingError("HTTP Gateway reported status %d" % ( 453 http_response.status,)) 454 455 content_type = http_response.getheader('Content-Type') 456 457 if content_type != remoting.CONTENT_TYPE: 458 self.logger.debug('Body = %s' % http_response.read()) 459 460 raise remoting.RemotingError("Incorrect MIME type received. (got: %s)" % content_type) 461 462 content_length = http_response.getheader('Content-Length') 463 bytes = '' 464 465 self.logger.debug('Content-Length: %s' % content_length) 466 self.logger.debug('Server: %s' % http_response.getheader('Server')) 467 468 if content_length is None: 469 bytes = http_response.read() 470 else: 471 bytes = http_response.read(content_length) 472 473 self.logger.debug('Read %d bytes for the response' % len(bytes)) 474 475 response = remoting.decode(bytes, strict=self.strict) 476 self.logger.debug('Response: %s' % response) 477 478 if remoting.APPEND_TO_GATEWAY_URL in response.headers: 479 self.original_url += response.headers[remoting.APPEND_TO_GATEWAY_URL] 480 481 self._setUrl(self.original_url) 482 elif remoting.REPLACE_GATEWAY_URL in response.headers: 483 self.original_url = response.headers[remoting.REPLACE_GATEWAY_URL] 484 485 self._setUrl(self.original_url) 486 487 if remoting.REQUEST_PERSISTENT_HEADER in response.headers: 488 data = response.headers[remoting.REQUEST_PERSISTENT_HEADER] 489 490 for k, v in data.iteritems(): 491 self.headers[k] = v 492 493 http_response.close() 494 495 return response
496
497 - def setCredentials(self, username, password):
498 """ 499 Sets authentication credentials for accessing the remote gateway. 500 """ 501 self.addHeader('Credentials', dict(userid=unicode(username), 502 password=unicode(password)), True)
503