RAHHH
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',
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
backend/lib/python3.12/site-packages/autobahn/websocket/protocol.py
Normal file
4149
backend/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.
|
||||
#
|
||||
###############################################################################
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,319 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import os
|
||||
import struct
|
||||
|
||||
|
||||
if os.environ.get('USE_TWISTED', False):
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet.address import IPv4Address
|
||||
from twisted.internet.task import Clock
|
||||
|
||||
from autobahn.twisted.websocket import WebSocketServerProtocol
|
||||
from autobahn.twisted.websocket import WebSocketServerFactory
|
||||
from autobahn.twisted.websocket import WebSocketClientProtocol
|
||||
from autobahn.twisted.websocket import WebSocketClientFactory
|
||||
from autobahn.websocket.compress_deflate import PerMessageDeflate
|
||||
from autobahn.testutil import FakeTransport
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from txaio.testutil import replace_loop
|
||||
|
||||
from base64 import b64decode
|
||||
|
||||
@patch('base64.b64encode')
|
||||
def create_client_frame(b64patch, **kwargs):
|
||||
"""
|
||||
Kind-of hack-y; maybe better to re-factor the Protocol to have a
|
||||
frame-encoder method-call? Anyway, makes a throwaway protocol
|
||||
encode a frame for us, collects the .sendData call and returns
|
||||
the data that would have gone out. Accepts all the kwargs that
|
||||
WebSocketClientProtocol.sendFrame() accepts.
|
||||
"""
|
||||
|
||||
# only real way to inject a "known" secret-key for the headers
|
||||
# to line up... :/
|
||||
b64patch.return_value = b'QIatSt9QkZPyS4QQfdufO8TgkL0='
|
||||
|
||||
factory = WebSocketClientFactory(protocols=['wamp.2.json'])
|
||||
factory.protocol = WebSocketClientProtocol
|
||||
factory.doStart()
|
||||
proto = factory.buildProtocol(IPv4Address('TCP', '127.0.0.9', 65534))
|
||||
proto.transport = FakeTransport()
|
||||
proto.connectionMade()
|
||||
proto.data = mock_handshake_server
|
||||
proto.processHandshake()
|
||||
|
||||
data = []
|
||||
|
||||
def collect(d, *args):
|
||||
data.append(d)
|
||||
proto.sendData = collect
|
||||
|
||||
proto.sendFrame(**kwargs)
|
||||
return b''.join(data)
|
||||
|
||||
# beware the evils of line-endings...
|
||||
mock_handshake_client = b'GET / HTTP/1.1\r\nUser-Agent: AutobahnPython/0.10.2\r\nHost: localhost:80\r\nUpgrade: WebSocket\r\nConnection: Upgrade\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nSec-WebSocket-Key: 6Jid6RgXpH0RVegaNSs/4g==\r\nSec-WebSocket-Protocol: wamp.2.json\r\nSec-WebSocket-Version: 13\r\n\r\n'
|
||||
|
||||
mock_handshake_server = b'HTTP/1.1 101 Switching Protocols\r\nServer: AutobahnPython/0.10.2\r\nX-Powered-By: AutobahnPython/0.10.2\r\nUpgrade: WebSocket\r\nConnection: Upgrade\r\nSec-WebSocket-Protocol: wamp.2.json\r\nSec-WebSocket-Accept: QIatSt9QkZPyS4QQfdufO8TgkL0=\r\n\r\n\x81~\x02\x19[1,"crossbar",{"roles":{"subscriber":{"features":{"publisher_identification":true,"pattern_based_subscription":true,"subscription_revocation":true}},"publisher":{"features":{"publisher_identification":true,"publisher_exclusion":true,"subscriber_blackwhite_listing":true}},"caller":{"features":{"caller_identification":true,"progressive_call_results":true}},"callee":{"features":{"progressive_call_results":true,"pattern_based_registration":true,"registration_revocation":true,"shared_registration":true,"caller_identification":true}}}}]\x18'
|
||||
|
||||
class TestDeflate(unittest.TestCase):
|
||||
|
||||
def test_max_size(self):
|
||||
decoder = PerMessageDeflate(
|
||||
is_server=False,
|
||||
server_no_context_takeover=False,
|
||||
client_no_context_takeover=False,
|
||||
server_max_window_bits=15,
|
||||
client_max_window_bits=15,
|
||||
mem_level=8,
|
||||
max_message_size=10,
|
||||
)
|
||||
|
||||
# 2000 'x' characters compressed
|
||||
compressed_data = b'\xab\xa8\x18\x05\xa3`\x14\x8c\x82Q0\nF\xc1P\x07\x00\xcf@\xa9\xae'
|
||||
|
||||
decoder.start_decompress_message()
|
||||
data = decoder.decompress_message_data(compressed_data)
|
||||
|
||||
# since we set max_message_size, we should only get that
|
||||
# many bytes back.
|
||||
self.assertEqual(data, b"x" * 10)
|
||||
|
||||
def test_no_max_size(self):
|
||||
decoder = PerMessageDeflate(
|
||||
is_server=False,
|
||||
server_no_context_takeover=False,
|
||||
client_no_context_takeover=False,
|
||||
server_max_window_bits=15,
|
||||
client_max_window_bits=15,
|
||||
mem_level=None,
|
||||
)
|
||||
|
||||
# 2000 'x' characters compressed
|
||||
compressed_data = b'\xab\xa8\x18\x05\xa3`\x14\x8c\x82Q0\nF\xc1P\x07\x00\xcf@\xa9\xae'
|
||||
|
||||
decoder.start_decompress_message()
|
||||
data = decoder.decompress_message_data(compressed_data)
|
||||
|
||||
self.assertEqual(data, b"x" * 2000)
|
||||
|
||||
class TestClient(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.factory = WebSocketClientFactory(protocols=['wamp.2.json'])
|
||||
self.factory.protocol = WebSocketClientProtocol
|
||||
self.factory.doStart()
|
||||
|
||||
self.proto = self.factory.buildProtocol(IPv4Address('TCP', '127.0.0.1', 65534))
|
||||
self.transport = FakeTransport()
|
||||
self.proto.transport = self.transport
|
||||
self.proto.connectionMade()
|
||||
|
||||
def tearDown(self):
|
||||
if self.proto.openHandshakeTimeoutCall:
|
||||
self.proto.openHandshakeTimeoutCall.cancel()
|
||||
self.factory.doStop()
|
||||
# not really necessary, but ...
|
||||
del self.factory
|
||||
del self.proto
|
||||
|
||||
def test_missing_reason_raw(self):
|
||||
# we want to hit the "STATE_OPEN" case, so pretend we're there
|
||||
self.proto.echoCloseCodeReason = True
|
||||
self.proto.state = self.proto.STATE_OPEN
|
||||
self.proto.websocket_version = 1
|
||||
|
||||
self.proto.sendCloseFrame = MagicMock()
|
||||
|
||||
self.proto.onCloseFrame(1000, None)
|
||||
|
||||
def test_unclean_timeout_client(self):
|
||||
"""
|
||||
make a delayed call to drop the connection (client-side)
|
||||
"""
|
||||
|
||||
if False:
|
||||
self.proto.factory._log = print
|
||||
|
||||
# get to STATE_OPEN
|
||||
self.proto.websocket_key = b64decode('6Jid6RgXpH0RVegaNSs/4g==')
|
||||
self.proto.data = mock_handshake_server
|
||||
self.proto.processHandshake()
|
||||
self.assertEqual(self.proto.state, WebSocketServerProtocol.STATE_OPEN)
|
||||
self.assertTrue(self.proto.serverConnectionDropTimeout > 0)
|
||||
|
||||
with replace_loop(Clock()) as reactor:
|
||||
# now 'do the test' and transition to CLOSING
|
||||
self.proto.sendCloseFrame()
|
||||
self.proto.onCloseFrame(1000, b"raw reason")
|
||||
|
||||
# check we scheduled a call
|
||||
self.assertEqual(len(reactor.calls), 1)
|
||||
self.assertEqual(reactor.calls[0].func, self.proto.onServerConnectionDropTimeout)
|
||||
self.assertEqual(reactor.calls[0].getTime(), self.proto.serverConnectionDropTimeout)
|
||||
|
||||
# now, advance the clock past the call (and thereby
|
||||
# execute it)
|
||||
reactor.advance(self.proto.closeHandshakeTimeout + 1)
|
||||
|
||||
# we should have called abortConnection
|
||||
self.assertTrue(self.proto.transport.abort_called())
|
||||
# ...too "internal" for an assert?
|
||||
self.assertEqual(self.proto.state, WebSocketServerProtocol.STATE_CLOSED)
|
||||
|
||||
class TestPing(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.factory = WebSocketServerFactory(protocols=['wamp.2.json'])
|
||||
self.factory.protocol = WebSocketServerProtocol
|
||||
self.factory.doStart()
|
||||
|
||||
self.proto = self.factory.buildProtocol(IPv4Address('TCP', '127.0.0.1', 65534))
|
||||
self.transport = MagicMock()
|
||||
self.proto.transport = self.transport
|
||||
self.proto.connectionMade()
|
||||
|
||||
def tearDown(self):
|
||||
if self.proto.openHandshakeTimeoutCall:
|
||||
self.proto.openHandshakeTimeoutCall.cancel()
|
||||
self.factory.doStop()
|
||||
# not really necessary, but ...
|
||||
del self.factory
|
||||
del self.proto
|
||||
|
||||
def test_unclean_timeout(self):
|
||||
"""
|
||||
make a delayed call to drop the connection
|
||||
"""
|
||||
# first we have to drive the protocol to STATE_CLOSING
|
||||
# ... which we achieve by sendCloseFrame after we're in
|
||||
# STATE_OPEN
|
||||
# XXX double-check this is the correct code-path to get here
|
||||
# "normally"?
|
||||
|
||||
# get to STATE_OPEN
|
||||
self.proto.data = mock_handshake_client
|
||||
self.proto.processHandshake()
|
||||
self.assertTrue(self.proto.state == WebSocketServerProtocol.STATE_OPEN)
|
||||
|
||||
with replace_loop(Clock()) as reactor:
|
||||
# now 'do the test' and transition to CLOSING
|
||||
self.proto.sendCloseFrame()
|
||||
|
||||
# check we scheduled a call
|
||||
self.assertEqual(len(reactor.calls), 1)
|
||||
|
||||
# now, advance the clock past the call (and thereby
|
||||
# execute it)
|
||||
reactor.advance(self.proto.closeHandshakeTimeout + 1)
|
||||
|
||||
# we should have called abortConnection
|
||||
self.assertEqual("call.abortConnection()", str(self.proto.transport.method_calls[-1]))
|
||||
self.assertTrue(self.proto.transport.abortConnection.called)
|
||||
# ...too "internal" for an assert?
|
||||
self.assertEqual(self.proto.state, WebSocketServerProtocol.STATE_CLOSED)
|
||||
|
||||
def test_auto_pingpong_timeout(self):
|
||||
"""
|
||||
autoping and autoping-timeout timing
|
||||
"""
|
||||
# options are evaluated in succeedHandshake, called below
|
||||
self.proto.autoPingInterval = 5
|
||||
self.proto.autoPingTimeout = 2
|
||||
|
||||
with replace_loop(Clock()) as reactor:
|
||||
# get to STATE_OPEN
|
||||
self.proto.data = mock_handshake_client
|
||||
self.proto.processHandshake()
|
||||
self.assertTrue(self.proto.state == WebSocketServerProtocol.STATE_OPEN)
|
||||
|
||||
# we should have scheduled an autoPing
|
||||
self.assertEqual(1, len(reactor.calls))
|
||||
|
||||
# advance past first auto-ping timeout
|
||||
reactor.advance(5)
|
||||
|
||||
# first element from args tuple from transport.write()
|
||||
# call is our data
|
||||
self.assertTrue(self.transport.write.called)
|
||||
data = self.transport.write.call_args[0][0]
|
||||
|
||||
_data = bytes([data[0]])
|
||||
|
||||
# the opcode is the lower 7 bits of the first byte.
|
||||
(opcode,) = struct.unpack("B", _data)
|
||||
opcode = opcode & (~0x80)
|
||||
|
||||
# ... and should be "9" for ping
|
||||
self.assertEqual(9, opcode)
|
||||
|
||||
# Because we have autoPingTimeout there should be
|
||||
# another delayed-called created now
|
||||
self.assertEqual(1, len(reactor.calls))
|
||||
self.assertNotEqual(self.proto.state, self.proto.STATE_CLOSED)
|
||||
|
||||
# ...which we'll now cause to trigger, aborting the connection
|
||||
reactor.advance(3)
|
||||
self.assertEqual(self.proto.state, self.proto.STATE_CLOSED)
|
||||
|
||||
def test_auto_ping_got_pong(self):
|
||||
"""
|
||||
auto-ping with correct reply cancels timeout
|
||||
"""
|
||||
# options are evaluated in succeedHandshake, called below
|
||||
self.proto.autoPingInterval = 5
|
||||
self.proto.autoPingTimeout = 2
|
||||
|
||||
with replace_loop(Clock()) as reactor:
|
||||
# get to STATE_OPEN
|
||||
self.proto.data = mock_handshake_client
|
||||
self.proto.processHandshake()
|
||||
self.assertTrue(self.proto.state == WebSocketServerProtocol.STATE_OPEN)
|
||||
|
||||
# we should have scheduled an autoPing
|
||||
self.assertEqual(1, len(reactor.calls))
|
||||
|
||||
# advance past first auto-ping timeout
|
||||
reactor.advance(5)
|
||||
|
||||
# should have an auto-ping timeout scheduled, and we
|
||||
# save it for later (to check it got cancelled)
|
||||
self.assertEqual(1, len(reactor.calls))
|
||||
timeout_call = reactor.calls[0]
|
||||
|
||||
# elsewhere we check that we actually send an opcode-9
|
||||
# message; now we just blindly inject our own reply
|
||||
# with a PONG frame
|
||||
|
||||
frame = create_client_frame(opcode=10, payload=self.proto.autoPingPending)
|
||||
self.proto.data = frame
|
||||
# really needed twice; does header first, then rest
|
||||
self.proto.processData()
|
||||
self.proto.processData()
|
||||
|
||||
# which should have cancelled the call
|
||||
self.assertTrue(timeout_call.cancelled)
|
||||
@@ -0,0 +1,281 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from hashlib import sha1
|
||||
from base64 import b64encode
|
||||
from unittest.mock import Mock
|
||||
|
||||
import txaio
|
||||
|
||||
from autobahn.websocket.protocol import WebSocketServerProtocol
|
||||
from autobahn.websocket.protocol import WebSocketServerFactory
|
||||
from autobahn.websocket.protocol import WebSocketClientProtocol
|
||||
from autobahn.websocket.protocol import WebSocketClientFactory
|
||||
from autobahn.websocket.protocol import WebSocketProtocol
|
||||
from autobahn.websocket.types import ConnectingRequest
|
||||
from autobahn.testutil import FakeTransport
|
||||
from autobahn.wamp.types import TransportDetails
|
||||
|
||||
|
||||
class WebSocketClientProtocolTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
t = FakeTransport()
|
||||
f = WebSocketClientFactory()
|
||||
f.log = txaio.make_logger()
|
||||
p = WebSocketClientProtocol()
|
||||
p.log = txaio.make_logger()
|
||||
p.factory = f
|
||||
p.transport = t
|
||||
p._transport_details = TransportDetails()
|
||||
|
||||
p._connectionMade()
|
||||
p.state = p.STATE_OPEN
|
||||
p.websocket_version = 18
|
||||
|
||||
self.protocol = p
|
||||
self.transport = t
|
||||
|
||||
def tearDown(self):
|
||||
for call in [
|
||||
self.protocol.autoPingPendingCall,
|
||||
self.protocol.autoPingTimeoutCall,
|
||||
self.protocol.openHandshakeTimeoutCall,
|
||||
self.protocol.closeHandshakeTimeoutCall,
|
||||
]:
|
||||
if call is not None:
|
||||
call.cancel()
|
||||
|
||||
def test_auto_ping(self):
|
||||
self.protocol.autoPingInterval = 1
|
||||
self.protocol.websocket_protocols = [Mock()]
|
||||
self.protocol.websocket_extensions = []
|
||||
self.protocol._onOpen = lambda: None
|
||||
self.protocol._wskey = '0' * 24
|
||||
self.protocol.peer = Mock()
|
||||
|
||||
# usually provided by the Twisted or asyncio specific
|
||||
# subclass, but we're testing the parent here...
|
||||
self.protocol._onConnect = Mock()
|
||||
self.protocol._closeConnection = Mock()
|
||||
|
||||
# set up a connection
|
||||
self.protocol._actuallyStartHandshake(
|
||||
ConnectingRequest(
|
||||
host="example.com",
|
||||
port=80,
|
||||
resource="/ws",
|
||||
)
|
||||
)
|
||||
|
||||
key = self.protocol.websocket_key + WebSocketProtocol._WS_MAGIC
|
||||
self.protocol.data = (
|
||||
b"HTTP/1.1 101 Switching Protocols\x0d\x0a"
|
||||
b"Upgrade: websocket\x0d\x0a"
|
||||
b"Connection: upgrade\x0d\x0a"
|
||||
b"Sec-Websocket-Accept: " + b64encode(sha1(key).digest()) + b"\x0d\x0a\x0d\x0a"
|
||||
)
|
||||
self.protocol.processHandshake()
|
||||
|
||||
self.assertTrue(self.protocol.autoPingPendingCall is not None)
|
||||
|
||||
|
||||
class WebSocketServerProtocolTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for autobahn.websocket.protocol.WebSocketProtocol.
|
||||
"""
|
||||
def setUp(self):
|
||||
t = FakeTransport()
|
||||
f = WebSocketServerFactory()
|
||||
f.log = txaio.make_logger()
|
||||
p = WebSocketServerProtocol()
|
||||
p.log = txaio.make_logger()
|
||||
p.factory = f
|
||||
p.transport = t
|
||||
|
||||
p._connectionMade()
|
||||
p.state = p.STATE_OPEN
|
||||
p.websocket_version = 18
|
||||
|
||||
self.protocol = p
|
||||
self.transport = t
|
||||
|
||||
def tearDown(self):
|
||||
for call in [
|
||||
self.protocol.autoPingPendingCall,
|
||||
self.protocol.autoPingTimeoutCall,
|
||||
self.protocol.openHandshakeTimeoutCall,
|
||||
self.protocol.closeHandshakeTimeoutCall,
|
||||
]:
|
||||
if call is not None:
|
||||
call.cancel()
|
||||
|
||||
def test_auto_ping(self):
|
||||
proto = Mock()
|
||||
proto._get_seconds = Mock(return_value=1)
|
||||
self.protocol.autoPingInterval = 1
|
||||
self.protocol.websocket_protocols = [proto]
|
||||
self.protocol.websocket_extensions = []
|
||||
self.protocol._onOpen = lambda: None
|
||||
self.protocol._wskey = '0' * 24
|
||||
self.protocol.succeedHandshake(proto)
|
||||
|
||||
self.assertTrue(self.protocol.autoPingPendingCall is not None)
|
||||
|
||||
def test_sendClose_none(self):
|
||||
"""
|
||||
sendClose with no code or reason works.
|
||||
"""
|
||||
self.protocol.sendClose()
|
||||
|
||||
# We closed properly
|
||||
self.assertEqual(self.transport._written, b"\x88\x00")
|
||||
self.assertEqual(self.protocol.state, self.protocol.STATE_CLOSING)
|
||||
|
||||
def test_sendClose_str_reason(self):
|
||||
"""
|
||||
sendClose with a str reason works.
|
||||
"""
|
||||
self.protocol.sendClose(code=1000, reason="oh no")
|
||||
|
||||
# We closed properly
|
||||
self.assertEqual(self.transport._written[2:], b"\x03\xe8oh no")
|
||||
self.assertEqual(self.protocol.state, self.protocol.STATE_CLOSING)
|
||||
|
||||
def test_sendClose_unicode_reason(self):
|
||||
"""
|
||||
sendClose with a unicode reason works.
|
||||
"""
|
||||
self.protocol.sendClose(code=1000, reason="oh no")
|
||||
|
||||
# We closed properly
|
||||
self.assertEqual(self.transport._written[2:], b"\x03\xe8oh no")
|
||||
self.assertEqual(self.protocol.state, self.protocol.STATE_CLOSING)
|
||||
|
||||
def test_sendClose_toolong(self):
|
||||
"""
|
||||
sendClose with a too-long reason will truncate it.
|
||||
"""
|
||||
self.protocol.sendClose(code=1000, reason="abc" * 1000)
|
||||
|
||||
# We closed properly
|
||||
self.assertEqual(self.transport._written[2:],
|
||||
b"\x03\xe8" + (b"abc" * 41))
|
||||
self.assertEqual(self.protocol.state, self.protocol.STATE_CLOSING)
|
||||
|
||||
def test_sendClose_reason_with_no_code(self):
|
||||
"""
|
||||
Trying to sendClose with a reason but no code will raise an Exception.
|
||||
"""
|
||||
with self.assertRaises(Exception) as e:
|
||||
self.protocol.sendClose(reason="abc")
|
||||
|
||||
self.assertIn("close reason without close code", str(e.exception))
|
||||
|
||||
# We shouldn't have closed
|
||||
self.assertEqual(self.transport._written, b"")
|
||||
self.assertEqual(self.protocol.state, self.protocol.STATE_OPEN)
|
||||
|
||||
def test_sendClose_invalid_code_type(self):
|
||||
"""
|
||||
Trying to sendClose with a non-int code will raise an Exception.
|
||||
"""
|
||||
with self.assertRaises(Exception) as e:
|
||||
self.protocol.sendClose(code="134")
|
||||
|
||||
self.assertIn("invalid type", str(e.exception))
|
||||
|
||||
# We shouldn't have closed
|
||||
self.assertEqual(self.transport._written, b"")
|
||||
self.assertEqual(self.protocol.state, self.protocol.STATE_OPEN)
|
||||
|
||||
def test_sendClose_invalid_code_value(self):
|
||||
"""
|
||||
Trying to sendClose with a non-valid int code will raise an Exception.
|
||||
"""
|
||||
with self.assertRaises(Exception) as e:
|
||||
self.protocol.sendClose(code=10)
|
||||
|
||||
self.assertIn("invalid close code 10", str(e.exception))
|
||||
|
||||
# We shouldn't have closed
|
||||
self.assertEqual(self.transport._written, b"")
|
||||
self.assertEqual(self.protocol.state, self.protocol.STATE_OPEN)
|
||||
|
||||
def test_interpolate_server_status_template(self):
|
||||
from autobahn.websocket.protocol import _SERVER_STATUS_TEMPLATE
|
||||
s = _SERVER_STATUS_TEMPLATE.format(None, '0.0.0')
|
||||
self.assertEqual(type(s), str)
|
||||
self.assertTrue(len(s) > 0)
|
||||
|
||||
|
||||
if os.environ.get('USE_TWISTED', False):
|
||||
class TwistedProtocolTests(unittest.TestCase):
|
||||
"""
|
||||
Tests which require a specific framework's protocol class to work
|
||||
(in this case, using Twisted)
|
||||
"""
|
||||
def setUp(self):
|
||||
from autobahn.twisted.websocket import WebSocketServerProtocol
|
||||
from autobahn.twisted.websocket import WebSocketServerFactory
|
||||
t = FakeTransport()
|
||||
f = WebSocketServerFactory()
|
||||
p = WebSocketServerProtocol()
|
||||
p.factory = f
|
||||
p.transport = t
|
||||
|
||||
p._connectionMade()
|
||||
p.state = p.STATE_OPEN
|
||||
p.websocket_version = 18
|
||||
|
||||
self.protocol = p
|
||||
self.transport = t
|
||||
|
||||
def tearDown(self):
|
||||
for call in [
|
||||
self.protocol.autoPingPendingCall,
|
||||
self.protocol.autoPingTimeoutCall,
|
||||
self.protocol.openHandshakeTimeoutCall,
|
||||
self.protocol.closeHandshakeTimeoutCall,
|
||||
]:
|
||||
if call is not None:
|
||||
call.cancel()
|
||||
|
||||
def test_loseConnection(self):
|
||||
"""
|
||||
If we lose our connection before openHandshakeTimeout fires, it is
|
||||
cleaned up
|
||||
"""
|
||||
# so, I guess a little cheezy, but we depend on the asyncio or
|
||||
# twisted class to call _connectionLost at some point; faking
|
||||
# that here
|
||||
self.protocol._connectionLost(txaio.create_failure(RuntimeError("testing")))
|
||||
self.assertTrue(self.protocol.openHandshakeTimeoutCall is None)
|
||||
|
||||
def test_send_server_status(self):
|
||||
self.protocol.sendServerStatus()
|
||||
@@ -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
backend/lib/python3.12/site-packages/autobahn/websocket/types.py
Normal file
433
backend/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.com/oberstet/scratchbox/tree/master/python/utf8
|
||||
|
||||
# Python 3 and above
|
||||
|
||||
# convert DFA table to bytes (performance)
|
||||
UTF8VALIDATOR_DFA_S = bytes(UTF8VALIDATOR_DFA)
|
||||
|
||||
class Utf8Validator(object):
|
||||
"""
|
||||
Incremental UTF-8 validator with constant memory consumption (minimal state).
|
||||
|
||||
Implements the algorithm "Flexible and Economical UTF-8 Decoder" by
|
||||
Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_codepoint',
|
||||
'_state',
|
||||
'_index',
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self._codepoint = None
|
||||
self._state = None
|
||||
self._index = None
|
||||
self.reset()
|
||||
|
||||
def decode(self, b):
|
||||
"""
|
||||
Eat one UTF-8 octet, and validate on the fly.
|
||||
|
||||
Returns ``UTF8_ACCEPT`` when enough octets have been consumed, in which case
|
||||
``self.codepoint`` contains the decoded Unicode code point.
|
||||
|
||||
Returns ``UTF8_REJECT`` when invalid UTF-8 was encountered.
|
||||
|
||||
Returns some other positive integer when more octets need to be eaten.
|
||||
"""
|
||||
tt = UTF8VALIDATOR_DFA_S[b]
|
||||
if self._state != UTF8_ACCEPT:
|
||||
self._codepoint = (b & 0x3f) | (self._codepoint << 6)
|
||||
else:
|
||||
self._codepoint = (0xff >> tt) & b
|
||||
self._state = UTF8VALIDATOR_DFA_S[256 + self._state * 16 + tt]
|
||||
return self._state
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset validator to start new incremental UTF-8 decode/validation.
|
||||
"""
|
||||
self._state = UTF8_ACCEPT # the empty string is valid UTF8
|
||||
self._codepoint = 0
|
||||
self._index = 0
|
||||
|
||||
def validate(self, ba):
|
||||
"""
|
||||
Incrementally validate a chunk of bytes provided as string.
|
||||
|
||||
Will return a quad ``(valid?, endsOnCodePoint?, currentIndex, totalIndex)``.
|
||||
|
||||
As soon as an octet is encountered which renders the octet sequence
|
||||
invalid, a quad with ``valid? == False`` is returned. ``currentIndex`` returns
|
||||
the index within the currently consumed chunk, and ``totalIndex`` the
|
||||
index within the total consumed sequence that was the point of bail out.
|
||||
When ``valid? == True``, currentIndex will be ``len(ba)`` and ``totalIndex`` the
|
||||
total amount of consumed bytes.
|
||||
"""
|
||||
#
|
||||
# The code here is written for optimal JITting in PyPy, not for best
|
||||
# readability by your grandma or particular elegance. Do NOT touch!
|
||||
#
|
||||
l = len(ba)
|
||||
i = 0
|
||||
state = self._state
|
||||
while i < l:
|
||||
# optimized version of decode(), since we are not interested in actual code points
|
||||
state = UTF8VALIDATOR_DFA_S[256 + (state << 4) + UTF8VALIDATOR_DFA_S[ba[i]]]
|
||||
if state == UTF8_REJECT:
|
||||
self._state = state
|
||||
self._index += i
|
||||
return False, False, i, self._index
|
||||
i += 1
|
||||
self._state = state
|
||||
self._index += l
|
||||
return True, state == UTF8_ACCEPT, l, self._index
|
||||
187
backend/lib/python3.12/site-packages/autobahn/websocket/util.py
Normal file
187
backend/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