1 """WSGI SOAP Middleware utilising the ZSI Python SOAP Package
2
3 NERC DataGrid Project
4
5 """
6 __author__ = "P J Kershaw"
7 __date__ = "19/08/09"
8 __copyright__ = "(C) 2009 Science and Technology Facilities Council"
9 __contact__ = "Philip.Kershaw@stfc.ac.uk"
10 __license__ = "BSD - see LICENSE file in top-level directory"
11 __revision__ = "$Id: $"
12 import logging
13 log = logging.getLogger(__name__)
14
15 import sys
16
17 from ZSI.parse import ParsedSoap
18 from ZSI.writer import SoapWriter
19 from ZSI import fault
20 from ZSI.ServiceContainer import ServiceSOAPBinding
21
22 from ndg.security.server.wsgi.soap import SOAPMiddleware, SOAPMiddlewareError,\
23 SOAPMiddlewareConfigError, SOAPMiddlewareReadError
24 from ndg.security.common.wssecurity.utils import DomletteReader, \
25 DomletteElementProxy
26 from ndg.security.common.utils.classfactory import instantiateClass, \
27 importClass
30 """Base class for ZSI Middleware type exceptions"""
31
33 """ZSI Middleware read error"""
34
36 """ZSI middleware configuration error"""
37
39 '''Middleware configurable to a given ZSI SOAP binding
40
41 @type SOAP_WRITER_KEYNAME: basestring
42 @cvar SOAP_WRITER_KEYNAME: environ key for ZSI SoapWriter instance
43 @type PARSED_SOAP_KEYNAME: basestring
44 @cvar PARSED_SOAP_KEYNAME: environ key for ZSI ParsedSoap instance
45 @type CHARSET_OPTNAME: basestring
46 @cvar CHARSET_OPTNAME: option name to for character set for output
47 @type DEFAULT_CHARSET: basestring
48 @cvar DEFAULT_CHARSET: default character setting is utf-8
49 @type PATH_OPTNAME: basestring
50 @cvar PATH_OPTNAME: option to set path for this endpoint (not including
51 domain name)
52 @type WRITE_RESPONSE_OPTNAME: basestring
53 @cvar WRITE_RESPONSE_OPTNAME: option name for flag to middleware to
54 serialise and output the SoapWriter instance
55 @type REFERENCED_FILTERS_OPTNAME: basestring
56 @cvar REFERENCED_FILTERS_OPTNAME: name for option to enable dereferencing
57 of other middleware via these environ keys
58 @type FILTER_ID_OPTNAME: basestring
59 @cvar FILTER_ID_OPTNAME: option name for environ key to enable other
60 middleware to reference this Filter
61 @type PUBLISHED_URI_OPTNAME: basestring
62 @cvar PUBLISHED_URI_OPTNAME: option name to define path for this endpoint
63 including domain name
64 @type READER_CLASS_OPTNAME: basestring
65 @cvar READER_CLASS_OPTNAME: option name for SOAP reader class
66 @type WRITERCLASS_OPTNAME: basestring
67 @cvar WRITERCLASS_OPTNAME: option name for SOAP writer class
68 '''
69
70 SOAP_WRITER_KEYNAME = 'ZSI.writer.SoapWriter'
71 PARSED_SOAP_KEYNAME = 'ZSI.parse.ParsedSoap'
72
73 CHARSET_OPTNAME = 'charset'
74 DEFAULT_CHARSET = '; charset=utf-8'
75 PATH_OPTNAME = 'path'
76 WRITE_RESPONSE_OPTNAME = 'writeResponse'
77 REFERENCED_FILTERS_OPTNAME = 'referencedFilters'
78 FILTER_ID_OPTNAME = 'filterID'
79 PUBLISHED_URI_OPTNAME = 'publishedURI'
80 READER_CLASS_OPTNAME = 'readerclass'
81 WRITERCLASS_OPTNAME = 'writerclass'
82
84 log.debug("ZSIMiddleware.__init__ ...")
85 super(ZSIMiddleware, self).__init__()
86
87 self._app = app
88 self.__charset = ZSIMiddleware.DEFAULT_CHARSET
89 self.__path = None
90 self.__referencedFilterKeys = None
91 self.__publishedURI = None
92 self.__readerClass = None
93 self.__writerClass = None
94 self.__writeResponseSet = None
95 self.__filterID = None
96
99
101 if not isinstance(value, basestring):
102 raise TypeError('Expecting string type for "charset" got %r' %
103 type(value))
104 self.__charset = value
105
108
110 if not isinstance(value, basestring):
111 raise TypeError('Expecting string type for "path" got %r' %
112 type(value))
113 self.__path = value
114
116 return self.__publishedURI
117
119 if not isinstance(value, (basestring, type(None))):
120 raise TypeError('Expecting string or None type for "publishedURI" '
121 'got %r' % type(value))
122 self.__publishedURI = value
123
125 return self.__readerClass
126
128 self.__readerClass = value
129
131 return self.__writerClass
132
134 self.__writerClass = value
135
137 return self.__writeResponseSet
138
140 if not isinstance(value, bool):
141 raise TypeError('Expecting %r for "writeResponseSet" type got %r' %
142 (bool, type(value)))
143
144 self.__writeResponseSet = value
145
147 return self.__filterID
148
150 if not isinstance(value, (basestring, type(None))):
151 raise TypeError('Expecting string or None type for "filterID" got '
152 '%r' % type(value))
153 self.__filterID = value
154
155 charset = property(_getCharset, _setCharset,
156 doc="character set for response")
157
158 path = property(_getPath, _setPath, doc="Path for endpoint")
159
160 publishedURI = property(_getPublishedURI, _setPublishedURI,
161 doc="fully qualified path for endpoint")
162
163 readerClass = property(_getReaderClass, _setReaderClass,
164 doc="ZSI Reader class")
165
166 writeResponseSet = property(_getWriteResponseSet, _setWriteResponseSet,
167 doc="boolean set to True to write out a "
168 "response from this middleware")
169
170 writerClass = property(_getWriterClass, _setWriterClass,
171 doc="ZSI Writer Class")
172
173 filterID = property(_getFilterID, _setFilterID,
174 doc="enable the instance of this middleware to be "
175 "referenced in environ by this identifier")
176
177 - def initialise(self, global_conf, prefix='', **app_conf):
243
244 - def __call__(self, environ, start_response):
245 log.debug("ZSIMiddleware.__call__")
246
247
248
249
250
251 return self.writeResponse(environ, start_response)
252
253
254 - def _initCall(self, environ, start_response):
255 '''Sub-divided out from __call__ to enable derived classes to easily
256 include this functionality:
257 - Set a reference to this WSGI filter in environ if filterID was
258 set in the config and
259 - check the request to see if this filter should handle it
260 '''
261
262
263
264
265 self.addFilter2Environ(environ)
266
267
268 if not self.__class__.isSOAPMessage(environ):
269 log.debug("ZSIMiddleware.__call__: skipping non-SOAP call")
270 return self._app(environ, start_response)
271
272 elif not self.pathMatch(environ):
273 log.debug("ZSIMiddleware.__call__: path doesn't match SOAP "
274 "service endpoint")
275 return self._app(environ, start_response)
276
277 elif self.__class__.isSOAPFaultSet(environ):
278
279
280
281 log.debug("ZSIMiddleware.__call__: SOAP fault set by previous "
282 "SOAP middleware filter")
283 return self._app(environ, start_response)
284
285
286 try:
287 self.parseRequest(environ)
288 except Exception, e:
289 sw = self.exception2SOAPFault(environ, e)
290 self.setSOAPWriter(environ, sw)
291 return self.writeResponse(environ, start_response)
292
293
294
295 return None
296
306
307 pathMatch = lambda self, environ: environ['PATH_INFO'] == self.path
308
310 '''Parse SOAP message from environ['wsgi.input']
311
312 Reading from environ['wsgi.input'] may be a destructive process so the
313 content is saved in a ZSI.parse.ParsedSoap object for use by SOAP
314 handlers which follow in the chain
315
316 environ['ZSI.parse.ParsedSoap'] may be set to a ParsedSoap object
317 parsed by a SOAP handler ahead of the current one in the chain. In
318 this case, don't re-parse. If NOT parsed, parse and set
319 'ZSI.parse.ParsedSoap' environ key'''
320
321
322
323 ps = environ.get(ZSIMiddleware.PARSED_SOAP_KEYNAME)
324 if ps is None:
325
326 contentLength = int(environ['CONTENT_LENGTH'])
327 soapIn = environ['wsgi.input'].read(contentLength)
328 if len(soapIn) < contentLength:
329 raise ZSIMiddlewareReadError("Expecting %d content length; "
330 "received %d instead." %
331 (contentLength, len(soapIn)))
332
333 log.debug("SOAP Request for handler %r" % ZSIMiddleware)
334 log.debug("_" * 80)
335 log.debug(soapIn)
336 log.debug("_" * 80)
337
338 ps = ParsedSoap(soapIn, readerclass=self.readerClass)
339 environ[ZSIMiddleware.PARSED_SOAP_KEYNAME] = ps
340
341 return environ[ZSIMiddleware.PARSED_SOAP_KEYNAME]
342
343
344 - def writeResponse(self, environ, start_response, errorCode=None):
345 '''This method serializes the SOAP output and sets the response header.
346 It's the final step and should be called in the last SOAP handler in
347 a chain of handlers or else specify it in the ini file as the last
348 SOAP handler'''
349
350
351
352 if self.writeResponseSet == False:
353 return self._app(environ, start_response)
354
355 sw = self.getSOAPWriter(environ)
356 soapOut = str(sw)
357
358 if errorCode is None:
359 if self.__class__.isSOAPFaultSet(environ):
360 errorCode = "500 Internal Server Error"
361 else:
362 errorCode = "200 OK"
363
364 log.debug("SOAP Response for handler %r" % self.__class__)
365 log.debug("_" * 80)
366 log.debug(soapOut)
367 log.debug("_" * 80)
368 start_response(errorCode,
369 [('content-type', 'text/xml' + self.charset),
370 ('content-length', str(len(soapOut)))])
371 return soapOut
372
373 @classmethod
383
384 @classmethod
388
390 '''Add a key to the current application in the environment so that
391 other middleware can reference it. This is dependent on filterID set
392 in app_conf'''
393 if self.filterID is not None:
394 if self.filterID in environ:
395 raise ZSIMiddlewareConfigError("An filterID key '%s' is "
396 "already set in environ" %
397 self.filterID)
398 environ[self.filterID] = self
399
402 '''Interface to apply a ZSI ServiceSOAPBinding type SOAP service'''
403
404 SERVICE_SOAP_BINDING_CLASSNAME_OPTNAME = 'serviceSOAPBindingClass'
405 SERVICE_SOAP_BINDING_PROPPREFIX_OPTNAME = 'serviceSOAPBindingPropPrefix'
406 DEFAULT_SERVICE_SOAP_BINDING_PROPPREFIX_OPTNAME = \
407 'ndg.security.server.wsgi.zsi.serviceSOAPBinding.'
408
409 SERVICE_SOAP_BINDING_ENVIRON_KEYNAME_OPTNAME = \
410 'serviceSOAPBindingEnvironKeyName'
411 DEFAULT_SERVICE_SOAP_BINDING_ENVIRON_KEYNAME = \
412 'ndg.security.servier.wsgi.zsi.serviceSOAPBinding'
413 ENABLE_WSDL_QUERY_OPTNAME = 'enableWSDLQuery'
414 DEFAULT_ENABLE_WSDL_QUERY_VALUE = False
415 SOAP_METHOD_STRING = 'soap_%s'
416
418 super(SOAPBindingMiddleware, self).__init__(app)
419 self.__serviceSOAPBindingKeyName = None
420 self.__serviceSOAPBinding = None
421 self.__enableWSDLQuery = False
422
424 return self.__serviceSOAPBinding
425
427 """Instance must be ZSI ServiceSOAPBinding derived type"""
428 if not isinstance(value, ServiceSOAPBinding):
429 raise TypeError('Expecting %r type for "serviceSOAPBinding"; got '
430 '%r' % (ServiceSOAPBinding, type(value)))
431 self.__serviceSOAPBinding = value
432
433 serviceSOAPBinding = property(fget=_getServiceSOAPBinding,
434 fset=_setServiceSOAPBinding,
435 doc="Instance of ZSI ServiceSOAPBinding "
436 "derived type determines the behaviour "
437 "of the SOAP callbacks")
438
440 return self.__serviceSOAPBindingKeyName
441
443 """Instance must be ZSI ServiceSOAPBindingKeyName derived type"""
444 if not isinstance(value, basestring):
445 raise TypeError('Expecting bool type for "enableWSDLQuery"; got '
446 '%r' % type(value))
447 self.__serviceSOAPBindingKeyName = value
448
449 serviceSOAPBindingKeyName = property(fget=_getServiceSOAPBindingKeyName,
450 fset=_setServiceSOAPBindingKeyName,
451 doc="Keyword to key WSGI environ for "
452 "SOAP Binding middleware instance")
453
455 return self.__enableWSDLQuery
456
458 if not isinstance(value, bool):
459 raise TypeError('Expecting bool type for "enableWSDLQuery"; got '
460 '%r' % type(value))
461 self.__enableWSDLQuery = value
462
463 enableWSDLQuery = property(fget=_getEnableWSDLQuery,
464 fset=_setEnableWSDLQuery,
465 doc="Enable service to publish the WSDL via "
466 "the ?wsdl query argument appended to the "
467 "endpoint")
468
469 - def initialise(self, global_conf, prefix='', **app_conf):
470 """Set-up ZSI SOAP Binding middleware interface attributes.
471 Overloaded base class method to enable custom settings from app_conf
472
473 @type global_conf: dict
474 @param global_conf: PasteDeploy global configuration dictionary
475 @type prefix: basestring
476 @param prefix: prefix for configuration items
477 @type app_conf: dict
478 @param app_conf: PasteDeploy application specific configuration
479 dictionary
480 """
481 super(SOAPBindingMiddleware, self).initialise(global_conf,
482 prefix=prefix,
483 **app_conf)
484
485 serviceSOAPBindingEnvironKeyNameOptName = prefix + \
486 SOAPBindingMiddleware.SERVICE_SOAP_BINDING_ENVIRON_KEYNAME_OPTNAME
487 serviceSOAPBindingClassNameOptName = prefix + \
488 SOAPBindingMiddleware.SERVICE_SOAP_BINDING_CLASSNAME_OPTNAME
489
490 if serviceSOAPBindingEnvironKeyNameOptName in app_conf and \
491 serviceSOAPBindingClassNameOptName in app_conf:
492 raise ZSIMiddlewareConfigError('Only "%s" or "%s" may be set; not '
493 ' both' %
494 (SOAPBindingMiddleware.SERVICE_SOAP_BINDING_ENVIRON_KEYNAME_OPTNAME,
495 SOAPBindingMiddleware.SERVICE_SOAP_BINDING_CLASSNAME_OPTNAME))
496
497 if serviceSOAPBindingClassNameOptName in app_conf:
498
499 modName, className = app_conf[
500 serviceSOAPBindingClassNameOptName].rsplit('.', 1)
501
502
503 prefixOptName = prefix + \
504 SOAPBindingMiddleware.SERVICE_SOAP_BINDING_PROPPREFIX_OPTNAME
505 prefix = app_conf.get(prefixOptName,
506 SOAPBindingMiddleware.
507 DEFAULT_SERVICE_SOAP_BINDING_PROPPREFIX_OPTNAME)
508
509 serviceSOAPBindingKw = dict([(k.replace(prefix, ''), v)
510 for k, v in app_conf.items()
511 if k.startswith(prefix)])
512
513 self.serviceSOAPBinding = instantiateClass(modName,
514 className,
515 objectType=ServiceSOAPBinding,
516 classProperties=serviceSOAPBindingKw)
517 else:
518
519
520 self.serviceSOAPBindingKeyName = app_conf.get(
521 serviceSOAPBindingEnvironKeyNameOptName,
522 SOAPBindingMiddleware.
523 DEFAULT_SERVICE_SOAP_BINDING_ENVIRON_KEYNAME)
524
525
526
527 enableWSDLQueryOptName = prefix + \
528 SOAPBindingMiddleware.ENABLE_WSDL_QUERY_OPTNAME
529 self.enableWSDLQuery = SOAPBindingMiddleware.str2Bool(
530 app_conf.get(enableWSDLQueryOptName, ''))
531
532 - def __call__(self, environ, start_response):
533 log.debug("SOAPBindingMiddleware.__call__ ...")
534
535
536
537 serviceSOAPBinding = environ.get(self.serviceSOAPBindingKeyName,
538 self.serviceSOAPBinding)
539 if serviceSOAPBinding is None:
540 raise ZSIMiddlewareConfigError('No SOAP service binding set in '
541 'environ or configured from start'
542 '-up')
543
544 if self.pathMatch(environ) and self.enableWSDLQuery and \
545 environ.get('REQUEST_METHOD', '') == 'GET' and \
546 environ.get('QUERY_STRING', '') == 'wsdl':
547 wsdl = serviceSOAPBinding._wsdl
548 start_response("200 OK", [('Content-type', 'text/xml'),
549 ('Content-length', str(len(wsdl)))])
550 return wsdl
551
552
553 response = self._initCall(environ, start_response)
554 if response is not None:
555 return response
556
557 try:
558
559
560
561
562 if hasattr(self, 'referencedFilterKeys'):
563 try:
564 serviceSOAPBinding.referencedWSGIFilters = \
565 dict([(i, environ[i])
566 for i in self.referencedFilterKeys])
567 except KeyError:
568 raise ZSIMiddlewareConfigError('No filter ID "%s" found '
569 'in environ' % i)
570 ps = self.parseRequest(environ)
571
572
573 soapActionName = environ[
574 SOAPBindingMiddleware.SOAP_ACTION_ENVIRON_KEYNAME
575 ].strip('"')
576 soapMethodName = SOAPBindingMiddleware.SOAP_METHOD_STRING % \
577 soapActionName
578
579
580 method = getattr(serviceSOAPBinding, soapMethodName)
581 resp = method(ps)
582 except Exception, e:
583 sw = self.exception2SOAPFault(environ, e)
584 else:
585
586 sw = SoapWriter(outputclass=self.writerClass)
587 sw.serialize(resp)
588
589
590 self.setSOAPWriter(environ, sw)
591 soapOut = str(sw)
592
593 return self.writeResponse(environ, start_response)
594