1###########################################################################
2# Joshua R. Boverhof, LBNL
3# See Copyright for copyright notice!
4# $Id: WSresource.py 1423 2007-11-01 20:33:33Z boverhof $
5###########################################################################
6
7import sys, warnings
8
9# twisted & related imports
10from zope.interface import classProvides, implements, Interface
11from twisted.python import log, failure
12from twisted.web.error import NoResource
13from twisted.web.server import NOT_DONE_YET
14import twisted.web.http
15import twisted.web.resource
16
17# ZSI imports
18from ZSI import _get_element_nsuri_name, EvaluateException, ParseException
19from ZSI.parse import ParsedSoap
20from ZSI.writer import SoapWriter
21from ZSI import fault
22
23# WS-Address related imports
24from ZSI.address import Address
25from ZSI.ServiceContainer import WSActionException
26
27from interfaces import CheckInputArgs, HandlerChainInterface, CallbackChainInterface,\
28    DataHandler
29
30
31class LoggingHandlerChain:
32
33    @CheckInputArgs(CallbackChainInterface, HandlerChainInterface)
34    def __init__(self, cb, *handlers):
35        self.handlercb = cb
36        self.handlers = handlers
37        self.debug = len(log.theLogPublisher.observers) > 0
38
39    def processRequest(self, arg, **kw):
40        debug = self.debug
41        if debug: log.msg('--->PROCESS REQUEST: %s' %arg, debug=1)
42
43        for h in self.handlers:
44            if debug: log.msg('\t%s handler: %s' %(arg, h), debug=1)
45            arg = h.processRequest(arg, **kw)
46
47        return self.handlercb.processRequest(arg, **kw)
48
49    def processResponse(self, arg, **kw):
50        debug = self.debug
51        if debug: log.msg('===>PROCESS RESPONSE: %s' %str(arg), debug=1)
52
53        if arg is None:
54            return
55
56        for h in self.handlers:
57            if debug: log.msg('\t%s handler: %s' %(arg, h), debug=1)
58            arg = h.processResponse(arg, **kw)
59
60        s = str(arg)
61        if debug: log.msg(s, debug=1)
62
63        return s
64
65
66#
67# Stability: Unstable
68#
69class DefaultCallbackHandler:
70    classProvides(CallbackChainInterface)
71
72    @classmethod
73    def processRequest(cls, ps, **kw):
74        """invokes callback that should return a (request,response) tuple.
75        representing the SOAP request and response respectively.
76        ps -- ParsedSoap instance representing HTTP Body.
77        request -- twisted.web.server.Request
78        """
79        resource = kw['resource']
80        request = kw['request']
81        method =  getattr(resource, 'soap_%s' %
82                           _get_element_nsuri_name(ps.body_root)[-1])
83
84        try:
85            req_pyobj,rsp_pyobj = method(ps, request=request)
86        except TypeError, ex:
87            log.err(
88                'ERROR: service %s is broken, method MUST return request, response'\
89                    % cls.__name__
90            )
91            raise
92        except Exception, ex:
93            log.err('failure when calling bound method')
94            raise
95
96        return rsp_pyobj
97
98
99class WSAddressHandler:
100    """General WS-Address handler.  This implementation depends on a
101    'wsAction' dictionary in the service stub which contains keys to
102    WS-Action values.
103
104    Implementation saves state on request response flow, so using this
105    handle is not  reliable if execution is deferred between proceesRequest
106    and processResponse.
107
108    TODO: sink this up with wsdl2dispatch
109    TODO: reduce coupling with WSAddressCallbackHandler.
110    """
111    implements(HandlerChainInterface)
112
113    def processRequest(self, ps, **kw):
114        # TODO: Clean this up
115        resource = kw['resource']
116
117        d = getattr(resource, 'root', None)
118        key = _get_element_nsuri_name(ps.body_root)
119        if d is None or d.has_key(key) is False:
120            raise RuntimeError,\
121                'Error looking for key(%s) in root dictionary(%s)' %(key, str(d))
122
123        self.op_name = d[key]
124        self.address = address = Address()
125        address.parse(ps)
126        action = address.getAction()
127        if not action:
128            raise WSActionException('No WS-Action specified in Request')
129
130        request = kw['request']
131        http_headers = request.getAllHeaders()
132        soap_action = http_headers.get('soapaction')
133        if soap_action and soap_action.strip('\'"') != action:
134            raise WSActionException(\
135                'SOAP Action("%s") must match WS-Action("%s") if specified.'\
136                %(soap_action,action)
137            )
138
139        # Save WS-Address in ParsedSoap instance.
140        ps.address = address
141        return ps
142
143    def processResponse(self, sw, **kw):
144        if sw is None:
145            self.address = None
146            return
147
148        request, resource = kw['request'], kw['resource']
149        if isinstance(request, twisted.web.http.Request) is False:
150            raise TypeError, '%s instance expected' %http.Request
151
152        d = getattr(resource, 'wsAction', None)
153        key = self.op_name
154        if d is None or d.has_key(key) is False:
155            raise WSActionNotSpecified,\
156                'Error looking for key(%s) in wsAction dictionary(%s)' %(key, str(d))
157
158        addressRsp = Address(action=d[key])
159        if request.transport.TLS == 0:
160            addressRsp.setResponseFromWSAddress(\
161                 self.address, 'http://%s:%d%s' %(
162                 request.host.host, request.host.port, request.path)
163            )
164        else:
165            addressRsp.setResponseFromWSAddress(\
166                 self.address, 'https://%s:%d%s' %(
167                 request.host.host, request.host.port, request.path)
168            )
169
170        addressRsp.serialize(sw, typed=False)
171        self.address = None
172        return sw
173
174
175class WSAddressCallbackHandler:
176    classProvides(CallbackChainInterface)
177
178    @classmethod
179    def processRequest(cls, ps, **kw):
180        """invokes callback that should return a (request,response) tuple.
181        representing the SOAP request and response respectively.
182        ps -- ParsedSoap instance representing HTTP Body.
183        request -- twisted.web.server.Request
184        """
185        resource = kw['resource']
186        request = kw['request']
187        method =  getattr(resource, 'wsa_%s' %
188                           _get_element_nsuri_name(ps.body_root)[-1])
189
190        # TODO: grab ps.address, clean this up.
191        try:
192            req_pyobj,rsp_pyobj = method(ps, ps.address, request=request)
193        except TypeError, ex:
194            log.err(
195                'ERROR: service %s is broken, method MUST return request, response'\
196                    %self.__class__.__name__
197            )
198            raise
199        except Exception, ex:
200            log.err('failure when calling bound method')
201            raise
202
203        return rsp_pyobj
204
205
206class DeferHandlerChain:
207    """Each handler is
208    """
209
210    @CheckInputArgs(CallbackChainInterface, HandlerChainInterface)
211    def __init__(self, cb, *handlers):
212        self.handlercb = cb
213        self.handlers = handlers
214        self.debug = len(log.theLogPublisher.observers) > 0
215
216    def processRequest(self, arg, **kw):
217        from twisted.internet import reactor
218        from twisted.internet.defer import Deferred
219
220        debug = self.debug
221        if debug: log.msg('--->DEFER PROCESS REQUEST: %s' %arg, debug=1)
222
223        d = Deferred()
224        for h in self.handlers:
225            if debug:
226                log.msg('\t%s handler: %s' %(arg, h), debug=1)
227                log.msg('\thandler callback: %s' %h.processRequest)
228            d.addCallback(h.processRequest, **kw)
229
230        d.addCallback(self.handlercb.processRequest, **kw)
231        reactor.callLater(.0001, d.callback, arg)
232
233        if debug: log.msg('===>DEFER PROCESS RESPONSE: %s' %str(arg), debug=1)
234
235
236
237        for h in self.handlers:
238            if debug: log.msg('\t%s handler: %s' %(arg, h), debug=1)
239            d.addCallback(h.processResponse, **kw)
240
241        d.addCallback(str)
242        return d
243
244    def processResponse(self, arg, **kw):
245        return arg
246
247
248class DefaultHandlerChainFactory:
249    protocol = LoggingHandlerChain
250
251    @classmethod
252    def newInstance(cls):
253        return cls.protocol(DefaultCallbackHandler, DataHandler)
254
255
256class WSAddressHandlerChainFactory:
257    protocol = DefaultHandlerChain
258
259    @classmethod
260    def newInstance(cls):
261        return cls.protocol(WSAddressCallbackHandler, DataHandler,
262            WSAddressHandler())
263
264
265class WSResource(twisted.web.resource.Resource, object):
266    """
267    class variables:
268        encoding  --
269        factory -- hander chain, which has a factory method "newInstance"
270		that returns a
271    """
272    encoding = "UTF-8"
273    factory = DefaultHandlerChainFactory
274
275    def __init__(self):
276        """
277        """
278        twisted.web.resource.Resource.__init__(self)
279
280    def _writeResponse(self, response, request, status=200):
281        """
282        request -- request message
283        response --- response message
284        status -- HTTP Status
285        """
286        request.setResponseCode(status)
287        if self.encoding is not None:
288            mimeType = 'text/xml; charset="%s"' % self.encoding
289        else:
290            mimeType = "text/xml"
291
292        request.setHeader("Content-Type", mimeType)
293        request.setHeader("Content-Length", str(len(response)))
294        request.write(response)
295        request.finish()
296
297    def _writeFault(self, fail, request):
298        """
299        fail -- failure
300        request -- request message
301        ex -- Exception
302        """
303        response = fault.FaultFromException(fail.value, False, fail.tb).AsSOAP()
304        self._writeResponse(response, request, status=500)
305
306    def render_POST(self, request):
307        """Dispatch Method called by twisted render, creates a
308        request/response handler chain.
309        request -- twisted.web.server.Request
310        """
311        from twisted.internet.defer import maybeDeferred
312
313        chain = self.factory.newInstance()
314        data = request.content.read()
315
316        d = maybeDeferred(chain.processRequest, data, request=request, resource=self)
317        d.addCallback(chain.processResponse, request=request, resource=self)
318        d.addCallback(self._writeResponse, request)
319        d.addErrback(self._writeFault, request)
320
321        return NOT_DONE_YET
322
323
324
325
326
327class DefaultHandlerChain:
328
329    @CheckInputArgs(CallbackChainInterface, HandlerChainInterface)
330    def __init__(self, cb, *handlers):
331        self.handlercb = cb
332        self.handlers = handlers
333        self.debug = len(log.theLogPublisher.observers) > 0
334
335    def processRequest(self, arg, **kw):
336        debug = self.debug
337        if debug: log.msg('--->PROCESS REQUEST: %s' %arg, debug=1)
338
339        for h in self.handlers:
340            if debug: log.msg('\t%s handler: %s' %(arg, h), debug=1)
341            arg = h.processRequest(arg, **kw)
342
343        return self.handlercb.processRequest(arg, **kw)
344
345    def processResponse(self, arg, **kw):
346        debug = self.debug
347        if debug: log.msg('===>PROCESS RESPONSE: %s' %str(arg), debug=1)
348
349        if arg is None:
350            return
351
352        for h in self.handlers:
353            if debug: log.msg('\t%s handler: %s' %(arg, h), debug=1)
354            arg = h.processResponse(arg, **kw)
355
356        s = str(arg)
357        if debug: log.msg(s, debug=1)
358
359        return s
360
361