00001
00002
00003
00004
00005
00006 from ZSI import _copyright, _seqtypes, ParsedSoap, SoapWriter, TC, ZSI_SCHEMA_URI,\
00007 EvaluateException, FaultFromFaultMessage, _child_elements, _attrs, _find_arraytype,\
00008 _find_type, _get_idstr, _get_postvalue_from_absoluteURI, FaultException, WSActionException,\
00009 UNICODE_ENCODING
00010 from ZSI.auth import AUTH
00011 from ZSI.TC import AnyElement, AnyType, String, TypeCode, _get_global_element_declaration,\
00012 _get_type_definition
00013 from ZSI.TCcompound import Struct
00014 import base64, httplib, Cookie, types, time, urlparse
00015 from ZSI.address import Address
00016 from ZSI.wstools.logging import getLogger as _GetLogger
00017 _b64_encode = base64.encodestring
00018
00019 class _AuthHeader:
00020 """<BasicAuth xmlns="ZSI_SCHEMA_URI">
00021 <Name>%s</Name><Password>%s</Password>
00022 </BasicAuth>
00023 """
00024 def __init__(self, name=None, password=None):
00025 self.Name = name
00026 self.Password = password
00027 _AuthHeader.typecode = Struct(_AuthHeader, ofwhat=(String((ZSI_SCHEMA_URI,'Name'), typed=False),
00028 String((ZSI_SCHEMA_URI,'Password'), typed=False)), pname=(ZSI_SCHEMA_URI,'BasicAuth'),
00029 typed=False)
00030
00031
00032 class _Caller:
00033 '''Internal class used to give the user a callable object
00034 that calls back to the Binding object to make an RPC call.
00035 '''
00036
00037 def __init__(self, binding, name, namespace=None):
00038 self.binding = binding
00039 self.name = name
00040 self.namespace = namespace
00041
00042 def __call__(self, *args):
00043 nsuri = self.namespace
00044 if nsuri is None:
00045 return self.binding.RPC(None, self.name, args,
00046 encodingStyle="http://schemas.xmlsoap.org/soap/encoding/",
00047 replytype=TC.Any(self.name+"Response"))
00048
00049 return self.binding.RPC(None, (nsuri,self.name), args,
00050 encodingStyle="http://schemas.xmlsoap.org/soap/encoding/",
00051 replytype=TC.Any((nsuri,self.name+"Response")))
00052
00053
00054 class _NamedParamCaller:
00055 '''Similar to _Caller, expect that there are named parameters
00056 not positional.
00057 '''
00058
00059 def __init__(self, binding, name, namespace=None):
00060 self.binding = binding
00061 self.name = name
00062 self.namespace = namespace
00063
00064 def __call__(self, **params):
00065
00066 kw = {}
00067 for key in [ 'auth_header', 'nsdict', 'requesttypecode', 'soapaction' ]:
00068 if params.has_key(key):
00069 kw[key] = params[key]
00070 del params[key]
00071
00072 nsuri = self.namespace
00073 if nsuri is None:
00074 return self.binding.RPC(None, self.name, None,
00075 encodingStyle="http://schemas.xmlsoap.org/soap/encoding/",
00076 _args=params,
00077 replytype=TC.Any(self.name+"Response", aslist=False),
00078 **kw)
00079
00080 return self.binding.RPC(None, (nsuri,self.name), None,
00081 encodingStyle="http://schemas.xmlsoap.org/soap/encoding/",
00082 _args=params,
00083 replytype=TC.Any((nsuri,self.name+"Response"), aslist=False),
00084 **kw)
00085
00086
00087 class _Binding:
00088 '''Object that represents a binding (connection) to a SOAP server.
00089 Once the binding is created, various ways of sending and
00090 receiving SOAP messages are available.
00091 '''
00092 defaultHttpTransport = httplib.HTTPConnection
00093 defaultHttpsTransport = httplib.HTTPSConnection
00094 logger = _GetLogger('ZSI.client.Binding')
00095
00096 def __init__(self, nsdict=None, transport=None, url=None, tracefile=None,
00097 readerclass=None, writerclass=None, soapaction='',
00098 wsAddressURI=None, sig_handler=None, transdict=None, **kw):
00099 '''Initialize.
00100 Keyword arguments include:
00101 transport -- default use HTTPConnection.
00102 transdict -- dict of values to pass to transport.
00103 url -- URL of resource, POST is path
00104 soapaction -- value of SOAPAction header
00105 auth -- (type, name, password) triplet; default is unauth
00106 nsdict -- namespace entries to add
00107 tracefile -- file to dump packet traces
00108 cert_file, key_file -- SSL data (q.v.)
00109 readerclass -- DOM reader class
00110 writerclass -- DOM writer class, implements MessageInterface
00111 wsAddressURI -- namespaceURI of WS-Address to use. By default
00112 it's not used.
00113 sig_handler -- XML Signature handler, must sign and verify.
00114 endPointReference -- optional Endpoint Reference.
00115 '''
00116 self.data = None
00117 self.ps = None
00118 self.user_headers = []
00119 self.nsdict = nsdict or {}
00120 self.transport = transport
00121 self.transdict = transdict or {}
00122 self.url = url
00123 self.trace = tracefile
00124 self.readerclass = readerclass
00125 self.writerclass = writerclass
00126 self.soapaction = soapaction
00127 self.wsAddressURI = wsAddressURI
00128 self.sig_handler = sig_handler
00129 self.address = None
00130 self.endPointReference = kw.get('endPointReference', None)
00131 self.cookies = Cookie.SimpleCookie()
00132 self.http_callbacks = {}
00133
00134 if kw.has_key('auth'):
00135 self.SetAuth(*kw['auth'])
00136 else:
00137 self.SetAuth(AUTH.none)
00138
00139 def SetAuth(self, style, user=None, password=None):
00140 '''Change auth style, return object to user.
00141 '''
00142 self.auth_style, self.auth_user, self.auth_pass = \
00143 style, user, password
00144 return self
00145
00146 def SetURL(self, url):
00147 '''Set the URL we post to.
00148 '''
00149 self.url = url
00150 return self
00151
00152 def ResetHeaders(self):
00153 '''Empty the list of additional headers.
00154 '''
00155 self.user_headers = []
00156 return self
00157
00158 def ResetCookies(self):
00159 '''Empty the list of cookies.
00160 '''
00161 self.cookies = Cookie.SimpleCookie()
00162
00163 def AddHeader(self, header, value):
00164 '''Add a header to send.
00165 '''
00166 self.user_headers.append((header, value))
00167 return self
00168
00169 def __addcookies(self):
00170 '''Add cookies from self.cookies to request in self.h
00171 '''
00172 for cname, morsel in self.cookies.items():
00173 attrs = []
00174 value = morsel.get('version', '')
00175 if value != '' and value != '0':
00176 attrs.append('$Version=%s' % value)
00177 attrs.append('%s=%s' % (cname, morsel.coded_value))
00178 value = morsel.get('path')
00179 if value:
00180 attrs.append('$Path=%s' % value)
00181 value = morsel.get('domain')
00182 if value:
00183 attrs.append('$Domain=%s' % value)
00184 self.h.putheader('Cookie', "; ".join(attrs))
00185
00186 def RPC(self, url, opname, obj, replytype=None, **kw):
00187 '''Send a request, return the reply. See Send() and Recieve()
00188 docstrings for details.
00189 '''
00190 self.Send(url, opname, obj, **kw)
00191 return self.Receive(replytype, **kw)
00192
00193 def Send(self, url, opname, obj, nsdict={}, soapaction=None, wsaction=None,
00194 endPointReference=None, soapheaders=(), **kw):
00195 '''Send a message. If url is None, use the value from the
00196 constructor (else error). obj is the object (data) to send.
00197 Data may be described with a requesttypecode keyword, the default
00198 is the class's typecode (if there is one), else Any.
00199
00200 Try to serialize as a Struct, if this is not possible serialize an Array. If
00201 data is a sequence of built-in python data types, it will be serialized as an
00202 Array, unless requesttypecode is specified.
00203
00204 arguments:
00205 url --
00206 opname -- struct wrapper
00207 obj -- python instance
00208
00209 key word arguments:
00210 nsdict --
00211 soapaction --
00212 wsaction -- WS-Address Action, goes in SOAP Header.
00213 endPointReference -- set by calling party, must be an
00214 EndPointReference type instance.
00215 soapheaders -- list of pyobj, typically w/typecode attribute.
00216 serialized in the SOAP:Header.
00217 requesttypecode --
00218
00219 '''
00220 url = url or self.url
00221 endPointReference = endPointReference or self.endPointReference
00222
00223
00224 d = {}
00225 d.update(self.nsdict)
00226 d.update(nsdict)
00227
00228 sw = SoapWriter(nsdict=d, header=True, outputclass=self.writerclass,
00229 encodingStyle=kw.get('encodingStyle'),)
00230
00231 requesttypecode = kw.get('requesttypecode')
00232 if kw.has_key('_args'):
00233 tc = requesttypecode or TC.Any(pname=opname, aslist=False)
00234 sw.serialize(kw['_args'], tc)
00235 elif not requesttypecode:
00236 tc = getattr(obj, 'typecode', None) or TC.Any(pname=opname, aslist=False)
00237 try:
00238 if type(obj) in _seqtypes:
00239 obj = dict(map(lambda i: (i.typecode.pname,i), obj))
00240 except AttributeError:
00241
00242 tc = TC.Any(pname=opname, aslist=True)
00243 else:
00244 tc = TC.Any(pname=opname, aslist=False)
00245
00246 sw.serialize(obj, tc)
00247 else:
00248 sw.serialize(obj, requesttypecode)
00249
00250 for i in soapheaders:
00251 sw.serialize_header(i)
00252
00253
00254
00255 if self.auth_style & AUTH.zsibasic:
00256 sw.serialize_header(_AuthHeader(self.auth_user, self.auth_pass),
00257 _AuthHeader.typecode)
00258
00259
00260
00261 if self.wsAddressURI is not None:
00262 if self.soapaction and wsaction.strip('\'"') != self.soapaction:
00263 raise WSActionException, 'soapAction(%s) and WS-Action(%s) must match'\
00264 %(self.soapaction,wsaction)
00265
00266 self.address = Address(url, self.wsAddressURI)
00267 self.address.setRequest(endPointReference, wsaction)
00268 self.address.serialize(sw)
00269
00270
00271
00272 if self.sig_handler is not None:
00273 self.sig_handler.sign(sw)
00274
00275 scheme,netloc,path,nil,nil,nil = urlparse.urlparse(url)
00276 transport = self.transport
00277 if transport is None and url is not None:
00278 if scheme == 'https':
00279 transport = self.defaultHttpsTransport
00280 elif scheme == 'http':
00281 transport = self.defaultHttpTransport
00282 else:
00283 raise RuntimeError, 'must specify transport or url startswith https/http'
00284
00285
00286 if issubclass(transport, httplib.HTTPConnection) is False:
00287 raise TypeError, 'transport must be a HTTPConnection'
00288
00289 soapdata = str(sw)
00290 self.h = transport(netloc, None, **self.transdict)
00291 self.h.connect()
00292 self.SendSOAPData(soapdata, url, soapaction, **kw)
00293
00294 def SendSOAPData(self, soapdata, url, soapaction, headers={}, **kw):
00295
00296 if self.trace:
00297 print >>self.trace, "_" * 33, time.ctime(time.time()), "REQUEST:"
00298 print >>self.trace, soapdata
00299
00300 url = url or self.url
00301 request_uri = _get_postvalue_from_absoluteURI(url)
00302 self.h.putrequest("POST", request_uri)
00303 self.h.putheader("Content-Length", "%d" % len(soapdata))
00304 self.h.putheader("Content-Type", 'text/xml; charset="%s"' %UNICODE_ENCODING)
00305 self.__addcookies()
00306
00307 for header,value in headers.items():
00308 self.h.putheader(header, value)
00309
00310 SOAPActionValue = '"%s"' % (soapaction or self.soapaction)
00311 self.h.putheader("SOAPAction", SOAPActionValue)
00312 if self.auth_style & AUTH.httpbasic:
00313 val = _b64_encode(self.auth_user + ':' + self.auth_pass) \
00314 .replace("\012", "")
00315 self.h.putheader('Authorization', 'Basic ' + val)
00316 elif self.auth_style == AUTH.httpdigest and not headers.has_key('Authorization') \
00317 and not headers.has_key('Expect'):
00318 def digest_auth_cb(response):
00319 self.SendSOAPDataHTTPDigestAuth(response, soapdata, url, request_uri, soapaction, **kw)
00320 self.http_callbacks[401] = None
00321 self.http_callbacks[401] = digest_auth_cb
00322
00323 for header,value in self.user_headers:
00324 self.h.putheader(header, value)
00325 self.h.endheaders()
00326 self.h.send(soapdata)
00327
00328
00329 self.data, self.ps = None, None
00330
00331 def SendSOAPDataHTTPDigestAuth(self, response, soapdata, url, request_uri, soapaction, **kw):
00332 '''Resend the initial request w/http digest authorization headers.
00333 The SOAP server has requested authorization. Fetch the challenge,
00334 generate the authdict for building a response.
00335 '''
00336 if self.trace:
00337 print >>self.trace, "------ Digest Auth Header"
00338 url = url or self.url
00339 if response.status != 401:
00340 raise RuntimeError, 'Expecting HTTP 401 response.'
00341 if self.auth_style != AUTH.httpdigest:
00342 raise RuntimeError,\
00343 'Auth style(%d) does not support requested digest authorization.' %self.auth_style
00344
00345 from ZSI.digest_auth import fetch_challenge,\
00346 generate_response,\
00347 build_authorization_arg,\
00348 dict_fetch
00349
00350 chaldict = fetch_challenge( response.getheader('www-authenticate') )
00351 if dict_fetch(chaldict,'challenge','').lower() == 'digest' and \
00352 dict_fetch(chaldict,'nonce',None) and \
00353 dict_fetch(chaldict,'realm',None) and \
00354 dict_fetch(chaldict,'qop',None):
00355 authdict = generate_response(chaldict,
00356 request_uri, self.auth_user, self.auth_pass, method='POST')
00357 headers = {\
00358 'Authorization':build_authorization_arg(authdict),
00359 'Expect':'100-continue',
00360 }
00361 self.SendSOAPData(soapdata, url, soapaction, headers, **kw)
00362 return
00363
00364 raise RuntimeError,\
00365 'Client expecting digest authorization challenge.'
00366
00367 def ReceiveRaw(self, **kw):
00368 '''Read a server reply, unconverted to any format and return it.
00369 '''
00370 if self.data: return self.data
00371 trace = self.trace
00372 while 1:
00373 response = self.h.getresponse()
00374 self.reply_code, self.reply_msg, self.reply_headers, self.data = \
00375 response.status, response.reason, response.msg, response.read()
00376 if trace:
00377 print >>trace, "_" * 33, time.ctime(time.time()), "RESPONSE:"
00378 for i in (self.reply_code, self.reply_msg,):
00379 print >>trace, str(i)
00380 print >>trace, "-------"
00381 print >>trace, str(self.reply_headers)
00382 print >>trace, self.data
00383 saved = None
00384 for d in response.msg.getallmatchingheaders('set-cookie'):
00385 if d[0] in [ ' ', '\t' ]:
00386 saved += d.strip()
00387 else:
00388 if saved: self.cookies.load(saved)
00389 saved = d.strip()
00390 if saved: self.cookies.load(saved)
00391 if response.status == 401:
00392 if not callable(self.http_callbacks.get(response.status,None)):
00393 raise RuntimeError, 'HTTP Digest Authorization Failed'
00394 self.http_callbacks[response.status](response)
00395 continue
00396 if response.status != 100: break
00397
00398
00399
00400 self.h._HTTPConnection__state = httplib._CS_REQ_SENT
00401 self.h._HTTPConnection__response = None
00402 return self.data
00403
00404 def IsSOAP(self):
00405 if self.ps: return 1
00406 self.ReceiveRaw()
00407 mimetype = self.reply_headers.type
00408 return mimetype == 'text/xml'
00409
00410 def ReceiveSOAP(self, readerclass=None, **kw):
00411 '''Get back a SOAP message.
00412 '''
00413 if self.ps: return self.ps
00414 if not self.IsSOAP():
00415 raise TypeError(
00416 'Response is "%s", not "text/xml"' % self.reply_headers.type)
00417 if len(self.data) == 0:
00418 raise TypeError('Received empty response')
00419
00420 self.ps = ParsedSoap(self.data,
00421 readerclass=readerclass or self.readerclass,
00422 encodingStyle=kw.get('encodingStyle'))
00423
00424 if self.sig_handler is not None:
00425 self.sig_handler.verify(self.ps)
00426
00427 return self.ps
00428
00429 def IsAFault(self):
00430 '''Get a SOAP message, see if it has a fault.
00431 '''
00432 self.ReceiveSOAP()
00433 return self.ps.IsAFault()
00434
00435 def ReceiveFault(self, **kw):
00436 '''Parse incoming message as a fault. Raise TypeError if no
00437 fault found.
00438 '''
00439 self.ReceiveSOAP(**kw)
00440 if not self.ps.IsAFault():
00441 raise TypeError("Expected SOAP Fault not found")
00442 return FaultFromFaultMessage(self.ps)
00443
00444 def Receive(self, replytype, **kw):
00445 '''Parse message, create Python object.
00446
00447 KeyWord data:
00448 faults -- list of WSDL operation.fault typecodes
00449 wsaction -- If using WS-Address, must specify Action value we expect to
00450 receive.
00451 '''
00452 self.ReceiveSOAP(**kw)
00453 if self.ps.IsAFault():
00454 msg = FaultFromFaultMessage(self.ps)
00455 raise FaultException(msg)
00456
00457 tc = replytype
00458 if hasattr(replytype, 'typecode'):
00459 tc = replytype.typecode
00460
00461 reply = self.ps.Parse(tc)
00462 if self.address is not None:
00463 self.address.checkResponse(self.ps, kw.get('wsaction'))
00464 return reply
00465
00466 def __repr__(self):
00467 return "<%s instance %s>" % (self.__class__.__name__, _get_idstr(self))
00468
00469
00470 class Binding(_Binding):
00471 '''Object that represents a binding (connection) to a SOAP server.
00472 Can be used in the "name overloading" style.
00473
00474 class attr:
00475 gettypecode -- funcion that returns typecode from typesmodule,
00476 can be set so can use whatever mapping you desire.
00477 '''
00478 gettypecode = staticmethod(lambda mod,e: getattr(mod, str(e.localName)).typecode)
00479 logger = _GetLogger('ZSI.client.Binding')
00480
00481 def __init__(self, url, namespace=None, typesmodule=None, **kw):
00482 """
00483 Parameters:
00484 url -- location of service
00485 namespace -- optional root element namespace
00486 typesmodule -- optional response only. dict(name=typecode),
00487 lookup for all children of root element.
00488 """
00489 self.typesmodule = typesmodule
00490 self.namespace = namespace
00491
00492 _Binding.__init__(self, url=url, **kw)
00493
00494 def __getattr__(self, name):
00495 '''Return a callable object that will invoke the RPC method
00496 named by the attribute.
00497 '''
00498 if name[:2] == '__' and len(name) > 5 and name[-2:] == '__':
00499 if hasattr(self, name): return getattr(self, name)
00500 return getattr(self.__class__, name)
00501 return _Caller(self, name, self.namespace)
00502
00503 def __parse_child(self, node):
00504 '''for rpc-style map each message part to a class in typesmodule
00505 '''
00506 try:
00507 tc = self.gettypecode(self.typesmodule, node)
00508 except:
00509 self.logger.debug('didnt find typecode for "%s" in typesmodule: %s',
00510 node.localName, self.typesmodule)
00511 tc = TC.Any(aslist=1)
00512 return tc.parse(node, self.ps)
00513
00514 self.logger.debug('parse child with typecode : %s', tc)
00515 try:
00516 return tc.parse(node, self.ps)
00517 except Exception:
00518 self.logger.debug('parse failed try Any : %s', tc)
00519
00520 tc = TC.Any(aslist=1)
00521 return tc.parse(node, self.ps)
00522
00523 def Receive(self, replytype, **kw):
00524 '''Parse message, create Python object.
00525
00526 KeyWord data:
00527 faults -- list of WSDL operation.fault typecodes
00528 wsaction -- If using WS-Address, must specify Action value we expect to
00529 receive.
00530 '''
00531 self.ReceiveSOAP(**kw)
00532 ps = self.ps
00533 tp = _find_type(ps.body_root)
00534 isarray = ((type(tp) in (tuple,list) and tp[1] == 'Array') or _find_arraytype(ps.body_root))
00535 if self.typesmodule is None or isarray:
00536 return _Binding.Receive(self, replytype, **kw)
00537
00538 if ps.IsAFault():
00539 msg = FaultFromFaultMessage(ps)
00540 raise FaultException(msg)
00541
00542 tc = replytype
00543 if hasattr(replytype, 'typecode'):
00544 tc = replytype.typecode
00545
00546
00547 reply = {}
00548 for elt in _child_elements(ps.body_root):
00549 name = str(elt.localName)
00550 reply[name] = self.__parse_child(elt)
00551
00552 if self.address is not None:
00553 self.address.checkResponse(ps, kw.get('wsaction'))
00554
00555 return reply
00556
00557
00558 class NamedParamBinding(Binding):
00559 '''Like Binding, except the argument list for invocation is
00560 named parameters.
00561 '''
00562 logger = _GetLogger('ZSI.client.Binding')
00563
00564 def __getattr__(self, name):
00565 '''Return a callable object that will invoke the RPC method
00566 named by the attribute.
00567 '''
00568 if name[:2] == '__' and len(name) > 5 and name[-2:] == '__':
00569 if hasattr(self, name): return getattr(self, name)
00570 return getattr(self.__class__, name)
00571 return _NamedParamCaller(self, name, self.namespace)
00572
00573
00574 if __name__ == '__main__': print _copyright