source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/soap.py @ 4404

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/soap.py@4404
Revision 4404, 12.5 KB checked in by pjkersha, 12 years ago (diff)

Updated contact e-mail address

Line 
1"""NDG Security SOAP Service Middleware
2
3NERC Data Grid Project
4
5This software may be distributed under the terms of the Q Public License,
6version 1.0 or later.
7"""
8__author__ = "P J Kershaw"
9__date__ = "27/05/08"
10__copyright__ = "(C) 2008 STFC & NERC"
11__contact__ = "Philip.Kershaw@stfc.ac.uk"
12__revision__ = "$Id$"
13import logging
14log = logging.getLogger(__name__)
15
16import paste.request
17from ZSI import EvaluateException, ParseException
18from ZSI.parse import ParsedSoap
19from ZSI.writer import SoapWriter
20from ZSI import fault
21
22from ZSI.ServiceContainer import ServiceSOAPBinding
23from ndg.security.common.utils.ClassFactory import instantiateClass
24     
25class SOAPMiddlewareError(Exception):
26    """Base error handling exception class for the SOAP WSGI middleware module
27    """
28   
29class SOAPMiddlewareReadError(SOAPMiddlewareError):
30    """SOAP Middleware read error"""
31
32class SOAPMiddlewareConfigError(SOAPMiddlewareError):
33    """SOAP Middleware configuration error"""
34
35class SOAPMiddleware(object):
36    '''Middleware configurable to a given ZSI SOAP binding''' 
37   
38    soapWriterKey = 'ZSI.writer.SoapWriter'
39    parsedSOAPKey = 'ZSI.parse.ParsedSoap'
40    soapFaultSetKey = 'ndg.security.server.wsgi.soap.soapFault'
41   
42    def __init__(self, app, app_conf, **kw):
43        log.debug("SOAPMiddleware.__init__ ...")
44        self.app = app
45        self.app_conf = app_conf
46        self.app_conf.update(kw)
47       
48        if 'charset' in self.app_conf:
49            self.app_conf['charset'] = '; charset=' + self.app_conf['charset']
50        else:
51            self.app_conf['charset'] = '; charset=utf-8'
52
53        if 'path' in self.app_conf:
54            if self.app_conf['path'] != '/':
55                self.app_conf['path'] = self.app_conf['path'].rstrip('/')
56        else:
57            self.app_conf['path'] = '/'
58
59        # This flag if set to True causes this handler to call the
60        # start_response method and output the SOAP response
61        self.writeResponseSet = bool(self.app_conf.get('writeResponse', False))
62
63        # Check for a list of other filters to be referenced by this one
64        if 'referencedFilters' in self.app_conf:
65            # __call__  may reference any filters in environ keyed by these
66            # keywords
67            self.referencedFilterKeys = \
68                                    self.app_conf['referencedFilters'].split()
69
70    def __call__(self, environ, start_response):
71        log.debug("SOAPMiddleware.__call__")
72                       
73        # Derived class must implement SOAP Response via overloaded version of
74        # this method.  ParsedSoap object is available as a key in environ via
75        # the parseRequest method
76       
77        return self.writeResponse(environ, start_response)
78
79   
80    def _initCall(self, environ, start_response):
81        # Apply filter for calls
82        if not self.isSOAPMessage(environ):
83            log.debug("SOAPMiddleware.__call__: skipping non-SOAP call")
84            return self.app(environ, start_response)
85       
86        elif not self.pathMatch(environ):
87            log.debug("SOAPMiddleware.__call__: path doesn't match SOAP "
88                      "service endpoint")
89            return self.app(environ, start_response)
90       
91        elif self.isSOAPFaultSet(environ):
92            # This MUST be checked in a overloaded version especially in
93            # consideration of security: e.g. an upstream signature
94            # verification may have found an error in a signature
95            log.debug("SOAPMiddleware.__call__: SOAP fault set by previous "
96                      "SOAP middleware filter")
97            return self.app(environ, start_response)
98
99        # Parse input into a ZSI ParsedSoap object set as a key in environ
100        try:
101            self.parseRequest(environ)
102        except Exception, e:
103            sw = self.exception2SOAPFault(environ, e)
104            self.setSOAPWriter(environ, sw)
105            return self.writeResponse(environ, start_response)
106       
107        self.addFilter2Environ(environ)
108       
109        # Return None to __call__ to indicate that it can proceed with
110        # processing the input
111        return None
112
113    @classmethod
114    def exception2SOAPFault(cls, environ, exception):
115        '''Convert an exception into a SOAP fault message'''
116        soapFault = fault.FaultFromException(exception, None)
117        sw = SoapWriter()
118        soapFault.serialize(sw)
119        environ[cls.soapFaultSetKey] = 'True'
120        return sw
121   
122    pathMatch = lambda self,environ:environ['PATH_INFO']==self.app_conf['path']
123       
124    @staticmethod
125    def isSOAPMessage(environ):
126        '''Generic method to filter out non-soap messages
127       
128        TODO: is HTTP_SOAPACTION only set for WSDL doc-literal wrapped style
129        generated content? - If so this test should be moved'''
130        return environ.get('REQUEST_METHOD', '') == 'POST' and \
131               environ.get('HTTP_SOAPACTION') is not None
132
133    @classmethod
134    def isSOAPFaultSet(cls, environ):
135        '''Check environment for SOAP fault flag set.  This variable is set
136        from exception2SOAPFault'''
137        return bool(environ.get(cls.soapFaultSetKey, False)) == True
138   
139    @classmethod
140    def parseRequest(cls, environ):
141        '''Parse SOAP message from environ['wsgi.input']
142       
143        Reading from environ['wsgi.input'] may be a destructive process so the
144        content is saved in a ZSI.parse.ParsedSoap object for use by SOAP
145        handlers which follow in the chain
146       
147        environ['ZSI.parse.ParsedSoap'] may be set to a ParsedSoap object
148        parsed by a SOAP handler ahead of the current one in the chain.  In
149        this case, don't re-parse.  If NOT parsed, parse and set
150        'ZSI.parse.ParsedSoap' environ key'''
151       
152        # Check for ParsedSoap object set in environment, if not present,
153        # make one
154        ps = environ.get(cls.parsedSOAPKey)
155        if ps is None:
156            # TODO: allow for chunked data
157            contentLength = int(environ['CONTENT_LENGTH'])
158            soapIn = environ['wsgi.input'].read(contentLength)
159            if len(soapIn) < contentLength:
160                raise SOAPMiddlewareReadError("Expecting %d content length; "
161                                              "received %d instead." % 
162                                              (contentLength, len(soapIn)))
163           
164            log.debug("SOAP Request for handler %r" % cls)
165            log.debug("_"*80)
166            log.debug(soapIn)
167            log.debug("_"*80)
168           
169            ps = ParsedSoap(soapIn)
170            environ[cls.parsedSOAPKey] = ps
171           
172        return environ[cls.parsedSOAPKey]
173
174
175    def writeResponse(self, environ, start_response, errorCode=None):
176        '''This method serializes the SOAP output and sets the response header.
177        It's the final step and should be called in the last SOAP handler in
178        a chain of handlers or else specify it in the ini file as the last
179        SOAP handler'''
180       
181        # This flag must be set to True to write out the final response from
182        # this handler
183        if self.writeResponseSet == False:
184            return self.app(environ, start_response)
185       
186        sw = self.getSOAPWriter(environ)
187        soapOut = str(sw)
188        charset = self.app_conf['charset']
189       
190        if errorCode is None:
191            if self.isSOAPFaultSet(environ):
192                errorCode = "500 Internal Server Error"
193            else:
194                errorCode = "200 OK"
195               
196        start_response(errorCode,
197                       [('content-type', 'text/xml'+charset),
198                        ('content-length', str(len(soapOut)))])
199        return soapOut
200
201    @classmethod
202    def getSOAPWriter(cls, environ):
203        '''Access SoapWriter object set in environment by this classes' call
204        method'''
205       
206        sw = environ.get(SOAPMiddleware.soapWriterKey)
207        if sw is None:
208            raise KeyError("Expecting '%s' key in environ: missing call to "
209                           "SOAPMiddleware?" % SOAPMiddleware.soapWriterKey)
210        return sw
211   
212    @classmethod
213    def setSOAPWriter(cls, environ, sw):
214        '''Set SoapWriter object in environment'''   
215        environ[SOAPMiddleware.soapWriterKey] = sw
216
217    def addFilter2Environ(self, environ):
218        '''Add a key to the current application in the environment so that
219        other middleware can reference it.  This is dependent on filterID set
220        in app_conf'''
221        filterID = self.app_conf.get('filterID')
222        if filterID is not None:           
223            if filterID in environ:
224                raise SOAPMiddlewareConfigError("An filterID key '%s' is "
225                                                "already set in environ" %
226                                                filterID)
227            environ[filterID] = self
228           
229       
230class SOAPBindingMiddleware(SOAPMiddleware): 
231    '''Apply a ZSI ServiceSOAPBinding type SOAP service'''
232         
233    def __init__(self, *arg, **kw):
234       
235        super(SOAPBindingMiddleware, self).__init__(*arg, **kw)
236       
237        # Check for Service binding in config
238        if 'ServiceSOAPBindingClass' in self.app_conf:
239            modName, dot, className = \
240                    self.app_conf['ServiceSOAPBindingClass'].rpartition('.')
241           
242            self.serviceSOAPBinding = instantiateClass(modName, 
243                                   className, 
244                                   objectType=ServiceSOAPBinding, 
245                                   classProperties=self.serviceSOAPBindingKw)           
246        else: 
247            self.serviceSOAPBinding = ServiceSOAPBinding()
248       
249        # Flag to enable display of WSDL via wsdl query arg in a GET request
250        self.enableWSDLQuery = self.app_conf.get('enableWSDLQuery', False) and\
251                                hasattr(self.serviceSOAPBinding, '_wsdl')
252
253
254    def _getServiceSOAPBindingKw(self):
255        '''Extract keywords to specific to SOAP Service Binding set in app_conf
256        '''
257        if 'ServiceSOAPBindingPropPrefix' not in self.app_conf:
258            return {}
259       
260        prefix = self.app_conf['ServiceSOAPBindingPropPrefix']+'.'
261        serviceSOAPBindingKw = dict([(k.replace(prefix, ''), v) \
262                                     for k,v in self.app_conf.items() \
263                                     if k.startswith(prefix)])
264        return serviceSOAPBindingKw
265   
266    serviceSOAPBindingKw = property(fget=_getServiceSOAPBindingKw)
267   
268    def __call__(self, environ, start_response):
269        log.debug("SOAPBindingMiddleware.__call__ ...")
270               
271        if self.pathMatch(environ) and self.enableWSDLQuery and \
272           environ.get('REQUEST_METHOD') == 'GET' and \
273           environ.get('QUERY_STRING') == 'wsdl':
274            wsdl = self.serviceSOAPBinding._wsdl
275            start_response("200 OK", [('Content-type', 'text/xml'),
276                                      ('Content-length', str(len(wsdl)))])
277            return wsdl
278               
279               
280        # Apply filter for calls
281        response = self._initCall(environ, start_response)
282        if response is not None:
283            return response
284       
285       
286        try:
287            # Other filters in the middleware chain may be passed by setting
288            # a reference to them in the config.  This is useful if the SOAP
289            # binding code needs to access results from upstream middleware
290            # e.g. check output from signature verification filter
291            if hasattr(self, 'referencedFilterKeys'):
292                self.serviceSOAPBinding.referencedWSGIFilters = \
293                                    dict([(i, environ[i]) \
294                                          for i in self.referencedFilterKeys])
295                   
296            ps = self.parseRequest(environ)
297           
298            # Map SOAP Action to method in binding class
299            soapMethodName = 'soap_%s' % environ['HTTP_SOAPACTION'].strip('"')
300           
301            method = getattr(self.serviceSOAPBinding, soapMethodName)           
302            resp = method(ps)
303        except Exception, e:
304            sw = self.exception2SOAPFault(environ, e)
305        else: 
306            # Serialize output using SOAP writer class
307            sw = SoapWriter()
308            sw.serialize(resp)
309       
310        # Make SoapWriter object available to any SOAP filters that follow
311        self.setSOAPWriter(environ, sw)
312       
313        soapOut = str(sw)
314        charset = self.app_conf['charset']
315               
316        log.debug("SOAP Response")
317        log.debug("_"*80)
318        log.debug(soapOut)
319        log.debug("_"*80)
320
321        return self.writeResponse(environ, start_response)
Note: See TracBrowser for help on using the repository browser.