#
# 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
#
"""The ``spyne.protocol.http`` module contains the HttpRpc protocol
implementation.
"""
import logging
logger = logging.getLogger(__name__)
import re
import pytz
import tempfile
from spyne.util.six import string_types, BytesIO, PY3
if PY3:
from http.cookies import SimpleCookie
else:
from Cookie import SimpleCookie
from spyne import BODY_STYLE_WRAPPED, MethodDescriptor
from spyne.error import ResourceNotFoundError
from spyne.model.binary import BINARY_ENCODING_URLSAFE_BASE64, File
from spyne.model.primitive import DateTime
from spyne.protocol.dictdoc import SimpleDictDocument
try:
from io import StringIO
except ImportError: # Python 2
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
TEMPORARY_DIR = None
STREAM_READ_BLOCK_SIZE = 0x4000
SWAP_DATA_TO_FILE_THRESHOLD = 512 * 1024
def get_stream_factory(dir=None, delete=True):
[docs] def stream_factory(total_content_length, filename, content_type,
content_length=None):
if total_content_length >= SWAP_DATA_TO_FILE_THRESHOLD or \
delete == False:
if delete == False:
# You need python >= 2.6 for this.
retval = tempfile.NamedTemporaryFile('wb+', dir=dir,
delete=delete)
else:
retval = tempfile.NamedTemporaryFile('wb+', dir=dir)
else:
retval = BytesIO()
return retval
return stream_factory
_weekday = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
_month = ['w00t', "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
"Oct", "Nov", "Dec"]
def _header_to_string(prot, val, cls):
if issubclass(cls, DateTime):
if val.tzinfo is not None:
val = val.astimezone(pytz.utc)
else:
val = val.replace(tzinfo=pytz.utc)
return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (
_weekday[val.weekday()], val.day, _month[val.month],
val.year, val.hour, val.minute, val.second)
else:
return prot.to_string(cls, val)
class HttpRpc(SimpleDictDocument):
[docs] """The so-called HttpRpc protocol implementation. It only works with Http
(wsgi and twisted) transports.
:param app: An :class:'spyne.application.Application` instance.
:param validator: Validation method to use. One of (None, 'soft')
:param mime_type: Default mime type to set. Default is 'application/octet-stream'
:param tmp_dir: Temporary directory to store partial file uploads. Default
is to use the OS default.
:param tmp_delete_on_close: The ``delete`` argument to the
:class:`tempfile.NamedTemporaryFile`.
See: http://docs.python.org/2/library/tempfile.html#tempfile.NamedTemporaryFile.
:param ignore_uncap: As HttpRpc can't serialize complex models, it throws a
server exception when the return type of the user function is Complex.
Passing ``True`` to this argument prevents that by ignoring the return
value.
"""
mime_type = 'text/plain'
default_binary_encoding = BINARY_ENCODING_URLSAFE_BASE64
type = set(SimpleDictDocument.type)
type.add('http')
def __init__(self, app=None, validator=None, mime_type=None,
tmp_dir=None, tmp_delete_on_close=True, ignore_uncap=False,
parse_cookie=True, hier_delim=".", strict_arrays=False):
super(HttpRpc, self).__init__(app, validator, mime_type,
ignore_uncap=ignore_uncap, hier_delim=hier_delim,
strict_arrays=strict_arrays)
self.tmp_dir = tmp_dir
self.tmp_delete_on_close = tmp_delete_on_close
self.parse_cookie = parse_cookie
def get_tmp_delete_on_close(self):
[docs] return self.__tmp_delete_on_close
def set_tmp_delete_on_close(self, val):
[docs] self.__tmp_delete_on_close = val
self.stream_factory = get_stream_factory(self.tmp_dir,
self.__tmp_delete_on_close)
tmp_delete_on_close = property(get_tmp_delete_on_close,
set_tmp_delete_on_close)
def set_validator(self, validator):
[docs] if validator == 'soft' or validator is self.SOFT_VALIDATION:
self.validator = self.SOFT_VALIDATION
elif validator is None:
self.validator = None
else:
raise ValueError(validator)
def create_in_document(self, ctx, in_string_encoding=None):
[docs] assert ctx.transport.type.endswith('http'), \
("This protocol only works with an http transport, not %r, (in %r)"
% (ctx.transport.type, ctx.transport))
ctx.in_document = ctx.transport.req
ctx.transport.request_encoding = in_string_encoding
def decompose_incoming_envelope(self, ctx, message):
[docs] assert message == SimpleDictDocument.REQUEST
ctx.transport.itself.decompose_incoming_envelope(self, ctx, message)
if self.parse_cookie:
cookies = ctx.in_header_doc.get('cookie', None)
if cookies is None:
cookies = ctx.in_header_doc.get('Cookie', None)
if cookies is not None:
for cookie_string in cookies:
cookie = SimpleCookie()
cookie.load(cookie_string)
for k,v in cookie.items():
l = ctx.in_header_doc.get(k, [])
l.append(v.coded_value)
ctx.in_header_doc[k] = l
logger.debug('\theader : %r' % (ctx.in_header_doc))
logger.debug('\tbody : %r' % (ctx.in_body_doc))
def deserialize(self, ctx, message):
[docs] assert message in (self.REQUEST,)
self.event_manager.fire_event('before_deserialize', ctx)
if ctx.descriptor is None:
raise ResourceNotFoundError(ctx.method_request_string)
req_enc = getattr(ctx.transport, 'request_encoding', None)
if ctx.descriptor.in_header is not None:
# HttpRpc supports only one header class
in_header_class = ctx.descriptor.in_header[0]
ctx.in_header = self.simple_dict_to_object(ctx.in_header_doc,
in_header_class, self.validator, req_enc=req_enc)
if ctx.descriptor.in_message is not None:
ctx.in_object = self.simple_dict_to_object(ctx.in_body_doc,
ctx.descriptor.in_message, self.validator, req_enc=req_enc)
self.event_manager.fire_event('after_deserialize', ctx)
def serialize(self, ctx, message):
[docs] assert message in (self.RESPONSE,)
if ctx.out_document is not None:
return
if ctx.out_error is None:
result_class = ctx.descriptor.out_message
header_class = ctx.descriptor.out_header
if header_class is not None:
# HttpRpc supports only one header class
header_class = header_class[0]
# assign raw result to its wrapper, result_message
if ctx.out_object is None or len(ctx.out_object) < 1:
ctx.out_document = ['']
else:
out_class = None
out_object = None
if ctx.descriptor.body_style is BODY_STYLE_WRAPPED:
fti = result_class.get_flat_type_info(result_class)
if len(fti) > 1 and not self.ignore_uncap:
raise TypeError("HttpRpc protocol can only "
"serialize functions with a single return type.")
if len(fti) == 1:
out_class, = fti.values()
out_object, = ctx.out_object
else:
out_class = result_class
out_object, = ctx.out_object
if out_class is not None:
ctx.out_document = self.to_string_iterable(out_class,
out_object)
if issubclass(out_class, File) and not \
isinstance(out_object, (list, tuple, string_types)) \
and out_object.type is not None:
ctx.transport.set_mime_type(str(out_object.type))
# header
if ctx.out_header is not None:
out_header = ctx.out_header
if isinstance(ctx.out_header, (list, tuple)):
out_header = ctx.out_header[0]
ctx.out_header_doc = self.object_to_simple_dict(header_class,
out_header, subvalue_eater=_header_to_string)
else:
ctx.transport.mime_type = 'text/plain'
ctx.out_document = ctx.out_error.to_string_iterable(ctx.out_error)
self.event_manager.fire_event('serialize', ctx)
def create_out_string(self, ctx, out_string_encoding='utf8'):
[docs] if ctx.out_string is not None:
return
ctx.out_string = ctx.out_document
_fragment_pattern_re = re.compile('<([A-Za-z0-9_]+)>')
_full_pattern_re = re.compile('{([A-Za-z0-9_]+)}')
class HttpPattern(object):
[docs] """Experimental. Stay away.
:param address: Address pattern
:param verb: HTTP Verb pattern
:param host: HTTP "Host:" header pattern
"""
@staticmethod
def _compile_url_pattern(pattern):
"""where <> placeholders don't contain slashes."""
if pattern is None:
return None
pattern = _fragment_pattern_re.sub(r'(?P<\1>[^/]*)', pattern)
pattern = _full_pattern_re.sub(r'(?P<\1>[^/]*)', pattern)
return re.compile(pattern)
@staticmethod
def _compile_host_pattern(pattern):
"""where <> placeholders don't contain dots."""
if pattern is None:
return None
pattern = _fragment_pattern_re.sub(r'(?P<\1>[^\.]*)', pattern)
pattern = _full_pattern_re.sub(r'(?P<\1>.*)', pattern)
return re.compile(pattern)
@staticmethod
def _compile_verb_pattern(pattern):
"""where <> placeholders are same as {} ones."""
if pattern is None:
return None
pattern = _fragment_pattern_re.sub(r'(?P<\1>.*)', pattern)
pattern = _full_pattern_re.sub(r'(?P<\1>.*)', pattern)
return re.compile(pattern)
def __init__(self, address=None, verb=None, host=None, endpoint=None):
self.address = address
self.host = host
self.verb = verb
self.endpoint = endpoint
if self.endpoint is not None:
assert isinstance(self.endpoint, MethodDescriptor)
def hello(self, descriptor):
[docs] if self.address is None:
self.address = descriptor.name
@property
def address(self):
return self.__address
@address.setter
def address(self, what):
[docs] self.__address = what
self.address_re = self._compile_url_pattern(what)
@property
def host(self):
return self.__host
@host.setter
def host(self, what):
[docs] self.__host = what
self.host_re = self._compile_host_pattern(what)
@property
def verb(self):
return self.__verb
@verb.setter
def verb(self, what):
[docs] self.__verb = what
self.verb_re = self._compile_verb_pattern(what)
def as_werkzeug_rule(self):
[docs] from werkzeug.routing import Rule
from spyne.util.invregexp import invregexp
methods = None
if self.verb is not None:
methods = invregexp(self.verb)
host = self.host
if host is None:
host = '<__ignored>' # for some reason, this is necessary when
# host_matching is enabled.
return Rule(self.address, host=host, endpoint=self.endpoint.name,
methods=methods)
def __repr__(self):
return "HttpPattern(address=%r, host=%r, verb=%r, endpoint=%r" % (
self.address, self.host, self.verb, self.endpoint.name)