source: TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/soap/client.py @ 6580

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/soap/client.py@6580
Revision 6580, 10.4 KB checked in by pjkersha, 10 years ago (diff)

SOAP client - added capability for custom headers and setting SOAPAction field.

Line 
1"""SOAP client module for NDG Security - initially for use with SAML SOAP
2binding based Attribute Authority interface
3
4NERC DataGrid Project
5"""
6__author__ = "P J Kershaw"
7__date__ = "27/07/09"
8__copyright__ = ""
9__license__ = "BSD - see LICENSE file in top-level directory"
10__contact__ = "Philip.Kershaw@stfc.ac.uk"
11__revision__ = '$Id:$'
12import logging
13log = logging.getLogger(__name__)
14
15from ndg.security.common.soap import SOAPEnvelopeBase
16
17
18class SOAPClientError(Exception):
19    """Base class for SOAP Client exceptions"""
20
21class SOAPParseError(SOAPClientError):
22    """Error parsing SOAP response"""
23   
24           
25class SOAPClientBase(object):
26    """Handle client request to a SOAP Service
27    @cvar RESPONSE_CONTENT_TYPES: expected content type to be returned in a response
28    from a service
29    @type RESPONSE_CONTENT_TYPES: string
30    """
31    RESPONSE_CONTENT_TYPES = ('text/xml', )
32   
33    def __init__(self):
34        self.__responseEnvelopeClass = None
35
36    def _getResponseEnvelopeClass(self):
37        return self.__responseEnvelopeClass
38
39    def _setResponseEnvelopeClass(self, value):
40        if not issubclass(value, SOAPEnvelopeBase):
41            raise TypeError("Setting SOAP envelope class: expecting %r, got "
42                            "%r" % (SOAPEnvelopeBase, type(value)))
43        self.__responseEnvelopeClass = value
44
45    responseEnvelopeClass = property(fget=_getResponseEnvelopeClass, 
46                                     fset=_setResponseEnvelopeClass, 
47                                     doc="Set the class for handling "
48                                         "the SOAP envelope responses")
49     
50    def send(self, soapRequest):
51        raise NotImplementedError()
52
53
54class _SoapIOBase(object):
55    """Base class for request and response classes"""
56   
57    def __init__(self):
58        self.__envelope = None
59
60    def _getEnvelope(self):
61        return self.__envelope
62
63    def _setEnvelope(self, value):
64        if not isinstance(value, SOAPEnvelopeBase):
65            raise TypeError('Setting SOAP envelope object: expecting %r; got '
66                            '%r' % (SOAPEnvelopeBase, type(value)))
67                           
68        self.__envelope = value
69
70    envelope = property(fget=_getEnvelope, 
71                        fset=_setEnvelope, 
72                        doc="SOAP Envelope object used in request/response")
73
74       
75class SOAPRequestBase(object):
76    """Interface for SOAP requests"""
77    def __init__(self):
78        self.__url = None
79        self.__envelope = None
80
81    def _getUrl(self):
82        return self.__url
83
84    def _setUrl(self, value):
85        if not isinstance(value, basestring):
86            raise TypeError('Setting request URL: expecting %r; got '
87                            '%r' % (basestring, type(value)))
88        self.__url = value
89
90    url = property(fget=_getUrl, fset=_setUrl, doc="URL of SOAP endpoint")
91
92   
93class SOAPResponseBase(_SoapIOBase):
94    """Interface for SOAP responses"""
95
96import httplib
97import urllib2
98from urllib import addinfourl
99 
100class UrlLib2SOAPClientError(SOAPClientError):
101    """Specialisation to enable the urllib2 response to be included in the
102    exception instance as context information for the caller
103    """
104    URLLIB2RESPONSE_TYPE = addinfourl
105   
106    def __init__(self, *arg, **kw):
107        Exception.__init__(self, *arg, **kw)
108        self.__urllib2Response = None
109
110    def _getUrllib2Response(self):
111        return self.__urllib2Response
112
113    def _setUrllib2Response(self, value):
114        if not isinstance(value, UrlLib2SOAPClientError.URLLIB2RESPONSE_TYPE):
115            raise TypeError('Expecting %r type for "urllib2Response"; got %r' %
116                            (UrlLib2SOAPClientError.URLLIB2RESPONSE_TYPE, 
117                             type(value)))
118        self.__urllib2Response = value
119
120    urllib2Response = property(_getUrllib2Response, 
121                               _setUrllib2Response, 
122                               doc="Urllib2Response")
123
124
125class SOAPResponseError(UrlLib2SOAPClientError):
126    """Raise for invalid SOAP response from server"""
127       
128class HTTPException(UrlLib2SOAPClientError):
129    """Server returned HTTP code error code"""
130
131class UrlLib2SOAPRequest(SOAPRequestBase): 
132    """Interface for UrlLib2 based SOAP Requests"""
133   
134   
135class UrlLib2SOAPResponse(SOAPResponseBase):
136    """Interface for UrlLib2 based SOAP Responses"""
137    def __init__(self):
138        self.__fileobject = None
139       
140    @property
141    def fileobject(self):
142        "urllib2 file object returned from request"
143        return self.__fileobject
144
145
146class CapitalizedKeysDict(dict):
147    """Extend dict type to make keys capitalized.  Keys must be string type"""
148    def __init__(self, *arg, **kw):
149        if len(arg) > 0:
150            if isinstance(arg[0], dict):
151                arg[0] = [(k.capitalize(), v) for k, v in arg[0].items()]
152            else:
153                arg[0] = [(k.capitalize(), v) for k, v in arg[0]] 
154       
155        kw = dict([(k.capitalize(), v) for k, v in kw.items()])
156       
157        super(CapitalizedKeysDict, self).__init__(*arg, **kw)
158       
159    def __setitem__(self, k, v):
160        if not isinstance(k, basestring):
161            raise TypeError('Key must be string type; got %r' % type(k))
162       
163        super(CapitalizedKeysDict, self).__setitem__(k.capitalize(), v)
164       
165   
166class UrlLib2SOAPClient(SOAPClientBase):
167    """urllib2 based SOAP Client"""
168    DEFAULT_HTTP_HEADER = CapitalizedKeysDict({'Content-type': 'text/xml'})
169   
170    def __init__(self):
171        super(UrlLib2SOAPClient, self).__init__()
172        self.__openerDirector = urllib2.OpenerDirector()
173        self.__openerDirector.add_handler(urllib2.UnknownHandler())
174        self.__openerDirector.add_handler(urllib2.HTTPHandler())
175        self.__timeout = None
176        self.__httpHeader = UrlLib2SOAPClient.DEFAULT_HTTP_HEADER.copy()
177
178    @property
179    def httpHeader(self):
180        "Set HTTP header fields in this dict object"
181        return self.__httpHeader
182
183    def _getSOAPAction(self):
184        return self.__httpHeader.get('Soapaction')
185
186    def _setSOAPAction(self, value):
187        if not isinstance(value, basestring):
188            raise TypeError("Setting request soapAction: got %r, expecting "
189                            "string type" % type(value))
190        self.__httpHeader['Soapaction'] = value
191       
192    soapAction = property(fget=_getSOAPAction, 
193                          fset=_setSOAPAction, 
194                          doc="SOAPAction HTTP header field setting") 
195       
196    def _getTimeout(self):
197        return self.__timeout
198
199    def _setTimeout(self, value):
200        if not isinstance(value, (int, float)):
201            raise TypeError("Setting request timeout: got %r, expecting int "
202                            "or float type" % type(value))
203        self.__timeout = value
204
205    timeout = property(fget=_getTimeout, 
206                       fset=_setTimeout, 
207                       doc="Timeout (seconds) for requests")
208
209    def _getOpenerDirector(self):
210        return self.__openerDirector
211
212    def _setOpenerDirector(self, value):
213        """This shouldn't need to be used much in practice because __init__
214        creates one"""
215        if not isinstance(value, urllib2.OpenerDirector):
216            raise TypeError("Setting opener: expecting %r; got %r" % 
217                            (urllib2.OpenerDirector, type(value)))
218        self.__openerDirector = value
219
220    openerDirector = property(fget=_getOpenerDirector, 
221                              fset=_setOpenerDirector, 
222                              doc="urllib2.OpenerDirector defines the "
223                                  "opener(s) for handling requests")
224   
225    def send(self, soapRequest):
226        """Make a request to the given URL with a SOAP Request object"""
227       
228        if not isinstance(soapRequest, UrlLib2SOAPRequest):
229            raise TypeError('UrlLib2SOAPClient.send: expecting %r '
230                            'derived type for SOAP request, got %r' % 
231                            (self.responseEnvelopeClass, type(soapRequest)))
232           
233        if not isinstance(soapRequest.envelope, self.responseEnvelopeClass):
234            raise TypeError('UrlLib2SOAPClient.send: expecting %r '
235                            'derived type for SOAP envelope, got %r' % 
236                            (self.responseEnvelopeClass, type(soapRequest)))
237                           
238        if self.timeout is not None:
239            arg = (self.timeout,)
240        else:
241            arg = ()
242           
243        soapRequestStr = soapRequest.envelope.serialize()
244
245        if log.getEffectiveLevel() <= logging.DEBUG:
246            from ndg.security.common.utils.etree import prettyPrint
247            log.debug("SOAP Request:")
248            log.debug("_"*80)
249            log.debug(prettyPrint(soapRequest.envelope.elem))
250
251        soapResponse = UrlLib2SOAPResponse()
252        urllib2Request = urllib2.Request(soapRequest.url) 
253        for i in self.httpHeader.items():
254            urllib2Request.add_header(*i)
255           
256        response = self.openerDirector.open(urllib2Request, 
257                                            soapRequestStr, 
258                                            *arg)
259        if response.code != httplib.OK:
260            excep = HTTPException("Response for request to [%s] is: %d %s" % 
261                                  (soapRequest.url, 
262                                   response.code, 
263                                   response.msg))
264            excep.urllib2Response = response
265            raise excep
266       
267        if (response.headers.typeheader not in 
268            UrlLib2SOAPClient.RESPONSE_CONTENT_TYPES):
269            responseType = ', '.join(UrlLib2SOAPClient.RESPONSE_CONTENT_TYPES)
270            excep = SOAPResponseError("Expecting %r response type; got %r for "
271                                      "request to [%s]" % 
272                                      (responseType, 
273                                       response.headers.typeheader,
274                                       soapRequest.url))
275            excep.urllib2Response = response
276            raise excep
277           
278        soapResponse.fileObject = response
279        soapResponse.envelope = self.responseEnvelopeClass() 
280       
281        try:
282            soapResponse.envelope.parse(soapResponse.fileObject)
283        except Exception, e:
284            raise SOAPParseError("%r type error raised parsing response for "
285                                 "request to [%s]: %s"
286                                 % (type(e), soapRequest.url, e))
287           
288        return soapResponse
Note: See TracBrowser for help on using the repository browser.