mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 06:51:09 -05:00
okay fine
This commit is contained in:
@@ -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',
|
||||
)
|
||||
@@ -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'])
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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
|
||||
@@ -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
|
||||
"""
|
||||
4149
.venv/lib/python3.12/site-packages/autobahn/websocket/protocol.py
Normal file
4149
.venv/lib/python3.12/site-packages/autobahn/websocket/protocol.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
#
|
||||
###############################################################################
|
||||
@@ -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***REMOVED***iron.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)
|
||||
@@ -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***REMOVED***iron.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()
|
||||
@@ -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://")
|
||||
433
.venv/lib/python3.12/site-packages/autobahn/websocket/types.py
Normal file
433
.venv/lib/python3.12/site-packages/autobahn/websocket/types.py
Normal 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
|
||||
@@ -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.[AWS-SECRET-REMOVED]on/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
|
||||
187
.venv/lib/python3.12/site-packages/autobahn/websocket/util.py
Normal file
187
.venv/lib/python3.12/site-packages/autobahn/websocket/util.py
Normal 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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user