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.txt for details. 
  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  #: Default AMF client type. 
 17  #: @see: L{ClientTypes<pyamf.ClientTypes>} 
 18  DEFAULT_CLIENT_TYPE = pyamf.ClientTypes.Flash6 
 19   
 20  #: Default user agent is C{PyAMF/x.x.x}. 
 21  DEFAULT_USER_AGENT = 'PyAMF/%s' % '.'.join(map(lambda x: str(x), 
 22      pyamf.__version__)) 
 23   
 24  HTTP_OK = 200 
 25   
 26   
27 -def convert_args(args):
28 if args == (tuple(),): 29 return [] 30 else: 31 return [x for x in args]
32 33
34 -class ServiceMethodProxy(object):
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
46 - def __init__(self, service, name):
47 self.service = service 48 self.name = name
49
50 - def __call__(self, *args):
51 """ 52 Inform the proxied service that this function has been called. 53 """ 54 55 return self.service._call(self, *args)
56
57 - def __str__(self):
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
70 -class ServiceProxy(object):
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
93 - def __getattr__(self, name):
94 return ServiceMethodProxy(self, name)
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 # XXX nick: What to do about Fault objects here? 108 return response.body 109 110 return request
111
112 - def __call__(self, *args):
113 """ 114 This allows services to be 'called' without a method name. 115 """ 116 return self._call(ServiceMethodProxy(self, None), *args)
117
118 - def __str__(self):
119 """ 120 Returns a string representation of the name of the service. 121 """ 122 return self._name
123 124
125 -class RequestWrapper(object):
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
145 - def __str__(self):
146 return str(self.id)
147
148 - def setResponse(self, response):
149 """ 150 A response has been received by the gateway 151 """ 152 # XXX nick: What to do about Fault objects here? 153 self.response = response 154 self.result = self.response.body 155 156 if isinstance(self.result, remoting.ErrorFault): 157 self.result.raiseException()
158
159 - def _get_result(self):
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
169 - def _set_result(self, result):
170 self._result = result
171 172 result = property(_get_result, _set_result)
173 174
175 -class RemotingService(object):
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
208 - def __init__(self, url, amf_version=pyamf.AMF0, client_type=DEFAULT_CLIENT_TYPE, 209 referer=None, user_agent=DEFAULT_USER_AGENT, strict=False, 210 logger=None):
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
226 - def _setUrl(self, url):
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
275 - def addHeader(self, name, value, must_understand=False):
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
287 - def addHTTPHeader(self, name, value):
288 """ 289 Adds a header to the underlying HTTP connection. 290 """ 291 self.http_headers[name] = value
292
293 - def removeHTTPHeader(self, name):
294 """ 295 Deletes an HTTP header. 296 """ 297 del self.http_headers[name]
298
299 - def getService(self, name, auto_execute=True):
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
314 - def getRequest(self, id_):
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
326 - def addRequest(self, service, *args):
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
341 - def removeRequest(self, service, *args):
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
366 - def getAMFRequest(self, requests):
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
391 - def _get_execute_headers(self):
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
404 - def execute_single(self, request):
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
429 - def execute(self):
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
454 - def _getResponse(self):
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
528 - def setCredentials(self, username, password):
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