• Main Page
  • Related Pages
  • Modules
  • Namespaces
  • Data Structures
  • Files
  • File List
  • Globals

contrib/opal/ZSI/build/lib/ZSI/client.py

00001 #! /usr/bin/env python
00002 # $Header$
00003 #
00004 # Copyright (c) 2001 Zolera Systems.  All rights reserved.
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         # Pull out arguments that Send() uses
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         # Serialize the object.
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'): #NamedParamBinding
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                 # can't do anything but serialize this in a SOAP:Array
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         # Determine the SOAP auth element.  SOAP:Header element
00255         if self.auth_style & AUTH.zsibasic:
00256             sw.serialize_header(_AuthHeader(self.auth_user, self.auth_pass),
00257                 _AuthHeader.typecode)
00258 
00259         # 
00260         # Serialize WS-Address
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         # WS-Security Signature Handler
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         # Send the request.
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         # Tracing?
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         # Clear prior receive state.
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             # The httplib doesn't understand the HTTP continuation header.
00399             # Horrible internals hack to patch things up.
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         #Ignore response wrapper
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

Generated on Wed Oct 20 2010 11:12:15 for APBS by  doxygen 1.7.2