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

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

Refactoring of SOAP and WS-Security middleware for better error handling and abstraction of functions

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__ = "P.J.Kershaw@rl.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
23       
24class SOAPMiddlewareError(Exception):
25    """Base error handling exception for this module"""
26   
27class SOAPMiddlewareReadError(SOAPMiddlewareError):
28    """SOAP read error"""
29   
30class SOAPMiddleware(object):
31    '''Middleware configurable to a given ZSI SOAP binding''' 
32   
33    soapWriterKey = 'ZSI.writer.SoapWriter'
34    parsedSOAPKey = 'ZSI.parse.ParsedSoap'
35   
36    def __init__(self, app, app_conf, **kw):
37        log.debug("SOAPMiddleware.__init__ ...")
38        self.app = app
39        self.app_conf = app_conf
40        self.app_conf.update(kw)
41       
42        if 'charset' in self.app_conf:
43            self.app_conf['charset'] = '; ' + self.app_conf['charset']
44        else:
45            self.app_conf['charset'] = '; charset=utf-8'
46
47           
48        if 'path' not in self.app_conf:
49            self.app_conf['path'] = '/'
50       
51    def __call__(self, environ, start_response):
52        log.debug("SOAPMiddleware.__call__")
53       
54        charset_ = self.app_conf['charset']
55        def start_response_wrapper(status, response_hdrs, exc_info=None):
56            '''Ensure text/xml content type and set content length'''
57           
58            log.debug("Altering content-type to text/xml...")
59            contentKeys = ('content-type', 'content-length')
60            response_hdrs_alt = [(name, val) for name, val in response_hdrs\
61                                 if name.lower() not in contentKeys]
62           
63            response_hdrs_alt += [('content-type', 'text/xml'+charset_)]
64
65                           
66            return start_response(status, response_hdrs_alt, exc_info)
67               
68        # Apply filter for calls
69        if not self.isSOAPMessage(environ) or not self.pathMatch(environ):
70            return self.app(environ, start_response)
71
72        self.parse(environ)
73
74        # Derived class must implement SOAP Response to overloaded version of
75        # this method.  ParsedSoap object is available as a key in environ via
76        # the parse method
77       
78        return self.app(environ, start_response_wrapper)
79
80    @staticmethod
81    def exception2SOAPFault(e):
82        '''Convert an exception into a SOAP fault message'''
83        soapFault = fault.FaultFromException(e, None)
84        sw = SoapWriter()
85        soapFault.serialize(sw)
86        return sw
87   
88    pathMatch = lambda self, environ: \
89                        environ['PATH_INFO']==self.app_conf['path'].rstrip('/')
90       
91    @staticmethod
92    def isSOAPMessage(cls, environ):
93        '''Generic method to filter out non-soap messages'''
94        return environ.get('REQUEST_METHOD', '') == 'POST' and \
95               environ.get('HTTP_SOAPACTION') is not None
96       
97    @classmethod
98    def parse(cls, environ):
99        '''Parse SOAP message from environ['wsgi.input']
100       
101        Reading from environ['wsgi.input'] may be a destructive process so the
102        content is saved in a ZSI.parse.ParsedSoap object for use by SOAP
103        handlers which follow in the chain
104       
105        environ['ZSI.parse.ParsedSoap'] may be set to a ParsedSoap object
106        parsed by a SOAP handler ahead of the current one in the chain.  In
107        this case, don't re-parse.  If NOT parsed, parse and set
108        'ZSI.parse.ParsedSoap' environ key'''
109       
110        # Check for ParsedSoap object set in environment, if not present,
111        # make one
112        ps = environ.get(cls.parsedSOAPKey)
113        if ps is None:
114            # TODO: allow for chunked data
115            contentLength = int(environ['CONTENT_LENGTH'])
116            soapIn = environ['wsgi.input'].read(contentLength)
117            if len(soapIn) < contentLength:
118                raise SOAPMiddlewareReadError("Expecting %s content length; "
119                                              "received %d instead." % \
120                                              (environ['CONTENT_LENGTH'],
121                                               len(soapIn)))
122           
123            log.debug("SOAP Request for handler %r" % cls)
124            log.debug("_"*80)
125            log.debug(soapIn)
126            log.debug("_"*80)
127           
128            ps = ParsedSoap(soapIn)
129            environ[cls.parsedSOAPKey] = ps
130           
131        return environ[cls.parsedSOAPKey]
132   
133    @classmethod
134    def getSOAPWriter(cls, environ):
135        '''Access SoapWriter object set in environment by this classes' call
136        method'''
137       
138        sw = environ.get(SOAPMiddleware.soapWriterKey)
139        if sw is None:
140            raise KeyError("Expecting '%s' key in environ: missing call to "
141                           "SOAPMiddleware?" % SOAPMiddleware.soapWriterKey)
142        return sw
143   
144     
145def makeFilter(app, app_conf): 
146    return SOAPMiddleware(app, app_conf)
147
148
149class SOAPBindingMiddleware(SOAPMiddleware): 
150    '''Apply a ZSI ServiceSOAPBinding type SOAP service'''
151         
152    def __init__(self, *arg, **kw):
153        super(SOAPMiddleware, self).__init__(*arg, **kw)
154       
155        # Check for Service binding in config
156        if 'ServiceSOAPBindingClass' in self.app_conf:
157            modName, dot, className = \
158                    self.app_conf['ServiceSOAPBindingClass'].rpartition('.')
159           
160            module = __import__(modName, globals(), locals(), [className])
161            serviceSOAPBindingClass = eval('module.' + className)
162           
163            # Check class inherits from ServiceSOAPBinding
164            if not issubclass(serviceSOAPBindingClass, ServiceSOAPBinding):
165                raise TypeError("%s class must be derived from "
166                                "ServiceSOAPBinding" % \
167                                self.app_conf['ServiceSOAPBindingClass'])
168        else: 
169            serviceSOAPBindingClass = ServiceSOAPBinding
170                 
171        self.serviceSOAPBinding = serviceSOAPBindingClass()
172       
173        # Flag to enable display of WSDL via wsdl query arg in a GET request
174        self.enableWSDLQuery = self.app_conf.get('enableWSDLQuery', False) and\
175                                hasattr(self.serviceSOAPBinding, '_wsdl')
176
177
178    def __call__(self, environ, start_response):
179        log.debug("SOAPBindingMiddleware.__call__")
180       
181        charset_ = self.app_conf['charset']
182        def start_response_wrapper(status, response_hdrs, exc_info=None):
183            '''Ensure text/xml content type and set content length'''
184           
185            log.debug("Altering content-type to text/xml...")
186            contentKeys = ('content-type', 'content-length')
187            response_hdrs_alt = [(name, val) for name, val in response_hdrs\
188                                 if name.lower() not in contentKeys]
189           
190            response_hdrs_alt += [('content-type', 'text/xml'+charset_),
191                                  ('content-length', str(len(soapOut_)))]
192
193                           
194            return start_response(status, response_hdrs_alt, exc_info)
195       
196        if environ.get('REQUEST_METHOD') == 'GET' and \
197           environ.get('QUERY_STRING') == 'wsdl':
198            if self.enableWSDLQuery:
199                wsdl = self.serviceSOAPBinding._wsdl
200                start_response("200 OK", [('Content-type', 'text/xml'),
201                                          ('Content-length', str(len(wsdl)))])
202                return wsdl
203               
204               
205        # Apply filter for calls
206        if not self.isSOAPMessage(environ) or not self.pathMatch(environ):
207            return self.app(environ, start_response)
208
209        try:
210            ps = self.parse(environ)
211           
212            # Map SOAP Action to method in binding class
213            soapMethodName = 'soap_%s' % environ['HTTP_SOAPACTION'].strip('"')
214           
215            method = getattr(self.serviceSOAPBinding, soapMethodName)
216            # TODO: change method to return response only: request, response
217            # tuple is carry over from Twisted based code
218            req, resp = method(ps)
219        except Exception, e:
220            sw = self.exception2SOAPFault(e)
221        else: 
222            # Serialize output using SOAP writer class
223            sw = SoapWriter()
224            sw.serialize(resp)
225       
226        # Make SoapWriter object available to any SOAP filters that follow
227        environ[SOAPMiddleware.soapWriterKey] = sw
228        self.soapOut = str(sw)
229       
230        log.debug("SOAP Response")
231        log.debug("_"*80)
232        log.debug(self.soapOut)
233        log.debug("_"*80)
234
235        return self.app(environ, start_response_wrapper)
Note: See TracBrowser for help on using the repository browser.