okay fine

This commit is contained in:
pacnpal
2024-11-03 17:47:26 +00:00
parent 387c4740e7
commit 27f3326e22
10020 changed files with 1935769 additions and 2364 deletions

View File

@@ -0,0 +1,41 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from autobahn.websocket.types import ConnectionRequest, ConnectionResponse, \
ConnectionAccept, ConnectionDeny, Message, IncomingMessage, OutgoingMessage
from autobahn.websocket.interfaces import IWebSocketChannel
__all__ = (
'IWebSocketChannel',
'Message',
'IncomingMessage',
'OutgoingMessage',
'ConnectionRequest',
'ConnectionResponse',
'ConnectionAccept',
'ConnectionDeny',
)

View File

@@ -0,0 +1,129 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from autobahn.websocket.compress_base import \
PerMessageCompressOffer, \
PerMessageCompressOfferAccept, \
PerMessageCompressResponse, \
PerMessageCompressResponseAccept, \
PerMessageCompress
from autobahn.websocket.compress_deflate import \
PerMessageDeflateMixin, \
PerMessageDeflateOffer, \
PerMessageDeflateOfferAccept, \
PerMessageDeflateResponse, \
PerMessageDeflateResponseAccept, \
PerMessageDeflate
# this must be a list (not tuple), since we dynamically
# extend it ..
__all__ = [
'PerMessageCompressOffer',
'PerMessageCompressOfferAccept',
'PerMessageCompressResponse',
'PerMessageCompressResponseAccept',
'PerMessageCompress',
'PerMessageDeflateOffer',
'PerMessageDeflateOfferAccept',
'PerMessageDeflateResponse',
'PerMessageDeflateResponseAccept',
'PerMessageDeflate',
'PERMESSAGE_COMPRESSION_EXTENSION'
]
# map of available compression extensions
PERMESSAGE_COMPRESSION_EXTENSION = {
# class for 'permessage-deflate' is always available
PerMessageDeflateMixin.EXTENSION_NAME: {
'Offer': PerMessageDeflateOffer,
'OfferAccept': PerMessageDeflateOfferAccept,
'Response': PerMessageDeflateResponse,
'ResponseAccept': PerMessageDeflateResponseAccept,
'PMCE': PerMessageDeflate
}
}
# include 'permessage-bzip2' classes if bzip2 is available
try:
import bz2
except ImportError:
bz2 = None
else:
from autobahn.websocket.compress_bzip2 import \
PerMessageBzip2Mixin, \
PerMessageBzip2Offer, \
PerMessageBzip2OfferAccept, \
PerMessageBzip2Response, \
PerMessageBzip2ResponseAccept, \
PerMessageBzip2
PMCE = {
'Offer': PerMessageBzip2Offer,
'OfferAccept': PerMessageBzip2OfferAccept,
'Response': PerMessageBzip2Response,
'ResponseAccept': PerMessageBzip2ResponseAccept,
'PMCE': PerMessageBzip2
}
PERMESSAGE_COMPRESSION_EXTENSION[PerMessageBzip2Mixin.EXTENSION_NAME] = PMCE
__all__.extend(['PerMessageBzip2Offer',
'PerMessageBzip2OfferAccept',
'PerMessageBzip2Response',
'PerMessageBzip2ResponseAccept',
'PerMessageBzip2'])
# include 'permessage-snappy' classes if Snappy is available
try:
# noinspection PyPackageRequirements
import snappy
except ImportError:
snappy = None
else:
from autobahn.websocket.compress_snappy import \
PerMessageSnappyMixin, \
PerMessageSnappyOffer, \
PerMessageSnappyOfferAccept, \
PerMessageSnappyResponse, \
PerMessageSnappyResponseAccept, \
PerMessageSnappy
PMCE = {
'Offer': PerMessageSnappyOffer,
'OfferAccept': PerMessageSnappyOfferAccept,
'Response': PerMessageSnappyResponse,
'ResponseAccept': PerMessageSnappyResponseAccept,
'PMCE': PerMessageSnappy
}
PERMESSAGE_COMPRESSION_EXTENSION[PerMessageSnappyMixin.EXTENSION_NAME] = PMCE
__all__.extend(['PerMessageSnappyOffer',
'PerMessageSnappyOfferAccept',
'PerMessageSnappyResponse',
'PerMessageSnappyResponseAccept',
'PerMessageSnappy'])

View File

@@ -0,0 +1,63 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
__all__ = (
'PerMessageCompressOffer',
'PerMessageCompressOfferAccept',
'PerMessageCompressResponse',
'PerMessageCompressResponseAccept',
'PerMessageCompress',
)
class PerMessageCompressOffer(object):
"""
Base class for WebSocket compression parameter client offers.
"""
class PerMessageCompressOfferAccept(object):
"""
Base class for WebSocket compression parameter client offer accepts by the server.
"""
class PerMessageCompressResponse(object):
"""
Base class for WebSocket compression parameter server responses.
"""
class PerMessageCompressResponseAccept(object):
"""
Base class for WebSocket compression parameter server response accepts by client.
"""
class PerMessageCompress(object):
"""
Base class for WebSocket compression negotiated parameters.
"""

View File

@@ -0,0 +1,446 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import bz2
from autobahn.websocket.compress_base import PerMessageCompressOffer, \
PerMessageCompressOfferAccept, \
PerMessageCompressResponse, \
PerMessageCompressResponseAccept, \
PerMessageCompress
__all__ = (
'PerMessageBzip2Mixin',
'PerMessageBzip2Offer',
'PerMessageBzip2OfferAccept',
'PerMessageBzip2Response',
'PerMessageBzip2ResponseAccept',
'PerMessageBzip2',
)
class PerMessageBzip2Mixin(object):
"""
Mixin class for this extension.
"""
EXTENSION_NAME = "permessage-bzip2"
"""
Name of this WebSocket extension.
"""
COMPRESS_LEVEL_PERMISSIBLE_VALUES = [1, 2, 3, 4, 5, 6, 7, 8, 9]
"""
Permissible value for compression level parameter.
"""
class PerMessageBzip2Offer(PerMessageCompressOffer, PerMessageBzip2Mixin):
"""
Set of extension parameters for `permessage-bzip2` WebSocket extension
offered by a client to a server.
"""
@classmethod
def parse(cls, params):
"""
Parses a WebSocket extension offer for `permessage-bzip2` provided by a client to a server.
:param params: Output from :func:`autobahn.websocket.WebSocketProtocol._parseExtensionsHeader`.
:type params: list
:returns: object -- A new instance of :class:`autobahn.compress.PerMessageBzip2Offer`.
"""
# extension parameter defaults
accept_max_compress_level = False
request_max_compress_level = 0
# verify/parse client ("client-to-server direction") parameters of permessage-bzip2 offer
for p in params:
if len(params[p]) > 1:
raise Exception("multiple occurrence of extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME))
val = params[p][0]
if p == 'client_max_compress_level':
# noinspection PySimplifyBooleanCheck
if val is not True:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
accept_max_compress_level = True
elif p == 'server_max_compress_level':
try:
val = int(val)
except:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
if val not in PerMessageBzip2Mixin.COMPRESS_LEVEL_PERMISSIBLE_VALUES:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
request_max_compress_level = val
else:
raise Exception("illegal extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME))
offer = cls(accept_max_compress_level,
request_max_compress_level)
return offer
def __init__(self,
accept_max_compress_level=True,
request_max_compress_level=0):
"""
Constructor.
:param accept_max_compress_level: Iff true, client accepts "maximum compression level" parameter.
:type accept_max_compress_level: bool
:param request_max_compress_level: Iff non-zero, client requests given "maximum compression level" - must be 1-9.
:type request_max_compress_level: int
"""
if type(accept_max_compress_level) != bool:
raise Exception("invalid type %s for accept_max_compress_level" % type(accept_max_compress_level))
self.accept_max_compress_level = accept_max_compress_level
if request_max_compress_level != 0 and request_max_compress_level not in self.COMPRESS_LEVEL_PERMISSIBLE_VALUES:
raise Exception("invalid value %s for request_max_compress_level - permissible values %s" % (request_max_compress_level, self.COMPRESS_LEVEL_PERMISSIBLE_VALUES))
self.request_max_compress_level = request_max_compress_level
def get_extension_string(self):
"""
Returns the WebSocket extension configuration string as sent to the server.
:returns: PMCE configuration string.
:rtype: str
"""
pmce_string = self.EXTENSION_NAME
if self.accept_max_compress_level:
pmce_string += "; client_max_compress_level"
if self.request_max_compress_level != 0:
pmce_string += "; server_max_compress_level=%d" % self.request_max_compress_level
return pmce_string
def __json__(self):
"""
Returns a JSON serializable object representation.
:returns: JSON serializable representation.
:rtype: dict
"""
return {'extension': self.EXTENSION_NAME,
'accept_max_compress_level': self.accept_max_compress_level,
'request_max_compress_level': self.request_max_compress_level}
def __repr__(self):
"""
Returns Python object representation that can be eval'ed to reconstruct the object.
:returns: Python string representation.
:rtype: str
"""
return "PerMessageBzip2Offer(accept_max_compress_level = %s, request_max_compress_level = %s)" % (self.accept_max_compress_level, self.request_max_compress_level)
class PerMessageBzip2OfferAccept(PerMessageCompressOfferAccept, PerMessageBzip2Mixin):
"""
Set of parameters with which to accept an `permessage-bzip2` offer
from a client by a server.
"""
def __init__(self,
offer,
request_max_compress_level=0,
compress_level=None):
"""
Constructor.
:param offer: The offer being accepted.
:type offer: Instance of :class:`autobahn.compress.PerMessageBzip2Offer`.
:param request_max_compress_level: Iff non-zero, server requests given "maximum compression level" - must be 1-9.
:type request_max_compress_level: int
:param compress_level: Override server ("server-to-client direction") compress level (this must be compatible with offer).
:type compress_level: int
"""
if not isinstance(offer, PerMessageBzip2Offer):
raise Exception("invalid type %s for offer" % type(offer))
self.offer = offer
if request_max_compress_level != 0 and request_max_compress_level not in self.COMPRESS_LEVEL_PERMISSIBLE_VALUES:
raise Exception("invalid value %s for request_max_compress_level - permissible values %s" % (request_max_compress_level, self.COMPRESS_LEVEL_PERMISSIBLE_VALUES))
if request_max_compress_level != 0 and not offer.accept_max_compress_level:
raise Exception("invalid value %s for request_max_compress_level - feature unsupported by client" % request_max_compress_level)
self.request_max_compress_level = request_max_compress_level
if compress_level is not None:
if compress_level not in self.COMPRESS_LEVEL_PERMISSIBLE_VALUES:
raise Exception("invalid value %s for compress_level - permissible values %s" % (compress_level, self.COMPRESS_LEVEL_PERMISSIBLE_VALUES))
if offer.request_max_compress_level != 0 and compress_level > offer.request_max_compress_level:
raise Exception("invalid value %s for compress_level - client requested lower maximum value" % compress_level)
self.compress_level = compress_level
def get_extension_string(self):
"""
Returns the WebSocket extension configuration string as sent to the server.
:returns: PMCE configuration string.
:rtype: str
"""
pmce_string = self.EXTENSION_NAME
if self.offer.request_max_compress_level != 0:
pmce_string += "; server_max_compress_level=%d" % self.offer.request_max_compress_level
if self.request_max_compress_level != 0:
pmce_string += "; client_max_compress_level=%d" % self.request_max_compress_level
return pmce_string
def __json__(self):
"""
Returns a JSON serializable object representation.
:returns: JSON serializable representation.
:rtype: dict
"""
return {'extension': self.EXTENSION_NAME,
'offer': self.offer.__json__(),
'request_max_compress_level': self.request_max_compress_level,
'compress_level': self.compress_level}
def __repr__(self):
"""
Returns Python object representation that can be eval'ed to reconstruct the object.
:returns: Python string representation.
:rtype: str
"""
return "PerMessageBzip2Accept(offer = %s, request_max_compress_level = %s, compress_level = %s)" % (self.offer.__repr__(), self.request_max_compress_level, self.compress_level)
class PerMessageBzip2Response(PerMessageCompressResponse, PerMessageBzip2Mixin):
"""
Set of parameters for `permessage-bzip2` responded by server.
"""
@classmethod
def parse(cls, params):
"""
Parses a WebSocket extension response for `permessage-bzip2` provided by a server to a client.
:param params: Output from :func:`autobahn.websocket.WebSocketProtocol._parseExtensionsHeader`.
:type params: list
:returns: A new instance of :class:`autobahn.compress.PerMessageBzip2Response`.
:rtype: obj
"""
client_max_compress_level = 0
server_max_compress_level = 0
for p in params:
if len(params[p]) > 1:
raise Exception("multiple occurrence of extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME))
val = params[p][0]
if p == 'client_max_compress_level':
try:
val = int(val)
except:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
if val not in PerMessageBzip2Mixin.COMPRESS_LEVEL_PERMISSIBLE_VALUES:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
client_max_compress_level = val
elif p == 'server_max_compress_level':
try:
val = int(val)
except:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
if val not in PerMessageBzip2Mixin.COMPRESS_LEVEL_PERMISSIBLE_VALUES:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
server_max_compress_level = val
else:
raise Exception("illegal extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME))
response = cls(client_max_compress_level,
server_max_compress_level)
return response
def __init__(self,
client_max_compress_level,
server_max_compress_level):
self.client_max_compress_level = client_max_compress_level
self.server_max_compress_level = server_max_compress_level
def __json__(self):
"""
Returns a JSON serializable object representation.
:returns: JSON serializable representation.
:rtype: dict
"""
return {'extension': self.EXTENSION_NAME,
'client_max_compress_level': self.client_max_compress_level,
'server_max_compress_level': self.server_max_compress_level}
def __repr__(self):
"""
Returns Python object representation that can be eval'ed to reconstruct the object.
:returns: Python string representation.
:rtype: str
"""
return "PerMessageBzip2Response(client_max_compress_level = %s, server_max_compress_level = %s)" % (self.client_max_compress_level, self.server_max_compress_level)
class PerMessageBzip2ResponseAccept(PerMessageCompressResponseAccept, PerMessageBzip2Mixin):
"""
Set of parameters with which to accept an `permessage-bzip2` response
from a server by a client.
"""
def __init__(self,
response,
compress_level=None):
"""
:param response: The response being accepted.
:type response: Instance of :class:`autobahn.compress.PerMessageBzip2Response`.
:param compress_level: Override client ("client-to-server direction") compress level (this must be compatible with response).
:type compress_level: int
"""
if not isinstance(response, PerMessageBzip2Response):
raise Exception("invalid type %s for response" % type(response))
self.response = response
if compress_level is not None:
if compress_level not in self.COMPRESS_LEVEL_PERMISSIBLE_VALUES:
raise Exception("invalid value %s for compress_level - permissible values %s" % (compress_level, self.COMPRESS_LEVEL_PERMISSIBLE_VALUES))
if response.client_max_compress_level != 0 and compress_level > response.client_max_compress_level:
raise Exception("invalid value %s for compress_level - server requested lower maximum value" % compress_level)
self.compress_level = compress_level
def __json__(self):
"""
Returns a JSON serializable object representation.
:returns: JSON serializable representation.
:rtype: dict
"""
return {'extension': self.EXTENSION_NAME,
'response': self.response.__json__(),
'compress_level': self.compress_level}
def __repr__(self):
"""
Returns Python object representation that can be eval'ed to reconstruct the object.
:returns: Python string representation.
:rtype: str
"""
return "PerMessageBzip2ResponseAccept(response = %s, compress_level = %s)" % (self.response.__repr__(), self.compress_level)
class PerMessageBzip2(PerMessageCompress, PerMessageBzip2Mixin):
"""
`permessage-bzip2` WebSocket extension processor.
"""
DEFAULT_COMPRESS_LEVEL = 9
@classmethod
def create_from_response_accept(cls, is_server, accept):
pmce = cls(is_server,
accept.response.server_max_compress_level,
accept.compress_level if accept.compress_level is not None else accept.response.client_max_compress_level)
return pmce
@classmethod
def create_from_offer_accept(cls, is_server, accept):
pmce = cls(is_server,
accept.compress_level if accept.compress_level is not None else accept.offer.request_max_compress_level,
accept.request_max_compress_level)
return pmce
def __init__(self,
is_server,
server_max_compress_level,
client_max_compress_level):
self._isServer = is_server
self._compressor = None
self._decompressor = None
self.server_max_compress_level = server_max_compress_level if server_max_compress_level != 0 else self.DEFAULT_COMPRESS_LEVEL
self.client_max_compress_level = client_max_compress_level if client_max_compress_level != 0 else self.DEFAULT_COMPRESS_LEVEL
def __json__(self):
return {'extension': self.EXTENSION_NAME,
'isServer': self._isServer,
'server_max_compress_level': self.server_max_compress_level,
'client_max_compress_level': self.client_max_compress_level}
def __repr__(self):
return "PerMessageBzip2(isServer = %s, server_max_compress_level = %s, client_max_compress_level = %s)" % (self._isServer, self.server_max_compress_level, self.client_max_compress_level)
def start_compress_message(self):
if self._isServer:
if self._compressor is None:
self._compressor = bz2.BZ2Compressor(self.server_max_compress_level)
else:
if self._compressor is None:
self._compressor = bz2.BZ2Compressor(self.client_max_compress_level)
def compress_message_data(self, data):
return self._compressor.compress(data)
def end_compress_message(self):
data = self._compressor.flush()
# there seems to be no "flush without close stream", and after
# full flush, compressor must not be reused
self._compressor = None
return data
def start_decompress_message(self):
if self._decompressor is None:
self._decompressor = bz2.BZ2Decompressor()
def decompress_message_data(self, data):
return self._decompressor.decompress(data)
def end_decompress_message(self):
self._decompressor = None

View File

@@ -0,0 +1,661 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import zlib
from autobahn.util import public
from autobahn.websocket.compress_base import PerMessageCompressOffer, \
PerMessageCompressOfferAccept, \
PerMessageCompressResponse, \
PerMessageCompressResponseAccept, \
PerMessageCompress
__all__ = (
'PerMessageDeflateMixin',
'PerMessageDeflateOffer',
'PerMessageDeflateOfferAccept',
'PerMessageDeflateResponse',
'PerMessageDeflateResponseAccept',
'PerMessageDeflate',
)
class PerMessageDeflateMixin(object):
"""
Mixin class for this extension.
"""
EXTENSION_NAME = "permessage-deflate"
"""
Name of this WebSocket extension.
"""
WINDOW_SIZE_PERMISSIBLE_VALUES = [9, 10, 11, 12, 13, 14, 15]
"""
Permissible value for window size parameter.
Higher values use more memory, but produce smaller output. The default is 15.
"""
MEM_LEVEL_PERMISSIBLE_VALUES = [1, 2, 3, 4, 5, 6, 7, 8, 9]
"""
Permissible value for memory level parameter.
Higher values use more memory, but are faster and produce smaller output. The default is 8.
"""
@public
class PerMessageDeflateOffer(PerMessageCompressOffer, PerMessageDeflateMixin):
"""
Set of extension parameters for `permessage-deflate` WebSocket extension
offered by a client to a server.
"""
@classmethod
def parse(cls, params):
"""
Parses a WebSocket extension offer for `permessage-deflate` provided by a client to a server.
:param params: Output from :func:`autobahn.websocket.WebSocketProtocol._parseExtensionsHeader`.
:type params: list
:returns: A new instance of :class:`autobahn.compress.PerMessageDeflateOffer`.
:rtype: obj
"""
# extension parameter defaults
accept_max_window_bits = False
accept_no_context_takeover = True
# accept_no_context_takeover = False # FIXME: this may change in draft
request_max_window_bits = 0
request_no_context_takeover = False
# verify/parse client ("client-to-server direction") parameters of permessage-deflate offer
for p in params:
if len(params[p]) > 1:
raise Exception("multiple occurrence of extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME))
val = params[p][0]
if p == 'client_max_window_bits':
#
# see: https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-18
# 8.1.2.2. client_max_window_bits
# ".. This parameter has no value or a decimal integer value without
# leading zeroes between 9 to 15 inclusive ..""
# noinspection PySimplifyBooleanCheck
if val is not True:
try:
val = int(val)
except:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
if val not in PerMessageDeflateMixin.WINDOW_SIZE_PERMISSIBLE_VALUES:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
# FIXME (maybe): possibly forward/process the client hint!
# accept_max_window_bits = val
accept_max_window_bits = True
else:
accept_max_window_bits = True
elif p == 'client_no_context_takeover':
# noinspection PySimplifyBooleanCheck
if val is not True:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
accept_no_context_takeover = True
elif p == 'server_max_window_bits':
try:
val = int(val)
except:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
if val not in PerMessageDeflateMixin.WINDOW_SIZE_PERMISSIBLE_VALUES:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
request_max_window_bits = val
elif p == 'server_no_context_takeover':
# noinspection PySimplifyBooleanCheck
if val is not True:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
request_no_context_takeover = True
else:
raise Exception("illegal extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME))
offer = cls(accept_no_context_takeover,
accept_max_window_bits,
request_no_context_takeover,
request_max_window_bits)
return offer
def __init__(self,
accept_no_context_takeover=True,
accept_max_window_bits=True,
request_no_context_takeover=False,
request_max_window_bits=0):
"""
:param accept_no_context_takeover: When ``True``, the client accepts the "no context takeover" feature.
:type accept_no_context_takeover: bool
:param accept_max_window_bits: When ``True``, the client accepts setting "max window size".
:type accept_max_window_bits: bool
:param request_no_context_takeover: When ``True``, the client request the "no context takeover" feature.
:type request_no_context_takeover: bool
:param request_max_window_bits: When non-zero, the client requests the given "max window size" (must be
and integer from the interval ``[9..15]``).
:type request_max_window_bits: int
"""
if type(accept_no_context_takeover) != bool:
raise Exception("invalid type %s for accept_no_context_takeover" % type(accept_no_context_takeover))
self.accept_no_context_takeover = accept_no_context_takeover
if type(accept_max_window_bits) != bool:
raise Exception("invalid type %s for accept_max_window_bits" % type(accept_max_window_bits))
self.accept_max_window_bits = accept_max_window_bits
if type(request_no_context_takeover) != bool:
raise Exception("invalid type %s for request_no_context_takeover" % type(request_no_context_takeover))
self.request_no_context_takeover = request_no_context_takeover
if request_max_window_bits != 0 and request_max_window_bits not in self.WINDOW_SIZE_PERMISSIBLE_VALUES:
raise Exception("invalid value %s for request_max_window_bits - permissible values %s" % (request_max_window_bits, self.WINDOW_SIZE_PERMISSIBLE_VALUES))
self.request_max_window_bits = request_max_window_bits
def get_extension_string(self):
"""
Returns the WebSocket extension configuration string as sent to the server.
:returns: PMCE configuration string.
:rtype: str
"""
pmce_string = self.EXTENSION_NAME
if self.accept_no_context_takeover:
pmce_string += "; client_no_context_takeover"
if self.accept_max_window_bits:
pmce_string += "; client_max_window_bits"
if self.request_no_context_takeover:
pmce_string += "; server_no_context_takeover"
if self.request_max_window_bits != 0:
pmce_string += "; server_max_window_bits=%d" % self.request_max_window_bits
return pmce_string
def __json__(self):
"""
Returns a JSON serializable object representation.
:returns: JSON serializable representation.
:rtype: dict
"""
return {'extension': self.EXTENSION_NAME,
'accept_no_context_takeover': self.accept_no_context_takeover,
'accept_max_window_bits': self.accept_max_window_bits,
'request_no_context_takeover': self.request_no_context_takeover,
'request_max_window_bits': self.request_max_window_bits}
def __repr__(self):
"""
Returns Python object representation that can be eval'ed to reconstruct the object.
:returns: Python string representation.
:rtype: str
"""
return "PerMessageDeflateOffer(accept_no_context_takeover = %s, accept_max_window_bits = %s, request_no_context_takeover = %s, request_max_window_bits = %s)" % (self.accept_no_context_takeover, self.accept_max_window_bits, self.request_no_context_takeover, self.request_max_window_bits)
@public
class PerMessageDeflateOfferAccept(PerMessageCompressOfferAccept, PerMessageDeflateMixin):
"""
Set of parameters with which to accept an `permessage-deflate` offer
from a client by a server.
"""
def __init__(self,
offer,
request_no_context_takeover=False,
request_max_window_bits=0,
no_context_takeover=None,
window_bits=None,
mem_level=None,
max_message_size=None):
"""
:param offer: The offer being accepted.
:type offer: Instance of :class:`autobahn.compress.PerMessageDeflateOffer`.
:param request_no_context_takeover: When ``True``, the server requests the "no context takeover" feature.
:type request_no_context_takeover: bool
:param request_max_window_bits: When non-zero, the server requests the given "max window size" (must be
and integer from the interval ``[9..15]``).
:param request_max_window_bits: int
:param no_context_takeover: Override server ("server-to-client direction") context takeover (this must
be compatible with the offer).
:type no_context_takeover: bool
:param window_bits: Override server ("server-to-client direction") window size (this must be
compatible with the offer).
:type window_bits: int
:param mem_level: Set server ("server-to-client direction") memory level.
:type mem_level: int
"""
if not isinstance(offer, PerMessageDeflateOffer):
raise Exception("invalid type %s for offer" % type(offer))
self.offer = offer
if type(request_no_context_takeover) != bool:
raise Exception("invalid type %s for request_no_context_takeover" % type(request_no_context_takeover))
if request_no_context_takeover and not offer.accept_no_context_takeover:
raise Exception("invalid value %s for request_no_context_takeover - feature unsupported by client" % request_no_context_takeover)
self.request_no_context_takeover = request_no_context_takeover
if request_max_window_bits != 0 and request_max_window_bits not in self.WINDOW_SIZE_PERMISSIBLE_VALUES:
raise Exception("invalid value %s for request_max_window_bits - permissible values %s" % (request_max_window_bits, self.WINDOW_SIZE_PERMISSIBLE_VALUES))
if request_max_window_bits != 0 and not offer.accept_max_window_bits:
raise Exception("invalid value %s for request_max_window_bits - feature unsupported by client" % request_max_window_bits)
self.request_max_window_bits = request_max_window_bits
if no_context_takeover is not None:
if type(no_context_takeover) != bool:
raise Exception("invalid type %s for no_context_takeover" % type(no_context_takeover))
if offer.request_no_context_takeover and not no_context_takeover:
raise Exception("invalid value %s for no_context_takeover - client requested feature" % no_context_takeover)
self.no_context_takeover = no_context_takeover
if window_bits is not None:
if window_bits not in self.WINDOW_SIZE_PERMISSIBLE_VALUES:
raise Exception("invalid value %s for window_bits - permissible values %s" % (window_bits, self.WINDOW_SIZE_PERMISSIBLE_VALUES))
if offer.request_max_window_bits != 0 and window_bits > offer.request_max_window_bits:
raise Exception("invalid value %s for window_bits - client requested lower maximum value" % window_bits)
self.window_bits = window_bits
if mem_level is not None:
if mem_level not in self.MEM_LEVEL_PERMISSIBLE_VALUES:
raise Exception("invalid value %s for mem_level - permissible values %s" % (mem_level, self.MEM_LEVEL_PERMISSIBLE_VALUES))
self.mem_level = mem_level
self.max_message_size = max_message_size # clamp/check values..?
def get_extension_string(self):
"""
Returns the WebSocket extension configuration string as sent to the server.
:returns: PMCE configuration string.
:rtype: str
"""
pmce_string = self.EXTENSION_NAME
if self.offer.request_no_context_takeover:
pmce_string += "; server_no_context_takeover"
if self.offer.request_max_window_bits != 0:
pmce_string += "; server_max_window_bits=%d" % self.offer.request_max_window_bits
if self.request_no_context_takeover:
pmce_string += "; client_no_context_takeover"
if self.request_max_window_bits != 0:
pmce_string += "; client_max_window_bits=%d" % self.request_max_window_bits
return pmce_string
def __json__(self):
"""
Returns a JSON serializable object representation.
:returns: JSON serializable representation.
:rtype: dict
"""
return {
'extension': self.EXTENSION_NAME,
'offer': self.offer.__json__(),
'request_no_context_takeover': self.request_no_context_takeover,
'request_max_window_bits': self.request_max_window_bits,
'no_context_takeover': self.no_context_takeover,
'window_bits': self.window_bits,
'mem_level': self.mem_level,
'max_message_size': self.max_message_size,
}
def __repr__(self):
"""
Returns Python object representation that can be eval'ed to reconstruct the object.
:returns: Python string representation.
:rtype: str
"""
return "PerMessageDeflateOfferAccept(offer = %s, request_no_context_takeover = %s, request_max_window_bits = %s, no_context_takeover = %s, window_bits = %s, mem_level = %s, max_message_size = %s)" % (self.offer.__repr__(), self.request_no_context_takeover, self.request_max_window_bits, self.no_context_takeover, self.window_bits, self.mem_level, self.max_message_size)
@public
class PerMessageDeflateResponse(PerMessageCompressResponse, PerMessageDeflateMixin):
"""
Set of parameters for `permessage-deflate` responded by server.
"""
@classmethod
def parse(cls, params):
"""
Parses a WebSocket extension response for `permessage-deflate` provided by a server to a client.
:param params: Output from :func:`autobahn.websocket.WebSocketProtocol._parseExtensionsHeader`.
:type params: list
:returns: A new instance of :class:`autobahn.compress.PerMessageDeflateResponse`.
:rtype: obj
"""
client_max_window_bits = 0
client_no_context_takeover = False
server_max_window_bits = 0
server_no_context_takeover = False
for p in params:
if len(params[p]) > 1:
raise Exception("multiple occurrence of extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME))
val = params[p][0]
if p == 'client_max_window_bits':
try:
val = int(val)
except:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
if val not in PerMessageDeflateMixin.WINDOW_SIZE_PERMISSIBLE_VALUES:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
client_max_window_bits = val
elif p == 'client_no_context_takeover':
# noinspection PySimplifyBooleanCheck
if val is not True:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
client_no_context_takeover = True
elif p == 'server_max_window_bits':
try:
val = int(val)
except:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
if val not in PerMessageDeflateMixin.WINDOW_SIZE_PERMISSIBLE_VALUES:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
server_max_window_bits = val
elif p == 'server_no_context_takeover':
# noinspection PySimplifyBooleanCheck
if val is not True:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
server_no_context_takeover = True
else:
raise Exception("illegal extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME))
response = cls(client_max_window_bits,
client_no_context_takeover,
server_max_window_bits,
server_no_context_takeover)
return response
def __init__(self,
client_max_window_bits,
client_no_context_takeover,
server_max_window_bits,
server_no_context_takeover):
"""
:param client_max_window_bits: FIXME
:type client_max_window_bits: int
:param client_no_context_takeover: FIXME
:type client_no_context_takeover: bool
:param server_max_window_bits: FIXME
:type server_max_window_bits: int
:param server_no_context_takeover: FIXME
:type server_no_context_takeover: bool
"""
self.client_max_window_bits = client_max_window_bits
self.client_no_context_takeover = client_no_context_takeover
self.server_max_window_bits = server_max_window_bits
self.server_no_context_takeover = server_no_context_takeover
def __json__(self):
"""
Returns a JSON serializable object representation.
:returns: JSON serializable representation.
:rtype: dict
"""
return {'extension': self.EXTENSION_NAME,
'client_max_window_bits': self.client_max_window_bits,
'client_no_context_takeover': self.client_no_context_takeover,
'server_max_window_bits': self.server_max_window_bits,
'server_no_context_takeover': self.server_no_context_takeover}
def __repr__(self):
"""
Returns Python object representation that can be eval'ed to reconstruct the object.
:returns: Python string representation.
:rtype: str
"""
return "PerMessageDeflateResponse(client_max_window_bits = %s, client_no_context_takeover = %s, server_max_window_bits = %s, server_no_context_takeover = %s)" % (self.client_max_window_bits, self.client_no_context_takeover, self.server_max_window_bits, self.server_no_context_takeover)
@public
class PerMessageDeflateResponseAccept(PerMessageCompressResponseAccept, PerMessageDeflateMixin):
"""
Set of parameters with which to accept an `permessage-deflate` response
from a server by a client.
"""
def __init__(self,
response,
no_context_takeover=None,
window_bits=None,
mem_level=None,
max_message_size=None):
"""
:param response: The response being accepted.
:type response: Instance of :class:`autobahn.compress.PerMessageDeflateResponse`.
:param no_context_takeover: Override client ("client-to-server direction") context takeover (this must be compatible with response).
:type no_context_takeover: bool
:param window_bits: Override client ("client-to-server direction") window size (this must be compatible with response).
:type window_bits: int
:param mem_level: Set client ("client-to-server direction") memory level.
:type mem_level: int
"""
if not isinstance(response, PerMessageDeflateResponse):
raise Exception("invalid type %s for response" % type(response))
self.response = response
if no_context_takeover is not None:
if type(no_context_takeover) != bool:
raise Exception("invalid type %s for no_context_takeover" % type(no_context_takeover))
if response.client_no_context_takeover and not no_context_takeover:
raise Exception("invalid value %s for no_context_takeover - server requested feature" % no_context_takeover)
self.no_context_takeover = no_context_takeover
if window_bits is not None:
if window_bits not in self.WINDOW_SIZE_PERMISSIBLE_VALUES:
raise Exception("invalid value %s for window_bits - permissible values %s" % (window_bits, self.WINDOW_SIZE_PERMISSIBLE_VALUES))
if response.client_max_window_bits != 0 and window_bits > response.client_max_window_bits:
raise Exception("invalid value %s for window_bits - server requested lower maximum value" % window_bits)
self.window_bits = window_bits
if mem_level is not None:
if mem_level not in self.MEM_LEVEL_PERMISSIBLE_VALUES:
raise Exception("invalid value %s for mem_level - permissible values %s" % (mem_level, self.MEM_LEVEL_PERMISSIBLE_VALUES))
self.mem_level = mem_level
self.max_message_size = max_message_size
def __json__(self):
"""
Returns a JSON serializable object representation.
:returns: JSON serializable representation.
:rtype: dict
"""
return {'extension': self.EXTENSION_NAME,
'response': self.response.__json__(),
'no_context_takeover': self.no_context_takeover,
'window_bits': self.window_bits,
'mem_level': self.mem_level}
def __repr__(self):
"""
Returns Python object representation that can be eval'ed to reconstruct the object.
:returns: Python string representation.
:rtype: str
"""
return "PerMessageDeflateResponseAccept(response = %s, no_context_takeover = %s, window_bits = %s, mem_level = %s)" % (self.response.__repr__(), self.no_context_takeover, self.window_bits, self.mem_level)
# noinspection PyArgumentList
class PerMessageDeflate(PerMessageCompress, PerMessageDeflateMixin):
"""
`permessage-deflate` WebSocket extension processor.
"""
DEFAULT_WINDOW_BITS = zlib.MAX_WBITS
DEFAULT_MEM_LEVEL = 8
@classmethod
def create_from_response_accept(cls, is_server, accept):
# accept: instance of PerMessageDeflateResponseAccept
pmce = cls(
is_server,
accept.response.server_no_context_takeover,
accept.no_context_takeover if accept.no_context_takeover is not None else accept.response.client_no_context_takeover,
accept.response.server_max_window_bits,
accept.window_bits if accept.window_bits is not None else accept.response.client_max_window_bits,
accept.mem_level,
accept.max_message_size,
)
return pmce
@classmethod
def create_from_offer_accept(cls, is_server, accept):
# accept: instance of PerMessageDeflateOfferAccept
pmce = cls(is_server,
accept.no_context_takeover if accept.no_context_takeover is not None else accept.offer.request_no_context_takeover,
accept.request_no_context_takeover,
accept.window_bits if accept.window_bits is not None else accept.offer.request_max_window_bits,
accept.request_max_window_bits,
accept.mem_level,
accept.max_message_size,)
return pmce
def __init__(self,
is_server,
server_no_context_takeover,
client_no_context_takeover,
server_max_window_bits,
client_max_window_bits,
mem_level,
max_message_size=None):
self._is_server = is_server
self.server_no_context_takeover = server_no_context_takeover
self.client_no_context_takeover = client_no_context_takeover
self.server_max_window_bits = server_max_window_bits if server_max_window_bits != 0 else self.DEFAULT_WINDOW_BITS
self.client_max_window_bits = client_max_window_bits if client_max_window_bits != 0 else self.DEFAULT_WINDOW_BITS
self.mem_level = mem_level if mem_level else self.DEFAULT_MEM_LEVEL
self.max_message_size = max_message_size # None means "no limit"
self._compressor = None
self._decompressor = None
def __json__(self):
return {'extension': self.EXTENSION_NAME,
'is_server': self._is_server,
'server_no_context_takeover': self.server_no_context_takeover,
'client_no_context_takeover': self.client_no_context_takeover,
'server_max_window_bits': self.server_max_window_bits,
'client_max_window_bits': self.client_max_window_bits,
'mem_level': self.mem_level}
def __repr__(self):
return "PerMessageDeflate(is_server = %s, server_no_context_takeover = %s, client_no_context_takeover = %s, server_max_window_bits = %s, client_max_window_bits = %s, mem_level = %s)" % (self._is_server, self.server_no_context_takeover, self.client_no_context_takeover, self.server_max_window_bits, self.client_max_window_bits, self.mem_level)
def start_compress_message(self):
# compressobj([level[, method[, wbits[, mem_level[, strategy]]]]])
# http://bugs.python.org/issue19278
# http://hg.python.org/cpython/rev/c54c8e71b79a
if self._is_server:
if self._compressor is None or self.server_no_context_takeover:
self._compressor = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -self.server_max_window_bits, self.mem_level)
else:
if self._compressor is None or self.client_no_context_takeover:
self._compressor = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -self.client_max_window_bits, self.mem_level)
def compress_message_data(self, data):
return self._compressor.compress(data)
def end_compress_message(self):
data = self._compressor.flush(zlib.Z_SYNC_FLUSH)
return data[:-4]
def start_decompress_message(self):
if self._is_server:
if self._decompressor is None or self.client_no_context_takeover:
self._decompressor = zlib.decompressobj(-self.client_max_window_bits)
else:
if self._decompressor is None or self.server_no_context_takeover:
self._decompressor = zlib.decompressobj(-self.server_max_window_bits)
def decompress_message_data(self, data):
if self.max_message_size is not None:
return self._decompressor.decompress(data, self.max_message_size)
return self._decompressor.decompress(data)
def end_decompress_message(self):
# Eat stripped LEN and NLEN field of a non-compressed block added
# for Z_SYNC_FLUSH.
self._decompressor.decompress(b'\x00\x00\xff\xff')

View File

@@ -0,0 +1,427 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import snappy
from autobahn.websocket.compress_base import PerMessageCompressOffer, \
PerMessageCompressOfferAccept, \
PerMessageCompressResponse, \
PerMessageCompressResponseAccept, \
PerMessageCompress
__all__ = (
'PerMessageSnappyMixin',
'PerMessageSnappyOffer',
'PerMessageSnappyOfferAccept',
'PerMessageSnappyResponse',
'PerMessageSnappyResponseAccept',
'PerMessageSnappy',
)
class PerMessageSnappyMixin(object):
"""
Mixin class for this extension.
"""
EXTENSION_NAME = "permessage-snappy"
"""
Name of this WebSocket extension.
"""
class PerMessageSnappyOffer(PerMessageCompressOffer, PerMessageSnappyMixin):
"""
Set of extension parameters for `permessage-snappy` WebSocket extension
offered by a client to a server.
"""
@classmethod
def parse(cls, params):
"""
Parses a WebSocket extension offer for `permessage-snappy` provided by a client to a server.
:param params: Output from :func:`autobahn.websocket.WebSocketProtocol._parseExtensionsHeader`.
:type params: list
:returns: A new instance of :class:`autobahn.compress.PerMessageSnappyOffer`.
:rtype: obj
"""
# extension parameter defaults
accept_no_context_takeover = False
request_no_context_takeover = False
# verify/parse client ("client-to-server direction") parameters of permessage-snappy offer
for p in params:
if len(params[p]) > 1:
raise Exception("multiple occurrence of extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME))
val = params[p][0]
if p == 'client_no_context_takeover':
# noinspection PySimplifyBooleanCheck
if val is not True:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
accept_no_context_takeover = True
elif p == 'server_no_context_takeover':
# noinspection PySimplifyBooleanCheck
if val is not True:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
request_no_context_takeover = True
else:
raise Exception("illegal extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME))
offer = cls(accept_no_context_takeover,
request_no_context_takeover)
return offer
def __init__(self,
accept_no_context_takeover=True,
request_no_context_takeover=False):
"""
:param accept_no_context_takeover: Iff true, client accepts "no context takeover" feature.
:type accept_no_context_takeover: bool
:param request_no_context_takeover: Iff true, client request "no context takeover" feature.
:type request_no_context_takeover: bool
"""
if type(accept_no_context_takeover) != bool:
raise Exception("invalid type %s for accept_no_context_takeover" % type(accept_no_context_takeover))
self.accept_no_context_takeover = accept_no_context_takeover
if type(request_no_context_takeover) != bool:
raise Exception("invalid type %s for request_no_context_takeover" % type(request_no_context_takeover))
self.request_no_context_takeover = request_no_context_takeover
def get_extension_string(self):
"""
Returns the WebSocket extension configuration string as sent to the server.
:returns: PMCE configuration string.
:rtype: str
"""
pmce_string = self.EXTENSION_NAME
if self.accept_no_context_takeover:
pmce_string += "; client_no_context_takeover"
if self.request_no_context_takeover:
pmce_string += "; server_no_context_takeover"
return pmce_string
def __json__(self):
"""
Returns a JSON serializable object representation.
:returns: JSON serializable representation.
:rtype: dict
"""
return {'extension': self.EXTENSION_NAME,
'accept_no_context_takeover': self.accept_no_context_takeover,
'request_no_context_takeover': self.request_no_context_takeover}
def __repr__(self):
"""
Returns Python object representation that can be eval'ed to reconstruct the object.
:returns: Python string representation.
:rtype: str
"""
return "PerMessageSnappyOffer(accept_no_context_takeover = %s, request_no_context_takeover = %s)" % (self.accept_no_context_takeover, self.request_no_context_takeover)
class PerMessageSnappyOfferAccept(PerMessageCompressOfferAccept, PerMessageSnappyMixin):
"""
Set of parameters with which to accept an `permessage-snappy` offer
from a client by a server.
"""
def __init__(self,
offer,
request_no_context_takeover=False,
no_context_takeover=None):
"""
:param offer: The offer being accepted.
:type offer: Instance of :class:`autobahn.compress.PerMessageSnappyOffer`.
:param request_no_context_takeover: Iff true, server request "no context takeover" feature.
:type request_no_context_takeover: bool
:param no_context_takeover: Override server ("server-to-client direction") context takeover (this must be compatible with offer).
:type no_context_takeover: bool
"""
if not isinstance(offer, PerMessageSnappyOffer):
raise Exception("invalid type %s for offer" % type(offer))
self.offer = offer
if type(request_no_context_takeover) != bool:
raise Exception("invalid type %s for request_no_context_takeover" % type(request_no_context_takeover))
if request_no_context_takeover and not offer.accept_no_context_takeover:
raise Exception("invalid value %s for request_no_context_takeover - feature unsupported by client" % request_no_context_takeover)
self.request_no_context_takeover = request_no_context_takeover
if no_context_takeover is not None:
if type(no_context_takeover) != bool:
raise Exception("invalid type %s for no_context_takeover" % type(no_context_takeover))
if offer.request_no_context_takeover and not no_context_takeover:
raise Exception("invalid value %s for no_context_takeover - client requested feature" % no_context_takeover)
self.no_context_takeover = no_context_takeover
def get_extension_string(self):
"""
Returns the WebSocket extension configuration string as sent to the server.
:returns: PMCE configuration string.
:rtype: str
"""
pmce_string = self.EXTENSION_NAME
if self.offer.request_no_context_takeover:
pmce_string += "; server_no_context_takeover"
if self.request_no_context_takeover:
pmce_string += "; client_no_context_takeover"
return pmce_string
def __json__(self):
"""
Returns a JSON serializable object representation.
:returns: JSON serializable representation.
:rtype: dict
"""
return {'extension': self.EXTENSION_NAME,
'offer': self.offer.__json__(),
'request_no_context_takeover': self.request_no_context_takeover,
'no_context_takeover': self.no_context_takeover}
def __repr__(self):
"""
Returns Python object representation that can be eval'ed to reconstruct the object.
:returns: Python string representation.
:rtype: str
"""
return "PerMessageSnappyAccept(offer = %s, request_no_context_takeover = %s, no_context_takeover = %s)" % (self.offer.__repr__(), self.request_no_context_takeover, self.no_context_takeover)
class PerMessageSnappyResponse(PerMessageCompressResponse, PerMessageSnappyMixin):
"""
Set of parameters for `permessage-snappy` responded by server.
"""
@classmethod
def parse(cls, params):
"""
Parses a WebSocket extension response for `permessage-snappy` provided by a server to a client.
:param params: Output from :func:`autobahn.websocket.WebSocketProtocol._parseExtensionsHeader`.
:type params: list
:returns: A new instance of :class:`autobahn.compress.PerMessageSnappyResponse`.
:rtype: obj
"""
client_no_context_takeover = False
server_no_context_takeover = False
for p in params:
if len(params[p]) > 1:
raise Exception("multiple occurrence of extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME))
val = params[p][0]
if p == 'client_no_context_takeover':
# noinspection PySimplifyBooleanCheck
if val is not True:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
client_no_context_takeover = True
elif p == 'server_no_context_takeover':
# noinspection PySimplifyBooleanCheck
if val is not True:
raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME))
else:
server_no_context_takeover = True
else:
raise Exception("illegal extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME))
response = cls(client_no_context_takeover,
server_no_context_takeover)
return response
def __init__(self,
client_no_context_takeover,
server_no_context_takeover):
self.client_no_context_takeover = client_no_context_takeover
self.server_no_context_takeover = server_no_context_takeover
def __json__(self):
"""
Returns a JSON serializable object representation.
:returns: JSON serializable representation.
:rtype: dict
"""
return {'extension': self.EXTENSION_NAME,
'client_no_context_takeover': self.client_no_context_takeover,
'server_no_context_takeover': self.server_no_context_takeover}
def __repr__(self):
"""
Returns Python object representation that can be eval'ed to reconstruct the object.
:returns: Python string representation.
:rtype: str
"""
return "PerMessageSnappyResponse(client_no_context_takeover = %s, server_no_context_takeover = %s)" % (self.client_no_context_takeover, self.server_no_context_takeover)
class PerMessageSnappyResponseAccept(PerMessageCompressResponseAccept, PerMessageSnappyMixin):
"""
Set of parameters with which to accept an `permessage-snappy` response
from a server by a client.
"""
def __init__(self,
response,
no_context_takeover=None):
"""
:param response: The response being accepted.
:type response: Instance of :class:`autobahn.compress.PerMessageSnappyResponse`.
:param no_context_takeover: Override client ("client-to-server direction") context takeover (this must be compatible with response).
:type no_context_takeover: bool
"""
if not isinstance(response, PerMessageSnappyResponse):
raise Exception("invalid type %s for response" % type(response))
self.response = response
if no_context_takeover is not None:
if type(no_context_takeover) != bool:
raise Exception("invalid type %s for no_context_takeover" % type(no_context_takeover))
if response.client_no_context_takeover and not no_context_takeover:
raise Exception("invalid value %s for no_context_takeover - server requested feature" % no_context_takeover)
self.no_context_takeover = no_context_takeover
def __json__(self):
"""
Returns a JSON serializable object representation.
:returns: JSON serializable representation.
:rtype: dict
"""
return {'extension': self.EXTENSION_NAME,
'response': self.response.__json__(),
'no_context_takeover': self.no_context_takeover}
def __repr__(self):
"""
Returns Python object representation that can be eval'ed to reconstruct the object.
:returns: Python string representation.
:rtype: str
"""
return "PerMessageSnappyResponseAccept(response = %s, no_context_takeover = %s)" % (self.response.__repr__(), self.no_context_takeover)
class PerMessageSnappy(PerMessageCompress, PerMessageSnappyMixin):
"""
`permessage-snappy` WebSocket extension processor.
"""
@classmethod
def create_from_response_accept(cls, is_server, accept):
pmce = cls(is_server,
accept.response.server_no_context_takeover,
accept.no_context_takeover if accept.no_context_takeover is not None else accept.response.client_no_context_takeover)
return pmce
@classmethod
def create_from_offer_accept(cls, is_server, accept):
pmce = cls(is_server,
accept.no_context_takeover if accept.no_context_takeover is not None else accept.offer.request_no_context_takeover,
accept.request_no_context_takeover)
return pmce
def __init__(self,
is_server,
server_no_context_takeover,
client_no_context_takeover):
self._is_server = is_server
self.server_no_context_takeover = server_no_context_takeover
self.client_no_context_takeover = client_no_context_takeover
self._compressor = None
self._decompressor = None
def __json__(self):
return {'extension': self.EXTENSION_NAME,
'server_no_context_takeover': self.server_no_context_takeover,
'client_no_context_takeover': self.client_no_context_takeover}
def __repr__(self):
return "PerMessageSnappy(is_server = %s, server_no_context_takeover = %s, client_no_context_takeover = %s)" % (self._is_server, self.server_no_context_takeover, self.client_no_context_takeover)
def start_compress_message(self):
if self._is_server:
if self._compressor is None or self.server_no_context_takeover:
self._compressor = snappy.StreamCompressor()
else:
if self._compressor is None or self.client_no_context_takeover:
self._compressor = snappy.StreamCompressor()
def compress_message_data(self, data):
return self._compressor.add_chunk(data)
def end_compress_message(self):
return b""
def start_decompress_message(self):
if self._is_server:
if self._decompressor is None or self.client_no_context_takeover:
self._decompressor = snappy.StreamDecompressor()
else:
if self._decompressor is None or self.server_no_context_takeover:
self._decompressor = snappy.StreamDecompressor()
def decompress_message_data(self, data):
return self._decompressor.decompress(data)
def end_decompress_message(self):
pass

View File

@@ -0,0 +1,739 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import abc
from typing import Optional, Union, Tuple, Dict
from autobahn.util import public
from autobahn.websocket.types import ConnectionRequest, ConnectionResponse, ConnectingRequest
from autobahn.wamp.types import TransportDetails
__all__ = ('IWebSocketServerChannelFactory',
'IWebSocketClientChannelFactory',
'IWebSocketChannel',
'IWebSocketChannelFrameApi',
'IWebSocketChannelStreamingApi')
class IWebSocketClientAgent(abc.ABC):
"""
Instances implementing this interface create WebSocket
connections.
"""
def open(self, transport_config, options, protocol_class=None):
"""
Open a new WebSocket connection.
:param transport_config: the endpoint to connect to. A string
containing a ws:// or wss:// URI (or a dict containing
transport configuration?)
:param options: any relevant options for this connection
attempt. Can include: headers: a dict() of headers to send,
anything currently in Factory / setProtocolOptions?
:returns: a future which fires with a new WebSocketClientProtocol instance
which has just completed the handshake, or an error.
"""
@public
class IWebSocketServerChannelFactory(abc.ABC):
"""
WebSocket server protocol factories implement this interface, and create
protocol instances which in turn implement
"""
@abc.abstractmethod
def __init__(self,
url=None,
protocols=None,
server=None,
headers=None,
externalPort=None):
"""
:param url: The WebSocket URL this factory is working for, e.g. ``ws://myhost.com/somepath``.
For non-TCP transports like pipes or Unix domain sockets, provide ``None``.
This will use an implicit URL of ``ws://localhost``.
:type url: str
:param protocols: List of subprotocols the server supports. The subprotocol used is the first from
the list of subprotocols announced by the client that is contained in this list.
:type protocols: list of str
:param server: Server as announced in HTTP response header during opening handshake.
:type server: str
:param headers: An optional mapping of additional HTTP headers to send during the WebSocket opening handshake.
:type headers: dict
:param externalPort: Optionally, the external visible port this server will be reachable under
(i.e. when running behind a L2/L3 forwarding device).
:type externalPort: int
"""
@public
@abc.abstractmethod
def setSessionParameters(self,
url=None,
protocols=None,
server=None,
headers=None,
externalPort=None):
"""
Set WebSocket session parameters.
:param url: The WebSocket URL this factory is working for, e.g. ``ws://myhost.com/somepath``.
For non-TCP transports like pipes or Unix domain sockets, provide ``None``.
This will use an implicit URL of ``ws://localhost``.
:type url: str
:param protocols: List of subprotocols the server supports. The subprotocol used is the first from the
list of subprotocols announced by the client that is contained in this list.
:type protocols: list of str
:param server: Server as announced in HTTP response header during opening handshake.
:type server: str
:param headers: An optional mapping of additional HTTP headers to send during the WebSocket opening handshake.
:type headers: dict
:param externalPort: Optionally, the external visible port this server will be reachable under
(i.e. when running behind a L2/L3 forwarding device).
:type externalPort: int
"""
@public
@abc.abstractmethod
def setProtocolOptions(self,
versions=None,
webStatus=None,
utf8validateIncoming=None,
maskServerFrames=None,
requireMaskedClientFrames=None,
applyMask=None,
maxFramePayloadSize=None,
maxMessagePayloadSize=None,
autoFragmentSize=None,
failByDrop=None,
echoCloseCodeReason=None,
openHandshakeTimeout=None,
closeHandshakeTimeout=None,
tcpNoDelay=None,
perMessageCompressionAccept=None,
autoPingInterval=None,
autoPingTimeout=None,
autoPingSize=None,
serveFlashSocketPolicy=None,
flashSocketPolicy=None,
allowedOrigins=None,
allowNullOrigin=False,
maxConnections=None,
trustXForwardedFor=0):
"""
Set WebSocket protocol options used as defaults for new protocol instances.
:param versions: The WebSocket protocol versions accepted by the server
(default: :func:`autobahn.websocket.protocol.WebSocketProtocol.SUPPORTED_PROTOCOL_VERSIONS`).
:type versions: list of ints or None
:param webStatus: Return server status/version on HTTP/GET without WebSocket upgrade header (default: `True`).
:type webStatus: bool or None
:param utf8validateIncoming: Validate incoming UTF-8 in text message payloads (default: `True`).
:type utf8validateIncoming: bool or None
:param maskServerFrames: Mask server-to-client frames (default: `False`).
:type maskServerFrames: bool or None
:param requireMaskedClientFrames: Require client-to-server frames to be masked (default: `True`).
:type requireMaskedClientFrames: bool or None
:param applyMask: Actually apply mask to payload when mask it present. Applies for outgoing and
incoming frames (default: `True`).
:type applyMask: bool or None
:param maxFramePayloadSize: Maximum frame payload size that will be accepted when receiving or
`0` for unlimited (default: `0`).
:type maxFramePayloadSize: int or None
:param maxMessagePayloadSize: Maximum message payload size (after reassembly of fragmented messages) that
will be accepted when receiving or `0` for unlimited (default: `0`).
:type maxMessagePayloadSize: int or None
:param autoFragmentSize: Automatic fragmentation of outgoing data messages (when using the message-based API)
into frames with payload length `<=` this size or `0` for no auto-fragmentation (default: `0`).
:type autoFragmentSize: int or None
:param failByDrop: Fail connections by dropping the TCP connection without performing closing
handshake (default: `True`).
:type failByDrop: bool or None
:param echoCloseCodeReason: Iff true, when receiving a close, echo back close code/reason. Otherwise reply
with `code == 1000, reason = ""` (default: `False`).
:type echoCloseCodeReason: bool or None
:param openHandshakeTimeout: Opening WebSocket handshake timeout, timeout in seconds or
`0` to deactivate (default: `0`).
:type openHandshakeTimeout: float or None
:param closeHandshakeTimeout: When we expect to receive a closing handshake reply, timeout
in seconds (default: `1`).
:type closeHandshakeTimeout: float or None
:param tcpNoDelay: TCP NODELAY ("Nagle") socket option (default: `True`).
:type tcpNoDelay: bool or None
:param perMessageCompressionAccept: Acceptor function for offers.
:type perMessageCompressionAccept: callable or None
:param autoPingInterval: Automatically send WebSocket pings every given seconds. When the peer does not respond
in `autoPingTimeout`, drop the connection. Set to `0` to disable. (default: `0`).
:type autoPingInterval: float or None
:param autoPingTimeout: Wait this many seconds for the peer to respond to automatically sent pings. If the
peer does not respond in time, drop the connection. Set to `0` to disable. (default: `0`).
:type autoPingTimeout: float or None
:param autoPingSize: Payload size for automatic pings/pongs. Must be an integer
from `[12, 125]`. (default: `12`).
:type autoPingSize: int or None
:param serveFlashSocketPolicy: Serve the Flash Socket Policy when we receive a policy file request
on this protocol. (default: `False`).
:type serveFlashSocketPolicy: bool or None
:param flashSocketPolicy: The flash socket policy to be served when we are serving
the Flash Socket Policy on this protocol
and when Flash tried to connect to the destination port. It must end with a null character.
:type flashSocketPolicy: str or None
:param allowedOrigins: A list of allowed WebSocket origins (with '*' as a wildcard character).
:type allowedOrigins: list or None
:param allowNullOrigin: if True, allow WebSocket connections whose `Origin:` is `"null"`.
:type allowNullOrigin: bool
:param maxConnections: Maximum number of concurrent connections. Set to `0` to disable (default: `0`).
:type maxConnections: int or None
:param trustXForwardedFor: Number of trusted web servers in front of this server that add their
own X-Forwarded-For header (default: `0`)
:type trustXForwardedFor: int
"""
@public
@abc.abstractmethod
def resetProtocolOptions(self):
"""
Reset all WebSocket protocol options to defaults.
"""
@public
class IWebSocketClientChannelFactory(abc.ABC):
"""
WebSocket client protocol factories implement this interface, and create
protocol instances which in turn implement
"""
@abc.abstractmethod
def __init__(self,
url=None,
origin=None,
protocols=None,
useragent=None,
headers=None,
proxy=None):
"""
Note that you MUST provide URL either here or set using
:meth:`autobahn.websocket.WebSocketClientFactory.setSessionParameters`
*before* the factory is started.
:param url: WebSocket URL this factory will connect to, e.g. ``ws://myhost.com/somepath?param1=23``.
For non-TCP transports like pipes or Unix domain sockets, provide ``None``.
This will use an implicit URL of ``ws://localhost``.
:type url: str
:param origin: The origin to be sent in WebSocket opening handshake or None (default: `None`).
:type origin: str
:param protocols: List of subprotocols the client should announce in WebSocket opening
handshake (default: `[]`).
:type protocols: list of strings
:param useragent: User agent as announced in HTTP request header or None (default: `AutobahnWebSocket/?.?.?`).
:type useragent: str
:param headers: An optional mapping of additional HTTP headers to send during the WebSocket opening handshake.
:type headers: dict
:param proxy: Explicit proxy server to use; a dict with ``host`` and ``port`` keys
:type proxy: dict or None
"""
@public
@abc.abstractmethod
def setSessionParameters(self,
url=None,
origin=None,
protocols=None,
useragent=None,
headers=None,
proxy=None):
"""
Set WebSocket session parameters.
:param url: WebSocket URL this factory will connect to, e.g. `ws://myhost.com/somepath?param1=23`.
For non-TCP transports like pipes or Unix domain sockets, provide `None`.
This will use an implicit URL of `ws://localhost`.
:type url: str
:param origin: The origin to be sent in opening handshake.
:type origin: str
:param protocols: List of WebSocket subprotocols the client should announce in opening handshake.
:type protocols: list of strings
:param useragent: User agent as announced in HTTP request header during opening handshake.
:type useragent: str
:param headers: An optional mapping of additional HTTP headers to send during the WebSocket opening handshake.
:type headers: dict
:param proxy: (Optional) a dict with ``host`` and ``port`` keys specifying a proxy to use
:type proxy: dict or None
"""
@public
@abc.abstractmethod
def setProtocolOptions(self,
version=None,
utf8validateIncoming=None,
acceptMaskedServerFrames=None,
maskClientFrames=None,
applyMask=None,
maxFramePayloadSize=None,
maxMessagePayloadSize=None,
autoFragmentSize=None,
failByDrop=None,
echoCloseCodeReason=None,
serverConnectionDropTimeout=None,
openHandshakeTimeout=None,
closeHandshakeTimeout=None,
tcpNoDelay=None,
perMessageCompressionOffers=None,
perMessageCompressionAccept=None,
autoPingInterval=None,
autoPingTimeout=None,
autoPingSize=None):
"""
Set WebSocket protocol options used as defaults for _new_ protocol instances.
:param version: The WebSocket protocol spec (draft) version to be used
(default: :func:`autobahn.websocket.protocol.WebSocketProtocol.SUPPORTED_PROTOCOL_VERSIONS`).
:type version: int
:param utf8validateIncoming: Validate incoming UTF-8 in text message payloads (default: `True`).
:type utf8validateIncoming: bool
:param acceptMaskedServerFrames: Accept masked server-to-client frames (default: `False`).
:type acceptMaskedServerFrames: bool
:param maskClientFrames: Mask client-to-server frames (default: `True`).
:type maskClientFrames: bool
:param applyMask: Actually apply mask to payload when mask it present. Applies for outgoing and
incoming frames (default: `True`).
:type applyMask: bool
:param maxFramePayloadSize: Maximum frame payload size that will be accepted when receiving or
`0` for unlimited (default: `0`).
:type maxFramePayloadSize: int
:param maxMessagePayloadSize: Maximum message payload size (after reassembly of fragmented messages) that
will be accepted when receiving or `0` for unlimited (default: `0`).
:type maxMessagePayloadSize: int
:param autoFragmentSize: Automatic fragmentation of outgoing data messages (when using the message-based API)
into frames with payload length `<=` this size or `0` for no auto-fragmentation (default: `0`).
:type autoFragmentSize: int
:param failByDrop: Fail connections by dropping the TCP connection without performing
closing handshake (default: `True`).
:type failByDrop: bool
:param echoCloseCodeReason: Iff true, when receiving a close, echo back close code/reason. Otherwise reply
with `code == 1000, reason = ""` (default: `False`).
:type echoCloseCodeReason: bool
:param serverConnectionDropTimeout: When the client expects the server to drop the TCP, timeout in
seconds (default: `1`).
:type serverConnectionDropTimeout: float
:param openHandshakeTimeout: Opening WebSocket handshake timeout, timeout in seconds or `0` to
deactivate (default: `0`).
:type openHandshakeTimeout: float
:param closeHandshakeTimeout: When we expect to receive a closing handshake reply, timeout
in seconds (default: `1`).
:type closeHandshakeTimeout: float
:param tcpNoDelay: TCP NODELAY ("Nagle"): bool socket option (default: `True`).
:type tcpNoDelay: bool
:param perMessageCompressionOffers: A list of offers to provide to the server for the permessage-compress
WebSocket extension. Must be a list of instances of subclass of PerMessageCompressOffer.
:type perMessageCompressionOffers: list of instance of subclass of PerMessageCompressOffer
:param perMessageCompressionAccept: Acceptor function for responses.
:type perMessageCompressionAccept: callable
:param autoPingInterval: Automatically send WebSocket pings every given seconds. When the peer does not respond
in `autoPingTimeout`, drop the connection. Set to `0` to disable. (default: `0`).
:type autoPingInterval: float or None
:param autoPingTimeout: Wait this many seconds for the peer to respond to automatically sent pings. If the
peer does not respond in time, drop the connection. Set to `0` to disable. (default: `0`).
:type autoPingTimeout: float or None
:param autoPingSize: Payload size for automatic pings/pongs. Must be an integer
from `[12, 125]`. (default: `12`).
:type autoPingSize: int
"""
@public
@abc.abstractmethod
def resetProtocolOptions(self):
"""
Reset all WebSocket protocol options to defaults.
"""
@public
class IWebSocketChannel(abc.ABC):
"""
A WebSocket channel is a bidirectional, full-duplex, ordered, reliable message channel
over a WebSocket connection as specified in RFC6455.
This interface defines a message-based API to WebSocket plus auxiliary hooks
and methods.
When a WebSocket connection is established, the following callbacks are fired:
* :meth:`IWebSocketChannel.onConnecting`: Transport bytestream open
* :meth:`IWebSocketChannel.onConnect`: WebSocket handshake
* :meth:`IWebSocketChannel.onOpen`: WebSocket connection open
Once a WebSocket connection is open, messages can be sent and received using:
* :meth:`IWebSocketChannel.sendMessage`
* :meth:`IWebSocketChannel.onMessage`
The WebSocket connection can be closed and closing observed using:
* :meth:`IWebSocketChannel.sendClose`
* :meth:`IWebSocketChannel.onClose`
Finally, WebSocket ping/pong messages
* :meth:`IWebSocketChannel.sendPing`
* :meth:`IWebSocketChannel.onPing`
* :meth:`IWebSocketChannel.sendPong`
* :meth:`IWebSocketChannel.onPong`
which are used for e.g. for heart-beating.
"""
@public
@abc.abstractmethod
def onConnecting(self, transport_details: TransportDetails) -> Optional[ConnectingRequest]:
"""
This method is called when the WebSocket peer is connected at the byte stream level (e.g. TCP,
TLS or Serial), but before the WebSocket opening handshake (e.g. at the HTTP request level).
:param transport_details: information about the transport.
:returns: A :class:`autobahn.websocket.types.ConnectingRequest`
instance is returned to indicate which options should be
used for this connection. If you wish to use the default
behavior, ``None`` may be returned (this is the default).
"""
@public
@abc.abstractmethod
def onConnect(self, request_or_response: Union[ConnectionRequest, ConnectionResponse]) -> \
Union[Optional[str], Tuple[Optional[str], Dict[str, str]]]:
"""
Callback fired during WebSocket opening handshake when a client connects to a server with
request with a :class:`ConnectionRequest` from the client or when a server connection was established
by a client with a :class:`ConnectionResponse` response from the server.
*This method may run asynchronously.*
:param request_or_response: Connection request (for servers) or response (for clients).
:type request_or_response: Instance of :class:`autobahn.websocket.types.ConnectionRequest`
or :class:`autobahn.websocket.types.ConnectionResponse`.
:returns: When this callback is fired on a WebSocket **server**, you may return one of:
``None``: the connection is accepted with no specific WebSocket subprotocol,
``str``: the connection is accepted with the returned name as the WebSocket subprotocol, or
``(str, dict)``: a pair of subprotocol accepted and HTTP headers to send to the client.
When this callback is fired on a WebSocket **client**, this method must return ``None``.
To deny a connection (client and server), raise an Exception.
You can also return a Deferred/Future that resolves/rejects to the above.
"""
@public
@abc.abstractmethod
def onOpen(self):
"""
Callback fired when the initial WebSocket opening handshake was completed.
You now can send and receive WebSocket messages.
*This method may run asynchronously.*
"""
@public
@abc.abstractmethod
def sendMessage(self, payload: bytes, isBinary: bool):
"""
Send a WebSocket message over the connection to the peer.
:param payload: The WebSocket message to be sent.
:param isBinary: Flag indicating whether payload is binary or UTF-8 encoded text.
"""
@public
@abc.abstractmethod
def onMessage(self, payload: bytes, isBinary: bool):
"""
Callback fired when a complete WebSocket message was received.
*This method may run asynchronously.*
:param payload: The WebSocket message received.
:param isBinary: Flag indicating whether payload is binary or UTF-8 encoded text.
"""
@public
@abc.abstractmethod
def sendClose(self, code: Optional[int] = None, reason: Optional[str] = None):
"""
Starts a WebSocket closing handshake tearing down the WebSocket connection.
:param code: An optional close status code (``1000`` for normal close or ``3000-4999`` for
application specific close).
:param reason: An optional close reason (a string that when present, a status
code MUST also be present).
"""
@public
@abc.abstractmethod
def onClose(self, wasClean: bool, code: int, reason: str):
"""
Callback fired when the WebSocket connection has been closed (WebSocket closing
handshake has been finished or the connection was closed uncleanly).
:param wasClean: ``True`` iff the WebSocket connection was closed cleanly.
:param code: Close status code as sent by the WebSocket peer.
:param reason: Close reason as sent by the WebSocket peer.
"""
@abc.abstractmethod
def sendPing(self, payload: Optional[bytes] = None):
"""
Send a WebSocket ping to the peer.
A peer is expected to pong back the payload a soon as "practical". When more than
one ping is outstanding at a peer, the peer may elect to respond only to the last ping.
:param payload: An (optional) arbitrary payload of length **less than 126** octets.
"""
@abc.abstractmethod
def onPing(self, payload: bytes):
"""
Callback fired when a WebSocket ping was received. A default implementation responds
by sending a WebSocket pong.
:param payload: Payload of ping (when there was any). Can be arbitrary, up to `125` octets.
"""
@abc.abstractmethod
def sendPong(self, payload: Optional[bytes] = None):
"""
Send a WebSocket pong to the peer.
A WebSocket pong may be sent unsolicited. This serves as a unidirectional heartbeat.
A response to an unsolicited pong is "not expected".
:param payload: An (optional) arbitrary payload of length < 126 octets.
"""
@abc.abstractmethod
def onPong(self, payload: bytes):
"""
Callback fired when a WebSocket pong was received. A default implementation does nothing.
:param payload: Payload of pong (when there was any). Can be arbitrary, up to 125 octets.
"""
class IWebSocketChannelFrameApi(IWebSocketChannel):
"""
Frame-based API to a WebSocket channel.
"""
@abc.abstractmethod
def onMessageBegin(self, isBinary: bool):
"""
Callback fired when receiving of a new WebSocket message has begun.
:param isBinary: ``True`` if payload is binary, else the payload is UTF-8 encoded text.
:type isBinary: bool
"""
@abc.abstractmethod
def onMessageFrame(self, payload: bytes):
"""
Callback fired when a complete WebSocket message frame for a previously begun
WebSocket message has been received.
:param payload: Message frame payload (a list of chunks received).
:type payload: list of bytes
"""
@abc.abstractmethod
def onMessageEnd(self):
"""
Callback fired when a WebSocket message has been completely received (the last
WebSocket frame for that message has been received).
"""
@abc.abstractmethod
def beginMessage(self, isBinary: bool = False, doNotCompress: bool = False):
"""
Begin sending a new WebSocket message.
:param isBinary: ``True`` if payload is binary, else the payload must be UTF-8 encoded text.
:type isBinary: bool
:param doNotCompress: If ``True``, never compress this message. This only applies to
Hybi-Mode and only when WebSocket compression has been negotiated on the WebSocket
connection. Use when you know the payload incompressible (e.g. encrypted or
already compressed).
:type doNotCompress: bool
"""
@abc.abstractmethod
def sendMessageFrame(self, payload: bytes, sync: bool = False):
"""
When a message has been previously begun, send a complete message frame in one go.
:param payload: The message frame payload. When sending a text message, the payload must
be UTF-8 encoded already.
:type payload: bytes
:param sync: If ``True``, try to force data onto the wire immediately.S
.. warning:: Do NOT use this feature for normal applications. Performance likely will suffer significantly. This feature is mainly here for use by Autobahn|Testsuite.
:type sync: bool
"""
@abc.abstractmethod
def endMessage(self):
"""
End a message previously begun message. No more frames may be sent (for that message).
You have to begin a new message before sending again.
"""
class IWebSocketChannelStreamingApi(IWebSocketChannelFrameApi):
"""
Streaming API to a WebSocket channel.
"""
@abc.abstractmethod
def onMessageFrameBegin(self, length: int):
"""
Callback fired when receiving a new message frame has begun.
A default implementation will prepare to buffer message frame data.
:param length: Payload length of message frame which is subsequently received.
:type length: int
"""
@abc.abstractmethod
def onMessageFrameData(self, payload: bytes):
"""
Callback fired when receiving data within a previously begun message frame.
A default implementation will buffer data for frame.
:param payload: Partial payload for message frame.
:type payload: bytes
"""
@abc.abstractmethod
def onMessageFrameEnd(self):
"""
Callback fired when a previously begun message frame has been completely received.
A default implementation will flatten the buffered frame data and
fire `onMessageFrame`.
"""
@abc.abstractmethod
def beginMessageFrame(self, length: int):
"""
Begin sending a new message frame.
:param length: Length of the frame which is to be started. Must be less or equal **2^63**.
:type length: int
"""
@abc.abstractmethod
def sendMessageFrameData(self, payload: bytes, sync: bool = False):
"""
Send out data when within a message frame (message was begun, frame was begun).
Note that the frame is automatically ended when enough data has been sent.
In other words, there is no ``endMessageFrame``, since you have begun the frame
specifying the frame length, which implicitly defined the frame end. This is different
from messages, which you begin *and* end explicitly , since a message can contain
an unlimited number of frames.
:param payload: Frame payload to send.
:type payload: bytes
:param sync: If ``True``, try to force data onto the wire immediately.
.. warning:: Do NOT use this feature for normal applications. Performance likely will suffer significantly. This feature is mainly here for use by Autobahn|Testsuite.
:type sync: bool
:returns: When the currently sent message frame is still incomplete, returns octets
remaining to be sent. When the frame is complete, returns **0**. Otherwise the amount
of unconsumed data in payload argument is returned.
:rtype: int
"""

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@@ -0,0 +1,319 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
import struct
if os.environ.get('USE_TWISTED', False):
from twisted.trial import unittest
from twisted.internet.address import IPv4Address
from twisted.internet.task import Clock
from autobahn.twisted.websocket import WebSocketServerProtocol
from autobahn.twisted.websocket import WebSocketServerFactory
from autobahn.twisted.websocket import WebSocketClientProtocol
from autobahn.twisted.websocket import WebSocketClientFactory
from autobahn.websocket.compress_deflate import PerMessageDeflate
from autobahn.testutil import FakeTransport
from unittest.mock import MagicMock, patch
from txaio.testutil import replace_loop
from base64 import b64decode
@patch('base64.b64encode')
def create_client_frame(b64patch, **kwargs):
"""
Kind-of hack-y; maybe better to re-factor the Protocol to have a
frame-encoder method-call? Anyway, makes a throwaway protocol
encode a frame for us, collects the .sendData call and returns
the data that would have gone out. Accepts all the kwargs that
WebSocketClientProtocol.sendFrame() accepts.
"""
# only real way to inject a "known" secret-key for the headers
# to line up... :/
b64patch.return_value = b'QIatSt9QkZPyS4QQfdufO8TgkL0='
factory = WebSocketClientFactory(protocols=['wamp.2.json'])
factory.protocol = WebSocketClientProtocol
factory.doStart()
proto = factory.buildProtocol(IPv4Address('TCP', '127.0.0.9', 65534))
proto.transport = FakeTransport()
proto.connectionMade()
proto.data = mock_handshake_server
proto.processHandshake()
data = []
def collect(d, *args):
data.append(d)
proto.sendData = collect
proto.sendFrame(**kwargs)
return b''.join(data)
# beware the evils of line-endings...
mock_handshake_client = b'GET / HTTP/1.1\r\nUser-Agent: AutobahnPython/0.10.2\r\nHost: localhost:80\r\nUpgrade: WebSocket\r\nConnection: Upgrade\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nSec-WebSocket-Key: 6Jid6RgXpH0RVegaNSs/4g==\r\nSec-WebSocket-Protocol: wamp.2.json\r\nSec-WebSocket-Version: 13\r\n\r\n'
mock_handshake_server = b'HTTP/1.1 101 Switching Protocols\r\nServer: AutobahnPython/0.10.2\r\nX-Powered-By: AutobahnPython/0.10.2\r\nUpgrade: WebSocket\r\nConnection: Upgrade\r\nSec-WebSocket-Protocol: wamp.2.json\r\nSec-WebSocket-Accept: QIatSt9QkZPyS4QQfdufO8TgkL0=\r\n\r\n\x81~\x02\x19[1,"crossbar",{"roles":{"subscriber":{"features":{"publisher_identification":true,"pattern_based_subscription":true,"subscription_revocation":true}},"publisher":{"features":{"publisher_identification":true,"publisher_exclusion":true,"subscriber_blackwhite_listing":true}},"caller":{"features":{"caller_identification":true,"progressive_call_results":true}},"callee":{"features":{"progressive_call_results":true,"pattern_based_registration":true,"registration_revocation":true,"shared_registration":true,"caller_identification":true}}}}]\x18'
class TestDeflate(unittest.TestCase):
def test_max_size(self):
decoder = PerMessageDeflate(
is_server=False,
server_no_context_takeover=False,
client_no_context_takeover=False,
server_max_window_bits=15,
client_max_window_bits=15,
mem_level=8,
max_message_size=10,
)
# 2000 'x' characters compressed
compressed_data = b'\xab\xa8\x18\x05\xa3`\x14\x8c\x82Q0\nF\xc1P\x07\x00\xcf@\xa9\xae'
decoder.start_decompress_message()
data = decoder.decompress_message_data(compressed_data)
# since we set max_message_size, we should only get that
# many bytes back.
self.assertEqual(data, b"x" * 10)
def test_no_max_size(self):
decoder = PerMessageDeflate(
is_server=False,
server_no_context_takeover=False,
client_no_context_takeover=False,
server_max_window_bits=15,
client_max_window_bits=15,
mem_level=None,
)
# 2000 'x' characters compressed
compressed_data = b'\xab\xa8\x18\x05\xa3`\x14\x8c\x82Q0\nF\xc1P\x07\x00\xcf@\xa9\xae'
decoder.start_decompress_message()
data = decoder.decompress_message_data(compressed_data)
self.assertEqual(data, b"x" * 2000)
class TestClient(unittest.TestCase):
def setUp(self):
self.factory = WebSocketClientFactory(protocols=['wamp.2.json'])
self.factory.protocol = WebSocketClientProtocol
self.factory.doStart()
self.proto = self.factory.buildProtocol(IPv4Address('TCP', '127.0.0.1', 65534))
self.transport = FakeTransport()
self.proto.transport = self.transport
self.proto.connectionMade()
def tearDown(self):
if self.proto.openHandshakeTimeoutCall:
self.proto.openHandshakeTimeoutCall.cancel()
self.factory.doStop()
# not really necessary, but ...
del self.factory
del self.proto
def test_missing_reason_raw(self):
# we want to hit the "STATE_OPEN" case, so pretend we're there
self.proto.echoCloseCodeReason = True
self.proto.state = self.proto.STATE_OPEN
self.proto.websocket_version = 1
self.proto.sendCloseFrame = MagicMock()
self.proto.onCloseFrame(1000, None)
def test_unclean_timeout_client(self):
"""
make a delayed call to drop the connection (client-side)
"""
if False:
self.proto.factory._log = print
# get to STATE_OPEN
self.proto.websocket_key = b64decode('6Jid6RgXpH0RVegaNSs/4g==')
self.proto.data = mock_handshake_server
self.proto.processHandshake()
self.assertEqual(self.proto.state, WebSocketServerProtocol.STATE_OPEN)
self.assertTrue(self.proto.serverConnectionDropTimeout > 0)
with replace_loop(Clock()) as reactor:
# now 'do the test' and transition to CLOSING
self.proto.sendCloseFrame()
self.proto.onCloseFrame(1000, b"raw reason")
# check we scheduled a call
self.assertEqual(len(reactor.calls), 1)
self.assertEqual(reactor.calls[0].func, self.proto.onServerConnectionDropTimeout)
self.assertEqual(reactor.calls[0].getTime(), self.proto.serverConnectionDropTimeout)
# now, advance the clock past the call (and thereby
# execute it)
reactor.advance(self.proto.closeHandshakeTimeout + 1)
# we should have called abortConnection
self.assertTrue(self.proto.transport.abort_called())
# ...too "internal" for an assert?
self.assertEqual(self.proto.state, WebSocketServerProtocol.STATE_CLOSED)
class TestPing(unittest.TestCase):
def setUp(self):
self.factory = WebSocketServerFactory(protocols=['wamp.2.json'])
self.factory.protocol = WebSocketServerProtocol
self.factory.doStart()
self.proto = self.factory.buildProtocol(IPv4Address('TCP', '127.0.0.1', 65534))
self.transport = MagicMock()
self.proto.transport = self.transport
self.proto.connectionMade()
def tearDown(self):
if self.proto.openHandshakeTimeoutCall:
self.proto.openHandshakeTimeoutCall.cancel()
self.factory.doStop()
# not really necessary, but ...
del self.factory
del self.proto
def test_unclean_timeout(self):
"""
make a delayed call to drop the connection
"""
# first we have to drive the protocol to STATE_CLOSING
# ... which we achieve by sendCloseFrame after we're in
# STATE_OPEN
# XXX double-check this is the correct code-path to get here
# "normally"?
# get to STATE_OPEN
self.proto.data = mock_handshake_client
self.proto.processHandshake()
self.assertTrue(self.proto.state == WebSocketServerProtocol.STATE_OPEN)
with replace_loop(Clock()) as reactor:
# now 'do the test' and transition to CLOSING
self.proto.sendCloseFrame()
# check we scheduled a call
self.assertEqual(len(reactor.calls), 1)
# now, advance the clock past the call (and thereby
# execute it)
reactor.advance(self.proto.closeHandshakeTimeout + 1)
# we should have called abortConnection
self.assertEqual("call.abortConnection()", str(self.proto.transport.method_calls[-1]))
self.assertTrue(self.proto.transport.abortConnection.called)
# ...too "internal" for an assert?
self.assertEqual(self.proto.state, WebSocketServerProtocol.STATE_CLOSED)
def test_auto_pingpong_timeout(self):
"""
autoping and autoping-timeout timing
"""
# options are evaluated in succeedHandshake, called below
self.proto.autoPingInterval = 5
self.proto.autoPingTimeout = 2
with replace_loop(Clock()) as reactor:
# get to STATE_OPEN
self.proto.data = mock_handshake_client
self.proto.processHandshake()
self.assertTrue(self.proto.state == WebSocketServerProtocol.STATE_OPEN)
# we should have scheduled an autoPing
self.assertEqual(1, len(reactor.calls))
# advance past first auto-ping timeout
reactor.advance(5)
# first element from args tuple from transport.write()
# call is our data
self.assertTrue(self.transport.write.called)
data = self.transport.write.call_args[0][0]
_data = bytes([data[0]])
# the opcode is the lower 7 bits of the first byte.
(opcode,) = struct.unpack("B", _data)
opcode = opcode & (~0x80)
# ... and should be "9" for ping
self.assertEqual(9, opcode)
# Because we have autoPingTimeout there should be
# another delayed-called created now
self.assertEqual(1, len(reactor.calls))
self.assertNotEqual(self.proto.state, self.proto.STATE_CLOSED)
# ...which we'll now cause to trigger, aborting the connection
reactor.advance(3)
self.assertEqual(self.proto.state, self.proto.STATE_CLOSED)
def test_auto_ping_got_pong(self):
"""
auto-ping with correct reply cancels timeout
"""
# options are evaluated in succeedHandshake, called below
self.proto.autoPingInterval = 5
self.proto.autoPingTimeout = 2
with replace_loop(Clock()) as reactor:
# get to STATE_OPEN
self.proto.data = mock_handshake_client
self.proto.processHandshake()
self.assertTrue(self.proto.state == WebSocketServerProtocol.STATE_OPEN)
# we should have scheduled an autoPing
self.assertEqual(1, len(reactor.calls))
# advance past first auto-ping timeout
reactor.advance(5)
# should have an auto-ping timeout scheduled, and we
# save it for later (to check it got cancelled)
self.assertEqual(1, len(reactor.calls))
timeout_call = reactor.calls[0]
# elsewhere we check that we actually send an opcode-9
# message; now we just blindly inject our own reply
# with a PONG frame
frame = create_client_frame(opcode=10, payload=self.proto.autoPingPending)
self.proto.data = frame
# really needed twice; does header first, then rest
self.proto.processData()
self.proto.processData()
# which should have cancelled the call
self.assertTrue(timeout_call.cancelled)

View File

@@ -0,0 +1,281 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
import unittest
from hashlib import sha1
from base64 import b64encode
from unittest.mock import Mock
import txaio
from autobahn.websocket.protocol import WebSocketServerProtocol
from autobahn.websocket.protocol import WebSocketServerFactory
from autobahn.websocket.protocol import WebSocketClientProtocol
from autobahn.websocket.protocol import WebSocketClientFactory
from autobahn.websocket.protocol import WebSocketProtocol
from autobahn.websocket.types import ConnectingRequest
from autobahn.testutil import FakeTransport
from autobahn.wamp.types import TransportDetails
class WebSocketClientProtocolTests(unittest.TestCase):
def setUp(self):
t = FakeTransport()
f = WebSocketClientFactory()
f.log = txaio.make_logger()
p = WebSocketClientProtocol()
p.log = txaio.make_logger()
p.factory = f
p.transport = t
p._transport_details = TransportDetails()
p._connectionMade()
p.state = p.STATE_OPEN
p.websocket_version = 18
self.protocol = p
self.transport = t
def tearDown(self):
for call in [
self.protocol.autoPingPendingCall,
self.protocol.autoPingTimeoutCall,
self.protocol.openHandshakeTimeoutCall,
self.protocol.closeHandshakeTimeoutCall,
]:
if call is not None:
call.cancel()
def test_auto_ping(self):
self.protocol.autoPingInterval = 1
self.protocol.websocket_protocols = [Mock()]
self.protocol.websocket_extensions = []
self.protocol._onOpen = lambda: None
self.protocol._wskey = '0' * 24
self.protocol.peer = Mock()
# usually provided by the Twisted or asyncio specific
# subclass, but we're testing the parent here...
self.protocol._onConnect = Mock()
self.protocol._closeConnection = Mock()
# set up a connection
self.protocol._actuallyStartHandshake(
ConnectingRequest(
host="example.com",
port=80,
resource="/ws",
)
)
key = self.protocol.websocket_key + WebSocketProtocol._WS_MAGIC
self.protocol.data = (
b"HTTP/1.1 101 Switching Protocols\x0d\x0a"
b"Upgrade: websocket\x0d\x0a"
b"Connection: upgrade\x0d\x0a"
b"Sec-Websocket-Accept: " + b64encode(sha1(key).digest()) + b"\x0d\x0a\x0d\x0a"
)
self.protocol.processHandshake()
self.assertTrue(self.protocol.autoPingPendingCall is not None)
class WebSocketServerProtocolTests(unittest.TestCase):
"""
Tests for autobahn.websocket.protocol.WebSocketProtocol.
"""
def setUp(self):
t = FakeTransport()
f = WebSocketServerFactory()
f.log = txaio.make_logger()
p = WebSocketServerProtocol()
p.log = txaio.make_logger()
p.factory = f
p.transport = t
p._connectionMade()
p.state = p.STATE_OPEN
p.websocket_version = 18
self.protocol = p
self.transport = t
def tearDown(self):
for call in [
self.protocol.autoPingPendingCall,
self.protocol.autoPingTimeoutCall,
self.protocol.openHandshakeTimeoutCall,
self.protocol.closeHandshakeTimeoutCall,
]:
if call is not None:
call.cancel()
def test_auto_ping(self):
proto = Mock()
proto._get_seconds = Mock(return_value=1)
self.protocol.autoPingInterval = 1
self.protocol.websocket_protocols = [proto]
self.protocol.websocket_extensions = []
self.protocol._onOpen = lambda: None
self.protocol._wskey = '0' * 24
self.protocol.succeedHandshake(proto)
self.assertTrue(self.protocol.autoPingPendingCall is not None)
def test_sendClose_none(self):
"""
sendClose with no code or reason works.
"""
self.protocol.sendClose()
# We closed properly
self.assertEqual(self.transport._written, b"\x88\x00")
self.assertEqual(self.protocol.state, self.protocol.STATE_CLOSING)
def test_sendClose_str_reason(self):
"""
sendClose with a str reason works.
"""
self.protocol.sendClose(code=1000, reason="oh no")
# We closed properly
self.assertEqual(self.transport._written[2:], b"\x03\xe8oh no")
self.assertEqual(self.protocol.state, self.protocol.STATE_CLOSING)
def test_sendClose_unicode_reason(self):
"""
sendClose with a unicode reason works.
"""
self.protocol.sendClose(code=1000, reason="oh no")
# We closed properly
self.assertEqual(self.transport._written[2:], b"\x03\xe8oh no")
self.assertEqual(self.protocol.state, self.protocol.STATE_CLOSING)
def test_sendClose_toolong(self):
"""
sendClose with a too-long reason will truncate it.
"""
self.protocol.sendClose(code=1000, reason="abc" * 1000)
# We closed properly
self.assertEqual(self.transport._written[2:],
b"\x03\xe8" + (b"abc" * 41))
self.assertEqual(self.protocol.state, self.protocol.STATE_CLOSING)
def test_sendClose_reason_with_no_code(self):
"""
Trying to sendClose with a reason but no code will raise an Exception.
"""
with self.assertRaises(Exception) as e:
self.protocol.sendClose(reason="abc")
self.assertIn("close reason without close code", str(e.exception))
# We shouldn't have closed
self.assertEqual(self.transport._written, b"")
self.assertEqual(self.protocol.state, self.protocol.STATE_OPEN)
def test_sendClose_invalid_code_type(self):
"""
Trying to sendClose with a non-int code will raise an Exception.
"""
with self.assertRaises(Exception) as e:
self.protocol.sendClose(code="134")
self.assertIn("invalid type", str(e.exception))
# We shouldn't have closed
self.assertEqual(self.transport._written, b"")
self.assertEqual(self.protocol.state, self.protocol.STATE_OPEN)
def test_sendClose_invalid_code_value(self):
"""
Trying to sendClose with a non-valid int code will raise an Exception.
"""
with self.assertRaises(Exception) as e:
self.protocol.sendClose(code=10)
self.assertIn("invalid close code 10", str(e.exception))
# We shouldn't have closed
self.assertEqual(self.transport._written, b"")
self.assertEqual(self.protocol.state, self.protocol.STATE_OPEN)
def test_interpolate_server_status_template(self):
from autobahn.websocket.protocol import _SERVER_STATUS_TEMPLATE
s = _SERVER_STATUS_TEMPLATE.format(None, '0.0.0')
self.assertEqual(type(s), str)
self.assertTrue(len(s) > 0)
if os.environ.get('USE_TWISTED', False):
class TwistedProtocolTests(unittest.TestCase):
"""
Tests which require a specific framework's protocol class to work
(in this case, using Twisted)
"""
def setUp(self):
from autobahn.twisted.websocket import WebSocketServerProtocol
from autobahn.twisted.websocket import WebSocketServerFactory
t = FakeTransport()
f = WebSocketServerFactory()
p = WebSocketServerProtocol()
p.factory = f
p.transport = t
p._connectionMade()
p.state = p.STATE_OPEN
p.websocket_version = 18
self.protocol = p
self.transport = t
def tearDown(self):
for call in [
self.protocol.autoPingPendingCall,
self.protocol.autoPingTimeoutCall,
self.protocol.openHandshakeTimeoutCall,
self.protocol.closeHandshakeTimeoutCall,
]:
if call is not None:
call.cancel()
def test_loseConnection(self):
"""
If we lose our connection before openHandshakeTimeout fires, it is
cleaned up
"""
# so, I guess a little cheezy, but we depend on the asyncio or
# twisted class to call _connectionLost at some point; faking
# that here
self.protocol._connectionLost(txaio.create_failure(RuntimeError("testing")))
self.assertTrue(self.protocol.openHandshakeTimeoutCall is None)
def test_send_server_status(self):
self.protocol.sendServerStatus()

View File

@@ -0,0 +1,125 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import unittest
from autobahn.websocket.util import create_url, parse_url
class TestCreateWsUrl(unittest.TestCase):
def test_create_url01(self):
self.assertEqual(create_url("localhost"), "ws://localhost:80/")
def test_create_url02(self):
self.assertEqual(create_url("localhost", port=8090), "ws://localhost:8090/")
def test_create_url03(self):
self.assertEqual(create_url("localhost", path="ws"), "ws://localhost:80/ws")
def test_create_url04(self):
self.assertEqual(create_url("localhost", path="/ws"), "ws://localhost:80/ws")
def test_create_url05(self):
self.assertEqual(create_url("localhost", path="/ws/foobar"), "ws://localhost:80/ws/foobar")
def test_create_url06(self):
self.assertEqual(create_url("localhost", isSecure=True), "wss://localhost:443/")
def test_create_url07(self):
self.assertEqual(create_url("localhost", isSecure=True, port=443), "wss://localhost:443/")
def test_create_url08(self):
self.assertEqual(create_url("localhost", isSecure=True, port=80), "wss://localhost:80/")
def test_create_url09(self):
self.assertEqual(create_url("localhost", isSecure=True, port=9090, path="ws", params={'foo': 'bar'}), "wss://localhost:9090/ws?foo=bar")
def test_create_url10(self):
wsurl = create_url("localhost", isSecure=True, port=9090, path="ws", params={'foo': 'bar', 'moo': 23})
self.assertTrue(wsurl == "wss://localhost:9090/ws?foo=bar&moo=23" or wsurl == "wss://localhost:9090/ws?moo=23&foo=bar")
def test_create_url11(self):
self.assertEqual(create_url("127.0.0.1", path="ws"), "ws://127.0.0.1:80/ws")
def test_create_url12(self):
self.assertEqual(create_url("62.146.25.34", path="ws"), "ws://62.146.25.34:80/ws")
def test_create_url13(self):
self.assertEqual(create_url("subsub1.sub1.something.com", path="ws"), "ws://subsub1.sub1.something.com:80/ws")
def test_create_url14(self):
self.assertEqual(create_url("::1", path="ws"), "ws://::1:80/ws")
def test_create_url15(self):
self.assertEqual(create_url("0:0:0:0:0:0:0:1", path="ws"), "ws://0:0:0:0:0:0:0:1:80/ws")
class TestParseWsUrl(unittest.TestCase):
# parse_url -> (isSecure, host, port, resource, path, params)
def test_parse_url01(self):
self.assertEqual(parse_url("ws://localhost"), (False, 'localhost', 80, '/', '/', {}))
def test_parse_url02(self):
self.assertEqual(parse_url("ws://localhost:80"), (False, 'localhost', 80, '/', '/', {}))
def test_parse_url03(self):
self.assertEqual(parse_url("wss://localhost"), (True, 'localhost', 443, '/', '/', {}))
def test_parse_url04(self):
self.assertEqual(parse_url("wss://localhost:443"), (True, 'localhost', 443, '/', '/', {}))
def test_parse_url05(self):
self.assertEqual(parse_url("wss://localhost/ws"), (True, 'localhost', 443, '/ws', '/ws', {}))
def test_parse_url06(self):
self.assertEqual(parse_url("wss://localhost/ws?foo=bar"), (True, 'localhost', 443, '/ws?foo=bar', '/ws', {'foo': ['bar']}))
def test_parse_url07(self):
self.assertEqual(parse_url("wss://localhost/ws?foo=bar&moo=23"), (True, 'localhost', 443, '/ws?foo=bar&moo=23', '/ws', {'moo': ['23'], 'foo': ['bar']}))
def test_parse_url08(self):
self.assertEqual(parse_url("wss://localhost/ws?foo=bar&moo=23&moo=44"), (True, 'localhost', 443, '/ws?foo=bar&moo=23&moo=44', '/ws', {'moo': ['23', '44'], 'foo': ['bar']}))
def test_parse_url09(self):
self.assertRaises(Exception, parse_url, "http://localhost")
def test_parse_url10(self):
self.assertRaises(Exception, parse_url, "https://localhost")
def test_parse_url11(self):
self.assertRaises(Exception, parse_url, "http://localhost:80")
def test_parse_url12(self):
self.assertRaises(Exception, parse_url, "http://localhost#frag1")
def test_parse_url13(self):
self.assertRaises(Exception, parse_url, "wss://")
def test_parse_url14(self):
self.assertRaises(Exception, parse_url, "ws://")

View File

@@ -0,0 +1,433 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from pprint import pformat
from autobahn.util import public
__all__ = (
'ConnectionRequest',
'ConnectingRequest',
'ConnectionResponse',
'ConnectionAccept',
'ConnectionDeny',
'Message',
'IncomingMessage',
'OutgoingMessage',
'Ping',
)
@public
class ConnectionRequest(object):
"""
Thin-wrapper for WebSocket connection request information provided in
:meth:`autobahn.websocket.protocol.WebSocketServerProtocol.onConnect` when
a WebSocket client want to establish a connection to a WebSocket server.
"""
__slots__ = (
'peer',
'headers',
'host',
'path',
'params',
'version',
'origin',
'protocols',
'extensions'
)
def __init__(self, peer, headers, host, path, params, version, origin, protocols, extensions):
"""
:param peer: Descriptor of the connecting client (e.g. IP address/port
in case of TCP transports).
:type peer: str
:param headers: HTTP headers from opening handshake request.
:type headers: dict
:param host: Host from opening handshake HTTP header.
:type host: str
:param path: Path from requested HTTP resource URI. For example, a resource URI of
``/myservice?foo=23&foo=66&bar=2`` will be parsed to ``/myservice``.
:type path: str
:param params: Query parameters (if any) from requested HTTP resource URI.
For example, a resource URI of ``/myservice?foo=23&foo=66&bar=2`` will be
parsed to ``{'foo': ['23', '66'], 'bar': ['2']}``.
:type params: dict
:param version: The WebSocket protocol version the client announced (and will be
spoken, when connection is accepted).
:type version: int
:param origin: The WebSocket origin header or None. Note that this only
a reliable source of information for browser clients!
:type origin: str
:param protocols: The WebSocket (sub)protocols the client announced. You must
select and return one of those (or ``None``) in
:meth:`autobahn.websocket.WebSocketServerProtocol.onConnect`.
:type protocols: list
:param extensions: The WebSocket extensions the client requested and the
server accepted, and thus will be spoken, once the WebSocket connection
has been fully established.
:type extensions: list
"""
self.peer = peer
self.headers = headers
self.host = host
self.path = path
self.params = params
self.version = version
self.origin = origin
self.protocols = protocols
self.extensions = extensions
def __json__(self):
return {'peer': self.peer,
'headers': self.headers,
'host': self.host,
'path': self.path,
'params': self.params,
'version': self.version,
'origin': self.origin,
'protocols': self.protocols,
'extensions': self.extensions}
def __str__(self):
return pformat(self.__json__())
@public
class ConnectingRequest(object):
"""
Thin-wrapper for WebSocket connection request information provided in
:meth:`autobahn.websocket.protocol.WebSocketClientProtocol.onConnecting`
after a client has connected, but before the handshake has
proceeded.
`host`, `port`, and `resource` are all required, everything else
is optional. Note that these are values that will be seen by the
client and should represent the public-facing host, port and
resource to which the client is connecting (not necessarily the
action host/port being used).
"""
__slots__ = (
'host',
'port',
'resource',
'headers',
'useragent',
'origin',
'protocols',
)
def __init__(self, host=None, port=None, resource=None, headers=None, useragent=None, origin=None, protocols=None):
"""
Any of the arguments can be `None`, which will provide a useful
default.
:param str host: the host to present to the server
:param int port: the port to present to the server
:param str resouce:
:param headers: extra HTTP headers to send in the opening handshake
:type headers: dict
"""
# required
self.host = host if host is not None else "localhost"
self.port = port if port is not None else 80
self.resource = resource if resource is not None else "/"
# optional
self.headers = headers if headers is not None else dict()
self.useragent = useragent
self.origin = origin
self.protocols = protocols if protocols is not None else []
def __json__(self):
return {
'host': self.host,
'port': self.port,
'resource': self.resource,
'headers': self.headers,
'useragent': self.useragent,
'origin': self.origin,
'protocols': self.protocols,
}
def __str__(self):
return pformat(self.__json__())
@public
class ConnectionResponse(object):
"""
Thin-wrapper for WebSocket connection response information provided in
:meth:`autobahn.websocket.protocol.WebSocketClientProtocol.onConnect` when
a WebSocket server has accepted a connection request by a client.
"""
__slots__ = (
'peer',
'headers',
'version',
'protocol',
'extensions'
)
def __init__(self, peer, headers, version, protocol, extensions):
"""
Constructor.
:param peer: Descriptor of the connected server (e.g. IP address/port in case of TCP transport).
:type peer: str
:param headers: HTTP headers from opening handshake response.
:type headers: dict
:param version: The WebSocket protocol version that is spoken.
:type version: int
:param protocol: The WebSocket (sub)protocol in use.
:type protocol: str
:param extensions: The WebSocket extensions in use.
:type extensions: list of str
"""
self.peer = peer
self.headers = headers
self.version = version
self.protocol = protocol
self.extensions = extensions
def __json__(self):
return {'peer': self.peer,
'headers': self.headers,
'version': self.version,
'protocol': self.protocol,
'extensions': self.extensions}
def __str__(self):
return pformat(self.__json__())
@public
class ConnectionAccept(object):
"""
Used by WebSocket servers to accept an incoming WebSocket connection.
If the client announced one or multiple subprotocols, the server MUST
select one of the subprotocols announced by the client.
"""
__slots__ = ('subprotocol', 'headers')
def __init__(self, subprotocol=None, headers=None):
"""
:param subprotocol: The WebSocket connection is accepted with the
this WebSocket subprotocol chosen. The value must be a token
as defined by RFC 2616.
:type subprotocol: unicode or None
:param headers: Additional HTTP headers to send on the WebSocket
opening handshake reply, e.g. cookies. The keys must be unicode,
and the values either unicode or tuple/list. In the latter case
a separate HTTP header line will be sent for each item in
tuple/list.
:type headers: dict or None
"""
assert(subprotocol is None or type(subprotocol) == str)
assert(headers is None or type(headers) == dict)
if headers is not None:
for k, v in headers.items():
assert(type(k) == str)
assert(type(v) == str or type(v) == list or type(v) == tuple)
if type(v) == list or type(v) == tuple:
for vv in v:
assert(type(vv) == str)
self.subprotocol = subprotocol
self.headers = headers
@public
class ConnectionDeny(Exception):
"""
Throw an instance of this class to deny a WebSocket connection
during handshake in :meth:`autobahn.websocket.protocol.WebSocketServerProtocol.onConnect`.
"""
__slots__ = ('code', 'reason')
BAD_REQUEST = 400
"""
Bad Request. The request cannot be fulfilled due to bad syntax.
"""
FORBIDDEN = 403
"""
Forbidden. The request was a legal request, but the server is refusing to respond to it.[2] Unlike a 401 Unauthorized response, authenticating will make no difference.
"""
NOT_FOUND = 404
"""
Not Found. The requested resource could not be found but may be available again in the future.[2] Subsequent requests by the client are permissible.
"""
NOT_ACCEPTABLE = 406
"""
Not Acceptable. The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request.
"""
REQUEST_TIMEOUT = 408
"""
Request Timeout. The server timed out waiting for the request. According to W3 HTTP specifications: 'The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time.
"""
INTERNAL_SERVER_ERROR = 500
"""
Internal Server Error. A generic error message, given when no more specific message is suitable.
"""
NOT_IMPLEMENTED = 501
"""
Not Implemented. The server either does not recognize the request method, or it lacks the ability to fulfill the request.
"""
SERVICE_UNAVAILABLE = 503
"""
Service Unavailable. The server is currently unavailable (because it is overloaded or down for maintenance). Generally, this is a temporary state.
"""
def __init__(self, code, reason=None):
"""
:param code: HTTP error code.
:type code: int
:param reason: HTTP error reason.
:type reason: unicode
"""
assert(type(code) == int)
assert(reason is None or type(reason) == str)
self.code = code
self.reason = reason
class Message(object):
"""
Abstract base class for WebSocket messages.
"""
__slots__ = ()
class IncomingMessage(Message):
"""
An incoming WebSocket message.
"""
__slots__ = ('payload', 'is_binary')
def __init__(self, payload, is_binary=False):
"""
:param payload: The WebSocket message payload, which can be UTF-8
encoded text or a binary string.
:type payload: bytes
:param is_binary: ``True`` for binary payload, else the payload
contains UTF-8 encoded text.
:type is_binary: bool
"""
assert(type(payload) == bytes)
assert(type(is_binary) == bool)
self.payload = payload
self.is_binary = is_binary
class OutgoingMessage(Message):
"""
An outgoing WebSocket message.
"""
__slots__ = ('payload', 'is_binary', 'skip_compress')
def __init__(self, payload, is_binary=False, skip_compress=False):
"""
:param payload: The WebSocket message payload, which can be UTF-8
encoded text or a binary string.
:type payload: bytes
:param is_binary: ``True`` iff payload is binary, else the payload
contains UTF-8 encoded text.
:type is_binary: bool
:param skip_compress: If ``True``, never compress this message.
This only has an effect when WebSocket compression has been negotiated
on the WebSocket connection. Use when you know the payload is
incompressible (e.g. encrypted or already compressed).
:type skip_compress: bool
"""
assert(type(payload) == bytes)
assert(type(is_binary) == bool)
assert(type(skip_compress) == bool)
self.payload = payload
self.is_binary = is_binary
self.skip_compress = skip_compress
class Ping(object):
"""
A WebSocket ping message.
"""
__slots__ = ('payload')
def __init__(self, payload=None):
"""
:param payload: The WebSocket ping message payload.
:type payload: bytes or None
"""
assert(payload is None or type(payload) == bytes), \
("invalid type {} for WebSocket ping payload - must be None or bytes".format(type(payload)))
if payload is not None:
assert(len(payload) < 126), \
("WebSocket ping payload too long ({} bytes) - must be <= 125 bytes".format(len(payload)))
self.payload = payload

View File

@@ -0,0 +1,149 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
# Note: This code is a Python implementation of the algorithm
# "Flexible and Economical UTF-8 Decoder" by Bjoern Hoehrmann
# bjoern@hoehrmann.de, http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
__all__ = ("Utf8Validator",)
# DFA transitions
UTF8VALIDATOR_DFA = (
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 00..1f
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 20..3f
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 40..5f
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 60..7f
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, # 80..9f
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, # a0..bf
8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, # c0..df
0xa, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, # e0..ef
0xb, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, # f0..ff
0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, # s0..s0
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, # s1..s2
1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, # s3..s4
1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, # s5..s6
1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, # s7..s8
)
UTF8_ACCEPT = 0
UTF8_REJECT = 1
# use Cython implementation of UTF8 validator if available
try:
from wsaccel.utf8validator import Utf8Validator
except ImportError:
# Fallback to pure Python implementation - also for PyPy.
#
# Do NOT touch this code unless you know what you are doing!
# https://github.com/oberstet/scratchbox/tree/master/python/utf8
# Python 3 and above
# convert DFA table to bytes (performance)
UTF8VALIDATOR_DFA_S = bytes(UTF8VALIDATOR_DFA)
class Utf8Validator(object):
"""
Incremental UTF-8 validator with constant memory consumption (minimal state).
Implements the algorithm "Flexible and Economical UTF-8 Decoder" by
Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
"""
__slots__ = (
'_codepoint',
'_state',
'_index',
)
def __init__(self):
self._codepoint = None
self._state = None
self._index = None
self.reset()
def decode(self, b):
"""
Eat one UTF-8 octet, and validate on the fly.
Returns ``UTF8_ACCEPT`` when enough octets have been consumed, in which case
``self.codepoint`` contains the decoded Unicode code point.
Returns ``UTF8_REJECT`` when invalid UTF-8 was encountered.
Returns some other positive integer when more octets need to be eaten.
"""
tt = UTF8VALIDATOR_DFA_S[b]
if self._state != UTF8_ACCEPT:
self._codepoint = (b & 0x3f) | (self._codepoint << 6)
else:
self._codepoint = (0xff >> tt) & b
self._state = UTF8VALIDATOR_DFA_S[256 + self._state * 16 + tt]
return self._state
def reset(self):
"""
Reset validator to start new incremental UTF-8 decode/validation.
"""
self._state = UTF8_ACCEPT # the empty string is valid UTF8
self._codepoint = 0
self._index = 0
def validate(self, ba):
"""
Incrementally validate a chunk of bytes provided as string.
Will return a quad ``(valid?, endsOnCodePoint?, currentIndex, totalIndex)``.
As soon as an octet is encountered which renders the octet sequence
invalid, a quad with ``valid? == False`` is returned. ``currentIndex`` returns
the index within the currently consumed chunk, and ``totalIndex`` the
index within the total consumed sequence that was the point of bail out.
When ``valid? == True``, currentIndex will be ``len(ba)`` and ``totalIndex`` the
total amount of consumed bytes.
"""
#
# The code here is written for optimal JITting in PyPy, not for best
# readability by your grandma or particular elegance. Do NOT touch!
#
l = len(ba)
i = 0
state = self._state
while i < l:
# optimized version of decode(), since we are not interested in actual code points
state = UTF8VALIDATOR_DFA_S[256 + (state << 4) + UTF8VALIDATOR_DFA_S[ba[i]]]
if state == UTF8_REJECT:
self._state = state
self._index += i
return False, False, i, self._index
i += 1
self._state = state
self._index += l
return True, state == UTF8_ACCEPT, l, self._index

View File

@@ -0,0 +1,187 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from autobahn.util import public
from urllib import parse as urlparse
# The Python urlparse module currently does not contain the ws/wss
# schemes, so we add those dynamically (which is a hack of course).
#
# Important: if you change this stuff (you shouldn't), make sure
# _all_ our unit tests for WS URLs succeed
#
wsschemes = ["ws", "wss"]
urlparse.uses_relative.extend(wsschemes)
urlparse.uses_netloc.extend(wsschemes)
urlparse.uses_params.extend(wsschemes)
urlparse.uses_query.extend(wsschemes)
urlparse.uses_fragment.extend(wsschemes)
__all__ = (
"create_url",
"parse_url",
)
@public
def create_url(hostname, port=None, isSecure=False, path=None, params=None):
"""
Create a WebSocket URL from components.
:param hostname: WebSocket server hostname (for TCP/IP sockets) or
filesystem path (for Unix domain sockets).
:type hostname: str
:param port: For TCP/IP sockets, WebSocket service port or ``None`` (to select default
ports ``80`` or ``443`` depending on ``isSecure``. When ``hostname=="unix"``,
this defines the path to the Unix domain socket instead of a TCP/IP network socket.
:type port: int or str
:param isSecure: Set ``True`` for secure WebSocket (``wss`` scheme).
:type isSecure: bool
:param path: WebSocket URL path of addressed resource (will be
properly URL escaped). Ignored for RawSocket.
:type path: str
:param params: A dictionary of key-values to construct the query
component of the addressed WebSocket resource (will be properly URL
escaped). Ignored for RawSocket.
:type params: dict
:returns: Constructed WebSocket URL.
:rtype: str
"""
# assert type(hostname) == str
assert type(isSecure) == bool
if hostname == 'unix':
netloc = "unix:%s" % port
else:
assert port is None or (type(port) == int and port in range(0, 65535))
if port is not None:
netloc = "%s:%d" % (hostname, port)
else:
if isSecure:
netloc = "%s:443" % hostname
else:
netloc = "%s:80" % hostname
if isSecure:
scheme = "wss"
else:
scheme = "ws"
if path is not None:
ppath = urlparse.quote(path)
else:
ppath = "/"
if params is not None:
query = urlparse.urlencode(params)
else:
query = None
return urlparse.urlunparse((scheme, netloc, ppath, None, query, None))
@public
def parse_url(url):
"""
Parses as WebSocket URL into it's components and returns a tuple:
- ``isSecure`` is a flag which is ``True`` for ``wss`` URLs.
- ``host`` is the hostname or IP from the URL.
and for TCP/IP sockets:
- ``tcp_port`` is the port from the URL or standard port derived from
scheme (``rs`` => ``80``, ``rss`` => ``443``).
or for Unix domain sockets:
- ``uds_path`` is the path on the local host filesystem.
:param url: A valid WebSocket URL, i.e. ``ws://localhost:9000`` for TCP/IP sockets or
``ws://unix:/tmp/file.sock`` for Unix domain sockets (UDS).
:type url: str
:returns: A 6-tuple ``(isSecure, host, tcp_port, resource, path, params)`` (TCP/IP) or
``(isSecure, host, uds_path, resource, path, params)`` (UDS).
:rtype: tuple
"""
parsed = urlparse.urlparse(url)
if parsed.scheme not in ["ws", "wss"]:
raise ValueError("invalid WebSocket URL: protocol scheme '{}' is not for WebSocket".format(parsed.scheme))
if not parsed.hostname or parsed.hostname == "":
raise ValueError("invalid WebSocket URL: missing hostname")
if parsed.fragment is not None and parsed.fragment != "":
raise ValueError("invalid WebSocket URL: non-empty fragment '%s" % parsed.fragment)
if parsed.path is not None and parsed.path != "":
ppath = parsed.path
path = urlparse.unquote(ppath)
else:
ppath = "/"
path = ppath
if parsed.query is not None and parsed.query != "":
resource = ppath + "?" + parsed.query
params = urlparse.parse_qs(parsed.query)
else:
resource = ppath
params = {}
if parsed.hostname == "unix":
# Unix domain sockets sockets
# ws://unix:/tmp/file.sock => unix:/tmp/file.sock => /tmp/file.sock
fp = parsed.netloc + parsed.path
uds_path = fp.split(':')[1]
# note: we don't interpret "path" in any further way: it needs to be
# a path on the local host with a listening Unix domain sockets at the other end ..
return parsed.scheme == "wss", parsed.hostname, uds_path, resource, path, params
else:
# TCP/IP sockets
if parsed.port is None or parsed.port == "":
if parsed.scheme == "ws":
tcp_port = 80
else:
tcp_port = 443
else:
tcp_port = int(parsed.port)
if tcp_port < 1 or tcp_port > 65535:
raise ValueError("invalid port {}".format(tcp_port))
return parsed.scheme == "wss", parsed.hostname, tcp_port, resource, path, params

View File

@@ -0,0 +1,125 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
try:
# use Cython implementation of XorMasker validator if available
from wsaccel.xormask import XorMaskerNull
# noinspection PyUnresolvedReferences
from wsaccel.xormask import createXorMasker
create_xor_masker = createXorMasker
except ImportError:
# fallback to pure Python implementation (this is faster on PyPy than above!)
# http://stackoverflow.com/questions/15014310/python3-xrange-lack-hurts
try:
# noinspection PyUnresolvedReferences
xrange
except NameError:
# Python 3
# noinspection PyShadowingBuiltins
xrange = range
from array import array
class XorMaskerNull(object):
__slots__ = ('_ptr',)
# noinspection PyUnusedLocal
def __init__(self, mask=None):
self._ptr = 0
def pointer(self):
return self._ptr
def reset(self):
self._ptr = 0
def process(self, data):
self._ptr += len(data)
return data
class XorMaskerSimple(object):
__slots__ = ('_ptr', '_msk')
def __init__(self, mask):
assert len(mask) == 4
self._ptr = 0
self._msk = array('B', mask)
def pointer(self):
return self._ptr
def reset(self):
self._ptr = 0
def process(self, data):
dlen = len(data)
payload = array('B', data)
for k in xrange(dlen):
payload[k] ^= self._msk[self._ptr & 3]
self._ptr += 1
return payload.tobytes()
class XorMaskerShifted1(object):
__slots__ = ('_ptr', '_mskarray')
def __init__(self, mask):
assert len(mask) == 4
self._ptr = 0
self._mskarray = [array('B'), array('B'), array('B'), array('B')]
for j in xrange(4):
self._mskarray[0].append(mask[j & 3])
self._mskarray[1].append(mask[(j + 1) & 3])
self._mskarray[2].append(mask[(j + 2) & 3])
self._mskarray[3].append(mask[(j + 3) & 3])
def pointer(self):
return self._ptr
def reset(self):
self._ptr = 0
def process(self, data):
dlen = len(data)
payload = array('B', data)
msk = self._mskarray[self._ptr & 3]
for k in xrange(dlen):
payload[k] ^= msk[k & 3]
self._ptr += dlen
return payload.tobytes()
def create_xor_masker(mask, length=None):
if length is None or length < 128:
return XorMaskerSimple(mask)
else:
return XorMaskerShifted1(mask)