#
# spyne - Copyright (C) Spyne contributors.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
#
"""A server transport uses http as transport, and wsgi as bridge api."""
import logging
logger = logging.getLogger(__name__)
import cgi
import threading
import itertools
from spyne.auxproc import process_contexts
from spyne.model.binary import File
try:
from cgi import parse_qs
except ImportError: # Python 3
from urllib.parse import parse_qs
try:
from werkzeug.formparser import parse_form_data
except ImportError:
parse_form_data = None
from spyne.server.http import HttpMethodContext
from spyne.server.http import HttpTransportContext
from spyne.error import RequestTooLongError
from spyne.protocol.http import HttpPattern
from spyne.protocol.soap.mime import apply_mtom
from spyne.util import reconstruct_url
from spyne.server.http import HttpBase
from spyne.const.ansi_color import LIGHT_GREEN
from spyne.const.ansi_color import END_COLOR
from spyne.const.http import HTTP_200
from spyne.const.http import HTTP_404
from spyne.const.http import HTTP_405
from spyne.const.http import HTTP_500
def _get_http_headers(req_env):
retval = {}
for k, v in req_env.items():
if k.startswith("HTTP_"):
retval[k[5:].lower()]= [v]
return retval
class WsgiTransportContext(HttpTransportContext):
[docs] """The class that is used in the transport attribute of the
:class:`WsgiMethodContext` class."""
def __init__(self, transport, req_env, content_type):
HttpTransportContext.__init__(self, transport, req_env, content_type)
self.req_env = self.req
"""WSGI Request environment"""
self.req_method = req_env.get('REQUEST_METHOD', None)
"""HTTP Request verb, as a convenience to users."""
class WsgiMethodContext(HttpMethodContext):
[docs] """The WSGI-Specific method context. WSGI-Specific information is stored in
the transport attribute using the :class:`WsgiTransportContext` class.
"""
def __init__(self, transport, req_env, content_type):
HttpMethodContext.__init__(self, transport, req_env, content_type)
self.transport = WsgiTransportContext(transport, req_env, content_type)
"""Holds the WSGI-specific information"""
class WsgiApplication(HttpBase):
[docs] '''A `PEP-3333 <http://www.python.org/dev/peps/pep-3333>`_
compliant callable class.
Supported events:
* ``wsdl``
Called right before the wsdl data is returned to the client.
* ``wsdl_exception``
Called right after an exception is thrown during wsdl generation.
The exception object is stored in ctx.transport.wsdl_error attribute.
* ``wsgi_call``
Called first when the incoming http request is identified as a rpc
request.
* ``wsgi_return``
Called right before the output stream is returned to the WSGI handler.
* ``wsgi_error``
Called right before returning the exception to the client.
* ``wsgi_close``
Called after the whole data has been returned to the client. It's
called both from success and error cases.
'''
def __init__(self, app, chunked=True,
max_content_length=2 * 1024 * 1024,
block_length=8 * 1024):
HttpBase.__init__(self, app, chunked, max_content_length, block_length)
self._allowed_http_verbs = app.in_protocol.allowed_http_verbs
self._verb_handlers = {
"GET": self.handle_rpc,
"POST": self.handle_rpc,
}
self._mtx_build_interface_document = threading.Lock()
self._wsdl = None
# Initialize HTTP Patterns
self._http_patterns = None
self._map_adapter = None
self._mtx_build_map_adapter = threading.Lock()
for k,v in self.app.interface.service_method_map.items():
p_service_class, p_method_descriptor = v[0]
for p in p_method_descriptor.patterns:
if isinstance(p, HttpPattern):
r = p.as_werkzeug_rule()
# We do this here because we don't want to import
# Werkzeug until the last moment.
if self._http_patterns is None:
from werkzeug.routing import Map
self._http_patterns = Map()
self._http_patterns.add(r)
@property
def has_patterns(self):
[docs] return self._http_patterns is not None
def __call__(self, req_env, start_response, wsgi_url=None):
'''This method conforms to the WSGI spec for callable wsgi applications
(PEP 333). It looks in environ['wsgi.input'] for a fully formed rpc
message envelope, will deserialize the request parameters and call the
method on the object returned by the get_handler() method.
'''
url = wsgi_url
verb = req_env['REQUEST_METHOD'].upper()
if url is None:
url = reconstruct_url(req_env).split('.wsdl')[0]
if self.__is_wsdl_request(req_env):
return self.__handle_wsdl_request(req_env, start_response, url)
elif not (self._allowed_http_verbs is None or
verb in self._allowed_http_verbs or verb in self._verb_handlers):
start_response(HTTP_405, [
('Content-Type', ''),
('Allow', ', '.join(self._allowed_http_verbs)),
])
return [HTTP_405]
else:
return self._verb_handlers[verb](req_env, start_response)
def __is_wsdl_request(self, req_env):
# Get the wsdl for the service. Assume path_info matches pattern:
# /stuff/stuff/stuff/serviceName.wsdl or
# /stuff/stuff/stuff/serviceName/?wsdl
return (
req_env['REQUEST_METHOD'].upper() == 'GET'
and (
req_env['QUERY_STRING'].lower() == 'wsdl'
or req_env['PATH_INFO'].endswith('.wsdl')
)
)
def __handle_wsdl_request(self, req_env, start_response, url):
ctx = WsgiMethodContext(self, req_env, 'text/xml; charset=utf-8')
if self.doc.wsdl11 is None:
start_response(HTTP_404, ctx.transport.resp_headers.items())
return [HTTP_404]
ctx.transport.wsdl = self._wsdl
if ctx.transport.wsdl is None:
try:
self._mtx_build_interface_document.acquire()
ctx.transport.wsdl = self._wsdl
if ctx.transport.wsdl is None:
self.doc.wsdl11.build_interface_document(url)
ctx.transport.wsdl = self._wsdl = self.doc.wsdl11.get_interface_document()
except Exception, e:
logger.exception(e)
ctx.transport.wsdl_error = e
# implementation hook
self.event_manager.fire_event('wsdl_exception', ctx)
start_response(HTTP_500, ctx.transport.resp_headers.items())
return [HTTP_500]
finally:
self._mtx_build_interface_document.release()
self.event_manager.fire_event('wsdl', ctx)
ctx.transport.resp_headers['Content-Length'] = \
str(len(ctx.transport.wsdl))
start_response(HTTP_200, ctx.transport.resp_headers.items())
return [ctx.transport.wsdl]
def handle_error(self, p_ctx, others, error, start_response):
[docs] if p_ctx.transport.resp_code is None:
p_ctx.transport.resp_code = \
self.app.out_protocol.fault_to_http_response_code(error)
self.get_out_string(p_ctx)
p_ctx.out_string = [''.join(p_ctx.out_string)]
p_ctx.transport.resp_headers['Content-Length'] = str(len(p_ctx.out_string[0]))
self.event_manager.fire_event('wsgi_exception', p_ctx)
start_response(p_ctx.transport.resp_code,
p_ctx.transport.resp_headers.items())
try:
process_contexts(self, others, p_ctx, error=error)
except Exception,e:
# Report but ignore any exceptions from auxiliary methods.
logger.exception(e)
return p_ctx.out_string
def handle_rpc(self, req_env, start_response):
[docs] initial_ctx = WsgiMethodContext(self, req_env,
self.app.out_protocol.mime_type)
# implementation hook
self.event_manager.fire_event('wsgi_call', initial_ctx)
initial_ctx.in_string, in_string_charset = \
self.__reconstruct_wsgi_request(req_env)
contexts = self.generate_contexts(initial_ctx, in_string_charset)
p_ctx, others = contexts[0], contexts[1:]
if p_ctx.in_error:
return self.handle_error(p_ctx, others, p_ctx.in_error, start_response)
self.get_in_object(p_ctx)
if p_ctx.in_error:
logger.error(p_ctx.in_error)
return self.handle_error(p_ctx, others, p_ctx.in_error, start_response)
self.get_out_object(p_ctx)
if p_ctx.out_error:
return self.handle_error(p_ctx, others, p_ctx.out_error, start_response)
if p_ctx.transport.resp_code is None:
p_ctx.transport.resp_code = HTTP_200
self.get_out_string(p_ctx)
if p_ctx.descriptor and p_ctx.descriptor.mtom:
# when there is more than one return type, the result is
# encapsulated inside a list. when there's just one, the result
# is returned in a non-encapsulated form. the apply_mtom always
# expects the objects to be inside an iterable, hence the
# following test.
out_type_info = p_ctx.descriptor.out_message._type_info
if len(out_type_info) == 1:
out_object = [out_object]
p_ctx.transport.resp_headers, p_ctx.out_string = apply_mtom(
p_ctx.transport.resp_headers, p_ctx.out_string,
p_ctx.descriptor.out_message._type_info.values(),
out_object
)
# implementation hook
self.event_manager.fire_event('wsgi_return', p_ctx)
if self.chunked:
# the client has not set a content-length, so we delete it as the
# input is just an iterable.
if 'Content-Length' in p_ctx.transport.resp_headers:
del p_ctx.transport.resp_headers['Content-Length']
else:
p_ctx.out_string = [''.join(p_ctx.out_string)]
# if the out_string is a generator function, this hack lets the user
# code run until first yield, which lets it set response headers and
# whatnot before calling start_response. Yes it causes an additional
# copy of the first fragment of the response to be made, but if you know
# a better way of having generator functions execute until first yield,
# just let us know.
try:
len(p_ctx.out_string) # generator?
# nope
p_ctx.transport.resp_headers['Content-Length'] = \
str(sum([len(a) for a in p_ctx.out_string]))
start_response(p_ctx.transport.resp_code,
p_ctx.transport.resp_headers.items())
retval = itertools.chain(p_ctx.out_string, self.__finalize(p_ctx))
except TypeError:
retval_iter = iter(p_ctx.out_string)
try:
first_chunk = retval_iter.next()
except StopIteration:
first_chunk = ''
start_response(p_ctx.transport.resp_code,
p_ctx.transport.resp_headers.items())
retval = itertools.chain([first_chunk], retval_iter,
self.__finalize(p_ctx))
try:
process_contexts(self, others, p_ctx, error=None)
except Exception, e:
# Report but ignore any exceptions from auxiliary methods.
logger.exception(e)
return retval
def __finalize(self, p_ctx):
self.event_manager.fire_event('wsgi_close', p_ctx)
return []
def __reconstruct_wsgi_request(self, http_env):
"""Reconstruct http payload using information in the http header."""
# fyi, here's what the parse_header function returns:
# >>> import cgi; cgi.parse_header("text/xml; charset=utf-8")
# ('text/xml', {'charset': 'utf-8'})
content_type = http_env.get("CONTENT_TYPE")
if content_type is None:
charset = 'utf-8'
else:
content_type = cgi.parse_header(content_type)
charset = content_type[1].get('charset', 'utf-8')
return self.__wsgi_input_to_iterable(http_env), charset
def __wsgi_input_to_iterable(self, http_env):
istream = http_env.get('wsgi.input')
length = str(http_env.get('CONTENT_LENGTH', self.max_content_length))
if len(length) == 0:
length = 0
else:
length = int(length)
if length > self.max_content_length:
raise RequestTooLongError()
bytes_read = 0
while bytes_read < length:
bytes_to_read = min(self.block_length, length - bytes_read)
if bytes_to_read + bytes_read > self.max_content_length:
raise RequestTooLongError()
data = istream.read(bytes_to_read)
if data is None or data == '':
break
bytes_read += len(data)
yield data
def generate_map_adapter(self, ctx):
[docs] try:
self._mtx_build_map_adapter.acquire()
if self._map_adapter is None:
# If url map is not binded before, binds url_map
req_env = ctx.transport.req_env
self._map_adapter = self._http_patterns.bind(
req_env['SERVER_NAME'], "/")
for k,v in ctx.app.interface.service_method_map.items():
#Compiles url patterns
p_service_class, p_method_descriptor = v[0]
for r in self._http_patterns.iter_rules():
params = {}
if r.endpoint == k:
for pk, pv in p_method_descriptor.in_message.\
_type_info.items():
if pk in r.rule:
from spyne.model.primitive import String
from spyne.model.primitive import Unicode
from spyne.model.primitive import Decimal
if issubclass(pv, Unicode):
params[pk] = ""
elif issubclass(pv, Decimal):
params[pk] = 0
self._map_adapter.build(r.endpoint, params)
finally:
self._mtx_build_map_adapter.release()
def decompose_incoming_envelope(self, prot, ctx, message):
[docs] """This function is only called by the HttpRpc protocol to have the wsgi
environment parsed into ``ctx.in_body_doc`` and ``ctx.in_header_doc``.
"""
if self.has_patterns:
from werkzeug.exceptions import NotFound
if self._map_adapter is None:
self.generate_map_adapter(ctx)
try:
#If PATH_INFO matches a url, Set method_request_string to mrs
mrs, params = self._map_adapter.match(ctx.in_document["PATH_INFO"],
ctx.in_document["REQUEST_METHOD"])
ctx.method_request_string = mrs
except NotFound:
# Else set method_request_string normally
params = {}
ctx.method_request_string = '{%s}%s' % (prot.app.interface.get_tns(),
ctx.in_document['PATH_INFO'].split('/')[-1])
else:
params = {}
ctx.method_request_string = '{%s}%s' % (prot.app.interface.get_tns(),
ctx.in_document['PATH_INFO'].split('/')[-1])
logger.debug("%sMethod name: %r%s" % (LIGHT_GREEN,
ctx.method_request_string, END_COLOR))
ctx.in_header_doc = _get_http_headers(ctx.in_document)
ctx.in_body_doc = parse_qs(ctx.in_document['QUERY_STRING'])
for k,v in params.items():
if k in ctx.in_body_doc:
ctx.in_body_doc[k].append(v)
else:
ctx.in_body_doc[k] = [v]
if ctx.in_document['REQUEST_METHOD'].upper() in ('POST', 'PUT', 'PATCH'):
stream, form, files = parse_form_data(ctx.in_document,
stream_factory=prot.stream_factory)
for k, v in form.lists():
val = ctx.in_body_doc.get(k, [])
val.extend(v)
ctx.in_body_doc[k] = val
for k, v in files.items():
val = ctx.in_body_doc.get(k, [])
mime_type = v.headers.get('Content-Type', 'application/octet-stream')
path = getattr(v.stream, 'name', None)
if path is None:
val.append(File.Value(name=v.filename, type=mime_type,
data=[v.stream.getvalue()]))
else:
v.stream.seek(0)
val.append(File.Value(name=v.filename, type=mime_type,
path=path, handle=v.stream))
ctx.in_body_doc[k] = val