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 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
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 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 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
501 - def setCredentials(self, username, password):
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