1#! /usr/bin/env python 2# $Header$ 3# 4# Copyright (c) 2001 Zolera Systems. All rights reserved. 5 6from ZSI import _copyright, _seqtypes, ParsedSoap, SoapWriter, TC, ZSI_SCHEMA_URI,\ 7 EvaluateException, FaultFromFaultMessage, _child_elements, _attrs, _find_arraytype,\ 8 _find_type, _get_idstr, _get_postvalue_from_absoluteURI, FaultException, WSActionException,\ 9 UNICODE_ENCODING 10from ZSI.auth import AUTH 11from ZSI.TC import AnyElement, AnyType, String, TypeCode, _get_global_element_declaration,\ 12 _get_type_definition 13from ZSI.TCcompound import Struct 14import base64, httplib, Cookie, types, time, urlparse 15from ZSI.address import Address 16from ZSI.wstools.logging import getLogger as _GetLogger 17_b64_encode = base64.encodestring 18 19class _AuthHeader: 20 """<BasicAuth xmlns="ZSI_SCHEMA_URI"> 21 <Name>%s</Name><Password>%s</Password> 22 </BasicAuth> 23 """ 24 def __init__(self, name=None, password=None): 25 self.Name = name 26 self.Password = password 27_AuthHeader.typecode = Struct(_AuthHeader, ofwhat=(String((ZSI_SCHEMA_URI,'Name'), typed=False), 28 String((ZSI_SCHEMA_URI,'Password'), typed=False)), pname=(ZSI_SCHEMA_URI,'BasicAuth'), 29 typed=False) 30 31 32class _Caller: 33 '''Internal class used to give the user a callable object 34 that calls back to the Binding object to make an RPC call. 35 ''' 36 37 def __init__(self, binding, name, namespace=None): 38 self.binding = binding 39 self.name = name 40 self.namespace = namespace 41 42 def __call__(self, *args): 43 nsuri = self.namespace 44 if nsuri is None: 45 return self.binding.RPC(None, self.name, args, 46 encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", 47 replytype=TC.Any(self.name+"Response")) 48 49 return self.binding.RPC(None, (nsuri,self.name), args, 50 encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", 51 replytype=TC.Any((nsuri,self.name+"Response"))) 52 53 54class _NamedParamCaller: 55 '''Similar to _Caller, expect that there are named parameters 56 not positional. 57 ''' 58 59 def __init__(self, binding, name, namespace=None): 60 self.binding = binding 61 self.name = name 62 self.namespace = namespace 63 64 def __call__(self, **params): 65 # Pull out arguments that Send() uses 66 kw = {} 67 for key in [ 'auth_header', 'nsdict', 'requesttypecode', 'soapaction' ]: 68 if params.has_key(key): 69 kw[key] = params[key] 70 del params[key] 71 72 nsuri = self.namespace 73 if nsuri is None: 74 return self.binding.RPC(None, self.name, None, 75 encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", 76 _args=params, 77 replytype=TC.Any(self.name+"Response", aslist=False), 78 **kw) 79 80 return self.binding.RPC(None, (nsuri,self.name), None, 81 encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", 82 _args=params, 83 replytype=TC.Any((nsuri,self.name+"Response"), aslist=False), 84 **kw) 85 86 87class _Binding: 88 '''Object that represents a binding (connection) to a SOAP server. 89 Once the binding is created, various ways of sending and 90 receiving SOAP messages are available. 91 ''' 92 defaultHttpTransport = httplib.HTTPConnection 93 defaultHttpsTransport = httplib.HTTPSConnection 94 logger = _GetLogger('ZSI.client.Binding') 95 96 def __init__(self, nsdict=None, transport=None, url=None, tracefile=None, 97 readerclass=None, writerclass=None, soapaction='', 98 wsAddressURI=None, sig_handler=None, transdict=None, **kw): 99 '''Initialize. 100 Keyword arguments include: 101 transport -- default use HTTPConnection. 102 transdict -- dict of values to pass to transport. 103 url -- URL of resource, POST is path 104 soapaction -- value of SOAPAction header 105 auth -- (type, name, password) triplet; default is unauth 106 nsdict -- namespace entries to add 107 tracefile -- file to dump packet traces 108 cert_file, key_file -- SSL data (q.v.) 109 readerclass -- DOM reader class 110 writerclass -- DOM writer class, implements MessageInterface 111 wsAddressURI -- namespaceURI of WS-Address to use. By default 112 it's not used. 113 sig_handler -- XML Signature handler, must sign and verify. 114 endPointReference -- optional Endpoint Reference. 115 ''' 116 self.data = None 117 self.ps = None 118 self.user_headers = [] 119 self.nsdict = nsdict or {} 120 self.transport = transport 121 self.transdict = transdict or {} 122 self.url = url 123 self.trace = tracefile 124 self.readerclass = readerclass 125 self.writerclass = writerclass 126 self.soapaction = soapaction 127 self.wsAddressURI = wsAddressURI 128 self.sig_handler = sig_handler 129 self.address = None 130 self.endPointReference = kw.get('endPointReference', None) 131 self.cookies = Cookie.SimpleCookie() 132 self.http_callbacks = {} 133 134 if kw.has_key('auth'): 135 self.SetAuth(*kw['auth']) 136 else: 137 self.SetAuth(AUTH.none) 138 139 def SetAuth(self, style, user=None, password=None): 140 '''Change auth style, return object to user. 141 ''' 142 self.auth_style, self.auth_user, self.auth_pass = \ 143 style, user, password 144 return self 145 146 def SetURL(self, url): 147 '''Set the URL we post to. 148 ''' 149 self.url = url 150 return self 151 152 def ResetHeaders(self): 153 '''Empty the list of additional headers. 154 ''' 155 self.user_headers = [] 156 return self 157 158 def ResetCookies(self): 159 '''Empty the list of cookies. 160 ''' 161 self.cookies = Cookie.SimpleCookie() 162 163 def AddHeader(self, header, value): 164 '''Add a header to send. 165 ''' 166 self.user_headers.append((header, value)) 167 return self 168 169 def __addcookies(self): 170 '''Add cookies from self.cookies to request in self.h 171 ''' 172 for cname, morsel in self.cookies.items(): 173 attrs = [] 174 value = morsel.get('version', '') 175 if value != '' and value != '0': 176 attrs.append('$Version=%s' % value) 177 attrs.append('%s=%s' % (cname, morsel.coded_value)) 178 value = morsel.get('path') 179 if value: 180 attrs.append('$Path=%s' % value) 181 value = morsel.get('domain') 182 if value: 183 attrs.append('$Domain=%s' % value) 184 self.h.putheader('Cookie', "; ".join(attrs)) 185 186 def RPC(self, url, opname, obj, replytype=None, **kw): 187 '''Send a request, return the reply. See Send() and Recieve() 188 docstrings for details. 189 ''' 190 self.Send(url, opname, obj, **kw) 191 return self.Receive(replytype, **kw) 192 193 def Send(self, url, opname, obj, nsdict={}, soapaction=None, wsaction=None, 194 endPointReference=None, soapheaders=(), **kw): 195 '''Send a message. If url is None, use the value from the 196 constructor (else error). obj is the object (data) to send. 197 Data may be described with a requesttypecode keyword, the default 198 is the class's typecode (if there is one), else Any. 199 200 Try to serialize as a Struct, if this is not possible serialize an Array. If 201 data is a sequence of built-in python data types, it will be serialized as an 202 Array, unless requesttypecode is specified. 203 204 arguments: 205 url -- 206 opname -- struct wrapper 207 obj -- python instance 208 209 key word arguments: 210 nsdict -- 211 soapaction -- 212 wsaction -- WS-Address Action, goes in SOAP Header. 213 endPointReference -- set by calling party, must be an 214 EndPointReference type instance. 215 soapheaders -- list of pyobj, typically w/typecode attribute. 216 serialized in the SOAP:Header. 217 requesttypecode -- 218 219 ''' 220 url = url or self.url 221 endPointReference = endPointReference or self.endPointReference 222 223 # Serialize the object. 224 d = {} 225 d.update(self.nsdict) 226 d.update(nsdict) 227 228 sw = SoapWriter(nsdict=d, header=True, outputclass=self.writerclass, 229 encodingStyle=kw.get('encodingStyle'),) 230 231 requesttypecode = kw.get('requesttypecode') 232 if kw.has_key('_args'): #NamedParamBinding 233 tc = requesttypecode or TC.Any(pname=opname, aslist=False) 234 sw.serialize(kw['_args'], tc) 235 elif not requesttypecode: 236 tc = getattr(obj, 'typecode', None) or TC.Any(pname=opname, aslist=False) 237 try: 238 if type(obj) in _seqtypes: 239 obj = dict(map(lambda i: (i.typecode.pname,i), obj)) 240 except AttributeError: 241 # can't do anything but serialize this in a SOAP:Array 242 tc = TC.Any(pname=opname, aslist=True) 243 else: 244 tc = TC.Any(pname=opname, aslist=False) 245 246 sw.serialize(obj, tc) 247 else: 248 sw.serialize(obj, requesttypecode) 249 250 for i in soapheaders: 251 sw.serialize_header(i) 252 253 # 254 # Determine the SOAP auth element. SOAP:Header element 255 if self.auth_style & AUTH.zsibasic: 256 sw.serialize_header(_AuthHeader(self.auth_user, self.auth_pass), 257 _AuthHeader.typecode) 258 259 # 260 # Serialize WS-Address 261 if self.wsAddressURI is not None: 262 if self.soapaction and wsaction.strip('\'"') != self.soapaction: 263 raise WSActionException, 'soapAction(%s) and WS-Action(%s) must match'\ 264 %(self.soapaction,wsaction) 265 266 self.address = Address(url, self.wsAddressURI) 267 self.address.setRequest(endPointReference, wsaction) 268 self.address.serialize(sw) 269 270 # 271 # WS-Security Signature Handler 272 if self.sig_handler is not None: 273 self.sig_handler.sign(sw) 274 275 scheme,netloc,path,nil,nil,nil = urlparse.urlparse(url) 276 transport = self.transport 277 if transport is None and url is not None: 278 if scheme == 'https': 279 transport = self.defaultHttpsTransport 280 elif scheme == 'http': 281 transport = self.defaultHttpTransport 282 else: 283 raise RuntimeError, 'must specify transport or url startswith https/http' 284 285 # Send the request. 286 if issubclass(transport, httplib.HTTPConnection) is False: 287 raise TypeError, 'transport must be a HTTPConnection' 288 289 soapdata = str(sw) 290 self.h = transport(netloc, None, **self.transdict) 291 self.h.connect() 292 self.SendSOAPData(soapdata, url, soapaction, **kw) 293 294 def SendSOAPData(self, soapdata, url, soapaction, headers={}, **kw): 295 # Tracing? 296 if self.trace: 297 print >>self.trace, "_" * 33, time.ctime(time.time()), "REQUEST:" 298 print >>self.trace, soapdata 299 300 url = url or self.url 301 request_uri = _get_postvalue_from_absoluteURI(url) 302 self.h.putrequest("POST", request_uri) 303 self.h.putheader("Content-Length", "%d" % len(soapdata)) 304 self.h.putheader("Content-Type", 'text/xml; charset="%s"' %UNICODE_ENCODING) 305 self.__addcookies() 306 307 for header,value in headers.items(): 308 self.h.putheader(header, value) 309 310 SOAPActionValue = '"%s"' % (soapaction or self.soapaction) 311 self.h.putheader("SOAPAction", SOAPActionValue) 312 if self.auth_style & AUTH.httpbasic: 313 val = _b64_encode(self.auth_user + ':' + self.auth_pass) \ 314 .replace("\012", "") 315 self.h.putheader('Authorization', 'Basic ' + val) 316 elif self.auth_style == AUTH.httpdigest and not headers.has_key('Authorization') \ 317 and not headers.has_key('Expect'): 318 def digest_auth_cb(response): 319 self.SendSOAPDataHTTPDigestAuth(response, soapdata, url, request_uri, soapaction, **kw) 320 self.http_callbacks[401] = None 321 self.http_callbacks[401] = digest_auth_cb 322 323 for header,value in self.user_headers: 324 self.h.putheader(header, value) 325 self.h.endheaders() 326 self.h.send(soapdata) 327 328 # Clear prior receive state. 329 self.data, self.ps = None, None 330 331 def SendSOAPDataHTTPDigestAuth(self, response, soapdata, url, request_uri, soapaction, **kw): 332 '''Resend the initial request w/http digest authorization headers. 333 The SOAP server has requested authorization. Fetch the challenge, 334 generate the authdict for building a response. 335 ''' 336 if self.trace: 337 print >>self.trace, "------ Digest Auth Header" 338 url = url or self.url 339 if response.status != 401: 340 raise RuntimeError, 'Expecting HTTP 401 response.' 341 if self.auth_style != AUTH.httpdigest: 342 raise RuntimeError,\ 343 'Auth style(%d) does not support requested digest authorization.' %self.auth_style 344 345 from ZSI.digest_auth import fetch_challenge,\ 346 generate_response,\ 347 build_authorization_arg,\ 348 dict_fetch 349 350 chaldict = fetch_challenge( response.getheader('www-authenticate') ) 351 if dict_fetch(chaldict,'challenge','').lower() == 'digest' and \ 352 dict_fetch(chaldict,'nonce',None) and \ 353 dict_fetch(chaldict,'realm',None) and \ 354 dict_fetch(chaldict,'qop',None): 355 authdict = generate_response(chaldict, 356 request_uri, self.auth_user, self.auth_pass, method='POST') 357 headers = {\ 358 'Authorization':build_authorization_arg(authdict), 359 'Expect':'100-continue', 360 } 361 self.SendSOAPData(soapdata, url, soapaction, headers, **kw) 362 return 363 364 raise RuntimeError,\ 365 'Client expecting digest authorization challenge.' 366 367 def ReceiveRaw(self, **kw): 368 '''Read a server reply, unconverted to any format and return it. 369 ''' 370 if self.data: return self.data 371 trace = self.trace 372 while 1: 373 response = self.h.getresponse() 374 self.reply_code, self.reply_msg, self.reply_headers, self.data = \ 375 response.status, response.reason, response.msg, response.read() 376 if trace: 377 print >>trace, "_" * 33, time.ctime(time.time()), "RESPONSE:" 378 for i in (self.reply_code, self.reply_msg,): 379 print >>trace, str(i) 380 print >>trace, "-------" 381 print >>trace, str(self.reply_headers) 382 print >>trace, self.data 383 saved = None 384 for d in response.msg.getallmatchingheaders('set-cookie'): 385 if d[0] in [ ' ', '\t' ]: 386 saved += d.strip() 387 else: 388 if saved: self.cookies.load(saved) 389 saved = d.strip() 390 if saved: self.cookies.load(saved) 391 if response.status == 401: 392 if not callable(self.http_callbacks.get(response.status,None)): 393 raise RuntimeError, 'HTTP Digest Authorization Failed' 394 self.http_callbacks[response.status](response) 395 continue 396 if response.status != 100: break 397 398 # The httplib doesn't understand the HTTP continuation header. 399 # Horrible internals hack to patch things up. 400 self.h._HTTPConnection__state = httplib._CS_REQ_SENT 401 self.h._HTTPConnection__response = None 402 return self.data 403 404 def IsSOAP(self): 405 if self.ps: return 1 406 self.ReceiveRaw() 407 mimetype = self.reply_headers.type 408 return mimetype == 'text/xml' 409 410 def ReceiveSOAP(self, readerclass=None, **kw): 411 '''Get back a SOAP message. 412 ''' 413 if self.ps: return self.ps 414 if not self.IsSOAP(): 415 raise TypeError( 416 'Response is "%s", not "text/xml"' % self.reply_headers.type) 417 if len(self.data) == 0: 418 raise TypeError('Received empty response') 419 420 self.ps = ParsedSoap(self.data, 421 readerclass=readerclass or self.readerclass, 422 encodingStyle=kw.get('encodingStyle')) 423 424 if self.sig_handler is not None: 425 self.sig_handler.verify(self.ps) 426 427 return self.ps 428 429 def IsAFault(self): 430 '''Get a SOAP message, see if it has a fault. 431 ''' 432 self.ReceiveSOAP() 433 return self.ps.IsAFault() 434 435 def ReceiveFault(self, **kw): 436 '''Parse incoming message as a fault. Raise TypeError if no 437 fault found. 438 ''' 439 self.ReceiveSOAP(**kw) 440 if not self.ps.IsAFault(): 441 raise TypeError("Expected SOAP Fault not found") 442 return FaultFromFaultMessage(self.ps) 443 444 def Receive(self, replytype, **kw): 445 '''Parse message, create Python object. 446 447 KeyWord data: 448 faults -- list of WSDL operation.fault typecodes 449 wsaction -- If using WS-Address, must specify Action value we expect to 450 receive. 451 ''' 452 self.ReceiveSOAP(**kw) 453 if self.ps.IsAFault(): 454 msg = FaultFromFaultMessage(self.ps) 455 raise FaultException(msg) 456 457 tc = replytype 458 if hasattr(replytype, 'typecode'): 459 tc = replytype.typecode 460 461 reply = self.ps.Parse(tc) 462 if self.address is not None: 463 self.address.checkResponse(self.ps, kw.get('wsaction')) 464 return reply 465 466 def __repr__(self): 467 return "<%s instance %s>" % (self.__class__.__name__, _get_idstr(self)) 468 469 470class Binding(_Binding): 471 '''Object that represents a binding (connection) to a SOAP server. 472 Can be used in the "name overloading" style. 473 474 class attr: 475 gettypecode -- funcion that returns typecode from typesmodule, 476 can be set so can use whatever mapping you desire. 477 ''' 478 gettypecode = staticmethod(lambda mod,e: getattr(mod, str(e.localName)).typecode) 479 logger = _GetLogger('ZSI.client.Binding') 480 481 def __init__(self, url, namespace=None, typesmodule=None, **kw): 482 """ 483 Parameters: 484 url -- location of service 485 namespace -- optional root element namespace 486 typesmodule -- optional response only. dict(name=typecode), 487 lookup for all children of root element. 488 """ 489 self.typesmodule = typesmodule 490 self.namespace = namespace 491 492 _Binding.__init__(self, url=url, **kw) 493 494 def __getattr__(self, name): 495 '''Return a callable object that will invoke the RPC method 496 named by the attribute. 497 ''' 498 if name[:2] == '__' and len(name) > 5 and name[-2:] == '__': 499 if hasattr(self, name): return getattr(self, name) 500 return getattr(self.__class__, name) 501 return _Caller(self, name, self.namespace) 502 503 def __parse_child(self, node): 504 '''for rpc-style map each message part to a class in typesmodule 505 ''' 506 try: 507 tc = self.gettypecode(self.typesmodule, node) 508 except: 509 self.logger.debug('didnt find typecode for "%s" in typesmodule: %s', 510 node.localName, self.typesmodule) 511 tc = TC.Any(aslist=1) 512 return tc.parse(node, self.ps) 513 514 self.logger.debug('parse child with typecode : %s', tc) 515 try: 516 return tc.parse(node, self.ps) 517 except Exception: 518 self.logger.debug('parse failed try Any : %s', tc) 519 520 tc = TC.Any(aslist=1) 521 return tc.parse(node, self.ps) 522 523 def Receive(self, replytype, **kw): 524 '''Parse message, create Python object. 525 526 KeyWord data: 527 faults -- list of WSDL operation.fault typecodes 528 wsaction -- If using WS-Address, must specify Action value we expect to 529 receive. 530 ''' 531 self.ReceiveSOAP(**kw) 532 ps = self.ps 533 tp = _find_type(ps.body_root) 534 isarray = ((type(tp) in (tuple,list) and tp[1] == 'Array') or _find_arraytype(ps.body_root)) 535 if self.typesmodule is None or isarray: 536 return _Binding.Receive(self, replytype, **kw) 537 538 if ps.IsAFault(): 539 msg = FaultFromFaultMessage(ps) 540 raise FaultException(msg) 541 542 tc = replytype 543 if hasattr(replytype, 'typecode'): 544 tc = replytype.typecode 545 546 #Ignore response wrapper 547 reply = {} 548 for elt in _child_elements(ps.body_root): 549 name = str(elt.localName) 550 reply[name] = self.__parse_child(elt) 551 552 if self.address is not None: 553 self.address.checkResponse(ps, kw.get('wsaction')) 554 555 return reply 556 557 558class NamedParamBinding(Binding): 559 '''Like Binding, except the argument list for invocation is 560 named parameters. 561 ''' 562 logger = _GetLogger('ZSI.client.Binding') 563 564 def __getattr__(self, name): 565 '''Return a callable object that will invoke the RPC method 566 named by the attribute. 567 ''' 568 if name[:2] == '__' and len(name) > 5 and name[-2:] == '__': 569 if hasattr(self, name): return getattr(self, name) 570 return getattr(self.__class__, name) 571 return _NamedParamCaller(self, name, self.namespace) 572 573 574if __name__ == '__main__': print _copyright 575