RAHHH
This commit is contained in:
45
backend/lib/python3.12/site-packages/autobahn/__init__.py
Normal file
45
backend/lib/python3.12/site-packages/autobahn/__init__.py
Normal file
@@ -0,0 +1,45 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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._version import __version__
|
||||
|
||||
version = __version__
|
||||
|
||||
import os
|
||||
import txaio
|
||||
|
||||
# this is used in the unit tests (trial/pytest), and when already done here, there
|
||||
# is no risk and headaches with finding out if/where an import implies a framework
|
||||
if os.environ.get('USE_TWISTED', False) and os.environ.get('USE_ASYNCIO', False):
|
||||
raise RuntimeError('fatal: _both_ USE_TWISTED and USE_ASYNCIO are set!')
|
||||
|
||||
if os.environ.get('USE_TWISTED', False):
|
||||
txaio.use_twisted()
|
||||
elif os.environ.get('USE_ASYNCIO', False):
|
||||
txaio.use_asyncio()
|
||||
else:
|
||||
# neither USE_TWISTED nor USE_ASYNCIO selected from env var
|
||||
pass
|
||||
410
backend/lib/python3.12/site-packages/autobahn/__main__.py
Normal file
410
backend/lib/python3.12/site-packages/autobahn/__main__.py
Normal file
@@ -0,0 +1,410 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
# this module is available as the 'wamp' command-line tool or as
|
||||
# 'python -m autobahn'
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import json
|
||||
from copy import copy
|
||||
|
||||
try:
|
||||
from autobahn.twisted.component import Component
|
||||
except ImportError:
|
||||
print("The 'wamp' command-line tool requires Twisted.")
|
||||
print(" pip install autobahn[twisted]")
|
||||
sys.exit(1)
|
||||
|
||||
from twisted.internet.defer import Deferred, inlineCallbacks
|
||||
from twisted.internet.task import react
|
||||
from twisted.internet.protocol import ProcessProtocol
|
||||
|
||||
from autobahn.wamp.exception import ApplicationError
|
||||
from autobahn.wamp.types import PublishOptions
|
||||
from autobahn.wamp.types import SubscribeOptions
|
||||
|
||||
import txaio
|
||||
txaio.use_twisted()
|
||||
|
||||
|
||||
# XXX other ideas to get 'connection config':
|
||||
# - if there .crossbar/ here, load that config and accept a --name or
|
||||
# so to indicate which transport to use
|
||||
|
||||
# wamp [options] {call,publish,subscribe,register} wamp-uri [args] [kwargs]
|
||||
#
|
||||
# kwargs are spec'd with a 2-value-consuming --keyword option:
|
||||
# --keyword name value
|
||||
|
||||
|
||||
top = argparse.ArgumentParser(prog="wamp")
|
||||
top.add_argument(
|
||||
'--url',
|
||||
action='store',
|
||||
help='A WAMP URL to connect to, like ws://127.0.0.1:8080/ws or rs://localhost:1234',
|
||||
required=True,
|
||||
)
|
||||
top.add_argument(
|
||||
'--realm', '-r',
|
||||
action='store',
|
||||
help='The realm to join',
|
||||
default='default',
|
||||
)
|
||||
top.add_argument(
|
||||
'--private-key', '-k',
|
||||
action='store',
|
||||
help='Hex-encoded private key (via WAMP_PRIVATE_KEY if not provided here)',
|
||||
default=os.environ.get('WAMP_PRIVATE_KEY', None),
|
||||
)
|
||||
top.add_argument(
|
||||
'--authid',
|
||||
action='store',
|
||||
help='The authid to use, if authenticating',
|
||||
default=None,
|
||||
)
|
||||
top.add_argument(
|
||||
'--authrole',
|
||||
action='store',
|
||||
help='The role to use, if authenticating',
|
||||
default=None,
|
||||
)
|
||||
top.add_argument(
|
||||
'--max-failures', '-m',
|
||||
action='store',
|
||||
type=int,
|
||||
help='Failures before giving up (0 forever)',
|
||||
default=0,
|
||||
)
|
||||
sub = top.add_subparsers(
|
||||
title="subcommands",
|
||||
dest="subcommand_name",
|
||||
)
|
||||
|
||||
|
||||
call = sub.add_parser(
|
||||
'call',
|
||||
help='Do a WAMP call() and print any results',
|
||||
)
|
||||
call.add_argument(
|
||||
'uri',
|
||||
type=str,
|
||||
help="A WAMP URI to call"
|
||||
)
|
||||
call.add_argument(
|
||||
'call_args',
|
||||
nargs='*',
|
||||
help="All additional arguments are positional args",
|
||||
)
|
||||
call.add_argument(
|
||||
'--keyword',
|
||||
nargs=2,
|
||||
action='append',
|
||||
help="Specify a keyword argument to send: name value",
|
||||
)
|
||||
|
||||
|
||||
publish = sub.add_parser(
|
||||
'publish',
|
||||
help='Do a WAMP publish() with the given args, kwargs',
|
||||
)
|
||||
publish.add_argument(
|
||||
'uri',
|
||||
type=str,
|
||||
help="A WAMP URI to publish"
|
||||
)
|
||||
publish.add_argument(
|
||||
'publish_args',
|
||||
nargs='*',
|
||||
help="All additional arguments are positional args",
|
||||
)
|
||||
publish.add_argument(
|
||||
'--keyword',
|
||||
nargs=2,
|
||||
action='append',
|
||||
help="Specify a keyword argument to send: name value",
|
||||
)
|
||||
|
||||
|
||||
register = sub.add_parser(
|
||||
'register',
|
||||
help='Do a WAMP register() and run a command when called',
|
||||
)
|
||||
register.add_argument(
|
||||
'uri',
|
||||
type=str,
|
||||
help="A WAMP URI to call"
|
||||
)
|
||||
register.add_argument(
|
||||
'--times',
|
||||
type=int,
|
||||
default=0,
|
||||
help="Listen for this number of events, then exit. Default: forever",
|
||||
)
|
||||
register.add_argument(
|
||||
'command',
|
||||
type=str,
|
||||
nargs='*',
|
||||
help=(
|
||||
"Takes one or more args: the executable to call, and any positional "
|
||||
"arguments. As well, the following environment variables are set: "
|
||||
"WAMP_ARGS, WAMP_KWARGS and _JSON variants."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
subscribe = sub.add_parser(
|
||||
'subscribe',
|
||||
help='Do a WAMP subscribe() and print one line of JSON per event',
|
||||
)
|
||||
subscribe.add_argument(
|
||||
'uri',
|
||||
type=str,
|
||||
help="A WAMP URI to call"
|
||||
)
|
||||
subscribe.add_argument(
|
||||
'--times',
|
||||
type=int,
|
||||
default=0,
|
||||
help="Listen for this number of events, then exit. Default: forever",
|
||||
)
|
||||
subscribe.add_argument(
|
||||
'--match',
|
||||
type=str,
|
||||
default='exact',
|
||||
choices=['exact', 'prefix'],
|
||||
help="Massed in the SubscribeOptions, how to match the URI",
|
||||
)
|
||||
|
||||
|
||||
def _create_component(options):
|
||||
"""
|
||||
Configure and return a Component instance according to the given
|
||||
`options`
|
||||
"""
|
||||
if options.url.startswith('ws://'):
|
||||
kind = 'websocket'
|
||||
elif options.url.startswith('rs://'):
|
||||
kind = 'rawsocket'
|
||||
else:
|
||||
raise ValueError(
|
||||
"URL should start with ws:// or rs://"
|
||||
)
|
||||
|
||||
authentication = dict()
|
||||
if options.private_key:
|
||||
if not options.authid:
|
||||
raise ValueError(
|
||||
"Require --authid and --authrole if --private-key (or WAMP_PRIVATE_KEY) is provided"
|
||||
)
|
||||
authentication["cryptosign"] = {
|
||||
"authid": options.authid,
|
||||
"authrole": options.authrole,
|
||||
"privkey": options.private_key,
|
||||
}
|
||||
|
||||
return Component(
|
||||
transports=[{
|
||||
"type": kind,
|
||||
"url": options.url,
|
||||
}],
|
||||
authentication=authentication if authentication else None,
|
||||
realm=options.realm,
|
||||
)
|
||||
|
||||
|
||||
@inlineCallbacks
|
||||
def do_call(reactor, session, options):
|
||||
call_args = list(options.call_args)
|
||||
call_kwargs = dict()
|
||||
if options.keyword is not None:
|
||||
call_kwargs = {
|
||||
k: v
|
||||
for k, v in options.keyword
|
||||
}
|
||||
|
||||
results = yield session.call(options.uri, *call_args, **call_kwargs)
|
||||
print("result: {}".format(results))
|
||||
|
||||
|
||||
@inlineCallbacks
|
||||
def do_publish(reactor, session, options):
|
||||
publish_args = list(options.publish_args)
|
||||
publish_kwargs = {} if options.keyword is None else {
|
||||
k: v
|
||||
for k, v in options.keyword
|
||||
}
|
||||
|
||||
yield session.publish(
|
||||
options.uri,
|
||||
*publish_args,
|
||||
options=PublishOptions(acknowledge=True),
|
||||
**publish_kwargs
|
||||
)
|
||||
|
||||
|
||||
@inlineCallbacks
|
||||
def do_register(reactor, session, options):
|
||||
"""
|
||||
run a command-line upon an RPC call
|
||||
"""
|
||||
|
||||
all_done = Deferred()
|
||||
countdown = [options.times]
|
||||
|
||||
@inlineCallbacks
|
||||
def called(*args, **kw):
|
||||
print("called: args={}, kwargs={}".format(args, kw), file=sys.stderr)
|
||||
env = copy(os.environ)
|
||||
env['WAMP_ARGS'] = ' '.join(args)
|
||||
env['WAMP_ARGS_JSON'] = json.dumps(args)
|
||||
env['WAMP_KWARGS'] = ' '.join('{}={}'.format(k, v) for k, v in kw.items())
|
||||
env['WAMP_KWARGS_JSON'] = json.dumps(kw)
|
||||
|
||||
exe = os.path.abspath(options.command[0])
|
||||
args = options.command
|
||||
done = Deferred()
|
||||
|
||||
class DumpOutput(ProcessProtocol):
|
||||
def outReceived(self, data):
|
||||
sys.stdout.write(data.decode('utf8'))
|
||||
|
||||
def errReceived(self, data):
|
||||
sys.stderr.write(data.decode('utf8'))
|
||||
|
||||
def processExited(self, reason):
|
||||
done.callback(reason.value.exitCode)
|
||||
|
||||
proto = DumpOutput()
|
||||
reactor.spawnProcess(
|
||||
proto, exe, args, env=env, path="."
|
||||
)
|
||||
code = yield done
|
||||
|
||||
if code != 0:
|
||||
print("Failed with exit-code {}".format(code))
|
||||
if countdown[0]:
|
||||
countdown[0] -= 1
|
||||
if countdown[0] <= 0:
|
||||
reactor.callLater(0, all_done.callback, None)
|
||||
|
||||
yield session.register(called, options.uri)
|
||||
yield all_done
|
||||
|
||||
|
||||
@inlineCallbacks
|
||||
def do_subscribe(reactor, session, options):
|
||||
"""
|
||||
print events (one line of JSON per event)
|
||||
"""
|
||||
|
||||
all_done = Deferred()
|
||||
countdown = [options.times]
|
||||
|
||||
@inlineCallbacks
|
||||
def published(*args, **kw):
|
||||
print(
|
||||
json.dumps({
|
||||
"args": args,
|
||||
"kwargs": kw,
|
||||
})
|
||||
)
|
||||
if countdown[0]:
|
||||
countdown[0] -= 1
|
||||
if countdown[0] <= 0:
|
||||
reactor.callLater(0, all_done.callback, None)
|
||||
|
||||
yield session.subscribe(published, options.uri, options=SubscribeOptions(match=options.match))
|
||||
yield all_done
|
||||
|
||||
|
||||
def _main():
|
||||
"""
|
||||
This is a magic name for `python -m autobahn`, and specified as
|
||||
our entry_point in setup.py
|
||||
"""
|
||||
react(_real_main)
|
||||
|
||||
|
||||
@inlineCallbacks
|
||||
def _real_main(reactor):
|
||||
"""
|
||||
Sanity check options, create a connection and run our subcommand
|
||||
"""
|
||||
options = top.parse_args()
|
||||
component = _create_component(options)
|
||||
|
||||
if options.subcommand_name is None:
|
||||
print("Must select a subcommand")
|
||||
sys.exit(1)
|
||||
|
||||
if options.subcommand_name == "register":
|
||||
exe = options.command[0]
|
||||
if not os.path.isabs(exe):
|
||||
print("Full path to the executable required. Found: {}".format(exe), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not os.path.exists(exe):
|
||||
print("Executable not found: {}".format(exe), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
subcommands = {
|
||||
"call": do_call,
|
||||
"register": do_register,
|
||||
"subscribe": do_subscribe,
|
||||
"publish": do_publish,
|
||||
}
|
||||
command_fn = subcommands[options.subcommand_name]
|
||||
|
||||
exit_code = [0]
|
||||
|
||||
@component.on_join
|
||||
@inlineCallbacks
|
||||
def _(session, details):
|
||||
print("connected: authrole={} authmethod={}".format(details.authrole, details.authmethod), file=sys.stderr)
|
||||
try:
|
||||
yield command_fn(reactor, session, options)
|
||||
except ApplicationError as e:
|
||||
print("\n{}: {}\n".format(e.error, ''.join(e.args)))
|
||||
exit_code[0] = 5
|
||||
yield session.leave()
|
||||
|
||||
failures = []
|
||||
|
||||
@component.on_connectfailure
|
||||
def _(comp, fail):
|
||||
print("connect failure: {}".format(fail))
|
||||
failures.append(fail)
|
||||
if options.max_failures > 0 and len(failures) > options.max_failures:
|
||||
print("Too many failures ({}). Exiting".format(len(failures)))
|
||||
reactor.stop()
|
||||
|
||||
yield component.start(reactor)
|
||||
# sys.exit(exit_code[0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_main()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
29
backend/lib/python3.12/site-packages/autobahn/_version.py
Normal file
29
backend/lib/python3.12/site-packages/autobahn/_version.py
Normal file
@@ -0,0 +1,29 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
__version__ = '24.4.2'
|
||||
|
||||
__build__ = '00000000-0000000'
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="e" version="1.1" viewBox="0 0 181.59 89.688" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs id="f"><clipPath id="d"><path id="g" d="m441.22 802.44h142.17v-68.65h-142.17v68.65z" clip-rule="evenodd"/></clipPath><clipPath id="c"><path id="h" d="m0 0h595.28v841.89h-595.28v-841.89z"/></clipPath><clipPath id="b"><path id="i" d="m441.22 733.69h142.07v68.65h-142.07v-68.65z" clip-rule="evenodd"/></clipPath><clipPath id="a"><path id="j" d="m0 0h595.28v841.89h-595.28v-841.89z"/></clipPath></defs><g id="k" transform="matrix(1.25 0 0 -1.25 -543.6 1966.7)"><g id="l" transform="translate(-4.7446 769.43)"><g id="m" clip-path="url(#d)"><g id="n"><g id="o"><g id="p" clip-path="url(#c)"><g id="q"><g id="r" clip-path="url(#b)"><g id="s"><g id="t"><g id="u" clip-path="url(#a)"><g id="v"><g id="w" transform="translate(441.22 802.44)"><path id="x" d="m38.099-68.65h-38.099v68.65h76.098v-68.65h-37.999z" fill="#333" fill-rule="evenodd"/></g></g><g id="y"><g id="z" transform="translate(451.95 784.2)"><path id="aa" d="m0-32.07h15.841l26.569 26.558h13.134v5.5121h-15.44l-26.569-26.558h-13.535v-5.5121z" fill="#ff0" fill-rule="evenodd"/></g></g><g id="ab"><g id="ac" transform="translate(481.63 766.06)"><path id="ad" d="m0-3.9086 3.9102 3.9086 8.4219-8.4185h13.535v-5.512h-15.841l-10.026 10.022z" fill="#ff0" fill-rule="evenodd"/></g></g><g id="ae"><g id="af" transform="translate(451.95 784.2)"><path id="ag" d="m13.134-5.5121h-13.134v5.5121h15.44l10.327-10.222-3.9102-4.0088-8.7227 8.7191z" fill="#ff0" fill-rule="evenodd"/></g></g><g id="ah"><g id="ai" transform="translate(503.59 784.3)"><path id="aj" d="m19.952 0h7.6199l-9.3243-15.935 9.6251-16.236h-7.7201l-6.2162 11.325-6.1159-11.325h-7.8204l9.6251 16.236-9.3243 15.935h7.6198l6.0157-11.024 6.0156 11.024z" fill="#fff" fill-rule="evenodd"/></g></g><g id="ak"><g id="al" transform="translate(532.56 784.3)"><path id="am" d="m0 0h11.33c3.9102 0 6.8177-0.70154 8.9232-2.2048 2.0052-1.5033 3.0079-3.7081 3.0079-6.6145 0-1.5033-0.4011-2.9064-1.2032-4.109-0.8021-1.2027-2.0052-2.1047-3.4089-2.706 1.6042-0.4009 2.9076-1.2026 3.9102-2.5055 0.9024-1.2026 1.4037-2.7059 1.4037-4.5099 0-3.1068-1.0026-5.5121-2.9076-7.0153-2.0052-1.6036-4.8125-2.5055-8.5222-2.5055h-12.533v32.17zm6.7175-26.859h5.6146c1.6042 0 2.8073 0.4009 3.7097 1.1024 0.8021 0.8018 1.3034 1.804 1.3034 3.1068 0 3.0066-1.504 4.5099-4.6121 4.5099h-6.0156v-8.7191zm4.8125 13.43c3.4089 0.1002 5.1133 1.403 5.1133 4.0087 0 1.4031-0.5013 2.5055-1.3034 3.1068-0.9023 0.70153-2.2057 1.0022-4.0104 1.0022h-4.612v-8.1178h4.8125z" fill="#fff" fill-rule="evenodd"/></g></g><g id="an"><g id="ao" transform="translate(558.23 784.3)"><path id="ap" d="m6.7175-20.345v-11.826h-6.7175v32.17h12.031c3.8099 0 6.7175-0.80176 8.823-2.5055 2.0052-1.7037 3.0078-4.109 3.0078-7.2158 0-2.1046-0.401-4.0088-1.4036-5.5121-0.9024-1.4031-2.4063-2.6057-4.3113-3.5077l7.0183-13.129v-0.3006h-7.2188l-6.0156 11.826h-5.2136zm5.3138 5.3116c1.7045 0 2.9076 0.5011 3.8099 1.3029 1.0027 0.8018 1.4037 2.0044 1.4037 3.5077 0 1.5033-0.401 2.7059-1.3034 3.6079-0.8021 0.80175-2.2057 1.3028-3.9102 1.3028h-5.3138v-9.7214h5.3138z" fill="#fff" fill-rule="evenodd"/></g></g></g></g></g></g></g></g></g></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="e" version="1.1" viewBox="0 0 131.56 65.675" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs id="f"><clipPath id="d"><path id="g" d="m453.54 805.04h102.05v-49.34h-102.05v49.34z" clip-rule="evenodd"/></clipPath><clipPath id="c"><path id="h" d="m0 0h595.28v841.89h-595.28v-841.89z"/></clipPath><clipPath id="b"><path id="i" d="m453.54 805.04h102.05v-49.34h-102.05v49.34z" clip-rule="evenodd"/></clipPath><clipPath id="a"><path id="j" d="m0 0h595.28v841.89h-595.28v-841.89z"/></clipPath></defs><g id="k" transform="matrix(1.25 0 0 -1.25 -564.93 1008.3)"><g id="l"/><g id="m"/><g id="n"/><g id="o"/><g id="p"/><g id="q"/><g id="r"/><g id="s"/><g id="t"/><g id="u"/><g id="v"/><g id="w"/><g id="x"/><g id="y"/><g id="z"/><g id="aa"/><g id="ab"/><g id="ac"/><g id="ad"/><g id="ae"/><g id="af"/><g id="ag"/><g id="ah"/><g id="ai"/><g id="aj"><g id="ak" clip-path="url(#d)"><g id="al"><g id="am"><g id="an" clip-path="url(#c)"><g id="ao"><g id="ap" clip-path="url(#b)"><g id="aq"><g id="ar"><g id="as" clip-path="url(#a)"><g id="at"><g id="au" transform="translate(453.54 805.04)"><path id="av" d="m0 0h54.609v-49.34h-54.609v49.34z" fill="#b3b3b3" fill-rule="evenodd"/></g></g><g id="aw"><g id="ax" transform="translate(461.19 791.96)"><path id="ay" d="m0-23.086h11.42l19.085 19.085h9.4009v4.0011h-11.058l-19.085-19.085h-9.763v-4.0012z" fill="#ff0" fill-rule="evenodd"/></g></g><g id="az"><g id="ba" transform="translate(482.56 778.91)"><path id="bb" d="m0-2.8292 2.8292 2.8292 6.0383-6.0383h9.6692v-4.0012h-11.326l-7.2102 7.2103z" fill="#ff0" fill-rule="evenodd"/></g></g><g id="bc"><g id="bd" transform="translate(461.19 791.96)"><path id="be" d="m9.4947-4.0011h-9.4947v4.0011h11.152l7.3936-7.3937-2.8291-2.8291-6.2218 6.2217z" fill="#ff0" fill-rule="evenodd"/></g></g><g id="bf"><g id="bg" transform="translate(498.34 792.03)"><path id="bh" d="m9.9906-7.9734 4.3361 7.9734h5.4797l-6.7345-11.468 6.9093-11.658h-5.5433l-4.4473 8.1005-4.4473-8.1005h-5.5432l6.9092 11.658-6.7345 11.468h5.4797l4.3361-7.9734z" fill="#333" fill-rule="evenodd"/></g></g><g id="bi"><g id="bj" transform="translate(519.14 792.03)"><path id="bk" d="m0-23.126v23.126h8.1004c2.806 0 4.9344-0.54004 6.3852-1.6201 1.4506-1.0695 2.176-2.6419 2.176-4.7172 0-1.1331-0.2913-2.1338-0.8736-3.002-0.5824-0.85772-1.3925-1.4877-2.4302-1.8901 1.186-0.2965 2.1178-0.8947 2.7955-1.7948 0.6883-0.9001 1.0324-2.0013 1.0324-3.3037 0-2.2236-0.7095-3.9073-2.1284-5.0509s-3.4414-1.726-6.0674-1.7471h-8.9899zm4.765 10.07v-6.2421h4.082c1.1224 0 1.996 0.2647 2.6208 0.7942 0.6353 0.54 0.953 1.2813 0.953 2.2236 0 2.1178-1.096 3.1926-3.2879 3.2243h-4.3679zm0 3.3673h3.5261c2.4037 0.04234 3.6056 1.0007 3.6056 2.8749 0 1.0483-0.3071 1.8001-0.9213 2.2554-0.6035 0.46589-1.5618 0.69886-2.8748 0.69886h-3.3355v-5.8292z" fill="#333" fill-rule="evenodd"/></g></g><g id="bl"><g id="bm" transform="translate(537.58 792.03)"><path id="bn" d="m8.5611-14.66h-3.796v-8.4657h-4.765v23.126h8.5928c2.732 0 4.8392-0.60886 6.3216-1.8266s2.2237-2.9384 2.2237-5.1621c0-1.5777-0.3442-2.896-1.0324-3.9549-0.6777-1.0483-1.7101-1.8848-3.0973-2.5096l5.0033-9.4505v-0.2223h-5.1145l-4.3361 8.4657zm-3.796 3.8597h3.8437c1.1966 0 2.123 0.3018 2.7795 0.9053 0.6565 0.61418 0.9848 1.456 0.9848 2.5254 0 1.0907-0.3124 1.9484-0.9371 2.5731-0.6142 0.62476-1.5619 0.93712-2.8432 0.93712h-3.8278v-6.941z" fill="#333" fill-rule="evenodd"/></g></g></g></g></g></g></g></g></g></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,53 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 platform
|
||||
|
||||
import autobahn
|
||||
|
||||
# WebSocket protocol support
|
||||
from autobahn.asyncio.websocket import \
|
||||
WebSocketServerProtocol, \
|
||||
WebSocketClientProtocol, \
|
||||
WebSocketServerFactory, \
|
||||
WebSocketClientFactory
|
||||
|
||||
# WAMP support
|
||||
from autobahn.asyncio.wamp import ApplicationSession
|
||||
|
||||
|
||||
__all__ = (
|
||||
'WebSocketServerProtocol',
|
||||
'WebSocketClientProtocol',
|
||||
'WebSocketServerFactory',
|
||||
'WebSocketClientFactory',
|
||||
'ApplicationSession',
|
||||
)
|
||||
|
||||
__ident__ = 'Autobahn/{}-asyncio-{}/{}'.format(autobahn.__version__, platform.python_implementation(), platform.python_version())
|
||||
"""
|
||||
AutobahnPython library implementation (eg. "Autobahn/0.13.0-asyncio-CPython/3.5.1")
|
||||
"""
|
||||
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,417 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 asyncio
|
||||
import ssl
|
||||
import signal
|
||||
from functools import wraps
|
||||
|
||||
import txaio
|
||||
from autobahn.asyncio.websocket import WampWebSocketClientFactory
|
||||
from autobahn.asyncio.rawsocket import WampRawSocketClientFactory
|
||||
|
||||
from autobahn.wamp import component
|
||||
from autobahn.wamp.exception import TransportLost
|
||||
|
||||
from autobahn.asyncio.wamp import Session
|
||||
from autobahn.wamp.serializer import create_transport_serializers, create_transport_serializer
|
||||
|
||||
|
||||
__all__ = ('Component', 'run')
|
||||
|
||||
|
||||
def _unique_list(seq):
|
||||
"""
|
||||
Return a list with unique elements from sequence, preserving order.
|
||||
"""
|
||||
seen = set()
|
||||
return [x for x in seq if x not in seen and not seen.add(x)]
|
||||
|
||||
|
||||
def _camel_case_from_snake_case(snake):
|
||||
parts = snake.split('_')
|
||||
return parts[0] + ''.join(s.capitalize() for s in parts[1:])
|
||||
|
||||
|
||||
def _create_transport_factory(loop, transport, session_factory):
|
||||
"""
|
||||
Create a WAMP-over-XXX transport factory.
|
||||
"""
|
||||
if transport.type == 'websocket':
|
||||
serializers = create_transport_serializers(transport)
|
||||
factory = WampWebSocketClientFactory(
|
||||
session_factory,
|
||||
url=transport.url,
|
||||
serializers=serializers,
|
||||
proxy=transport.proxy, # either None or a dict with host, port
|
||||
)
|
||||
|
||||
elif transport.type == 'rawsocket':
|
||||
serializer = create_transport_serializer(transport.serializers[0])
|
||||
factory = WampRawSocketClientFactory(session_factory, serializer=serializer)
|
||||
|
||||
else:
|
||||
assert(False), 'should not arrive here'
|
||||
|
||||
# set the options one at a time so we can give user better feedback
|
||||
for k, v in transport.options.items():
|
||||
try:
|
||||
factory.setProtocolOptions(**{k: v})
|
||||
except (TypeError, KeyError):
|
||||
# this allows us to document options as snake_case
|
||||
# until everything internally is upgraded from
|
||||
# camelCase
|
||||
try:
|
||||
factory.setProtocolOptions(
|
||||
**{_camel_case_from_snake_case(k): v}
|
||||
)
|
||||
except (TypeError, KeyError):
|
||||
raise ValueError(
|
||||
"Unknown {} transport option: {}={}".format(transport.type, k, v)
|
||||
)
|
||||
return factory
|
||||
|
||||
|
||||
class Component(component.Component):
|
||||
"""
|
||||
A component establishes a transport and attached a session
|
||||
to a realm using the transport for communication.
|
||||
|
||||
The transports a component tries to use can be configured,
|
||||
as well as the auto-reconnect strategy.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
session_factory = Session
|
||||
"""
|
||||
The factory of the session we will instantiate.
|
||||
"""
|
||||
|
||||
def _is_ssl_error(self, e):
|
||||
"""
|
||||
Internal helper.
|
||||
"""
|
||||
return isinstance(e, ssl.SSLError)
|
||||
|
||||
def _check_native_endpoint(self, endpoint):
|
||||
if isinstance(endpoint, dict):
|
||||
if 'tls' in endpoint:
|
||||
tls = endpoint['tls']
|
||||
if isinstance(tls, (dict, bool)):
|
||||
pass
|
||||
elif isinstance(tls, ssl.SSLContext):
|
||||
pass
|
||||
else:
|
||||
raise ValueError(
|
||||
"'tls' configuration must be a dict, bool or "
|
||||
"SSLContext instance"
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"'endpoint' configuration must be a dict or IStreamClientEndpoint"
|
||||
" provider"
|
||||
)
|
||||
|
||||
# async function
|
||||
def _connect_transport(self, loop, transport, session_factory, done):
|
||||
"""
|
||||
Create and connect a WAMP-over-XXX transport.
|
||||
"""
|
||||
factory = _create_transport_factory(loop, transport, session_factory)
|
||||
|
||||
# XXX the rest of this should probably be factored into its
|
||||
# own method (or three!)...
|
||||
|
||||
if transport.proxy:
|
||||
timeout = transport.endpoint.get('timeout', 10) # in seconds
|
||||
if type(timeout) != int:
|
||||
raise ValueError('invalid type {} for timeout in client endpoint configuration'.format(type(timeout)))
|
||||
# do we support HTTPS proxies?
|
||||
|
||||
f = loop.create_connection(
|
||||
protocol_factory=factory,
|
||||
host=transport.proxy['host'],
|
||||
port=transport.proxy['port'],
|
||||
)
|
||||
time_f = asyncio.ensure_future(asyncio.wait_for(f, timeout=timeout))
|
||||
return self._wrap_connection_future(transport, done, time_f)
|
||||
|
||||
elif transport.endpoint['type'] == 'tcp':
|
||||
|
||||
version = transport.endpoint.get('version', 4)
|
||||
if version not in [4, 6]:
|
||||
raise ValueError('invalid IP version {} in client endpoint configuration'.format(version))
|
||||
|
||||
host = transport.endpoint['host']
|
||||
if type(host) != str:
|
||||
raise ValueError('invalid type {} for host in client endpoint configuration'.format(type(host)))
|
||||
|
||||
port = transport.endpoint['port']
|
||||
if type(port) != int:
|
||||
raise ValueError('invalid type {} for port in client endpoint configuration'.format(type(port)))
|
||||
|
||||
timeout = transport.endpoint.get('timeout', 10) # in seconds
|
||||
if type(timeout) != int:
|
||||
raise ValueError('invalid type {} for timeout in client endpoint configuration'.format(type(timeout)))
|
||||
|
||||
tls = transport.endpoint.get('tls', None)
|
||||
tls_hostname = None
|
||||
|
||||
# create a TLS enabled connecting TCP socket
|
||||
if tls:
|
||||
if isinstance(tls, dict):
|
||||
for k in tls.keys():
|
||||
if k not in ["hostname", "trust_root"]:
|
||||
raise ValueError("Invalid key '{}' in 'tls' config".format(k))
|
||||
hostname = tls.get('hostname', host)
|
||||
if type(hostname) != str:
|
||||
raise ValueError('invalid type {} for hostname in TLS client endpoint configuration'.format(hostname))
|
||||
cert_fname = tls.get('trust_root', None)
|
||||
|
||||
tls_hostname = hostname
|
||||
tls = True
|
||||
if cert_fname is not None:
|
||||
tls = ssl.create_default_context(
|
||||
purpose=ssl.Purpose.SERVER_AUTH,
|
||||
cafile=cert_fname,
|
||||
)
|
||||
|
||||
elif isinstance(tls, ssl.SSLContext):
|
||||
# tls=<an SSLContext> is valid
|
||||
tls_hostname = host
|
||||
|
||||
elif tls in [False, True]:
|
||||
if tls:
|
||||
tls_hostname = host
|
||||
|
||||
else:
|
||||
raise RuntimeError('unknown type {} for "tls" configuration in transport'.format(type(tls)))
|
||||
|
||||
f = loop.create_connection(
|
||||
protocol_factory=factory,
|
||||
host=host,
|
||||
port=port,
|
||||
ssl=tls,
|
||||
server_hostname=tls_hostname,
|
||||
)
|
||||
time_f = asyncio.ensure_future(asyncio.wait_for(f, timeout=timeout))
|
||||
return self._wrap_connection_future(transport, done, time_f)
|
||||
|
||||
elif transport.endpoint['type'] == 'unix':
|
||||
path = transport.endpoint['path']
|
||||
timeout = int(transport.endpoint.get('timeout', 10)) # in seconds
|
||||
|
||||
f = loop.create_unix_connection(
|
||||
protocol_factory=factory,
|
||||
path=path,
|
||||
)
|
||||
time_f = asyncio.ensure_future(asyncio.wait_for(f, timeout=timeout))
|
||||
return self._wrap_connection_future(transport, done, time_f)
|
||||
|
||||
else:
|
||||
assert(False), 'should not arrive here'
|
||||
|
||||
def _wrap_connection_future(self, transport, done, conn_f):
|
||||
|
||||
def on_connect_success(result):
|
||||
# async connect call returns a 2-tuple
|
||||
transport, proto = result
|
||||
|
||||
# in the case where we .abort() the transport / connection
|
||||
# during setup, we still get on_connect_success but our
|
||||
# transport is already closed (this will happen if
|
||||
# e.g. there's an "open handshake timeout") -- I don't
|
||||
# know if there's a "better" way to detect this? #python
|
||||
# doesn't know of one, anyway
|
||||
if transport.is_closing():
|
||||
if not txaio.is_called(done):
|
||||
reason = getattr(proto, "_onclose_reason", "Connection already closed")
|
||||
txaio.reject(done, TransportLost(reason))
|
||||
return
|
||||
|
||||
# if e.g. an SSL handshake fails, we will have
|
||||
# successfully connected (i.e. get here) but need to
|
||||
# 'listen' for the "connection_lost" from the underlying
|
||||
# protocol in case of handshake failure .. so we wrap
|
||||
# it. Also, we don't increment transport.success_count
|
||||
# here on purpose (because we might not succeed).
|
||||
|
||||
# XXX double-check that asyncio behavior on TLS handshake
|
||||
# failures is in fact as described above
|
||||
orig = proto.connection_lost
|
||||
|
||||
@wraps(orig)
|
||||
def lost(fail):
|
||||
rtn = orig(fail)
|
||||
if not txaio.is_called(done):
|
||||
# asyncio will call connection_lost(None) in case of
|
||||
# a transport failure, in which case we create an
|
||||
# appropriate exception
|
||||
if fail is None:
|
||||
fail = TransportLost("failed to complete connection")
|
||||
txaio.reject(done, fail)
|
||||
return rtn
|
||||
proto.connection_lost = lost
|
||||
|
||||
def on_connect_failure(err):
|
||||
transport.connect_failures += 1
|
||||
# failed to establish a connection in the first place
|
||||
txaio.reject(done, err)
|
||||
|
||||
txaio.add_callbacks(conn_f, on_connect_success, None)
|
||||
# the errback is added as a second step so it gets called if
|
||||
# there as an error in on_connect_success itself.
|
||||
txaio.add_callbacks(conn_f, None, on_connect_failure)
|
||||
return conn_f
|
||||
|
||||
# async function
|
||||
def start(self, loop=None):
|
||||
"""
|
||||
This starts the Component, which means it will start connecting
|
||||
(and re-connecting) to its configured transports. A Component
|
||||
runs until it is "done", which means one of:
|
||||
- There was a "main" function defined, and it completed successfully;
|
||||
- Something called ``.leave()`` on our session, and we left successfully;
|
||||
- ``.stop()`` was called, and completed successfully;
|
||||
- none of our transports were able to connect successfully (failure);
|
||||
|
||||
:returns: a Future which will resolve (to ``None``) when we are
|
||||
"done" or with an error if something went wrong.
|
||||
"""
|
||||
|
||||
if loop is None:
|
||||
self.log.warn("Using default loop")
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
return self._start(loop=loop)
|
||||
|
||||
|
||||
def run(components, start_loop=True, log_level='info'):
|
||||
"""
|
||||
High-level API to run a series of components.
|
||||
|
||||
This will only return once all the components have stopped
|
||||
(including, possibly, after all re-connections have failed if you
|
||||
have re-connections enabled). Under the hood, this calls
|
||||
|
||||
XXX fixme for asyncio
|
||||
|
||||
-- if you wish to manage the loop yourself, use the
|
||||
:meth:`autobahn.asyncio.component.Component.start` method to start
|
||||
each component yourself.
|
||||
|
||||
:param components: the Component(s) you wish to run
|
||||
:type components: instance or list of :class:`autobahn.asyncio.component.Component`
|
||||
|
||||
:param start_loop: When ``True`` (the default) this method
|
||||
start a new asyncio loop.
|
||||
:type start_loop: bool
|
||||
|
||||
:param log_level: a valid log-level (or None to avoid calling start_logging)
|
||||
:type log_level: string
|
||||
"""
|
||||
|
||||
# actually, should we even let people "not start" the logging? I'm
|
||||
# not sure that's wise... (double-check: if they already called
|
||||
# txaio.start_logging() what happens if we call it again?)
|
||||
if log_level is not None:
|
||||
txaio.start_logging(level=log_level)
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_closed():
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
loop = asyncio.get_event_loop()
|
||||
txaio.config.loop = loop
|
||||
log = txaio.make_logger()
|
||||
|
||||
# see https://github.com/python/asyncio/issues/341 asyncio has
|
||||
# "odd" handling of KeyboardInterrupt when using Tasks (as
|
||||
# run_until_complete does). Another option is to just resture
|
||||
# default SIGINT handling, which is to exit:
|
||||
# import signal
|
||||
# signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
async def nicely_exit(signal):
|
||||
log.info("Shutting down due to {signal}", signal=signal)
|
||||
|
||||
try:
|
||||
tasks = asyncio.Task.all_tasks()
|
||||
except AttributeError:
|
||||
# this changed with python >= 3.7
|
||||
tasks = asyncio.all_tasks()
|
||||
|
||||
for task in tasks:
|
||||
# Do not cancel the current task.
|
||||
|
||||
try:
|
||||
current_task = asyncio.Task.current_task()
|
||||
except AttributeError:
|
||||
current_task = asyncio.current_task()
|
||||
|
||||
if task is not current_task:
|
||||
task.cancel()
|
||||
|
||||
def cancel_all_callback(fut):
|
||||
try:
|
||||
fut.result()
|
||||
except asyncio.CancelledError:
|
||||
log.debug("All task cancelled")
|
||||
except Exception as e:
|
||||
log.error("Error while shutting down: {exception}", exception=e)
|
||||
finally:
|
||||
loop.stop()
|
||||
|
||||
fut = asyncio.gather(*tasks)
|
||||
fut.add_done_callback(cancel_all_callback)
|
||||
|
||||
try:
|
||||
loop.add_signal_handler(signal.SIGINT, lambda: asyncio.ensure_future(nicely_exit("SIGINT")))
|
||||
loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.ensure_future(nicely_exit("SIGTERM")))
|
||||
except NotImplementedError:
|
||||
# signals are not available on Windows
|
||||
pass
|
||||
|
||||
def done_callback(loop, arg):
|
||||
loop.stop()
|
||||
|
||||
# returns a future; could run_until_complete() but see below
|
||||
component._run(loop, components, done_callback)
|
||||
|
||||
if start_loop:
|
||||
try:
|
||||
loop.run_forever()
|
||||
# this is probably more-correct, but then you always get
|
||||
# "Event loop stopped before Future completed":
|
||||
# loop.run_until_complete(f)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
# finally:
|
||||
# signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
# signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
||||
|
||||
# Close the event loop at the end, otherwise an exception is
|
||||
# thrown. https://bugs.python.org/issue23548
|
||||
loop.close()
|
||||
@@ -0,0 +1,517 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 asyncio
|
||||
import struct
|
||||
import math
|
||||
import copy
|
||||
from typing import Optional
|
||||
|
||||
import txaio
|
||||
from autobahn.util import public, _LazyHexFormatter, hltype
|
||||
from autobahn.wamp.exception import ProtocolError, SerializationError, TransportLost
|
||||
from autobahn.wamp.types import TransportDetails
|
||||
from autobahn.asyncio.util import get_serializers, create_transport_details, transport_channel_id
|
||||
|
||||
__all__ = (
|
||||
'WampRawSocketServerProtocol',
|
||||
'WampRawSocketClientProtocol',
|
||||
'WampRawSocketServerFactory',
|
||||
'WampRawSocketClientFactory'
|
||||
)
|
||||
|
||||
FRAME_TYPE_DATA = 0
|
||||
FRAME_TYPE_PING = 1
|
||||
FRAME_TYPE_PONG = 2
|
||||
|
||||
MAGIC_BYTE = 0x7F
|
||||
|
||||
|
||||
class PrefixProtocol(asyncio.Protocol):
|
||||
|
||||
prefix_format = '!L'
|
||||
prefix_length = struct.calcsize(prefix_format)
|
||||
max_length = 16 * 1024 * 1024
|
||||
max_length_send = max_length
|
||||
log = txaio.make_logger() # @UndefinedVariable
|
||||
peer: Optional[str] = None
|
||||
is_server: Optional[bool] = None
|
||||
|
||||
@property
|
||||
def transport_details(self) -> Optional[TransportDetails]:
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.transport_details`
|
||||
"""
|
||||
return self._transport_details
|
||||
|
||||
def connection_made(self, transport):
|
||||
# asyncio networking framework entry point, called by asyncio
|
||||
# when the connection is established (either a client or a server)
|
||||
self.log.debug('RawSocker Asyncio: Connection made with peer {peer}', peer=self.peer)
|
||||
|
||||
self.transport = transport
|
||||
|
||||
# determine preliminary transport details (what is known at this point)
|
||||
self._transport_details = create_transport_details(self.transport, self.is_server)
|
||||
self._transport_details.channel_framing = TransportDetails.CHANNEL_FRAMING_RAWSOCKET
|
||||
|
||||
# backward compatibility
|
||||
self.peer = self._transport_details.peer
|
||||
|
||||
self._buffer = b''
|
||||
self._header = None
|
||||
self._wait_closed = txaio.create_future()
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
if hasattr(self, '_wait_closed'):
|
||||
return self._wait_closed
|
||||
else:
|
||||
f = txaio.create_future()
|
||||
f.set_result(True)
|
||||
return f
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.log.debug('RawSocker Asyncio: Connection lost')
|
||||
self.transport = None
|
||||
self._wait_closed.set_result(True)
|
||||
self._on_connection_lost(exc)
|
||||
|
||||
def _on_connection_lost(self, exc):
|
||||
pass
|
||||
|
||||
def protocol_error(self, msg):
|
||||
self.log.error(msg)
|
||||
self.transport.close()
|
||||
|
||||
def sendString(self, data):
|
||||
l = len(data)
|
||||
if l > self.max_length_send:
|
||||
raise ValueError('Data too big')
|
||||
header = struct.pack(self.prefix_format, len(data))
|
||||
self.transport.write(header)
|
||||
self.transport.write(data)
|
||||
|
||||
def ping(self, data):
|
||||
raise NotImplementedError()
|
||||
|
||||
def pong(self, data):
|
||||
raise NotImplementedError()
|
||||
|
||||
def data_received(self, data):
|
||||
self._buffer += data
|
||||
pos = 0
|
||||
remaining = len(self._buffer)
|
||||
while remaining >= self.prefix_length:
|
||||
# do not recalculate header if available from previous call
|
||||
if self._header:
|
||||
frame_type, frame_length = self._header
|
||||
else:
|
||||
header = self._buffer[pos:pos + self.prefix_length]
|
||||
frame_type = ord(header[0:1]) & 0b00000111
|
||||
if frame_type > FRAME_TYPE_PONG:
|
||||
self.protocol_error('Invalid frame type')
|
||||
return
|
||||
frame_length = struct.unpack(self.prefix_format, b'\0' + header[1:])[0]
|
||||
if frame_length > self.max_length:
|
||||
self.protocol_error('Frame too big')
|
||||
return
|
||||
|
||||
if remaining - self.prefix_length >= frame_length:
|
||||
self._header = None
|
||||
pos += self.prefix_length
|
||||
remaining -= self.prefix_length
|
||||
data = self._buffer[pos:pos + frame_length]
|
||||
pos += frame_length
|
||||
remaining -= frame_length
|
||||
|
||||
if frame_type == FRAME_TYPE_DATA:
|
||||
self.stringReceived(data)
|
||||
elif frame_type == FRAME_TYPE_PING:
|
||||
self.ping(data)
|
||||
elif frame_type == FRAME_TYPE_PONG:
|
||||
self.pong(data)
|
||||
else:
|
||||
# save heaader
|
||||
self._header = frame_type, frame_length
|
||||
break
|
||||
|
||||
self._buffer = self._buffer[pos:]
|
||||
|
||||
def stringReceived(self, data):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class RawSocketProtocol(PrefixProtocol):
|
||||
|
||||
def __init__(self):
|
||||
max_size = None
|
||||
if max_size:
|
||||
exp = int(math.ceil(math.log(max_size, 2))) - 9
|
||||
if exp > 15:
|
||||
raise ValueError('Maximum length is 16M')
|
||||
self.max_length = 2**(exp + 9)
|
||||
self._length_exp = exp
|
||||
else:
|
||||
self._length_exp = 15
|
||||
self.max_length = 2**24
|
||||
|
||||
def connection_made(self, transport):
|
||||
PrefixProtocol.connection_made(self, transport)
|
||||
self._handshake_done = False
|
||||
|
||||
def _on_handshake_complete(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def parse_handshake(self):
|
||||
buf = bytearray(self._buffer[:4])
|
||||
if buf[0] != MAGIC_BYTE:
|
||||
raise HandshakeError('Invalid magic byte in handshake')
|
||||
ser = buf[1] & 0x0F
|
||||
lexp = buf[1] >> 4
|
||||
self.max_length_send = 2**(lexp + 9)
|
||||
if buf[2] != 0 or buf[3] != 0:
|
||||
raise HandshakeError('Reserved bytes must be zero')
|
||||
return ser, lexp
|
||||
|
||||
def process_handshake(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def data_received(self, data):
|
||||
self.log.debug('RawSocker Asyncio: data received {data}', data=_LazyHexFormatter(data))
|
||||
if self._handshake_done:
|
||||
return PrefixProtocol.data_received(self, data)
|
||||
else:
|
||||
self._buffer += data
|
||||
if len(self._buffer) >= 4:
|
||||
try:
|
||||
self.process_handshake()
|
||||
except HandshakeError as e:
|
||||
self.protocol_error('Handshake error : {err}'.format(err=e))
|
||||
return
|
||||
self._handshake_done = True
|
||||
self._on_handshake_complete()
|
||||
data = self._buffer[4:]
|
||||
self._buffer = b''
|
||||
if data:
|
||||
PrefixProtocol.data_received(self, data)
|
||||
|
||||
|
||||
ERR_SERIALIZER_UNSUPPORTED = 1
|
||||
|
||||
ERRMAP = {
|
||||
0: "illegal (must not be used)",
|
||||
1: "serializer unsupported",
|
||||
2: "maximum message length unacceptable",
|
||||
3: "use of reserved bits (unsupported feature)",
|
||||
4: "maximum connection count reached"
|
||||
}
|
||||
|
||||
|
||||
class HandshakeError(Exception):
|
||||
def __init__(self, msg, code=0):
|
||||
Exception.__init__(self, msg if not code else msg + ' : %s' % ERRMAP.get(code))
|
||||
|
||||
|
||||
class RawSocketClientProtocol(RawSocketProtocol):
|
||||
|
||||
is_server = False
|
||||
|
||||
def check_serializer(self, ser_id):
|
||||
return True
|
||||
|
||||
def process_handshake(self):
|
||||
ser_id, err = self.parse_handshake()
|
||||
if ser_id == 0:
|
||||
raise HandshakeError('Server returned handshake error', err)
|
||||
if self.serializer_id != ser_id:
|
||||
raise HandshakeError('Server returned different serializer {0} then requested {1}'
|
||||
.format(ser_id, self.serializer_id))
|
||||
|
||||
@property
|
||||
def serializer_id(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def connection_made(self, transport):
|
||||
RawSocketProtocol.connection_made(self, transport)
|
||||
# start handshake
|
||||
hs = bytes(bytearray([MAGIC_BYTE,
|
||||
self._length_exp << 4 | self.serializer_id,
|
||||
0, 0]))
|
||||
transport.write(hs)
|
||||
self.log.debug('RawSocket Asyncio: Client handshake sent')
|
||||
|
||||
|
||||
class RawSocketServerProtocol(RawSocketProtocol):
|
||||
|
||||
is_server = True
|
||||
|
||||
def supports_serializer(self, ser_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
def process_handshake(self):
|
||||
def send_response(lexp, ser_id):
|
||||
b2 = lexp << 4 | (ser_id & 0x0f)
|
||||
self.transport.write(bytes(bytearray([MAGIC_BYTE, b2, 0, 0])))
|
||||
ser_id, _lexp = self.parse_handshake()
|
||||
if not self.supports_serializer(ser_id):
|
||||
send_response(ERR_SERIALIZER_UNSUPPORTED, 0)
|
||||
raise HandshakeError('Serializer unsupported : {ser_id}'.format(ser_id=ser_id))
|
||||
send_response(self._length_exp, ser_id)
|
||||
|
||||
|
||||
# this is transport independent part of WAMP protocol
|
||||
class WampRawSocketMixinGeneral(object):
|
||||
|
||||
def _on_handshake_complete(self):
|
||||
self.log.debug("WampRawSocketProtocol: Handshake complete")
|
||||
# RawSocket connection established. Now let the user WAMP session factory
|
||||
# create a new WAMP session and fire off session open callback.
|
||||
try:
|
||||
if self._transport_details.is_secure:
|
||||
# now that the TLS opening handshake is complete, the actual TLS channel ID
|
||||
# will be available. make sure to set it!
|
||||
channel_id = {
|
||||
'tls-unique': transport_channel_id(self.transport, self._transport_details.is_server, 'tls-unique'),
|
||||
}
|
||||
self._transport_details.channel_id = channel_id
|
||||
|
||||
self._session = self.factory._factory()
|
||||
self._session.onOpen(self)
|
||||
except Exception as e:
|
||||
# Exceptions raised in onOpen are fatal ..
|
||||
self.log.warn("WampRawSocketProtocol: ApplicationSession constructor / onOpen raised ({err})", err=e)
|
||||
self.abort()
|
||||
else:
|
||||
self.log.info("ApplicationSession started.")
|
||||
|
||||
def stringReceived(self, payload):
|
||||
self.log.debug("WampRawSocketProtocol: RX octets: {octets}", octets=_LazyHexFormatter(payload))
|
||||
try:
|
||||
for msg in self._serializer.unserialize(payload):
|
||||
self.log.debug("WampRawSocketProtocol: RX WAMP message: {msg}", msg=msg)
|
||||
self._session.onMessage(msg)
|
||||
|
||||
except ProtocolError as e:
|
||||
self.log.warn("WampRawSocketProtocol: WAMP Protocol Error ({err}) - aborting connection", err=e)
|
||||
self.abort()
|
||||
|
||||
except Exception as e:
|
||||
self.log.warn("WampRawSocketProtocol: WAMP Internal Error ({err}) - aborting connection", err=e)
|
||||
self.abort()
|
||||
|
||||
def send(self, msg):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.send`
|
||||
"""
|
||||
if self.isOpen():
|
||||
self.log.debug('{func}: TX WAMP message: {msg}', func=hltype(self.send), msg=msg)
|
||||
try:
|
||||
payload, _ = self._serializer.serialize(msg)
|
||||
except Exception as e:
|
||||
# all exceptions raised from above should be serialization errors ..
|
||||
raise SerializationError("WampRawSocketProtocol: unable to serialize WAMP application payload ({0})"
|
||||
.format(e))
|
||||
else:
|
||||
self.sendString(payload)
|
||||
self.log.debug("WampRawSocketProtocol: TX octets: {octets}", octets=_LazyHexFormatter(payload))
|
||||
else:
|
||||
raise TransportLost()
|
||||
|
||||
def isOpen(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.isOpen`
|
||||
"""
|
||||
return hasattr(self, '_session') and self._session is not None
|
||||
|
||||
|
||||
# this is asyncio dependent part of WAMP protocol
|
||||
class WampRawSocketMixinAsyncio(object):
|
||||
"""
|
||||
Base class for asyncio-based WAMP-over-RawSocket protocols.
|
||||
"""
|
||||
|
||||
def _on_connection_lost(self, exc):
|
||||
try:
|
||||
wasClean = exc is None
|
||||
self._session.onClose(wasClean)
|
||||
except Exception as e:
|
||||
# silently ignore exceptions raised here ..
|
||||
self.log.warn("WampRawSocketProtocol: ApplicationSession.onClose raised ({err})", err=e)
|
||||
self._session = None
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.close`
|
||||
"""
|
||||
if self.isOpen():
|
||||
self.transport.close()
|
||||
else:
|
||||
raise TransportLost()
|
||||
|
||||
def abort(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.abort`
|
||||
"""
|
||||
if self.isOpen():
|
||||
if hasattr(self.transport, 'abort'):
|
||||
# ProcessProtocol lacks abortConnection()
|
||||
self.transport.abort()
|
||||
else:
|
||||
self.transport.close()
|
||||
else:
|
||||
raise TransportLost()
|
||||
|
||||
|
||||
@public
|
||||
class WampRawSocketServerProtocol(WampRawSocketMixinGeneral, WampRawSocketMixinAsyncio, RawSocketServerProtocol):
|
||||
"""
|
||||
asyncio-based WAMP-over-RawSocket server protocol.
|
||||
|
||||
Implements:
|
||||
|
||||
* :class:`autobahn.wamp.interfaces.ITransport`
|
||||
"""
|
||||
|
||||
def supports_serializer(self, ser_id):
|
||||
if ser_id in self.factory._serializers:
|
||||
self._serializer = copy.copy(self.factory._serializers[ser_id])
|
||||
self.log.debug(
|
||||
"WampRawSocketProtocol: client wants to use serializer '{serializer}'",
|
||||
serializer=ser_id,
|
||||
)
|
||||
return True
|
||||
else:
|
||||
self.log.debug(
|
||||
"WampRawSocketProtocol: opening handshake - no suitable serializer found (client requested {serializer}, and we have {serializers}",
|
||||
serializer=ser_id,
|
||||
serializers=self.factory._serializers.keys(),
|
||||
)
|
||||
self.abort()
|
||||
return False
|
||||
|
||||
|
||||
@public
|
||||
class WampRawSocketClientProtocol(WampRawSocketMixinGeneral, WampRawSocketMixinAsyncio, RawSocketClientProtocol):
|
||||
"""
|
||||
asyncio-based WAMP-over-RawSocket client protocol.
|
||||
|
||||
Implements:
|
||||
|
||||
* :class:`autobahn.wamp.interfaces.ITransport`
|
||||
"""
|
||||
|
||||
@property
|
||||
def serializer_id(self):
|
||||
if not hasattr(self, '_serializer'):
|
||||
self._serializer = copy.copy(self.factory._serializer)
|
||||
return self._serializer.RAWSOCKET_SERIALIZER_ID
|
||||
|
||||
|
||||
class WampRawSocketFactory(object):
|
||||
"""
|
||||
Adapter class for asyncio-based WebSocket client and server factories.def dataReceived(self, data):
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
@public
|
||||
def __call__(self):
|
||||
proto = self.protocol()
|
||||
proto.factory = self
|
||||
return proto
|
||||
|
||||
|
||||
@public
|
||||
class WampRawSocketServerFactory(WampRawSocketFactory):
|
||||
"""
|
||||
asyncio-based WAMP-over-RawSocket server protocol factory.
|
||||
"""
|
||||
protocol = WampRawSocketServerProtocol
|
||||
|
||||
def __init__(self, factory, serializers=None):
|
||||
"""
|
||||
|
||||
:param factory: A callable that produces instances that implement
|
||||
:class:`autobahn.wamp.interfaces.ITransportHandler`
|
||||
:type factory: callable
|
||||
|
||||
:param serializers: A list of WAMP serializers to use (or ``None``
|
||||
for all available serializers).
|
||||
:type serializers: list of objects implementing
|
||||
:class:`autobahn.wamp.interfaces.ISerializer`
|
||||
"""
|
||||
if callable(factory):
|
||||
self._factory = factory
|
||||
else:
|
||||
self._factory = lambda: factory
|
||||
|
||||
# when no serializers were requested specifically, then support
|
||||
# all that are available
|
||||
if serializers is None:
|
||||
serializers = [serializer() for serializer in get_serializers()]
|
||||
|
||||
if not serializers:
|
||||
raise Exception("could not import any WAMP serializers")
|
||||
|
||||
self._serializers = {ser.RAWSOCKET_SERIALIZER_ID: ser for ser in serializers}
|
||||
|
||||
|
||||
@public
|
||||
class WampRawSocketClientFactory(WampRawSocketFactory):
|
||||
"""
|
||||
asyncio-based WAMP-over-RawSocket client factory.
|
||||
"""
|
||||
protocol = WampRawSocketClientProtocol
|
||||
|
||||
def __init__(self, factory, serializer=None):
|
||||
"""
|
||||
|
||||
:param factory: A callable that produces instances that implement
|
||||
:class:`autobahn.wamp.interfaces.ITransportHandler`
|
||||
:type factory: callable
|
||||
|
||||
:param serializer: The WAMP serializer to use (or ``None`` for
|
||||
"best" serializer, chosen as the first serializer available from
|
||||
this list: CBOR, MessagePack, UBJSON, JSON).
|
||||
:type serializer: object implementing :class:`autobahn.wamp.interfaces.ISerializer`
|
||||
"""
|
||||
if callable(factory):
|
||||
self._factory = factory
|
||||
else:
|
||||
self._factory = lambda: factory
|
||||
|
||||
# when no serializer was requested specifically, use the first
|
||||
# one available
|
||||
if serializer is None:
|
||||
serializers = get_serializers()
|
||||
if serializers:
|
||||
serializer = serializers[0]()
|
||||
|
||||
if serializer is None:
|
||||
raise Exception("could not import any WAMP serializer")
|
||||
|
||||
self._serializer = serializer
|
||||
@@ -0,0 +1,26 @@
|
||||
**DO NOT ADD a __init__.py file in this directory**
|
||||
|
||||
"Why not?" you ask; read on!
|
||||
|
||||
1. If we're running asyncio tests, we can't ever call txaio.use_twisted()
|
||||
|
||||
2. If we're running twisted tests, we can't ever call txaio.use_asycnio()...
|
||||
|
||||
3. ...and these are decided/called at import time
|
||||
|
||||
4. so: we can't *import* any of the autobahn.asyncio.* modules if we're
|
||||
running twisted tests (or vice versa)
|
||||
|
||||
5. ...but test-runners (py.test and trial) import things automagically
|
||||
(to "discover" tests)
|
||||
|
||||
6. We use py.test to run asyncio tests; see "setup.cfg" where we tell
|
||||
it "norecursedirs = autobahn/twisted/*" so it doesn't ipmort twisted
|
||||
stuff (and hence call txaio.use_twisted())
|
||||
|
||||
7. We use trial to run twisted tests; the lack of __init__ in here
|
||||
stops it from trying to import this (and hence the parent
|
||||
package). (The only files matching test_*.py are in this
|
||||
directory.)
|
||||
|
||||
*Therefore*, we don't put a __init__ file in this directory.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,235 @@
|
||||
import pytest
|
||||
import os
|
||||
|
||||
from unittest.mock import Mock, call
|
||||
|
||||
from autobahn.asyncio.rawsocket import PrefixProtocol, RawSocketClientProtocol, RawSocketServerProtocol, \
|
||||
WampRawSocketClientFactory, WampRawSocketServerFactory
|
||||
from autobahn.asyncio.util import get_serializers
|
||||
from autobahn.wamp import message
|
||||
from autobahn.wamp.types import TransportDetails
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only')
|
||||
def test_sers(event_loop):
|
||||
serializers = get_serializers()
|
||||
assert len(serializers) > 0
|
||||
m = serializers[0]().serialize(message.Abort('close'))
|
||||
assert m
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only')
|
||||
def test_prefix(event_loop):
|
||||
p = PrefixProtocol()
|
||||
transport = Mock()
|
||||
receiver = Mock()
|
||||
p.stringReceived = receiver
|
||||
p.connection_made(transport)
|
||||
small_msg = b'\x00\x00\x00\x04abcd'
|
||||
p.data_received(small_msg)
|
||||
receiver.assert_called_once_with(b'abcd')
|
||||
assert len(p._buffer) == 0
|
||||
|
||||
p.sendString(b'abcd')
|
||||
|
||||
# print(transport.write.call_args_list)
|
||||
transport.write.assert_has_calls([call(b'\x00\x00\x00\x04'), call(b'abcd')])
|
||||
|
||||
transport.reset_mock()
|
||||
receiver.reset_mock()
|
||||
|
||||
big_msg = b'\x00\x00\x00\x0C' + b'0123456789AB'
|
||||
|
||||
p.data_received(big_msg[0:2])
|
||||
assert not receiver.called
|
||||
|
||||
p.data_received(big_msg[2:6])
|
||||
assert not receiver.called
|
||||
|
||||
p.data_received(big_msg[6:11])
|
||||
assert not receiver.called
|
||||
|
||||
p.data_received(big_msg[11:16])
|
||||
receiver.assert_called_once_with(b'0123456789AB')
|
||||
|
||||
transport.reset_mock()
|
||||
receiver.reset_mock()
|
||||
|
||||
two_messages = b'\x00\x00\x00\x04' + b'abcd' + b'\x00\x00\x00\x05' + b'12345' + b'\x00'
|
||||
p.data_received(two_messages)
|
||||
receiver.assert_has_calls([call(b'abcd'), call(b'12345')])
|
||||
assert p._buffer == b'\x00'
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only')
|
||||
def test_is_closed(event_loop):
|
||||
class CP(RawSocketClientProtocol):
|
||||
@property
|
||||
def serializer_id(self):
|
||||
return 1
|
||||
client = CP()
|
||||
|
||||
on_hs = Mock()
|
||||
transport = Mock()
|
||||
receiver = Mock()
|
||||
client.stringReceived = receiver
|
||||
client._on_handshake_complete = on_hs
|
||||
assert client.is_closed.done()
|
||||
client.connection_made(transport)
|
||||
assert not client.is_closed.done()
|
||||
client.connection_lost(None)
|
||||
|
||||
assert client.is_closed.done()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only')
|
||||
def test_raw_socket_server1(event_loop):
|
||||
|
||||
server = RawSocketServerProtocol()
|
||||
ser = Mock(return_value=True)
|
||||
on_hs = Mock()
|
||||
transport = Mock()
|
||||
receiver = Mock()
|
||||
server.supports_serializer = ser
|
||||
server.stringReceived = receiver
|
||||
server._on_handshake_complete = on_hs
|
||||
server.stringReceived = receiver
|
||||
|
||||
server.connection_made(transport)
|
||||
hs = b'\x7F\xF1\x00\x00' + b'\x00\x00\x00\x04abcd'
|
||||
server.data_received(hs)
|
||||
|
||||
ser.assert_called_once_with(1)
|
||||
on_hs.assert_called_once_with()
|
||||
assert transport.write.called
|
||||
transport.write.assert_called_once_with(b'\x7F\xF1\x00\x00')
|
||||
assert not transport.close.called
|
||||
receiver.assert_called_once_with(b'abcd')
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only')
|
||||
def test_raw_socket_server_errors(event_loop):
|
||||
|
||||
server = RawSocketServerProtocol()
|
||||
ser = Mock(return_value=True)
|
||||
on_hs = Mock()
|
||||
transport = Mock()
|
||||
receiver = Mock()
|
||||
server.supports_serializer = ser
|
||||
server.stringReceived = receiver
|
||||
server._on_handshake_complete = on_hs
|
||||
server.stringReceived = receiver
|
||||
server.connection_made(transport)
|
||||
server.data_received(b'abcdef')
|
||||
transport.close.assert_called_once_with()
|
||||
|
||||
server = RawSocketServerProtocol()
|
||||
ser = Mock(return_value=False)
|
||||
on_hs = Mock()
|
||||
transport = Mock(spec_set=('close', 'write', 'get_extra_info'))
|
||||
receiver = Mock()
|
||||
server.supports_serializer = ser
|
||||
server.stringReceived = receiver
|
||||
server._on_handshake_complete = on_hs
|
||||
server.stringReceived = receiver
|
||||
server.connection_made(transport)
|
||||
server.data_received(b'\x7F\xF1\x00\x00')
|
||||
transport.close.assert_called_once_with()
|
||||
transport.write.assert_called_once_with(b'\x7F\x10\x00\x00')
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only')
|
||||
def test_raw_socket_client1(event_loop):
|
||||
class CP(RawSocketClientProtocol):
|
||||
@property
|
||||
def serializer_id(self):
|
||||
return 1
|
||||
client = CP()
|
||||
|
||||
on_hs = Mock()
|
||||
transport = Mock()
|
||||
receiver = Mock()
|
||||
client.stringReceived = receiver
|
||||
client._on_handshake_complete = on_hs
|
||||
|
||||
client.connection_made(transport)
|
||||
client.data_received(b'\x7F\xF1\x00\x00' + b'\x00\x00\x00\x04abcd')
|
||||
on_hs.assert_called_once_with()
|
||||
assert transport.write.called
|
||||
transport.write.called_one_with(b'\x7F\xF1\x00\x00')
|
||||
assert not transport.close.called
|
||||
receiver.assert_called_once_with(b'abcd')
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only')
|
||||
def test_raw_socket_client_error(event_loop):
|
||||
class CP(RawSocketClientProtocol):
|
||||
@property
|
||||
def serializer_id(self):
|
||||
return 1
|
||||
client = CP()
|
||||
|
||||
on_hs = Mock()
|
||||
transport = Mock(spec_set=('close', 'write', 'get_extra_info'))
|
||||
receiver = Mock()
|
||||
client.stringReceived = receiver
|
||||
client._on_handshake_complete = on_hs
|
||||
client.connection_made(transport)
|
||||
|
||||
client.data_received(b'\x7F\xF1\x00\x01')
|
||||
transport.close.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only')
|
||||
def test_wamp_server(event_loop):
|
||||
transport = Mock(spec_set=('abort', 'close', 'write', 'get_extra_info'))
|
||||
transport.write = Mock(side_effect=lambda m: messages.append(m))
|
||||
server = Mock(spec=['onOpen', 'onMessage'])
|
||||
|
||||
def fact_server():
|
||||
return server
|
||||
|
||||
messages = []
|
||||
|
||||
proto = WampRawSocketServerFactory(fact_server)()
|
||||
proto.connection_made(transport)
|
||||
assert proto.transport_details.is_server is True
|
||||
assert proto.transport_details.channel_framing == TransportDetails.CHANNEL_FRAMING_RAWSOCKET
|
||||
assert proto.factory._serializers
|
||||
s = proto.factory._serializers[1].RAWSOCKET_SERIALIZER_ID
|
||||
proto.data_received(bytes(bytearray([0x7F, 0xF0 | s, 0, 0])))
|
||||
assert proto._serializer
|
||||
server.onOpen.assert_called_once_with(proto)
|
||||
|
||||
proto.send(message.Abort('close'))
|
||||
for d in messages[1:]:
|
||||
proto.data_received(d)
|
||||
assert server.onMessage.called
|
||||
assert isinstance(server.onMessage.call_args[0][0], message.Abort)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only')
|
||||
def test_wamp_client(event_loop):
|
||||
transport = Mock(spec_set=('abort', 'close', 'write', 'get_extra_info'))
|
||||
transport.write = Mock(side_effect=lambda m: messages.append(m))
|
||||
client = Mock(spec=['onOpen', 'onMessage'])
|
||||
|
||||
def fact_client():
|
||||
return client
|
||||
|
||||
messages = []
|
||||
|
||||
proto = WampRawSocketClientFactory(fact_client)()
|
||||
proto.connection_made(transport)
|
||||
assert proto.transport_details.is_server is False
|
||||
assert proto.transport_details.channel_framing == TransportDetails.CHANNEL_FRAMING_RAWSOCKET
|
||||
assert proto._serializer
|
||||
s = proto._serializer.RAWSOCKET_SERIALIZER_ID
|
||||
proto.data_received(bytes(bytearray([0x7F, 0xF0 | s, 0, 0])))
|
||||
client.onOpen.assert_called_once_with(proto)
|
||||
|
||||
proto.send(message.Abort('close'))
|
||||
for d in messages[1:]:
|
||||
proto.data_received(d)
|
||||
assert client.onMessage.called
|
||||
assert isinstance(client.onMessage.call_args[0][0], message.Abort)
|
||||
@@ -0,0 +1,71 @@
|
||||
import os
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
import txaio
|
||||
|
||||
# because py.test tries to collect it as a test-case
|
||||
from unittest.mock import Mock
|
||||
|
||||
from autobahn.asyncio.websocket import WebSocketServerFactory
|
||||
|
||||
|
||||
async def echo_async(what, when):
|
||||
await asyncio.sleep(when)
|
||||
return what
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only')
|
||||
@pytest.mark.asyncio
|
||||
async def test_echo_async():
|
||||
assert 'Hello!' == await echo_async('Hello!', 0)
|
||||
|
||||
|
||||
# @pytest.mark.asyncio(forbid_global_loop=True)
|
||||
@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only')
|
||||
def test_websocket_custom_loop(event_loop):
|
||||
factory = WebSocketServerFactory(loop=event_loop)
|
||||
server = factory()
|
||||
transport = Mock()
|
||||
server.connection_made(transport)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only')
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_on_connect_server(event_loop):
|
||||
|
||||
num = 42
|
||||
done = txaio.create_future()
|
||||
values = []
|
||||
|
||||
async def foo(x):
|
||||
await asyncio.sleep(1)
|
||||
return x * x
|
||||
|
||||
async def on_connect(req):
|
||||
v = await foo(num)
|
||||
values.append(v)
|
||||
txaio.resolve(done, req)
|
||||
|
||||
factory = WebSocketServerFactory()
|
||||
server = factory()
|
||||
server.onConnect = on_connect
|
||||
transport = Mock()
|
||||
|
||||
server.connection_made(transport)
|
||||
server.data = b'\r\n'.join([
|
||||
b'GET /ws HTTP/1.1',
|
||||
b'Host: www.example.com',
|
||||
b'Sec-WebSocket-Version: 13',
|
||||
b'Origin: http://www.example.com.malicious.com',
|
||||
b'Sec-WebSocket-Extensions: permessage-deflate',
|
||||
b'Sec-WebSocket-Key: tXAxWFUqnhi86Ajj7dRY5g==',
|
||||
b'Connection: keep-alive, Upgrade',
|
||||
b'Upgrade: websocket',
|
||||
b'\r\n', # last string doesn't get a \r\n from join()
|
||||
])
|
||||
server.processHandshake()
|
||||
await done
|
||||
|
||||
assert len(values) == 1
|
||||
assert values[0] == num * num
|
||||
@@ -0,0 +1,134 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 txaio.testutil import replace_loop
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
from autobahn.asyncio.wamp import ApplicationRunner
|
||||
|
||||
|
||||
class TestApplicationRunner(unittest.TestCase):
|
||||
"""
|
||||
Test the autobahn.asyncio.wamp.ApplicationRunner class.
|
||||
"""
|
||||
def _assertRaisesRegex(self, exception, error, *args, **kw):
|
||||
try:
|
||||
self.assertRaisesRegex
|
||||
except AttributeError:
|
||||
f = self.assertRaisesRegexp
|
||||
else:
|
||||
f = self.assertRaisesRegex
|
||||
f(exception, error, *args, **kw)
|
||||
|
||||
def test_explicit_SSLContext(self):
|
||||
"""
|
||||
Ensure that loop.create_connection is called with the exact SSL
|
||||
context object that is passed (as ssl) to the __init__ method of
|
||||
ApplicationRunner.
|
||||
"""
|
||||
with replace_loop(Mock()) as loop:
|
||||
with patch.object(asyncio, 'get_event_loop', return_value=loop):
|
||||
loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
|
||||
ssl = {}
|
||||
runner = ApplicationRunner('ws://127.0.0.1:8080/ws', 'realm',
|
||||
ssl=ssl)
|
||||
runner.run('_unused_')
|
||||
self.assertIs(ssl, loop.create_connection.call_args[1]['ssl'])
|
||||
|
||||
def test_omitted_SSLContext_insecure(self):
|
||||
"""
|
||||
Ensure that loop.create_connection is called with ssl=False
|
||||
if no ssl argument is passed to the __init__ method of
|
||||
ApplicationRunner and the websocket URL starts with "ws:".
|
||||
"""
|
||||
with replace_loop(Mock()) as loop:
|
||||
with patch.object(asyncio, 'get_event_loop', return_value=loop):
|
||||
loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
|
||||
runner = ApplicationRunner('ws://127.0.0.1:8080/ws', 'realm')
|
||||
runner.run('_unused_')
|
||||
self.assertIs(False, loop.create_connection.call_args[1]['ssl'])
|
||||
|
||||
def test_omitted_SSLContext_secure(self):
|
||||
"""
|
||||
Ensure that loop.create_connection is called with ssl=True
|
||||
if no ssl argument is passed to the __init__ method of
|
||||
ApplicationRunner and the websocket URL starts with "wss:".
|
||||
"""
|
||||
with replace_loop(Mock()) as loop:
|
||||
with patch.object(asyncio, 'get_event_loop', return_value=loop):
|
||||
loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
|
||||
runner = ApplicationRunner('wss://127.0.0.1:8080/wss', 'realm')
|
||||
runner.run(self.fail)
|
||||
self.assertIs(True, loop.create_connection.call_args[1]['ssl'])
|
||||
|
||||
def test_conflict_SSL_True_with_ws_url(self):
|
||||
"""
|
||||
ApplicationRunner must raise an exception if given an ssl value of True
|
||||
but only a "ws:" URL.
|
||||
"""
|
||||
with replace_loop(Mock()) as loop:
|
||||
loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
|
||||
runner = ApplicationRunner('ws://127.0.0.1:8080/wss', 'realm',
|
||||
ssl=True)
|
||||
error = (r'^ssl argument value passed to ApplicationRunner '
|
||||
r'conflicts with the "ws:" prefix of the url '
|
||||
r'argument\. Did you mean to use "wss:"\?$')
|
||||
self._assertRaisesRegex(Exception, error, runner.run, '_unused_')
|
||||
|
||||
def test_conflict_SSLContext_with_ws_url(self):
|
||||
"""
|
||||
ApplicationRunner must raise an exception if given an ssl value that is
|
||||
an instance of SSLContext, but only a "ws:" URL.
|
||||
"""
|
||||
import ssl
|
||||
try:
|
||||
# Try to create an SSLContext, to be as rigorous as we can be
|
||||
# by avoiding making assumptions about the ApplicationRunner
|
||||
# implementation. If we happen to be on a Python that has no
|
||||
# SSLContext, we pass ssl=True, which will simply cause this
|
||||
# test to degenerate to the behavior of
|
||||
# test_conflict_SSL_True_with_ws_url (above). In fact, at the
|
||||
# moment (2015-05-10), none of this matters because the
|
||||
# ApplicationRunner implementation does not check to require
|
||||
# that its ssl argument is either a bool or an SSLContext. But
|
||||
# that may change, so we should be careful.
|
||||
ssl.create_default_context
|
||||
except AttributeError:
|
||||
context = True
|
||||
else:
|
||||
context = ssl.create_default_context()
|
||||
|
||||
with replace_loop(Mock()) as loop:
|
||||
loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
|
||||
runner = ApplicationRunner('ws://127.0.0.1:8080/wss', 'realm',
|
||||
ssl=context)
|
||||
error = (r'^ssl argument value passed to ApplicationRunner '
|
||||
r'conflicts with the "ws:" prefix of the url '
|
||||
r'argument\. Did you mean to use "wss:"\?$')
|
||||
self._assertRaisesRegex(Exception, error, runner.run, '_unused_')
|
||||
147
backend/lib/python3.12/site-packages/autobahn/asyncio/util.py
Normal file
147
backend/lib/python3.12/site-packages/autobahn/asyncio/util.py
Normal file
@@ -0,0 +1,147 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 hashlib
|
||||
from subprocess import Popen
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
from asyncio import sleep # noqa
|
||||
|
||||
from autobahn.wamp.types import TransportDetails
|
||||
|
||||
__all = (
|
||||
'sleep',
|
||||
'peer2str',
|
||||
'transport_channel_id',
|
||||
'create_transport_details',
|
||||
)
|
||||
|
||||
|
||||
def transport_channel_id(transport, is_server: bool, channel_id_type: Optional[str] = None) -> bytes:
|
||||
"""
|
||||
Application-layer user authentication protocols are vulnerable to generic
|
||||
credential forwarding attacks, where an authentication credential sent by
|
||||
a client C to a server M may then be used by M to impersonate C at another
|
||||
server S. To prevent such credential forwarding attacks, modern authentication
|
||||
protocols rely on channel bindings. For example, WAMP-cryptosign can use
|
||||
the tls-unique channel identifier provided by the TLS layer to strongly bind
|
||||
authentication credentials to the underlying channel, so that a credential
|
||||
received on one TLS channel cannot be forwarded on another.
|
||||
|
||||
:param transport: The asyncio TLS transport to extract the TLS channel ID from.
|
||||
:param is_server: Flag indicating the transport is for a server.
|
||||
:param channel_id_type: TLS channel ID type, currently only "tls-unique" is supported.
|
||||
:returns: The TLS channel id (32 bytes).
|
||||
"""
|
||||
if channel_id_type is None:
|
||||
return b'\x00' * 32
|
||||
|
||||
# ssl.CHANNEL_BINDING_TYPES
|
||||
if channel_id_type not in ['tls-unique']:
|
||||
raise Exception("invalid channel ID type {}".format(channel_id_type))
|
||||
|
||||
ssl_obj = transport.get_extra_info('ssl_object')
|
||||
if ssl_obj is None:
|
||||
raise Exception("TLS transport channel_id for tls-unique requested, but ssl_obj not found on transport")
|
||||
|
||||
if not hasattr(ssl_obj, 'get_channel_binding'):
|
||||
raise Exception("TLS transport channel_id for tls-unique requested, but get_channel_binding not found on ssl_obj")
|
||||
|
||||
# https://python.readthedocs.io/en/latest/library/ssl.html#ssl.SSLSocket.get_channel_binding
|
||||
# https://tools.ietf.org/html/rfc5929.html
|
||||
tls_finished_msg: bytes = ssl_obj.get_channel_binding(cb_type='tls-unique')
|
||||
|
||||
if type(tls_finished_msg) != bytes:
|
||||
return b'\x00' * 32
|
||||
else:
|
||||
m = hashlib.sha256()
|
||||
m.update(tls_finished_msg)
|
||||
channel_id = m.digest()
|
||||
return channel_id
|
||||
|
||||
|
||||
def peer2str(transport: asyncio.transports.BaseTransport) -> str:
|
||||
# https://docs.python.org/3.9/library/asyncio-protocol.html?highlight=get_extra_info#asyncio.BaseTransport.get_extra_info
|
||||
# https://docs.python.org/3.9/library/socket.html#socket.socket.getpeername
|
||||
try:
|
||||
peer = transport.get_extra_info('peername')
|
||||
if isinstance(peer, tuple):
|
||||
ip_ver = 4 if len(peer) == 2 else 6
|
||||
return "tcp{2}:{0}:{1}".format(peer[0], peer[1], ip_ver)
|
||||
elif isinstance(peer, str):
|
||||
return "unix:{0}".format(peer)
|
||||
else:
|
||||
return "?:{0}".format(peer)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
proc: Popen = transport.get_extra_info('subprocess')
|
||||
# return 'process:{}'.format(transport.pid)
|
||||
return 'process:{}'.format(proc.pid)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
pipe = transport.get_extra_info('pipe')
|
||||
return 'pipe:{}'.format(pipe)
|
||||
except:
|
||||
pass
|
||||
|
||||
# gracefully fallback if we can't map the peer's transport
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def get_serializers():
|
||||
from autobahn.wamp import serializer
|
||||
|
||||
serializers = ['CBORSerializer', 'MsgPackSerializer', 'UBJSONSerializer', 'JsonSerializer']
|
||||
serializers = list(filter(lambda x: x, map(lambda s: getattr(serializer, s) if hasattr(serializer, s)
|
||||
else None, serializers)))
|
||||
return serializers
|
||||
|
||||
|
||||
def create_transport_details(transport, is_server: bool) -> TransportDetails:
|
||||
# Internal helper. Base class calls this to create a TransportDetails
|
||||
peer = peer2str(transport)
|
||||
|
||||
# https://docs.python.org/3.9/library/asyncio-protocol.html?highlight=get_extra_info#asyncio.BaseTransport.get_extra_info
|
||||
is_secure = transport.get_extra_info('peercert', None) is not None
|
||||
if is_secure:
|
||||
channel_id = {
|
||||
'tls-unique': transport_channel_id(transport, is_server, 'tls-unique'),
|
||||
}
|
||||
channel_type = TransportDetails.CHANNEL_TYPE_TLS
|
||||
peer_cert = None
|
||||
else:
|
||||
channel_id = {}
|
||||
channel_type = TransportDetails.CHANNEL_TYPE_TCP
|
||||
peer_cert = None
|
||||
channel_framing = TransportDetails.CHANNEL_FRAMING_WEBSOCKET
|
||||
|
||||
return TransportDetails(channel_type=channel_type, channel_framing=channel_framing,
|
||||
peer=peer, is_server=is_server, is_secure=is_secure,
|
||||
channel_id=channel_id, peer_cert=peer_cert)
|
||||
309
backend/lib/python3.12/site-packages/autobahn/asyncio/wamp.py
Normal file
309
backend/lib/python3.12/site-packages/autobahn/asyncio/wamp.py
Normal file
@@ -0,0 +1,309 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 asyncio
|
||||
import signal
|
||||
|
||||
import txaio
|
||||
txaio.use_asyncio() # noqa
|
||||
|
||||
from autobahn.util import public
|
||||
from autobahn.wamp import protocol
|
||||
from autobahn.wamp.types import ComponentConfig
|
||||
|
||||
from autobahn.websocket.util import parse_url as parse_ws_url
|
||||
from autobahn.rawsocket.util import parse_url as parse_rs_url
|
||||
|
||||
from autobahn.asyncio.websocket import WampWebSocketClientFactory
|
||||
from autobahn.asyncio.rawsocket import WampRawSocketClientFactory
|
||||
|
||||
from autobahn.websocket.compress import PerMessageDeflateOffer, \
|
||||
PerMessageDeflateResponse, PerMessageDeflateResponseAccept
|
||||
|
||||
from autobahn.wamp.interfaces import ITransportHandler, ISession
|
||||
|
||||
__all__ = (
|
||||
'ApplicationSession',
|
||||
'ApplicationSessionFactory',
|
||||
'ApplicationRunner'
|
||||
)
|
||||
|
||||
|
||||
@public
|
||||
class ApplicationSession(protocol.ApplicationSession):
|
||||
"""
|
||||
WAMP application session for asyncio-based applications.
|
||||
|
||||
Implements:
|
||||
|
||||
* ``autobahn.wamp.interfaces.ITransportHandler``
|
||||
* ``autobahn.wamp.interfaces.ISession``
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
|
||||
ITransportHandler.register(ApplicationSession)
|
||||
|
||||
# ISession.register collides with the abc.ABCMeta.register method
|
||||
ISession.abc_register(ApplicationSession)
|
||||
|
||||
|
||||
class ApplicationSessionFactory(protocol.ApplicationSessionFactory):
|
||||
"""
|
||||
WAMP application session factory for asyncio-based applications.
|
||||
"""
|
||||
|
||||
session: ApplicationSession = ApplicationSession
|
||||
"""
|
||||
The application session class this application session factory will use.
|
||||
Defaults to :class:`autobahn.asyncio.wamp.ApplicationSession`.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
|
||||
@public
|
||||
class ApplicationRunner(object):
|
||||
"""
|
||||
This class is a convenience tool mainly for development and quick hosting
|
||||
of WAMP application components.
|
||||
|
||||
It can host a WAMP application component in a WAMP-over-WebSocket client
|
||||
connecting to a WAMP router.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __init__(self,
|
||||
url,
|
||||
realm=None,
|
||||
extra=None,
|
||||
serializers=None,
|
||||
ssl=None,
|
||||
proxy=None,
|
||||
headers=None):
|
||||
"""
|
||||
|
||||
:param url: The WebSocket URL of the WAMP router to connect to (e.g. `ws://somehost.com:8090/somepath`)
|
||||
:type url: str
|
||||
|
||||
:param realm: The WAMP realm to join the application session to.
|
||||
:type realm: str
|
||||
|
||||
:param extra: Optional extra configuration to forward to the application component.
|
||||
:type extra: dict
|
||||
|
||||
:param serializers: A list of WAMP serializers to use (or None for default serializers).
|
||||
Serializers must implement :class:`autobahn.wamp.interfaces.ISerializer`.
|
||||
:type serializers: list
|
||||
|
||||
:param ssl: An (optional) SSL context instance or a bool. See
|
||||
the documentation for the `loop.create_connection` asyncio
|
||||
method, to which this value is passed as the ``ssl``
|
||||
keyword parameter.
|
||||
:type ssl: :class:`ssl.SSLContext` or bool
|
||||
|
||||
:param proxy: Explicit proxy server to use; a dict with ``host`` and ``port`` keys
|
||||
:type proxy: dict or None
|
||||
|
||||
:param headers: Additional headers to send (only applies to WAMP-over-WebSocket).
|
||||
:type headers: dict
|
||||
"""
|
||||
assert(type(url) == str)
|
||||
assert(realm is None or type(realm) == str)
|
||||
assert(extra is None or type(extra) == dict)
|
||||
assert(headers is None or type(headers) == dict)
|
||||
assert(proxy is None or type(proxy) == dict)
|
||||
self.url = url
|
||||
self.realm = realm
|
||||
self.extra = extra or dict()
|
||||
self.serializers = serializers
|
||||
self.ssl = ssl
|
||||
self.proxy = proxy
|
||||
self.headers = headers
|
||||
|
||||
@public
|
||||
def stop(self):
|
||||
"""
|
||||
Stop reconnecting, if auto-reconnecting was enabled.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@public
|
||||
def run(self, make, start_loop=True, log_level='info'):
|
||||
"""
|
||||
Run the application component. Under the hood, this runs the event
|
||||
loop (unless `start_loop=False` is passed) so won't return
|
||||
until the program is done.
|
||||
|
||||
:param make: A factory that produces instances of :class:`autobahn.asyncio.wamp.ApplicationSession`
|
||||
when called with an instance of :class:`autobahn.wamp.types.ComponentConfig`.
|
||||
:type make: callable
|
||||
|
||||
:param start_loop: When ``True`` (the default) this method
|
||||
start a new asyncio loop.
|
||||
:type start_loop: bool
|
||||
|
||||
:returns: None is returned, unless you specify
|
||||
`start_loop=False` in which case the coroutine from calling
|
||||
`loop.create_connection()` is returned. This will yield the
|
||||
(transport, protocol) pair.
|
||||
"""
|
||||
if callable(make):
|
||||
def create():
|
||||
cfg = ComponentConfig(self.realm, self.extra)
|
||||
try:
|
||||
session = make(cfg)
|
||||
except Exception as e:
|
||||
self.log.error('ApplicationSession could not be instantiated: {}'.format(e))
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
loop.stop()
|
||||
raise
|
||||
else:
|
||||
return session
|
||||
else:
|
||||
create = make
|
||||
|
||||
if self.url.startswith('rs'):
|
||||
# try to parse RawSocket URL ..
|
||||
isSecure, host, port = parse_rs_url(self.url)
|
||||
|
||||
# use the first configured serializer if any (which means, auto-choose "best")
|
||||
serializer = self.serializers[0] if self.serializers else None
|
||||
|
||||
# create a WAMP-over-RawSocket transport client factory
|
||||
transport_factory = WampRawSocketClientFactory(create, serializer=serializer)
|
||||
|
||||
else:
|
||||
# try to parse WebSocket URL ..
|
||||
isSecure, host, port, resource, path, params = parse_ws_url(self.url)
|
||||
|
||||
# create a WAMP-over-WebSocket transport client factory
|
||||
transport_factory = WampWebSocketClientFactory(create, url=self.url, serializers=self.serializers, proxy=self.proxy, headers=self.headers)
|
||||
|
||||
# client WebSocket settings - similar to:
|
||||
# - http://crossbar.io/docs/WebSocket-Compression/#production-settings
|
||||
# - http://crossbar.io/docs/WebSocket-Options/#production-settings
|
||||
|
||||
# The permessage-deflate extensions offered to the server ..
|
||||
offers = [PerMessageDeflateOffer()]
|
||||
|
||||
# Function to accept permessage_delate responses from the server ..
|
||||
def accept(response):
|
||||
if isinstance(response, PerMessageDeflateResponse):
|
||||
return PerMessageDeflateResponseAccept(response)
|
||||
|
||||
# set WebSocket options for all client connections
|
||||
transport_factory.setProtocolOptions(maxFramePayloadSize=1048576,
|
||||
maxMessagePayloadSize=1048576,
|
||||
autoFragmentSize=65536,
|
||||
failByDrop=False,
|
||||
openHandshakeTimeout=2.5,
|
||||
closeHandshakeTimeout=1.,
|
||||
tcpNoDelay=True,
|
||||
autoPingInterval=10.,
|
||||
autoPingTimeout=5.,
|
||||
autoPingSize=12,
|
||||
perMessageCompressionOffers=offers,
|
||||
perMessageCompressionAccept=accept)
|
||||
# SSL context for client connection
|
||||
if self.ssl is None:
|
||||
ssl = isSecure
|
||||
else:
|
||||
if self.ssl and not isSecure:
|
||||
raise RuntimeError(
|
||||
'ssl argument value passed to %s conflicts with the "ws:" '
|
||||
'prefix of the url argument. Did you mean to use "wss:"?' %
|
||||
self.__class__.__name__)
|
||||
ssl = self.ssl
|
||||
|
||||
# start the client connection
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_closed() and start_loop:
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
loop = asyncio.get_event_loop()
|
||||
if hasattr(transport_factory, 'loop'):
|
||||
transport_factory.loop = loop
|
||||
|
||||
# assure we are using asyncio
|
||||
# txaio.use_asyncio()
|
||||
assert txaio._explicit_framework == 'asyncio'
|
||||
|
||||
txaio.config.loop = loop
|
||||
coro = loop.create_connection(transport_factory, host, port, ssl=ssl)
|
||||
|
||||
# start a asyncio loop
|
||||
if not start_loop:
|
||||
return coro
|
||||
else:
|
||||
(transport, protocol) = loop.run_until_complete(coro)
|
||||
|
||||
# start logging
|
||||
txaio.start_logging(level=log_level)
|
||||
|
||||
try:
|
||||
loop.add_signal_handler(signal.SIGTERM, loop.stop)
|
||||
except NotImplementedError:
|
||||
# signals are not available on Windows
|
||||
pass
|
||||
|
||||
# 4) now enter the asyncio event loop
|
||||
try:
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
# wait until we send Goodbye if user hit ctrl-c
|
||||
# (done outside this except so SIGTERM gets the same handling)
|
||||
pass
|
||||
|
||||
# give Goodbye message a chance to go through, if we still
|
||||
# have an active session
|
||||
if protocol._session:
|
||||
loop.run_until_complete(protocol._session.leave())
|
||||
|
||||
loop.close()
|
||||
|
||||
|
||||
# new API
|
||||
class Session(protocol._SessionShim):
|
||||
# XXX these methods are redundant, but put here for possibly
|
||||
# better clarity; maybe a bad idea.
|
||||
|
||||
def on_welcome(self, welcome_msg):
|
||||
pass
|
||||
|
||||
def on_join(self, details):
|
||||
pass
|
||||
|
||||
def on_leave(self, details):
|
||||
self.disconnect()
|
||||
|
||||
def on_connect(self):
|
||||
self.join(self.config.realm)
|
||||
|
||||
def on_disconnect(self):
|
||||
pass
|
||||
@@ -0,0 +1,386 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 asyncio
|
||||
from asyncio import iscoroutine
|
||||
from asyncio import Future
|
||||
from collections import deque
|
||||
from typing import Optional
|
||||
|
||||
import txaio
|
||||
txaio.use_asyncio() # noqa
|
||||
|
||||
from autobahn.util import public, hltype
|
||||
from autobahn.asyncio.util import create_transport_details, transport_channel_id
|
||||
from autobahn.wamp import websocket
|
||||
from autobahn.websocket import protocol
|
||||
|
||||
__all__ = (
|
||||
'WebSocketServerProtocol',
|
||||
'WebSocketClientProtocol',
|
||||
'WebSocketServerFactory',
|
||||
'WebSocketClientFactory',
|
||||
'WampWebSocketServerProtocol',
|
||||
'WampWebSocketClientProtocol',
|
||||
'WampWebSocketServerFactory',
|
||||
'WampWebSocketClientFactory',
|
||||
)
|
||||
|
||||
|
||||
def yields(value):
|
||||
"""
|
||||
Returns ``True`` iff the value yields.
|
||||
|
||||
.. seealso:: http://stackoverflow.com/questions/20730248/maybedeferred-analog-with-asyncio
|
||||
"""
|
||||
return isinstance(value, Future) or iscoroutine(value)
|
||||
|
||||
|
||||
class WebSocketAdapterProtocol(asyncio.Protocol):
|
||||
"""
|
||||
Adapter class for asyncio-based WebSocket client and server protocols.
|
||||
"""
|
||||
log = txaio.make_logger()
|
||||
|
||||
peer: Optional[str] = None
|
||||
is_server: Optional[bool] = None
|
||||
|
||||
def connection_made(self, transport):
|
||||
# asyncio networking framework entry point, called by asyncio
|
||||
# when the connection is established (either a client or a server)
|
||||
self.log.debug('{func}(transport={transport})', func=hltype(self.connection_made),
|
||||
transport=transport)
|
||||
|
||||
self.transport = transport
|
||||
|
||||
# determine preliminary transport details (what is know at this point)
|
||||
self._transport_details = create_transport_details(self.transport, self.is_server)
|
||||
|
||||
# backward compatibility
|
||||
self.peer = self._transport_details.peer
|
||||
|
||||
self.receive_queue = deque()
|
||||
self._consume()
|
||||
|
||||
self._connectionMade()
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self._connectionLost(exc)
|
||||
# according to asyncio docs, connection_lost(None) is called
|
||||
# if something else called transport.close()
|
||||
if exc is not None:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
|
||||
def _consume(self):
|
||||
self.waiter = Future(loop=self.factory.loop or txaio.config.loop)
|
||||
|
||||
def process(_):
|
||||
while self.receive_queue:
|
||||
data = self.receive_queue.popleft()
|
||||
if self.transport:
|
||||
self._dataReceived(data)
|
||||
self._consume()
|
||||
|
||||
self.waiter.add_done_callback(process)
|
||||
|
||||
def data_received(self, data):
|
||||
self.receive_queue.append(data)
|
||||
if not self.waiter.done():
|
||||
self.waiter.set_result(None)
|
||||
|
||||
def _closeConnection(self, abort=False):
|
||||
if abort and hasattr(self.transport, 'abort'):
|
||||
self.transport.abort()
|
||||
else:
|
||||
self.transport.close()
|
||||
|
||||
def _onOpen(self):
|
||||
if self._transport_details.is_secure:
|
||||
# now that the TLS opening handshake is complete, the actual TLS channel ID
|
||||
# will be available. make sure to set it!
|
||||
channel_id = {
|
||||
'tls-unique': transport_channel_id(self.transport, self._transport_details.is_server, 'tls-unique'),
|
||||
}
|
||||
self._transport_details.channel_id = channel_id
|
||||
|
||||
res = self.onOpen()
|
||||
if yields(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
def _onMessageBegin(self, isBinary):
|
||||
res = self.onMessageBegin(isBinary)
|
||||
if yields(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
def _onMessageFrameBegin(self, length):
|
||||
res = self.onMessageFrameBegin(length)
|
||||
if yields(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
def _onMessageFrameData(self, payload):
|
||||
res = self.onMessageFrameData(payload)
|
||||
if yields(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
def _onMessageFrameEnd(self):
|
||||
res = self.onMessageFrameEnd()
|
||||
if yields(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
def _onMessageFrame(self, payload):
|
||||
res = self.onMessageFrame(payload)
|
||||
if yields(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
def _onMessageEnd(self):
|
||||
res = self.onMessageEnd()
|
||||
if yields(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
def _onMessage(self, payload, isBinary):
|
||||
res = self.onMessage(payload, isBinary)
|
||||
if yields(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
def _onPing(self, payload):
|
||||
res = self.onPing(payload)
|
||||
if yields(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
def _onPong(self, payload):
|
||||
res = self.onPong(payload)
|
||||
if yields(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
def _onClose(self, wasClean, code, reason):
|
||||
res = self.onClose(wasClean, code, reason)
|
||||
if yields(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
def registerProducer(self, producer, streaming):
|
||||
raise Exception("not implemented")
|
||||
|
||||
def unregisterProducer(self):
|
||||
# note that generic websocket/protocol.py code calls
|
||||
# .unregisterProducer whenever we dropConnection -- that's
|
||||
# correct behavior on Twisted so either we'd have to
|
||||
# try/except there, or special-case Twisted, ..or just make
|
||||
# this "not an error"
|
||||
pass
|
||||
|
||||
|
||||
@public
|
||||
class WebSocketServerProtocol(WebSocketAdapterProtocol, protocol.WebSocketServerProtocol):
|
||||
"""
|
||||
Base class for asyncio-based WebSocket server protocols.
|
||||
|
||||
Implements:
|
||||
|
||||
* :class:`autobahn.websocket.interfaces.IWebSocketChannel`
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
|
||||
@public
|
||||
class WebSocketClientProtocol(WebSocketAdapterProtocol, protocol.WebSocketClientProtocol):
|
||||
"""
|
||||
Base class for asyncio-based WebSocket client protocols.
|
||||
|
||||
Implements:
|
||||
|
||||
* :class:`autobahn.websocket.interfaces.IWebSocketChannel`
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
def _onConnect(self, response):
|
||||
res = self.onConnect(response)
|
||||
self.log.debug('{func}: {res}', func=hltype(self._onConnect), res=res)
|
||||
if yields(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
def startTLS(self):
|
||||
raise Exception("WSS over explicit proxies not implemented")
|
||||
|
||||
|
||||
class WebSocketAdapterFactory(object):
|
||||
"""
|
||||
Adapter class for asyncio-based WebSocket client and server factories.
|
||||
"""
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __call__(self):
|
||||
proto = self.protocol()
|
||||
proto.factory = self
|
||||
return proto
|
||||
|
||||
|
||||
@public
|
||||
class WebSocketServerFactory(WebSocketAdapterFactory, protocol.WebSocketServerFactory):
|
||||
"""
|
||||
Base class for asyncio-based WebSocket server factories.
|
||||
|
||||
Implements:
|
||||
|
||||
* :class:`autobahn.websocket.interfaces.IWebSocketServerChannelFactory`
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
protocol = WebSocketServerProtocol
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
.. note::
|
||||
In addition to all arguments to the constructor of
|
||||
:meth:`autobahn.websocket.interfaces.IWebSocketServerChannelFactory`,
|
||||
you can supply a ``loop`` keyword argument to specify the
|
||||
asyncio event loop to be used.
|
||||
"""
|
||||
loop = kwargs.pop('loop', None)
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
|
||||
protocol.WebSocketServerFactory.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
@public
|
||||
class WebSocketClientFactory(WebSocketAdapterFactory, protocol.WebSocketClientFactory):
|
||||
"""
|
||||
Base class for asyncio-based WebSocket client factories.
|
||||
|
||||
Implements:
|
||||
|
||||
* :class:`autobahn.websocket.interfaces.IWebSocketClientChannelFactory`
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
|
||||
.. note::
|
||||
In addition to all arguments to the constructor of
|
||||
:meth:`autobahn.websocket.interfaces.IWebSocketClientChannelFactory`,
|
||||
you can supply a ``loop`` keyword argument to specify the
|
||||
asyncio event loop to be used.
|
||||
"""
|
||||
loop = kwargs.pop('loop', None)
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
|
||||
protocol.WebSocketClientFactory.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
@public
|
||||
class WampWebSocketServerProtocol(websocket.WampWebSocketServerProtocol, WebSocketServerProtocol):
|
||||
"""
|
||||
asyncio-based WAMP-over-WebSocket server protocol.
|
||||
|
||||
Implements:
|
||||
|
||||
* :class:`autobahn.wamp.interfaces.ITransport`
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
|
||||
@public
|
||||
class WampWebSocketServerFactory(websocket.WampWebSocketServerFactory, WebSocketServerFactory):
|
||||
"""
|
||||
asyncio-based WAMP-over-WebSocket server factory.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
protocol = WampWebSocketServerProtocol
|
||||
|
||||
def __init__(self, factory, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param factory: A callable that produces instances that implement
|
||||
:class:`autobahn.wamp.interfaces.ITransportHandler`
|
||||
:type factory: callable
|
||||
|
||||
:param serializers: A list of WAMP serializers to use (or ``None``
|
||||
for all available serializers).
|
||||
:type serializers: list of objects implementing
|
||||
:class:`autobahn.wamp.interfaces.ISerializer`
|
||||
"""
|
||||
|
||||
serializers = kwargs.pop('serializers', None)
|
||||
|
||||
websocket.WampWebSocketServerFactory.__init__(self, factory, serializers)
|
||||
|
||||
kwargs['protocols'] = self._protocols
|
||||
|
||||
# noinspection PyCallByClass
|
||||
WebSocketServerFactory.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
@public
|
||||
class WampWebSocketClientProtocol(websocket.WampWebSocketClientProtocol, WebSocketClientProtocol):
|
||||
"""
|
||||
asyncio-based WAMP-over-WebSocket client protocols.
|
||||
|
||||
Implements:
|
||||
|
||||
* :class:`autobahn.wamp.interfaces.ITransport`
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
|
||||
@public
|
||||
class WampWebSocketClientFactory(websocket.WampWebSocketClientFactory, WebSocketClientFactory):
|
||||
"""
|
||||
asyncio-based WAMP-over-WebSocket client factory.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
protocol = WampWebSocketClientProtocol
|
||||
|
||||
def __init__(self, factory, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param factory: A callable that produces instances that implement
|
||||
:class:`autobahn.wamp.interfaces.ITransportHandler`
|
||||
:type factory: callable
|
||||
|
||||
:param serializer: The WAMP serializer to use (or ``None`` for
|
||||
"best" serializer, chosen as the first serializer available from
|
||||
this list: CBOR, MessagePack, UBJSON, JSON).
|
||||
:type serializer: object implementing :class:`autobahn.wamp.interfaces.ISerializer`
|
||||
"""
|
||||
|
||||
serializers = kwargs.pop('serializers', None)
|
||||
|
||||
websocket.WampWebSocketClientFactory.__init__(self, factory, serializers)
|
||||
|
||||
kwargs['protocols'] = self._protocols
|
||||
|
||||
WebSocketClientFactory.__init__(self, *args, **kwargs)
|
||||
@@ -0,0 +1,93 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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:
|
||||
from autobahn import xbr
|
||||
HAS_XBR = True
|
||||
except:
|
||||
HAS_XBR = False
|
||||
|
||||
|
||||
if HAS_XBR:
|
||||
import uuid
|
||||
import asyncio
|
||||
import txaio
|
||||
from autobahn.util import hl
|
||||
from autobahn.xbr._interfaces import IProvider, ISeller, IConsumer, IBuyer
|
||||
|
||||
def run_in_executor(*args, **kwargs):
|
||||
return asyncio.get_running_loop().run_in_executor(None, *args, **kwargs)
|
||||
|
||||
class SimpleBlockchain(xbr.SimpleBlockchain):
|
||||
backgroundCaller = run_in_executor
|
||||
|
||||
class KeySeries(xbr.KeySeries):
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __init__(self, api_id, price, interval, on_rotate=None):
|
||||
super().__init__(api_id, price, interval, on_rotate)
|
||||
self.running = False
|
||||
|
||||
async def start(self):
|
||||
"""
|
||||
Start offering and selling data encryption keys in the background.
|
||||
"""
|
||||
assert not self.running
|
||||
|
||||
self.log.info('Starting key rotation every {interval} seconds for api_id="{api_id}" ..',
|
||||
interval=hl(self._interval), api_id=hl(uuid.UUID(bytes=self._api_id)))
|
||||
self.running = True
|
||||
|
||||
async def rotate_with_interval():
|
||||
while self.running:
|
||||
await self._rotate()
|
||||
await asyncio.sleep(self._interval)
|
||||
|
||||
asyncio.create_task(rotate_with_interval())
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop offering/selling data encryption keys.
|
||||
"""
|
||||
if not self.running:
|
||||
raise RuntimeError('cannot stop {} - not currently running'.format(self.__class__.__name__))
|
||||
|
||||
self.running = False
|
||||
|
||||
class SimpleSeller(xbr.SimpleSeller):
|
||||
"""
|
||||
Simple XBR seller component. This component can be used by a XBR seller delegate to
|
||||
handle the automated selling of data encryption keys to the XBR market maker.
|
||||
"""
|
||||
xbr.SimpleSeller.KeySeries = KeySeries
|
||||
|
||||
class SimpleBuyer(xbr.SimpleBuyer):
|
||||
pass
|
||||
|
||||
ISeller.register(SimpleSeller)
|
||||
IProvider.register(SimpleSeller)
|
||||
IBuyer.register(SimpleBuyer)
|
||||
IConsumer.register(SimpleBuyer)
|
||||
Binary file not shown.
48
backend/lib/python3.12/site-packages/autobahn/exception.py
Normal file
48
backend/lib/python3.12/site-packages/autobahn/exception.py
Normal file
@@ -0,0 +1,48 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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
|
||||
|
||||
__all__ = (
|
||||
'PayloadExceededError',
|
||||
)
|
||||
|
||||
|
||||
@public
|
||||
class PayloadExceededError(RuntimeError):
|
||||
"""
|
||||
Exception raised when the serialized and framed (eg WebSocket/RawSocket) WAMP payload
|
||||
exceeds the transport message size limit.
|
||||
"""
|
||||
|
||||
|
||||
@public
|
||||
class Disconnected(RuntimeError):
|
||||
"""
|
||||
Exception raised when trying to perform an operation which
|
||||
requires a connection when the WebSocket/RawSocket is not
|
||||
currently connected
|
||||
"""
|
||||
@@ -0,0 +1,29 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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.nvx._utf8validator import Utf8Validator # noqa
|
||||
|
||||
__all__ = ('Utf8Validator',)
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,648 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// http://stackoverflow.com/questions/11228855/header-files-for-simd-intrinsics
|
||||
#if defined(__SSE2__) || defined(__SSE4_1__)
|
||||
#include <x86intrin.h>
|
||||
#endif
|
||||
|
||||
|
||||
#define UTF8_ACCEPT 0
|
||||
#define UTF8_REJECT 1
|
||||
|
||||
|
||||
typedef struct {
|
||||
size_t current_index;
|
||||
size_t total_index;
|
||||
int state;
|
||||
int impl;
|
||||
} utf8_validator_t;
|
||||
|
||||
|
||||
#define UTF8_VALIDATOR_OPTIMAL 0
|
||||
#define UTF8_VALIDATOR_TABLE_DFA 1
|
||||
#define UTF8_VALIDATOR_UNROLLED_DFA 2
|
||||
#define UTF8_VALIDATOR_SSE2_DFA 3
|
||||
#define UTF8_VALIDATOR_SSE41_DFA 4
|
||||
|
||||
|
||||
int nvx_utf8vld_get_impl (void* utf8vld) {
|
||||
utf8_validator_t* vld = (utf8_validator_t*) utf8vld;
|
||||
|
||||
return vld->impl;
|
||||
}
|
||||
|
||||
int nvx_utf8vld_set_impl (void* utf8vld, int impl) {
|
||||
utf8_validator_t* vld = (utf8_validator_t*) utf8vld;
|
||||
|
||||
if (impl) {
|
||||
// set requested implementation
|
||||
//
|
||||
#ifndef __SSE4_1__
|
||||
# ifdef __SSE2__
|
||||
if (impl <= UTF8_VALIDATOR_SSE2_DFA) {
|
||||
vld->impl = impl;
|
||||
}
|
||||
# else
|
||||
if (impl <= UTF8_VALIDATOR_UNROLLED_DFA) {
|
||||
vld->impl = impl;
|
||||
}
|
||||
# endif
|
||||
#else
|
||||
if (impl <= UTF8_VALIDATOR_SSE41_DFA) {
|
||||
vld->impl = impl;
|
||||
}
|
||||
#endif
|
||||
|
||||
} else {
|
||||
// set optimal implementation
|
||||
//
|
||||
#ifndef __SSE4_1__
|
||||
# ifdef __SSE2__
|
||||
vld->impl = UTF8_VALIDATOR_SSE2_DFA;
|
||||
# else
|
||||
vld->impl = UTF8_VALIDATOR_UNROLLED_DFA;
|
||||
# endif
|
||||
#else
|
||||
vld->impl = UTF8_VALIDATOR_SSE41_DFA;
|
||||
#endif
|
||||
|
||||
}
|
||||
return vld->impl;
|
||||
}
|
||||
|
||||
|
||||
void nvx_utf8vld_reset (void* utf8vld) {
|
||||
utf8_validator_t* vld = (utf8_validator_t*) utf8vld;
|
||||
|
||||
vld->state = 0;
|
||||
vld->current_index = -1;
|
||||
vld->total_index = -1;
|
||||
}
|
||||
|
||||
|
||||
void* nvx_utf8vld_new () {
|
||||
void* p = malloc(sizeof(utf8_validator_t));
|
||||
nvx_utf8vld_reset(p);
|
||||
nvx_utf8vld_set_impl(p, 0);
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
void nvx_utf8vld_free (void* utf8vld) {
|
||||
free (utf8vld);
|
||||
}
|
||||
|
||||
|
||||
// unrolled DFA from http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
||||
//
|
||||
static const uint8_t UTF8VALIDATOR_DFA[] __attribute__((aligned(64))) =
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
int _nvx_utf8vld_validate_table (void* utf8vld, const uint8_t* data, size_t length) {
|
||||
|
||||
utf8_validator_t* vld = (utf8_validator_t*) utf8vld;
|
||||
|
||||
int state = vld->state;
|
||||
|
||||
const uint8_t* end = data + length;
|
||||
|
||||
while (data < end && state != 1) {
|
||||
state = UTF8VALIDATOR_DFA[256 + state * 16 + UTF8VALIDATOR_DFA[*data++]];
|
||||
}
|
||||
|
||||
vld->state = state;
|
||||
|
||||
if (state == 0) {
|
||||
// UTF8 is valid and ends on codepoint
|
||||
return 0;
|
||||
} else {
|
||||
if (state == 1) {
|
||||
// UTF8 is invalid
|
||||
return -1;
|
||||
} else {
|
||||
// UTF8 is valid, but does not end on codepoint (needs more data)
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// unrolled DFA from http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
||||
//
|
||||
#define DFA_TRANSITION(state, octet) \
|
||||
if (state == 0) { \
|
||||
if (octet >= 0x00 && octet <= 0x7f) { \
|
||||
/* reflective state 0 */ \
|
||||
} else if (octet >= 0xc2 && octet <= 0xdf) { \
|
||||
state = 2; \
|
||||
} else if ((octet >= 0xe1 && octet <= 0xec) || octet == 0xee || octet == 0xef) { \
|
||||
state = 3; \
|
||||
} else if (octet == 0xe0) { \
|
||||
state = 4; \
|
||||
} else if (octet == 0xed) { \
|
||||
state = 5; \
|
||||
} else if (octet == 0xf4) { \
|
||||
state = 8; \
|
||||
} else if (octet == 0xf1 || octet == 0xf2 || octet == 0xf3) { \
|
||||
state = 7; \
|
||||
} else if (octet == 0xf0) { \
|
||||
state = 6; \
|
||||
} else { \
|
||||
state = 1; \
|
||||
} \
|
||||
} else if (state == 2) { \
|
||||
if (octet >= 0x80 && octet <= 0xbf) { \
|
||||
state = 0; \
|
||||
} else { \
|
||||
state = 1; \
|
||||
} \
|
||||
} else if (state == 3) { \
|
||||
if (octet >= 0x80 && octet <= 0xbf) { \
|
||||
state = 2; \
|
||||
} else { \
|
||||
state = 1; \
|
||||
} \
|
||||
} else if (state == 4) { \
|
||||
if (octet >= 0xa0 && octet <= 0xbf) { \
|
||||
state = 2; \
|
||||
} else { \
|
||||
state = 1; \
|
||||
} \
|
||||
} else if (state == 5) { \
|
||||
if (octet >= 0x80 && octet <= 0x9f) { \
|
||||
state = 2; \
|
||||
} else { \
|
||||
state = 1; \
|
||||
} \
|
||||
} else if (state == 6) { \
|
||||
if (octet >= 0x90 && octet <= 0xbf) { \
|
||||
state = 3; \
|
||||
} else { \
|
||||
state = 1; \
|
||||
} \
|
||||
} else if (state == 7) { \
|
||||
if (octet >= 0x80 && octet <= 0xbf) { \
|
||||
state = 3; \
|
||||
} else { \
|
||||
state = 1; \
|
||||
} \
|
||||
} else if (state == 8) { \
|
||||
if (octet >= 0x80 && octet <= 0x8f) { \
|
||||
state = 3; \
|
||||
} else { \
|
||||
state = 1; \
|
||||
} \
|
||||
} else if (state == 1) { \
|
||||
/* refective state 1 */ \
|
||||
} else { \
|
||||
/* should not arrive here */ \
|
||||
}
|
||||
|
||||
|
||||
int _nvx_utf8vld_validate_unrolled (void* utf8vld, const uint8_t* data, size_t length) {
|
||||
|
||||
utf8_validator_t* vld = (utf8_validator_t*) utf8vld;
|
||||
|
||||
int state = vld->state;
|
||||
|
||||
const uint8_t* tail_end = data + length;
|
||||
|
||||
while (data < tail_end && state != 1) {
|
||||
|
||||
// get tail octet
|
||||
int octet = *data;
|
||||
|
||||
// do the DFA
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
++data;
|
||||
}
|
||||
|
||||
vld->state = state;
|
||||
|
||||
if (state == 0) {
|
||||
// UTF8 is valid and ends on codepoint
|
||||
return 0;
|
||||
} else {
|
||||
if (state == 1) {
|
||||
// UTF8 is invalid
|
||||
return -1;
|
||||
} else {
|
||||
// UTF8 is valid, but does not end on codepoint (needs more data)
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
__m128i _mm_load_si128 (__m128i const* mem_addr)
|
||||
#include "emmintrin.h"
|
||||
Instruction: movdqa
|
||||
CPUID Feature Flag: SSE2
|
||||
|
||||
int _mm_movemask_epi8 (__m128i a)
|
||||
#include "emmintrin.h"
|
||||
Instruction: pmovmskb
|
||||
CPUID Feature Flag: SSE2
|
||||
|
||||
__m128i _mm_srli_si128 (__m128i a, int imm)
|
||||
#include "emmintrin.h"
|
||||
Instruction: psrldq
|
||||
CPUID Feature Flag: SSE2
|
||||
|
||||
int _mm_cvtsi128_si32 (__m128i a)
|
||||
#include "emmintrin.h"
|
||||
Instruction: movd
|
||||
CPUID Feature Flag: SSE2
|
||||
|
||||
int _mm_extract_epi16 (__m128i a, int imm)
|
||||
#include "emmintrin.h"
|
||||
Instruction: pextrw
|
||||
CPUID Feature Flag: SSE2
|
||||
|
||||
int _mm_extract_epi8 (__m128i a, const int imm)
|
||||
#include "smmintrin.h"
|
||||
Instruction: pextrb
|
||||
CPUID Feature Flag: SSE4.1
|
||||
*/
|
||||
|
||||
#ifdef __SSE2__
|
||||
int _nvx_utf8vld_validate_sse2 (void* utf8vld, const uint8_t* data, size_t length) {
|
||||
|
||||
utf8_validator_t* vld = (utf8_validator_t*) utf8vld;
|
||||
|
||||
int state = vld->state;
|
||||
|
||||
const uint8_t* tail_end = data + length;
|
||||
|
||||
// process unaligned head (sub 16 octets)
|
||||
//
|
||||
size_t head_len = ((size_t) data) % sizeof(__m128i);
|
||||
if (head_len) {
|
||||
|
||||
const uint8_t* head_end = data + head_len;
|
||||
|
||||
while (data < head_end && state != UTF8_REJECT) {
|
||||
|
||||
// get head octet
|
||||
int octet = *data;
|
||||
|
||||
// do the DFA
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
++data;
|
||||
}
|
||||
}
|
||||
|
||||
// process aligned middle (16 octet chunks)
|
||||
//
|
||||
const __m128i* ptr = ((const __m128i*) data);
|
||||
const __m128i* end = ((const __m128i*) data) + ((length - head_len) / sizeof(__m128i));
|
||||
|
||||
while (ptr < end && state != UTF8_REJECT) {
|
||||
|
||||
__builtin_prefetch(ptr + 1, 0, 3);
|
||||
//__builtin_prefetch(ptr + 4, 0, 3); // 16*4=64: cache-line prefetch
|
||||
|
||||
__m128i xmm1 = _mm_load_si128(ptr);
|
||||
|
||||
if (__builtin_expect(state || _mm_movemask_epi8(xmm1), 0)) {
|
||||
|
||||
// copy to different reg - this allows the prefetching to
|
||||
// do its job in the meantime (I guess ..)
|
||||
|
||||
// SSE2 variant
|
||||
//
|
||||
int octet;
|
||||
|
||||
// octet 0
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 1
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 2
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 3
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 4
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 5
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 6
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 7
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 8
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 9
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 10
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 11
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 12
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 13
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 14
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 15
|
||||
xmm1 = _mm_srli_si128(xmm1, 1);
|
||||
octet = 0xff & _mm_cvtsi128_si32(xmm1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
}
|
||||
++ptr;
|
||||
}
|
||||
|
||||
// process unaligned tail (sub 16 octets)
|
||||
//
|
||||
const uint8_t* tail_ptr = (const uint8_t*) ptr;
|
||||
|
||||
while (tail_ptr < tail_end && state != UTF8_REJECT) {
|
||||
|
||||
// get tail octet
|
||||
int octet = *tail_ptr;
|
||||
|
||||
// do the DFA
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
++tail_ptr;
|
||||
}
|
||||
|
||||
vld->state = state;
|
||||
|
||||
if (state == UTF8_ACCEPT) {
|
||||
// UTF8 is valid and ends on codepoint
|
||||
return 0;
|
||||
} else {
|
||||
if (state == UTF8_REJECT) {
|
||||
// UTF8 is invalid
|
||||
return -1;
|
||||
} else {
|
||||
// UTF8 is valid, but does not end on codepoint (needs more data)
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef __SSE4_1__
|
||||
int _nvx_utf8vld_validate_sse4 (void* utf8vld, const uint8_t* data, size_t length) {
|
||||
|
||||
utf8_validator_t* vld = (utf8_validator_t*) utf8vld;
|
||||
|
||||
int state = vld->state;
|
||||
|
||||
const uint8_t* tail_end = data + length;
|
||||
|
||||
// process unaligned head (sub 16 octets)
|
||||
//
|
||||
size_t head_len = ((size_t) data) % sizeof(__m128i);
|
||||
if (head_len) {
|
||||
|
||||
const uint8_t* head_end = data + head_len;
|
||||
|
||||
while (data < head_end && state != UTF8_REJECT) {
|
||||
|
||||
// get head octet
|
||||
int octet = *data;
|
||||
|
||||
// do the DFA
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
++data;
|
||||
}
|
||||
}
|
||||
|
||||
// process aligned middle (16 octet chunks)
|
||||
//
|
||||
const __m128i* ptr = ((const __m128i*) data);
|
||||
const __m128i* end = ((const __m128i*) data) + ((length - head_len) / sizeof(__m128i));
|
||||
|
||||
while (ptr < end && state != UTF8_REJECT) {
|
||||
|
||||
__builtin_prefetch(ptr + 1, 0, 3);
|
||||
//__builtin_prefetch(ptr + 4, 0, 3); // 16*4=64: cache-line prefetch
|
||||
|
||||
__m128i xmm1 = _mm_load_si128(ptr);
|
||||
|
||||
|
||||
if (__builtin_expect(state || _mm_movemask_epi8(xmm1), 0)) {
|
||||
|
||||
// copy to different reg - this allows the prefetching to
|
||||
// do its job in the meantime (I guess ..)
|
||||
|
||||
// SSE4.1 variant
|
||||
//
|
||||
int octet;
|
||||
|
||||
// octet 0
|
||||
octet = _mm_extract_epi8(xmm1, 0);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 1
|
||||
octet = _mm_extract_epi8(xmm1, 1);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 2
|
||||
octet = _mm_extract_epi8(xmm1, 2);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 3
|
||||
octet = _mm_extract_epi8(xmm1, 3);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 4
|
||||
octet = _mm_extract_epi8(xmm1, 4);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 5
|
||||
octet = _mm_extract_epi8(xmm1, 5);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 6
|
||||
octet = _mm_extract_epi8(xmm1, 6);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 7
|
||||
octet = _mm_extract_epi8(xmm1, 7);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 8
|
||||
octet = _mm_extract_epi8(xmm1, 8);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 9
|
||||
octet = _mm_extract_epi8(xmm1, 9);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 10
|
||||
octet = _mm_extract_epi8(xmm1, 10);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 11
|
||||
octet = _mm_extract_epi8(xmm1, 11);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 12
|
||||
octet = _mm_extract_epi8(xmm1, 12);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 13
|
||||
octet = _mm_extract_epi8(xmm1, 13);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 14
|
||||
octet = _mm_extract_epi8(xmm1, 14);
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
// octet 15
|
||||
octet = _mm_extract_epi8(xmm1, 15);
|
||||
DFA_TRANSITION(state, octet);
|
||||
}
|
||||
++ptr;
|
||||
}
|
||||
|
||||
// process unaligned tail (sub 16 octets)
|
||||
//
|
||||
const uint8_t* tail_ptr = (const uint8_t*) ptr;
|
||||
|
||||
while (tail_ptr < tail_end && state != UTF8_REJECT) {
|
||||
|
||||
// get tail octet
|
||||
int octet = *tail_ptr;
|
||||
|
||||
// do the DFA
|
||||
DFA_TRANSITION(state, octet);
|
||||
|
||||
++tail_ptr;
|
||||
}
|
||||
|
||||
vld->state = state;
|
||||
|
||||
if (state == UTF8_ACCEPT) {
|
||||
// UTF8 is valid and ends on codepoint
|
||||
return 0;
|
||||
} else {
|
||||
if (state == UTF8_REJECT) {
|
||||
// UTF8 is invalid
|
||||
return -1;
|
||||
} else {
|
||||
// UTF8 is valid, but does not end on codepoint (needs more data)
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
int nvx_utf8vld_validate (void* utf8vld, const uint8_t* data, size_t length) {
|
||||
|
||||
utf8_validator_t* vld = (utf8_validator_t*) utf8vld;
|
||||
|
||||
switch (vld->impl) {
|
||||
case UTF8_VALIDATOR_TABLE_DFA:
|
||||
return _nvx_utf8vld_validate_table(utf8vld, data, length);
|
||||
case UTF8_VALIDATOR_UNROLLED_DFA:
|
||||
return _nvx_utf8vld_validate_unrolled(utf8vld, data, length);
|
||||
#ifdef __SSE2__
|
||||
case UTF8_VALIDATOR_SSE2_DFA:
|
||||
return _nvx_utf8vld_validate_table(utf8vld, data, length);
|
||||
#endif
|
||||
#ifdef __SSE4_1__
|
||||
case UTF8_VALIDATOR_SSE41_DFA:
|
||||
return _nvx_utf8vld_validate_table(utf8vld, data, length);
|
||||
#endif
|
||||
default:
|
||||
return _nvx_utf8vld_validate_table(utf8vld, data, length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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
|
||||
from cffi import FFI
|
||||
|
||||
|
||||
ffi = FFI()
|
||||
|
||||
ffi.cdef("""
|
||||
void* nvx_utf8vld_new ();
|
||||
|
||||
void nvx_utf8vld_reset (void* utf8vld);
|
||||
|
||||
int nvx_utf8vld_validate (void* utf8vld, const uint8_t* data, size_t length);
|
||||
|
||||
void nvx_utf8vld_free (void* utf8vld);
|
||||
|
||||
int nvx_utf8vld_set_impl(void* utf8vld, int impl);
|
||||
|
||||
int nvx_utf8vld_get_impl(void* utf8vld);
|
||||
""")
|
||||
|
||||
if 'AUTOBAHN_USE_NVX' in os.environ and os.environ['AUTOBAHN_USE_NVX'] in ['1', 'true']:
|
||||
optional = False # :noindex:
|
||||
else:
|
||||
optional = True # :noindex:
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), '_utf8validator.c')) as fd:
|
||||
c_source = fd.read()
|
||||
ffi.set_source(
|
||||
"_nvx_utf8validator",
|
||||
c_source,
|
||||
libraries=[],
|
||||
extra_compile_args=['-std=c99', '-Wall', '-Wno-strict-prototypes', '-O3', '-march=native'],
|
||||
optional=optional
|
||||
)
|
||||
|
||||
|
||||
class Utf8Validator:
|
||||
"""
|
||||
:noindex:
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.ffi = ffi
|
||||
|
||||
from _nvx_utf8validator import lib
|
||||
self.lib = lib
|
||||
|
||||
self._vld = self.ffi.gc(self.lib.nvx_utf8vld_new(), self.lib.nvx_utf8vld_free)
|
||||
# print(self.lib.nvx_utf8vld_get_impl(self._vld))
|
||||
|
||||
def reset(self):
|
||||
self.lib.nvx_utf8vld_reset(self._vld)
|
||||
|
||||
def validate(self, ba):
|
||||
res = self.lib.nvx_utf8vld_validate(self._vld, ba, len(ba))
|
||||
return (res >= 0, res == 0, None, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ffi.compile()
|
||||
@@ -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.
@@ -0,0 +1,359 @@
|
||||
# coding=utf-8
|
||||
|
||||
###############################################################################
|
||||
#
|
||||
# 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 struct
|
||||
import unittest
|
||||
|
||||
from autobahn.websocket.utf8validator import Utf8Validator as StandardUtf8Validator
|
||||
|
||||
try:
|
||||
from _nvx_utf8validator import lib # noqa
|
||||
from autobahn.nvx import Utf8Validator as NvxUtf8Validator
|
||||
except ImportError:
|
||||
HAS_NVX = False
|
||||
else:
|
||||
HAS_NVX = True
|
||||
|
||||
|
||||
def _create_utf8_test_sequences():
|
||||
"""
|
||||
Create test sequences for UTF-8 decoder tests from
|
||||
http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
|
||||
"""
|
||||
|
||||
UTF8_TEST_SEQUENCES = []
|
||||
|
||||
# 1 Some correct UTF-8 text
|
||||
vss = b'\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5'
|
||||
vs = [b"Some valid UTF-8 sequences", []]
|
||||
vs[1].append((True, b'hello\x24world')) # U+0024
|
||||
vs[1].append((True, b'hello\xC2\xA2world')) # U+00A2
|
||||
vs[1].append((True, b'hello\xE2\x82\xACworld')) # U+20AC
|
||||
vs[1].append((True, b'hello\xF0\xA4\xAD\xA2world')) # U+24B62
|
||||
vs[1].append((True, vss))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# All prefixes of correct UTF-8 text
|
||||
vs = [
|
||||
b"All prefixes of a valid UTF-8 string that contains multi-byte code points",
|
||||
[]]
|
||||
v = StandardUtf8Validator()
|
||||
for i in range(1, len(vss) + 1):
|
||||
v.reset()
|
||||
res = v.validate(vss[:i])
|
||||
vs[1].append((res[0] and res[1], vss[:i]))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# 2.1 First possible sequence of a certain length
|
||||
vs = [b"First possible sequence of a certain length", []]
|
||||
vs[1].append((True, b'\x00'))
|
||||
vs[1].append((True, b'\xc2\x80'))
|
||||
vs[1].append((True, b'\xe0\xa0\x80'))
|
||||
vs[1].append((True, b'\xf0\x90\x80\x80'))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# the following conform to the UTF-8 integer encoding scheme, but
|
||||
# valid UTF-8 only allows for Unicode code points up to U+10FFFF
|
||||
vs = [b"First possible sequence length 5/6 (invalid codepoints)", []]
|
||||
vs[1].append((False, b'\xf8\x88\x80\x80\x80'))
|
||||
vs[1].append((False, b'\xfc\x84\x80\x80\x80\x80'))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# 2.2 Last possible sequence of a certain length
|
||||
vs = [b"Last possible sequence of a certain length", []]
|
||||
vs[1].append((True, b'\x7f'))
|
||||
vs[1].append((True, b'\xdf\xbf'))
|
||||
vs[1].append((True, b'\xef\xbf\xbf'))
|
||||
vs[1].append((True, b'\xf4\x8f\xbf\xbf'))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# the following conform to the UTF-8 integer encoding scheme, but
|
||||
# valid UTF-8 only allows for Unicode code points up to U+10FFFF
|
||||
vs = [b"Last possible sequence length 4/5/6 (invalid codepoints)", []]
|
||||
vs[1].append((False, b'\xf7\xbf\xbf\xbf'))
|
||||
vs[1].append((False, b'\xfb\xbf\xbf\xbf\xbf'))
|
||||
vs[1].append((False, b'\xfd\xbf\xbf\xbf\xbf\xbf'))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# 2.3 Other boundary conditions
|
||||
vs = [b"Other boundary conditions", []]
|
||||
vs[1].append((True, b'\xed\x9f\xbf'))
|
||||
vs[1].append((True, b'\xee\x80\x80'))
|
||||
vs[1].append((True, b'\xef\xbf\xbd'))
|
||||
vs[1].append((True, b'\xf4\x8f\xbf\xbf'))
|
||||
vs[1].append((False, b'\xf4\x90\x80\x80'))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# 3.1 Unexpected continuation bytes
|
||||
vs = [b"Unexpected continuation bytes", []]
|
||||
vs[1].append((False, b'\x80'))
|
||||
vs[1].append((False, b'\xbf'))
|
||||
vs[1].append((False, b'\x80\xbf'))
|
||||
vs[1].append((False, b'\x80\xbf\x80'))
|
||||
vs[1].append((False, b'\x80\xbf\x80\xbf'))
|
||||
vs[1].append((False, b'\x80\xbf\x80\xbf\x80'))
|
||||
vs[1].append((False, b'\x80\xbf\x80\xbf\x80\xbf'))
|
||||
s = b''
|
||||
|
||||
# 3.2 Lonely start characters
|
||||
vs = [b"Lonely start characters", []]
|
||||
m = [(0xc0, 0xdf), (0xe0, 0xef), (0xf0, 0xf7), (0xf8, 0xfb), (0xfc, 0xfd)]
|
||||
for mm in m:
|
||||
s = b''
|
||||
for i in range(mm[0], mm[1]):
|
||||
s += struct.pack('BB', i, 0x20)
|
||||
# s += chr(i)
|
||||
# s += chr(0x20)
|
||||
vs[1].append((False, s))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# 3.3 Sequences with last continuation byte missing
|
||||
vs = [b"Sequences with last continuation byte missing", []]
|
||||
k = [b'\xc0', b'\xe0\x80', b'\xf0\x80\x80', b'\xf8\x80\x80\x80', b'\xfc\x80\x80\x80\x80',
|
||||
b'\xdf', b'\xef\xbf', b'\xf7\xbf\xbf', b'\xfb\xbf\xbf\xbf', b'\xfd\xbf\xbf\xbf\xbf']
|
||||
for kk in k:
|
||||
vs[1].append((False, kk))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# 3.4 Concatenation of incomplete sequences
|
||||
vs = [b"Concatenation of incomplete sequences", []]
|
||||
vs[1].append((False, b''.join(k)))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# 3.5 Impossible bytes
|
||||
vs = [b"Impossible bytes", []]
|
||||
vs[1].append((False, b'\xfe'))
|
||||
vs[1].append((False, b'\xff'))
|
||||
vs[1].append((False, b'\xfe\xfe\xff\xff'))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# 4.1 Examples of an overlong ASCII character
|
||||
vs = [b"Examples of an overlong ASCII character", []]
|
||||
vs[1].append((False, b'\xc0\xaf'))
|
||||
vs[1].append((False, b'\xe0\x80\xaf'))
|
||||
vs[1].append((False, b'\xf0\x80\x80\xaf'))
|
||||
vs[1].append((False, b'\xf8\x80\x80\x80\xaf'))
|
||||
vs[1].append((False, b'\xfc\x80\x80\x80\x80\xaf'))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# 4.2 Maximum overlong sequences
|
||||
vs = [b"Maximum overlong sequences", []]
|
||||
vs[1].append((False, b'\xc1\xbf'))
|
||||
vs[1].append((False, b'\xe0\x9f\xbf'))
|
||||
vs[1].append((False, b'\xf0\x8f\xbf\xbf'))
|
||||
vs[1].append((False, b'\xf8\x87\xbf\xbf\xbf'))
|
||||
vs[1].append((False, b'\xfc\x83\xbf\xbf\xbf\xbf'))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# 4.3 Overlong representation of the NUL character
|
||||
vs = [b"Overlong representation of the NUL character", []]
|
||||
vs[1].append((False, b'\xc0\x80'))
|
||||
vs[1].append((False, b'\xe0\x80\x80'))
|
||||
vs[1].append((False, b'\xf0\x80\x80\x80'))
|
||||
vs[1].append((False, b'\xf8\x80\x80\x80\x80'))
|
||||
vs[1].append((False, b'\xfc\x80\x80\x80\x80\x80'))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# 5.1 Single UTF-16 surrogates
|
||||
vs = [b"Single UTF-16 surrogates", []]
|
||||
vs[1].append((False, b'\xed\xa0\x80'))
|
||||
vs[1].append((False, b'\xed\xad\xbf'))
|
||||
vs[1].append((False, b'\xed\xae\x80'))
|
||||
vs[1].append((False, b'\xed\xaf\xbf'))
|
||||
vs[1].append((False, b'\xed\xb0\x80'))
|
||||
vs[1].append((False, b'\xed\xbe\x80'))
|
||||
vs[1].append((False, b'\xed\xbf\xbf'))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# 5.2 Paired UTF-16 surrogates
|
||||
vs = [b"Paired UTF-16 surrogates", []]
|
||||
vs[1].append((False, b'\xed\xa0\x80\xed\xb0\x80'))
|
||||
vs[1].append((False, b'\xed\xa0\x80\xed\xbf\xbf'))
|
||||
vs[1].append((False, b'\xed\xad\xbf\xed\xb0\x80'))
|
||||
vs[1].append((False, b'\xed\xad\xbf\xed\xbf\xbf'))
|
||||
vs[1].append((False, b'\xed\xae\x80\xed\xb0\x80'))
|
||||
vs[1].append((False, b'\xed\xae\x80\xed\xbf\xbf'))
|
||||
vs[1].append((False, b'\xed\xaf\xbf\xed\xb0\x80'))
|
||||
vs[1].append((False, b'\xed\xaf\xbf\xed\xbf\xbf'))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# 5.3 Other illegal code positions
|
||||
# Those are non-character code points and valid UTF-8 by RFC 3629
|
||||
vs = [b"Non-character code points (valid UTF-8)", []]
|
||||
# https://bug686312.bugzilla.mozilla.org/attachment.cgi?id=561257
|
||||
# non-characters: EF BF [BE-BF]
|
||||
vs[1].append((True, b'\xef\xbf\xbe'))
|
||||
vs[1].append((True, b'\xef\xbf\xbf'))
|
||||
# non-characters: F[0-7] [89AB]F BF [BE-BF]
|
||||
for z1 in [b'\xf0', b'\xf1', b'\xf2', b'\xf3', b'\xf4']:
|
||||
for z2 in [b'\x8f', b'\x9f', b'\xaf', b'\xbf']:
|
||||
# those encode codepoints >U+10FFFF
|
||||
if not (z1 == b'\xf4' and z2 != b'\x8f'):
|
||||
for z3 in [b'\xbe', b'\xbf']:
|
||||
zz = z1 + z2 + b'\xbf' + z3
|
||||
if zz not in [b'\xf0\x8f\xbf\xbe',
|
||||
b'\xf0\x8f\xbf\xbf']: # filter overlong sequences
|
||||
vs[1].append((True, zz))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
# Unicode "specials", such as replacement char etc
|
||||
# http://en.wikipedia.org/wiki/Specials_%28Unicode_block%29
|
||||
vs = [b"Unicode specials (i.e. replacement char)", []]
|
||||
vs[1].append((True, b'\xef\xbf\xb9'))
|
||||
vs[1].append((True, b'\xef\xbf\xba'))
|
||||
vs[1].append((True, b'\xef\xbf\xbb'))
|
||||
vs[1].append((True, b'\xef\xbf\xbc'))
|
||||
vs[1].append((True, b'\xef\xbf\xbd')) # replacement char
|
||||
vs[1].append((True, b'\xef\xbf\xbe'))
|
||||
vs[1].append((True, b'\xef\xbf\xbf'))
|
||||
UTF8_TEST_SEQUENCES.append(vs)
|
||||
|
||||
return UTF8_TEST_SEQUENCES
|
||||
|
||||
|
||||
def _create_valid_utf8_test_sequences():
|
||||
"""
|
||||
Generate some exotic, but valid UTF8 test strings.
|
||||
"""
|
||||
VALID_UTF8_TEST_SEQUENCES = []
|
||||
for test in _create_utf8_test_sequences():
|
||||
valids = [x[1] for x in test[1] if x[0]]
|
||||
if len(valids) > 0:
|
||||
VALID_UTF8_TEST_SEQUENCES.append([test[0], valids])
|
||||
return VALID_UTF8_TEST_SEQUENCES
|
||||
|
||||
|
||||
@unittest.skipIf(not HAS_NVX, 'NVX native extensions not present')
|
||||
class TestNvxUtf8Validator(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# These tests verify the UTF-8 decoder/validator on the various test cases from
|
||||
# http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
|
||||
vs = []
|
||||
for k in _create_utf8_test_sequences():
|
||||
vs.extend(k[1])
|
||||
|
||||
# All Unicode code points
|
||||
for i in range(
|
||||
0, 0xffff): # should by 0x10ffff, but non-wide Python build is limited to 16-bits
|
||||
if i < 0xD800 or i > 0xDFFF: # filter surrogate code points, which are disallowed to encode in UTF-8
|
||||
vs.append((True, chr(i).encode("utf-8")))
|
||||
|
||||
# FIXME: UnicodeEncodeError: 'utf-8' codec can't encode character '\ud800'
|
||||
# in position 0: surrogates not allowed
|
||||
if False:
|
||||
# 5.1 Single UTF-16 surrogates
|
||||
for i in range(0xD800, 0xDBFF): # high-surrogate
|
||||
ss = chr(i).encode("utf-8")
|
||||
vs.append((False, ss))
|
||||
for i in range(0xDC00, 0xDFFF): # low-surrogate
|
||||
ss = chr(i).encode("utf-8")
|
||||
vs.append((False, ss))
|
||||
|
||||
# 5.2 Paired UTF-16 surrogates
|
||||
for i in range(0xD800, 0xDBFF): # high-surrogate
|
||||
for j in range(0xDC00, 0xDFFF): # low-surrogate
|
||||
ss1 = chr(i).encode("utf-8")
|
||||
ss2 = chr(j).encode("utf-8")
|
||||
vs.append((False, ss1 + ss2))
|
||||
vs.append((False, ss2 + ss1))
|
||||
|
||||
self._TEST_SEQUENCES = vs
|
||||
|
||||
def test_standard_utf8validator(self):
|
||||
"""
|
||||
Test standard implementation of UTF8 validator.
|
||||
"""
|
||||
validator = StandardUtf8Validator()
|
||||
return self._test_utf8(validator)
|
||||
|
||||
def test_nvx_utf8validator(self):
|
||||
"""
|
||||
Test NVX implementation of UTF8 validator.
|
||||
"""
|
||||
validator = NvxUtf8Validator()
|
||||
return self._test_utf8(validator)
|
||||
|
||||
def test_standard_utf8validator_incremental(self):
|
||||
"""
|
||||
Test standard implementation of UTF8 validator in incremental mode.
|
||||
"""
|
||||
validator = StandardUtf8Validator()
|
||||
return self._test_utf8_incremental(validator)
|
||||
|
||||
# FIXME
|
||||
# see also (I think ..): https://twistedmatrix.com/trac/ticket/4811
|
||||
#
|
||||
# import pytest
|
||||
#
|
||||
# @pytest.mark.xfail(reason='NVX UTF8 validator lacks incremental mode implementation')
|
||||
# @unittest.expectedFailure
|
||||
# def test_nvx_utf8validator_incremental(self):
|
||||
# """
|
||||
# Test NVX implementation of UTF8 validator in incremental mode.
|
||||
# """
|
||||
# validator = NvxUtf8Validator()
|
||||
# return self._test_utf8_incremental(validator)
|
||||
|
||||
def _test_utf8(self, validator):
|
||||
for s in self._TEST_SEQUENCES:
|
||||
validator.reset()
|
||||
r = validator.validate(s[1])
|
||||
|
||||
# no UTF-8 decode error _and_ everything consumed
|
||||
res = r[0] and r[1]
|
||||
|
||||
self.assertEqual(res, s[0])
|
||||
|
||||
def _test_utf8_incremental(self, validator, withPositions=True):
|
||||
# These tests verify that the UTF-8 decoder/validator can operate incrementally.
|
||||
if withPositions:
|
||||
# testing validator 4 on incremental detection with positions
|
||||
k = 4
|
||||
else:
|
||||
# testing validator 2 on incremental detection without positions
|
||||
k = 2
|
||||
|
||||
validator.reset()
|
||||
self.assertEqual((True, True, 15, 15)[:k], validator.validate('µ@ßöäüàá'.encode('utf8'))[:k])
|
||||
|
||||
validator.reset()
|
||||
self.assertEqual((False, False, 0, 0)[:k], validator.validate(b"\xF5")[:k])
|
||||
|
||||
# the following 3 all fail on eating byte 7 (0xA0)
|
||||
validator.reset()
|
||||
self.assertEqual((True, True, 6, 6)[:k], validator.validate(b"\x65\x64\x69\x74\x65\x64")[:k])
|
||||
self.assertEqual((False, False, 1, 7)[:k], validator.validate(b"\xED\xA0\x80")[:k])
|
||||
|
||||
validator.reset()
|
||||
self.assertEqual((True, True, 4, 4)[:k], validator.validate(b"\x65\x64\x69\x74")[:k])
|
||||
self.assertEqual((False, False, 3, 7)[:k], validator.validate(b"\x65\x64\xED\xA0\x80")[:k])
|
||||
|
||||
validator.reset()
|
||||
self.assertEqual((True, False, 7, 7)[:k], validator.validate(b"\x65\x64\x69\x74\x65\x64\xED")[:k])
|
||||
self.assertEqual((False, False, 0, 7)[:k], validator.validate(b"\xA0\x80")[:k])
|
||||
@@ -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.
@@ -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.
@@ -0,0 +1,124 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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.rawsocket.util import create_url, parse_url
|
||||
|
||||
|
||||
class TestCreateRsUrl(unittest.TestCase):
|
||||
|
||||
def test_create_url01(self):
|
||||
self.assertEqual(create_url("localhost"), "rs://localhost:80")
|
||||
|
||||
def test_create_url02(self):
|
||||
self.assertEqual(create_url("localhost", port=8090), "rs://localhost:8090")
|
||||
|
||||
def test_create_url03(self):
|
||||
self.assertEqual(create_url("localhost", isSecure=True), "rss://localhost:443")
|
||||
|
||||
def test_create_url04(self):
|
||||
self.assertEqual(create_url("localhost", isSecure=True, port=443), "rss://localhost:443")
|
||||
|
||||
def test_create_url05(self):
|
||||
self.assertEqual(create_url("localhost", isSecure=True, port=80), "rss://localhost:80")
|
||||
|
||||
def test_create_url06(self):
|
||||
self.assertEqual(create_url("unix", port="file.sock"), "rs://unix:file.sock")
|
||||
|
||||
def test_create_url07(self):
|
||||
self.assertEqual(create_url("unix", port="/tmp/file.sock"), "rs://unix:/tmp/file.sock")
|
||||
|
||||
def test_create_url08(self):
|
||||
self.assertEqual(create_url("unix", port="../file.sock"), "rs://unix:../file.sock")
|
||||
|
||||
def test_create_url09(self):
|
||||
self.assertEqual(create_url("unix", isSecure=True, port="file.sock"), "rss://unix:file.sock")
|
||||
|
||||
def test_create_url10(self):
|
||||
self.assertEqual(create_url("unix", isSecure=True, port="/tmp/file.sock"), "rss://unix:/tmp/file.sock")
|
||||
|
||||
def test_create_url11(self):
|
||||
self.assertEqual(create_url("unix", isSecure=True, port="../file.sock"), "rss://unix:../file.sock")
|
||||
|
||||
|
||||
class TestParseWsUrl(unittest.TestCase):
|
||||
|
||||
# parse_url -> (isSecure, host, port)
|
||||
|
||||
def test_parse_url01(self):
|
||||
self.assertEqual(parse_url("rs://localhost"), (False, 'localhost', 80))
|
||||
|
||||
def test_parse_url02(self):
|
||||
self.assertEqual(parse_url("rss://localhost"), (True, 'localhost', 443))
|
||||
|
||||
def test_parse_url03(self):
|
||||
self.assertEqual(parse_url("rs://localhost:9000"), (False, 'localhost', 9000))
|
||||
|
||||
def test_parse_url04(self):
|
||||
self.assertEqual(parse_url("rss://localhost:9000"), (True, 'localhost', 9000))
|
||||
|
||||
def test_parse_url05(self):
|
||||
self.assertRaises(Exception, parse_url, "ws://localhost")
|
||||
|
||||
def test_parse_url06(self):
|
||||
self.assertRaises(Exception, parse_url, "wss://localhost")
|
||||
|
||||
def test_parse_url07(self):
|
||||
self.assertRaises(Exception, parse_url, "ws://localhost:80")
|
||||
|
||||
def test_parse_url08(self):
|
||||
self.assertRaises(Exception, parse_url, "rs://localhost/somepath")
|
||||
|
||||
def test_parse_url09(self):
|
||||
self.assertRaises(Exception, parse_url, "rs://localhost#somefrag")
|
||||
|
||||
def test_parse_url10(self):
|
||||
self.assertRaises(Exception, parse_url, "rs://localhost?foo=bar")
|
||||
|
||||
def test_parse_url11(self):
|
||||
self.assertRaises(Exception, parse_url, "rss://")
|
||||
|
||||
def test_parse_url12(self):
|
||||
self.assertRaises(Exception, parse_url, "rs://")
|
||||
|
||||
def test_parse_url13(self):
|
||||
self.assertEqual(parse_url("rs://unix:file.sock"), (False, 'unix', 'file.sock'))
|
||||
|
||||
def test_parse_url14(self):
|
||||
self.assertEqual(parse_url("rs://unix:/tmp/file.sock"), (False, 'unix', '/tmp/file.sock'))
|
||||
|
||||
def test_parse_url15(self):
|
||||
self.assertEqual(parse_url("rs://unix:../file.sock"), (False, 'unix', '../file.sock'))
|
||||
|
||||
def test_parse_url16(self):
|
||||
self.assertEqual(parse_url("rss://unix:file.sock"), (True, 'unix', 'file.sock'))
|
||||
|
||||
def test_parse_url17(self):
|
||||
self.assertEqual(parse_url("rss://unix:/tmp/file.sock"), (True, 'unix', '/tmp/file.sock'))
|
||||
|
||||
def test_parse_url18(self):
|
||||
self.assertEqual(parse_url("rss://unix:../file.sock"), (True, 'unix', '../file.sock'))
|
||||
163
backend/lib/python3.12/site-packages/autobahn/rawsocket/util.py
Normal file
163
backend/lib/python3.12/site-packages/autobahn/rawsocket/util.py
Normal file
@@ -0,0 +1,163 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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
|
||||
|
||||
# The Python urlparse module currently does not contain the rs/rss
|
||||
# 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
|
||||
#
|
||||
from urllib import parse as urlparse
|
||||
|
||||
wsschemes = ["rs", "rss"]
|
||||
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):
|
||||
"""
|
||||
Create a RawSocket URL from components.
|
||||
|
||||
:param hostname: RawSocket server hostname (for TCP/IP sockets) or
|
||||
filesystem path (for Unix domain sockets).
|
||||
:type hostname: str
|
||||
|
||||
:param port: For TCP/IP sockets, RawSocket 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 RawSocket (``rss`` scheme).
|
||||
:type isSecure: bool
|
||||
|
||||
:returns: Constructed RawSocket 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 = "{}:443".format(hostname)
|
||||
else:
|
||||
netloc = "{}:80".format(hostname)
|
||||
|
||||
if isSecure:
|
||||
scheme = "rss"
|
||||
else:
|
||||
scheme = "rs"
|
||||
|
||||
return "{}://{}".format(scheme, netloc)
|
||||
|
||||
|
||||
@public
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses as RawSocket URL into it's components and returns a tuple:
|
||||
|
||||
- ``isSecure`` is a flag which is ``True`` for ``rss`` 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 RawSocket URL, i.e. ``rs://localhost:9000`` for TCP/IP sockets or
|
||||
``rs://unix:/tmp/file.sock`` for Unix domain sockets (UDS).
|
||||
:type url: str
|
||||
|
||||
:returns: A 3-tuple ``(isSecure, host, tcp_port)`` (TCP/IP) or ``(isSecure, host, uds_path)`` (UDS).
|
||||
:rtype: tuple
|
||||
"""
|
||||
parsed = urlparse.urlparse(url)
|
||||
|
||||
if parsed.scheme not in ["rs", "rss"]:
|
||||
raise Exception("invalid RawSocket URL: protocol scheme '{}' is not for RawSocket".format(parsed.scheme))
|
||||
|
||||
if not parsed.hostname or parsed.hostname == "":
|
||||
raise Exception("invalid RawSocket URL: missing hostname")
|
||||
|
||||
if parsed.query is not None and parsed.query != "":
|
||||
raise Exception("invalid RawSocket URL: non-empty query '{}'".format(parsed.query))
|
||||
|
||||
if parsed.fragment is not None and parsed.fragment != "":
|
||||
raise Exception("invalid RawSocket URL: non-empty fragment '{}'".format(parsed.fragment))
|
||||
|
||||
if parsed.hostname == "unix":
|
||||
# Unix domain sockets sockets
|
||||
|
||||
# rs://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 "uds_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 == "rss", parsed.hostname, uds_path
|
||||
|
||||
else:
|
||||
# TCP/IP sockets
|
||||
|
||||
if parsed.path is not None and parsed.path != "":
|
||||
raise Exception("invalid RawSocket URL: non-empty path '{}'".format(parsed.path))
|
||||
|
||||
if parsed.port is None or parsed.port == "":
|
||||
if parsed.scheme == "rs":
|
||||
tcp_port = 80
|
||||
else:
|
||||
tcp_port = 443
|
||||
else:
|
||||
tcp_port = int(parsed.port)
|
||||
|
||||
if tcp_port < 1 or tcp_port > 65535:
|
||||
raise Exception("invalid port {}".format(tcp_port))
|
||||
|
||||
return parsed.scheme == "rss", parsed.hostname, tcp_port
|
||||
@@ -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.
111
backend/lib/python3.12/site-packages/autobahn/test/test_rng.py
Normal file
111
backend/lib/python3.12/site-packages/autobahn/test/test_rng.py
Normal file
@@ -0,0 +1,111 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 sys
|
||||
import unittest
|
||||
|
||||
import uuid
|
||||
import random
|
||||
from nacl import utils, public
|
||||
|
||||
from autobahn import util
|
||||
|
||||
|
||||
@unittest.skipIf(not ('AUTOBAHN_CI_ENABLE_RNG_DEPLETION_TESTS' in os.environ and os.environ['AUTOBAHN_CI_ENABLE_RNG_DEPLETION_TESTS']), 'entropy depletion tests not enabled (env var AUTOBAHN_CI_ENABLE_RNG_DEPLETION_TESTS not set)')
|
||||
@unittest.skipIf(not sys.platform.startswith('linux'), 'entropy depletion tests only available on Linux')
|
||||
class TestEntropy(unittest.TestCase):
|
||||
|
||||
def test_non_depleting(self):
|
||||
res = {}
|
||||
|
||||
with open('/dev/urandom', 'rb') as rng:
|
||||
for i in range(1000):
|
||||
for j in range(100):
|
||||
|
||||
# "reseed" (seems pointless, but ..)
|
||||
random.seed()
|
||||
|
||||
# random UUIDs
|
||||
v1 = uuid.uuid4() # noqa
|
||||
|
||||
# stdlib random
|
||||
v2 = random.random() # noqa
|
||||
v3 = random.getrandbits(32) # noqa
|
||||
v4 = random.randint(0, 9007199254740992) # noqa
|
||||
v5 = random.normalvariate(10, 100) # noqa
|
||||
v6 = random.choice(range(100)) # noqa
|
||||
|
||||
# PyNaCl
|
||||
v7 = utils.random(public.Box.NONCE_SIZE) # noqa
|
||||
|
||||
# Autobahn utils
|
||||
v8 = util.generate_token(4, 4) # noqa
|
||||
v9 = util.id() # noqa
|
||||
v10 = util.rid() # noqa
|
||||
v11 = util.newid() # noqa
|
||||
|
||||
# direct procfs access to PRNG
|
||||
d = rng.read(1000) # noqa
|
||||
|
||||
# check available entropy
|
||||
with open('/proc/sys/kernel/random/entropy_avail', 'r') as ent:
|
||||
ea = int(ent.read()) // 100
|
||||
if ea not in res:
|
||||
res[ea] = 0
|
||||
res[ea] += 1
|
||||
|
||||
skeys = sorted(res.keys())
|
||||
|
||||
print('\nsystem entropy depletion stats:')
|
||||
for k in skeys:
|
||||
print('{}: {}'.format(k, res[k]))
|
||||
|
||||
self.assertTrue(skeys[0] > 0)
|
||||
|
||||
def test_depleting(self):
|
||||
res = {}
|
||||
|
||||
with open('/dev/random', 'rb') as rng:
|
||||
for i in range(10000):
|
||||
|
||||
# direct procfs access to "real" RNG
|
||||
d = rng.read(1000) # noqa
|
||||
|
||||
# check available entropy
|
||||
with open('/proc/sys/kernel/random/entropy_avail', 'r') as ent:
|
||||
ea = int(ent.read()) // 100
|
||||
if ea not in res:
|
||||
res[ea] = 0
|
||||
res[ea] += 1
|
||||
|
||||
skeys = sorted(res.keys())
|
||||
|
||||
print('\nsystem entropy depletion stats:')
|
||||
for k in skeys:
|
||||
print('{}: {}'.format(k, res[k]))
|
||||
|
||||
self.assertTrue(skeys[0] == 0)
|
||||
@@ -0,0 +1,67 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 binascii import b2a_hex
|
||||
|
||||
from autobahn.util import IdGenerator, parse_activation_code, generate_activation_code, generate_token
|
||||
|
||||
|
||||
class TestIdGenerator(unittest.TestCase):
|
||||
def test_idgenerator_is_generator(self):
|
||||
"IdGenerator follows the generator protocol"
|
||||
g = IdGenerator()
|
||||
self.assertEqual(1, next(g))
|
||||
self.assertEqual(2, next(g))
|
||||
|
||||
def test_generator_wrap(self):
|
||||
g = IdGenerator()
|
||||
g._next = 2 ** 53 - 1 # cheat a little
|
||||
|
||||
v = next(g)
|
||||
self.assertEqual(v, 2 ** 53)
|
||||
v = next(g)
|
||||
self.assertEqual(v, 1)
|
||||
|
||||
def test_parse_valid_activation_codes(self):
|
||||
for i in range(20):
|
||||
code = generate_activation_code()
|
||||
parsed_code = parse_activation_code(code)
|
||||
self.assertTupleEqual(tuple(code.split('-')), parsed_code.groups())
|
||||
|
||||
def test_parse_invalid_activation_codes(self):
|
||||
for i in range(20):
|
||||
code = b2a_hex(os.urandom(20)).decode()
|
||||
parsed_code = parse_activation_code(code)
|
||||
self.assertEqual(None, parsed_code)
|
||||
|
||||
def test_generate_token(self):
|
||||
token = generate_token(5, 4)
|
||||
self.assertEqual(len(token), len('NUAG-UPQJ-MFGA-K5P5-MUGA'))
|
||||
self.assertEqual(len(token.split('-')), 5)
|
||||
for part in token.split('-'):
|
||||
self.assertEqual(len(part), 4)
|
||||
67
backend/lib/python3.12/site-packages/autobahn/testutil.py
Normal file
67
backend/lib/python3.12/site-packages/autobahn/testutil.py
Normal file
@@ -0,0 +1,67 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
class FakeTransport(object):
|
||||
_written = b""
|
||||
_open = True
|
||||
|
||||
def __init__(self):
|
||||
self._abort_calls = []
|
||||
|
||||
def abortConnection(self, *args, **kw):
|
||||
self._abort_calls.append((args, kw))
|
||||
|
||||
def write(self, msg):
|
||||
if not self._open:
|
||||
raise Exception("Can't write to a closed connection")
|
||||
self._written = self._written + msg
|
||||
|
||||
def loseConnection(self):
|
||||
self._open = False
|
||||
|
||||
def registerProducer(self, producer, streaming):
|
||||
# https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IConsumer.html
|
||||
raise NotImplementedError
|
||||
|
||||
def unregisterProducer(self):
|
||||
# do nothing is correct! until we fake implement registerProducer ..;)
|
||||
pass
|
||||
|
||||
def getPeer(self):
|
||||
# for Twisted, this would be an IAddress
|
||||
class _FakePeer(object):
|
||||
pass
|
||||
return _FakePeer()
|
||||
|
||||
def getHost(self):
|
||||
# for Twisted, this would be an IAddress
|
||||
class _FakeHost(object):
|
||||
pass
|
||||
return _FakeHost()
|
||||
|
||||
def abort_called(self):
|
||||
return len(self._abort_calls) > 0
|
||||
@@ -0,0 +1,85 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 platform
|
||||
|
||||
import twisted
|
||||
|
||||
import autobahn
|
||||
|
||||
# Twisted specific utilities (these should really be in Twisted, but
|
||||
# they aren't, and we use these in example code, so it must be part of
|
||||
# the public API)
|
||||
from autobahn.twisted.util import sleep
|
||||
from autobahn.twisted.choosereactor import install_reactor
|
||||
|
||||
# WebSocket protocol support
|
||||
from autobahn.twisted.websocket import \
|
||||
WebSocketServerProtocol, \
|
||||
WebSocketClientProtocol, \
|
||||
WebSocketServerFactory, \
|
||||
WebSocketClientFactory
|
||||
|
||||
# support for running Twisted stream protocols over WebSocket
|
||||
from autobahn.twisted.websocket import WrappingWebSocketServerFactory, \
|
||||
WrappingWebSocketClientFactory
|
||||
|
||||
# Twisted Web support - FIXME: these imports trigger import of Twisted reactor!
|
||||
# from autobahn.twisted.resource import WebSocketResource, WSGIRootResource
|
||||
|
||||
# WAMP support
|
||||
from autobahn.twisted.wamp import ApplicationSession
|
||||
|
||||
|
||||
__all__ = (
|
||||
# this should really be in Twisted
|
||||
'sleep',
|
||||
'install_reactor',
|
||||
|
||||
# WebSocket
|
||||
'WebSocketServerProtocol',
|
||||
'WebSocketClientProtocol',
|
||||
'WebSocketServerFactory',
|
||||
'WebSocketClientFactory',
|
||||
|
||||
# wrapping stream protocols in WebSocket
|
||||
'WrappingWebSocketServerFactory',
|
||||
'WrappingWebSocketClientFactory',
|
||||
|
||||
# Twisted Web - FIXME: see comment for import above
|
||||
# 'WebSocketResource',
|
||||
|
||||
# this should really be in Twisted - FIXME: see comment for import above
|
||||
# 'WSGIRootResource',
|
||||
|
||||
# WAMP support
|
||||
'ApplicationSession',
|
||||
)
|
||||
|
||||
__ident__ = 'Autobahn/{}-Twisted/{}-{}/{}'.format(autobahn.__version__, twisted.__version__, platform.python_implementation(), platform.python_version())
|
||||
"""
|
||||
AutobahnPython library implementation (eg. "Autobahn/0.13.0-Twisted/15.5.0-CPython/3.5.1")
|
||||
"""
|
||||
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,226 @@
|
||||
########################################
|
||||
#
|
||||
# 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 sys
|
||||
import traceback
|
||||
|
||||
import txaio
|
||||
txaio.use_twisted()
|
||||
|
||||
from twisted.python import reflect
|
||||
from twisted.internet.error import ReactorAlreadyInstalledError
|
||||
|
||||
__all__ = (
|
||||
'install_optimal_reactor',
|
||||
'install_reactor',
|
||||
'current_reactor_klass'
|
||||
)
|
||||
|
||||
|
||||
def current_reactor_klass():
|
||||
"""
|
||||
Return class name of currently installed Twisted reactor or None.
|
||||
"""
|
||||
if 'twisted.internet.reactor' in sys.modules:
|
||||
current_reactor = reflect.qual(sys.modules['twisted.internet.reactor'].__class__).split('.')[-1]
|
||||
else:
|
||||
current_reactor = None
|
||||
return current_reactor
|
||||
|
||||
|
||||
def install_optimal_reactor(require_optimal_reactor=True):
|
||||
"""
|
||||
Try to install the optimal Twisted reactor for this platform:
|
||||
|
||||
- Linux: epoll
|
||||
- BSD/OSX: kqueue
|
||||
- Windows: iocp
|
||||
- Other: select
|
||||
|
||||
Notes:
|
||||
|
||||
- This function exists, because the reactor types selected based on platform
|
||||
in `twisted.internet.default` are different from here.
|
||||
- The imports are inlined, because the Twisted code base is notorious for
|
||||
importing the reactor as a side-effect of merely importing. Hence we postpone
|
||||
all importing.
|
||||
|
||||
See: http://twistedmatrix.com/documents/current/core/howto/choosing-reactor.html#reactor-functionality
|
||||
|
||||
:param require_optimal_reactor: If ``True`` and the desired reactor could not be installed,
|
||||
raise ``ReactorAlreadyInstalledError``, else fallback to another reactor.
|
||||
:type require_optimal_reactor: bool
|
||||
|
||||
:returns: The Twisted reactor in place (`twisted.internet.reactor`).
|
||||
"""
|
||||
log = txaio.make_logger()
|
||||
|
||||
# determine currently installed reactor, if any
|
||||
#
|
||||
current_reactor = current_reactor_klass()
|
||||
|
||||
# depending on platform, install optimal reactor
|
||||
#
|
||||
if 'bsd' in sys.platform or sys.platform.startswith('darwin'):
|
||||
|
||||
# *BSD and MacOSX
|
||||
#
|
||||
if current_reactor != 'KQueueReactor':
|
||||
if current_reactor is None:
|
||||
try:
|
||||
from twisted.internet import kqreactor
|
||||
kqreactor.install()
|
||||
except:
|
||||
log.warn('Running on *BSD or MacOSX, but cannot install kqueue Twisted reactor: {tb}', tb=traceback.format_exc())
|
||||
else:
|
||||
log.debug('Running on *BSD or MacOSX and optimal reactor (kqueue) was installed.')
|
||||
else:
|
||||
log.warn('Running on *BSD or MacOSX, but cannot install kqueue Twisted reactor, because another reactor ({klass}) is already installed.', klass=current_reactor)
|
||||
if require_optimal_reactor:
|
||||
raise ReactorAlreadyInstalledError()
|
||||
else:
|
||||
log.debug('Running on *BSD or MacOSX and optimal reactor (kqueue) already installed.')
|
||||
|
||||
elif sys.platform in ['win32']:
|
||||
|
||||
# Windows
|
||||
#
|
||||
if current_reactor != 'IOCPReactor':
|
||||
if current_reactor is None:
|
||||
try:
|
||||
from twisted.internet.iocpreactor import reactor as iocpreactor
|
||||
iocpreactor.install()
|
||||
except:
|
||||
log.warn('Running on Windows, but cannot install IOCP Twisted reactor: {tb}', tb=traceback.format_exc())
|
||||
else:
|
||||
log.debug('Running on Windows and optimal reactor (ICOP) was installed.')
|
||||
else:
|
||||
log.warn('Running on Windows, but cannot install IOCP Twisted reactor, because another reactor ({klass}) is already installed.', klass=current_reactor)
|
||||
if require_optimal_reactor:
|
||||
raise ReactorAlreadyInstalledError()
|
||||
else:
|
||||
log.debug('Running on Windows and optimal reactor (ICOP) already installed.')
|
||||
|
||||
elif sys.platform.startswith('linux'):
|
||||
|
||||
# Linux
|
||||
#
|
||||
if current_reactor != 'EPollReactor':
|
||||
if current_reactor is None:
|
||||
try:
|
||||
from twisted.internet import epollreactor
|
||||
epollreactor.install()
|
||||
except:
|
||||
log.warn('Running on Linux, but cannot install Epoll Twisted reactor: {tb}', tb=traceback.format_exc())
|
||||
else:
|
||||
log.debug('Running on Linux and optimal reactor (epoll) was installed.')
|
||||
else:
|
||||
log.warn('Running on Linux, but cannot install Epoll Twisted reactor, because another reactor ({klass}) is already installed.', klass=current_reactor)
|
||||
if require_optimal_reactor:
|
||||
raise ReactorAlreadyInstalledError()
|
||||
else:
|
||||
log.debug('Running on Linux and optimal reactor (epoll) already installed.')
|
||||
|
||||
else:
|
||||
|
||||
# Other platform
|
||||
#
|
||||
if current_reactor != 'SelectReactor':
|
||||
if current_reactor is None:
|
||||
try:
|
||||
from twisted.internet import selectreactor
|
||||
selectreactor.install()
|
||||
# from twisted.internet import default as defaultreactor
|
||||
# defaultreactor.install()
|
||||
except:
|
||||
log.warn('Running on "{platform}", but cannot install Select Twisted reactor: {tb}', tb=traceback.format_exc(), platform=sys.platform)
|
||||
else:
|
||||
log.debug('Running on "{platform}" and optimal reactor (Select) was installed.', platform=sys.platform)
|
||||
else:
|
||||
log.warn('Running on "{platform}", but cannot install Select Twisted reactor, because another reactor ({klass}) is already installed.', klass=current_reactor, platform=sys.platform)
|
||||
if require_optimal_reactor:
|
||||
raise ReactorAlreadyInstalledError()
|
||||
else:
|
||||
log.debug('Running on "{platform}" and optimal reactor (Select) already installed.', platform=sys.platform)
|
||||
|
||||
from twisted.internet import reactor
|
||||
txaio.config.loop = reactor
|
||||
|
||||
return reactor
|
||||
|
||||
|
||||
def install_reactor(explicit_reactor=None, verbose=False, log=None, require_optimal_reactor=True):
|
||||
"""
|
||||
Install Twisted reactor.
|
||||
|
||||
:param explicit_reactor: If provided, install this reactor. Else, install
|
||||
the optimal reactor.
|
||||
:type explicit_reactor: obj
|
||||
|
||||
:param verbose: If ``True``, log (at level "info") the reactor that is
|
||||
in place afterwards.
|
||||
:type verbose: bool
|
||||
|
||||
:param log: Explicit logging to this txaio logger object.
|
||||
:type log: obj
|
||||
|
||||
:param require_optimal_reactor: If ``True`` and the desired reactor could not be installed,
|
||||
raise ``ReactorAlreadyInstalledError``, else fallback to another reactor.
|
||||
:type require_optimal_reactor: bool
|
||||
|
||||
:returns: The Twisted reactor in place (`twisted.internet.reactor`).
|
||||
"""
|
||||
if not log:
|
||||
log = txaio.make_logger()
|
||||
|
||||
if explicit_reactor:
|
||||
# install explicitly given reactor
|
||||
#
|
||||
from twisted.application.reactors import installReactor
|
||||
if verbose:
|
||||
log.info('Trying to install explicitly specified Twisted reactor "{reactor}" ..', reactor=explicit_reactor)
|
||||
try:
|
||||
installReactor(explicit_reactor)
|
||||
except:
|
||||
log.failure('Could not install Twisted reactor {reactor}\n{log_failure.value}',
|
||||
reactor=explicit_reactor)
|
||||
sys.exit(1)
|
||||
else:
|
||||
# automatically choose optimal reactor
|
||||
#
|
||||
if verbose:
|
||||
log.info('Automatically choosing optimal Twisted reactor ..')
|
||||
install_optimal_reactor(require_optimal_reactor)
|
||||
|
||||
# now the reactor is installed, import it
|
||||
from twisted.internet import reactor
|
||||
txaio.config.loop = reactor
|
||||
|
||||
if verbose:
|
||||
from twisted.python.reflect import qual
|
||||
log.info('Running on Twisted reactor {reactor}', reactor=qual(reactor.__class__))
|
||||
|
||||
return reactor
|
||||
@@ -0,0 +1,380 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 functools import wraps
|
||||
from typing import List
|
||||
|
||||
from twisted.internet.interfaces import IStreamClientEndpoint
|
||||
from twisted.internet.endpoints import UNIXClientEndpoint
|
||||
from twisted.internet.endpoints import TCP4ClientEndpoint
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.internet.error import ReactorNotRunning
|
||||
|
||||
try:
|
||||
_TLS = True
|
||||
from twisted.internet.endpoints import SSL4ClientEndpoint
|
||||
from twisted.internet.ssl import optionsForClientTLS, CertificateOptions, Certificate
|
||||
from twisted.internet.interfaces import IOpenSSLClientConnectionCreator
|
||||
from OpenSSL import SSL
|
||||
except ImportError:
|
||||
_TLS = False
|
||||
# there's no optionsForClientTLS in older Twisteds or we might be
|
||||
# missing OpenSSL entirely.
|
||||
|
||||
import txaio
|
||||
|
||||
from autobahn.twisted.websocket import WampWebSocketClientFactory
|
||||
from autobahn.twisted.rawsocket import WampRawSocketClientFactory
|
||||
|
||||
from autobahn.wamp import component
|
||||
|
||||
from autobahn.twisted.wamp import Session
|
||||
from autobahn.wamp.serializer import create_transport_serializers, create_transport_serializer
|
||||
|
||||
__all__ = ('Component', 'run')
|
||||
|
||||
|
||||
def _unique_list(seq):
|
||||
"""
|
||||
Return a list with unique elements from sequence, preserving order.
|
||||
"""
|
||||
seen = set()
|
||||
return [x for x in seq if x not in seen and not seen.add(x)]
|
||||
|
||||
|
||||
def _camel_case_from_snake_case(snake):
|
||||
parts = snake.split('_')
|
||||
return parts[0] + ''.join(s.capitalize() for s in parts[1:])
|
||||
|
||||
|
||||
def _create_transport_factory(reactor, transport, session_factory):
|
||||
"""
|
||||
Create a WAMP-over-XXX transport factory.
|
||||
"""
|
||||
if transport.type == 'websocket':
|
||||
serializers = create_transport_serializers(transport)
|
||||
factory = WampWebSocketClientFactory(
|
||||
session_factory,
|
||||
url=transport.url,
|
||||
serializers=serializers,
|
||||
proxy=transport.proxy, # either None or a dict with host, port
|
||||
)
|
||||
|
||||
elif transport.type == 'rawsocket':
|
||||
serializer = create_transport_serializer(transport.serializers[0])
|
||||
factory = WampRawSocketClientFactory(session_factory, serializer=serializer)
|
||||
|
||||
else:
|
||||
assert(False), 'should not arrive here'
|
||||
|
||||
# set the options one at a time so we can give user better feedback
|
||||
for k, v in transport.options.items():
|
||||
try:
|
||||
factory.setProtocolOptions(**{k: v})
|
||||
except (TypeError, KeyError):
|
||||
# this allows us to document options as snake_case
|
||||
# until everything internally is upgraded from
|
||||
# camelCase
|
||||
try:
|
||||
factory.setProtocolOptions(
|
||||
**{_camel_case_from_snake_case(k): v}
|
||||
)
|
||||
except (TypeError, KeyError):
|
||||
raise ValueError(
|
||||
"Unknown {} transport option: {}={}".format(transport.type, k, v)
|
||||
)
|
||||
return factory
|
||||
|
||||
|
||||
def _create_transport_endpoint(reactor, endpoint_config):
|
||||
"""
|
||||
Create a Twisted client endpoint for a WAMP-over-XXX transport.
|
||||
"""
|
||||
if IStreamClientEndpoint.providedBy(endpoint_config):
|
||||
endpoint = IStreamClientEndpoint(endpoint_config)
|
||||
else:
|
||||
# create a connecting TCP socket
|
||||
if endpoint_config['type'] == 'tcp':
|
||||
|
||||
version = endpoint_config.get('version', 4)
|
||||
if version not in [4, 6]:
|
||||
raise ValueError('invalid IP version {} in client endpoint configuration'.format(version))
|
||||
|
||||
host = endpoint_config['host']
|
||||
if type(host) != str:
|
||||
raise ValueError('invalid type {} for host in client endpoint configuration'.format(type(host)))
|
||||
|
||||
port = endpoint_config['port']
|
||||
if type(port) != int:
|
||||
raise ValueError('invalid type {} for port in client endpoint configuration'.format(type(port)))
|
||||
|
||||
timeout = endpoint_config.get('timeout', 10) # in seconds
|
||||
if type(timeout) != int:
|
||||
raise ValueError('invalid type {} for timeout in client endpoint configuration'.format(type(timeout)))
|
||||
|
||||
tls = endpoint_config.get('tls', None)
|
||||
|
||||
# create a TLS enabled connecting TCP socket
|
||||
if tls:
|
||||
if not _TLS:
|
||||
raise RuntimeError('TLS configured in transport, but TLS support is not installed (eg OpenSSL?)')
|
||||
|
||||
# FIXME: create TLS context from configuration
|
||||
if IOpenSSLClientConnectionCreator.providedBy(tls):
|
||||
# eg created from twisted.internet.ssl.optionsForClientTLS()
|
||||
context = IOpenSSLClientConnectionCreator(tls)
|
||||
|
||||
elif isinstance(tls, dict):
|
||||
for k in tls.keys():
|
||||
if k not in ["hostname", "trust_root"]:
|
||||
raise ValueError("Invalid key '{}' in 'tls' config".format(k))
|
||||
hostname = tls.get('hostname', host)
|
||||
if type(hostname) != str:
|
||||
raise ValueError('invalid type {} for hostname in TLS client endpoint configuration'.format(hostname))
|
||||
trust_root = None
|
||||
cert_fname = tls.get("trust_root", None)
|
||||
if cert_fname is not None:
|
||||
trust_root = Certificate.loadPEM(open(cert_fname, 'r').read())
|
||||
context = optionsForClientTLS(hostname, trustRoot=trust_root)
|
||||
|
||||
elif isinstance(tls, CertificateOptions):
|
||||
context = tls
|
||||
|
||||
elif tls is True:
|
||||
context = optionsForClientTLS(host)
|
||||
|
||||
else:
|
||||
raise RuntimeError('unknown type {} for "tls" configuration in transport'.format(type(tls)))
|
||||
|
||||
if version == 4:
|
||||
endpoint = SSL4ClientEndpoint(reactor, host, port, context, timeout=timeout)
|
||||
elif version == 6:
|
||||
# there is no SSL6ClientEndpoint!
|
||||
raise RuntimeError('TLS on IPv6 not implemented')
|
||||
else:
|
||||
assert(False), 'should not arrive here'
|
||||
|
||||
# create a non-TLS connecting TCP socket
|
||||
else:
|
||||
if host.endswith(".onion"):
|
||||
# hmm, can't log here?
|
||||
# self.log.info("{host} appears to be a Tor endpoint", host=host)
|
||||
try:
|
||||
import txtorcon
|
||||
endpoint = txtorcon.TorClientEndpoint(host, port)
|
||||
except ImportError:
|
||||
raise RuntimeError(
|
||||
"{} appears to be a Tor Onion service, but txtorcon is not installed".format(
|
||||
host,
|
||||
)
|
||||
)
|
||||
elif version == 4:
|
||||
endpoint = TCP4ClientEndpoint(reactor, host, port, timeout=timeout)
|
||||
elif version == 6:
|
||||
try:
|
||||
from twisted.internet.endpoints import TCP6ClientEndpoint
|
||||
except ImportError:
|
||||
raise RuntimeError('IPv6 is not supported (please upgrade Twisted)')
|
||||
endpoint = TCP6ClientEndpoint(reactor, host, port, timeout=timeout)
|
||||
else:
|
||||
assert(False), 'should not arrive here'
|
||||
|
||||
# create a connecting Unix domain socket
|
||||
elif endpoint_config['type'] == 'unix':
|
||||
path = endpoint_config['path']
|
||||
timeout = int(endpoint_config.get('timeout', 10)) # in seconds
|
||||
endpoint = UNIXClientEndpoint(reactor, path, timeout=timeout)
|
||||
|
||||
else:
|
||||
assert(False), 'should not arrive here'
|
||||
|
||||
return endpoint
|
||||
|
||||
|
||||
class Component(component.Component):
|
||||
"""
|
||||
A component establishes a transport and attached a session
|
||||
to a realm using the transport for communication.
|
||||
|
||||
The transports a component tries to use can be configured,
|
||||
as well as the auto-reconnect strategy.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
session_factory = Session
|
||||
"""
|
||||
The factory of the session we will instantiate.
|
||||
"""
|
||||
|
||||
def _is_ssl_error(self, e):
|
||||
"""
|
||||
Internal helper.
|
||||
|
||||
This is so we can just return False if we didn't import any
|
||||
TLS/SSL libraries. Otherwise, returns True if this is an
|
||||
OpenSSL.SSL.Error
|
||||
"""
|
||||
if _TLS:
|
||||
return isinstance(e, SSL.Error)
|
||||
return False
|
||||
|
||||
def _check_native_endpoint(self, endpoint):
|
||||
if IStreamClientEndpoint.providedBy(endpoint):
|
||||
pass
|
||||
elif isinstance(endpoint, dict):
|
||||
if 'tls' in endpoint:
|
||||
tls = endpoint['tls']
|
||||
if isinstance(tls, (dict, bool)):
|
||||
pass
|
||||
elif IOpenSSLClientConnectionCreator.providedBy(tls):
|
||||
pass
|
||||
elif isinstance(tls, CertificateOptions):
|
||||
pass
|
||||
else:
|
||||
raise ValueError(
|
||||
"'tls' configuration must be a dict, CertificateOptions or"
|
||||
" IOpenSSLClientConnectionCreator provider"
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"'endpoint' configuration must be a dict or IStreamClientEndpoint"
|
||||
" provider"
|
||||
)
|
||||
|
||||
def _connect_transport(self, reactor, transport, session_factory, done):
|
||||
"""
|
||||
Create and connect a WAMP-over-XXX transport.
|
||||
|
||||
:param done: is a Deferred/Future from the parent which we
|
||||
should signal upon error if it is not done yet (XXX maybe an
|
||||
"on_error" callable instead?)
|
||||
"""
|
||||
transport_factory = _create_transport_factory(reactor, transport, session_factory)
|
||||
if transport.proxy:
|
||||
transport_endpoint = _create_transport_endpoint(
|
||||
reactor,
|
||||
{
|
||||
"type": "tcp",
|
||||
"host": transport.proxy["host"],
|
||||
"port": transport.proxy["port"],
|
||||
}
|
||||
)
|
||||
else:
|
||||
transport_endpoint = _create_transport_endpoint(reactor, transport.endpoint)
|
||||
d = transport_endpoint.connect(transport_factory)
|
||||
|
||||
def on_connect_success(proto):
|
||||
|
||||
# if e.g. an SSL handshake fails, we will have
|
||||
# successfully connected (i.e. get here) but need to
|
||||
# 'listen' for the "connectionLost" from the underlying
|
||||
# protocol in case of handshake failure .. so we wrap
|
||||
# it. Also, we don't increment transport.success_count
|
||||
# here on purpose (because we might not succeed).
|
||||
orig = proto.connectionLost
|
||||
|
||||
@wraps(orig)
|
||||
def lost(fail):
|
||||
rtn = orig(fail)
|
||||
if not txaio.is_called(done):
|
||||
txaio.reject(done, fail)
|
||||
return rtn
|
||||
proto.connectionLost = lost
|
||||
|
||||
def on_connect_failure(err):
|
||||
transport.connect_failures += 1
|
||||
# failed to establish a connection in the first place
|
||||
txaio.reject(done, err)
|
||||
|
||||
txaio.add_callbacks(d, on_connect_success, None)
|
||||
txaio.add_callbacks(d, None, on_connect_failure)
|
||||
return d
|
||||
|
||||
def start(self, reactor=None):
|
||||
"""
|
||||
This starts the Component, which means it will start connecting
|
||||
(and re-connecting) to its configured transports. A Component
|
||||
runs until it is "done", which means one of:
|
||||
- There was a "main" function defined, and it completed successfully;
|
||||
- Something called ``.leave()`` on our session, and we left successfully;
|
||||
- ``.stop()`` was called, and completed successfully;
|
||||
- none of our transports were able to connect successfully (failure);
|
||||
|
||||
:returns: a Deferred that fires (with ``None``) when we are
|
||||
"done" or with a Failure if something went wrong.
|
||||
"""
|
||||
if reactor is None:
|
||||
self.log.warn("Using default reactor")
|
||||
from twisted.internet import reactor
|
||||
|
||||
return self._start(loop=reactor)
|
||||
|
||||
|
||||
def run(components: List[Component], log_level: str = 'info', stop_at_close: bool = True):
|
||||
"""
|
||||
High-level API to run a series of components.
|
||||
|
||||
This will only return once all the components have stopped
|
||||
(including, possibly, after all re-connections have failed if you
|
||||
have re-connections enabled). Under the hood, this calls
|
||||
:meth:`twisted.internet.reactor.run` -- if you wish to manage the
|
||||
reactor loop yourself, use the
|
||||
:meth:`autobahn.twisted.component.Component.start` method to start
|
||||
each component yourself.
|
||||
|
||||
:param components: the Component(s) you wish to run
|
||||
:param log_level: a valid log-level (or None to avoid calling start_logging)
|
||||
:param stop_at_close: Flag to control whether to stop the reactor when done.
|
||||
"""
|
||||
# only for Twisted > 12
|
||||
# ...so this isn't in all Twisted versions we test against -- need
|
||||
# to do "something else" if we can't import .. :/ (or drop some
|
||||
# support)
|
||||
from twisted.internet.task import react
|
||||
|
||||
# actually, should we even let people "not start" the logging? I'm
|
||||
# not sure that's wise... (double-check: if they already called
|
||||
# txaio.start_logging() what happens if we call it again?)
|
||||
if log_level is not None:
|
||||
txaio.start_logging(level=log_level)
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
if stop_at_close:
|
||||
def done_callback(reactor, arg):
|
||||
if isinstance(arg, Failure):
|
||||
log.error('Something went wrong: {log_failure}', failure=arg)
|
||||
try:
|
||||
log.warn('Stopping reactor ..')
|
||||
reactor.stop()
|
||||
except ReactorNotRunning:
|
||||
pass
|
||||
else:
|
||||
done_callback = None
|
||||
|
||||
react(component._run, (components, done_callback))
|
||||
@@ -0,0 +1,152 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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.wamp.cryptosign import HAS_CRYPTOSIGN, CryptosignKey
|
||||
|
||||
from twisted.internet.defer import inlineCallbacks, returnValue
|
||||
|
||||
__all__ = [
|
||||
'HAS_CRYPTOSIGN_SSHAGENT'
|
||||
]
|
||||
|
||||
if HAS_CRYPTOSIGN:
|
||||
try:
|
||||
# WAMP-cryptosign support for SSH agent is currently
|
||||
# only available on Twisted (on Python 2)
|
||||
from twisted.internet.protocol import Factory
|
||||
from twisted.internet.endpoints import UNIXClientEndpoint
|
||||
from twisted.conch.ssh.agent import SSHAgentClient
|
||||
except ImportError:
|
||||
# twisted.conch is not yet fully ported to Python 3
|
||||
HAS_CRYPTOSIGN_SSHAGENT = False
|
||||
else:
|
||||
HAS_CRYPTOSIGN_SSHAGENT = True
|
||||
__all__.append('SSHAgentCryptosignKey')
|
||||
|
||||
|
||||
if HAS_CRYPTOSIGN_SSHAGENT:
|
||||
|
||||
import os
|
||||
from nacl import signing
|
||||
from autobahn.wamp.cryptosign import _read_ssh_ed25519_pubkey, _unpack, _pack
|
||||
|
||||
class SSHAgentCryptosignKey(CryptosignKey):
|
||||
"""
|
||||
A WAMP-cryptosign signing key that is a proxy to a private Ed25510 key
|
||||
actually held in SSH agent.
|
||||
|
||||
An instance of this class must be create via the class method new().
|
||||
The instance only holds the public key part, whereas the private key
|
||||
counterpart is held in SSH agent.
|
||||
"""
|
||||
|
||||
def __init__(self, key, comment=None, reactor=None):
|
||||
CryptosignKey.__init__(self, key, comment)
|
||||
if not reactor:
|
||||
from twisted.internet import reactor
|
||||
self._reactor = reactor
|
||||
|
||||
@classmethod
|
||||
def new(cls, pubkey=None, reactor=None):
|
||||
"""
|
||||
Create a proxy for a key held in SSH agent.
|
||||
|
||||
:param pubkey: A string with a public Ed25519 key in SSH format.
|
||||
:type pubkey: unicode
|
||||
"""
|
||||
if not HAS_CRYPTOSIGN_SSHAGENT:
|
||||
raise Exception("SSH agent integration is not supported on this platform")
|
||||
|
||||
pubkey, _ = _read_ssh_ed25519_pubkey(pubkey)
|
||||
|
||||
if not reactor:
|
||||
from twisted.internet import reactor
|
||||
|
||||
if "SSH_AUTH_SOCK" not in os.environ:
|
||||
raise Exception("no ssh-agent is running!")
|
||||
|
||||
factory = Factory()
|
||||
factory.noisy = False
|
||||
factory.protocol = SSHAgentClient
|
||||
endpoint = UNIXClientEndpoint(reactor, os.environ["SSH_AUTH_SOCK"])
|
||||
d = endpoint.connect(factory)
|
||||
|
||||
@inlineCallbacks
|
||||
def on_connect(agent):
|
||||
keys = yield agent.requestIdentities()
|
||||
|
||||
# if the key is found in ssh-agent, the raw public key (32 bytes), and the
|
||||
# key comment as returned from ssh-agent
|
||||
key_data = None
|
||||
key_comment = None
|
||||
|
||||
for blob, comment in keys:
|
||||
raw = _unpack(blob)
|
||||
algo = raw[0].decode('utf8')
|
||||
if algo == 'ssh-ed25519':
|
||||
algo, _pubkey = raw
|
||||
if _pubkey == pubkey:
|
||||
key_data = _pubkey
|
||||
key_comment = comment.decode('utf8')
|
||||
break
|
||||
|
||||
agent.transport.loseConnection()
|
||||
|
||||
if key_data:
|
||||
key = signing.VerifyKey(key_data)
|
||||
returnValue(cls(key, key_comment, reactor))
|
||||
else:
|
||||
raise Exception("Ed25519 key not held in ssh-agent")
|
||||
|
||||
return d.addCallback(on_connect)
|
||||
|
||||
def sign(self, challenge):
|
||||
if "SSH_AUTH_SOCK" not in os.environ:
|
||||
raise Exception("no ssh-agent is running!")
|
||||
|
||||
factory = Factory()
|
||||
factory.noisy = False
|
||||
factory.protocol = SSHAgentClient
|
||||
endpoint = UNIXClientEndpoint(self._reactor, os.environ["SSH_AUTH_SOCK"])
|
||||
d = endpoint.connect(factory)
|
||||
|
||||
@inlineCallbacks
|
||||
def on_connect(agent):
|
||||
# we are now connected to the locally running ssh-agent
|
||||
# that agent might be the openssh-agent, or eg on Ubuntu 14.04 by
|
||||
# default the gnome-keyring / ssh-askpass-gnome application
|
||||
blob = _pack(['ssh-ed25519'.encode(), self.public_key(binary=True)])
|
||||
|
||||
# now ask the agent
|
||||
signature_blob = yield agent.signData(blob, challenge)
|
||||
algo, signature = _unpack(signature_blob)
|
||||
|
||||
agent.transport.loseConnection()
|
||||
|
||||
returnValue(signature)
|
||||
|
||||
return d.addCallback(on_connect)
|
||||
@@ -0,0 +1,128 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 txaio
|
||||
txaio.use_twisted()
|
||||
|
||||
from twisted.python import usage
|
||||
from twisted.internet.defer import inlineCallbacks
|
||||
from twisted.internet.protocol import Factory, Protocol
|
||||
from twisted.internet.endpoints import clientFromString, serverFromString
|
||||
from twisted.application import service
|
||||
|
||||
|
||||
class DestEndpointForwardingProtocol(Protocol):
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
def connectionMade(self):
|
||||
self.log.debug("DestEndpointForwardingProtocol.connectionMade")
|
||||
pass
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.log.debug(
|
||||
"DestEndpointForwardingProtocol.dataReceived: {data}",
|
||||
data=data,
|
||||
)
|
||||
if self.factory._sourceProtocol:
|
||||
self.factory._sourceProtocol.transport.write(data)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
self.log.debug("DestEndpointForwardingProtocol.connectionLost")
|
||||
if self.factory._sourceProtocol:
|
||||
self.factory._sourceProtocol.transport.loseConnection()
|
||||
|
||||
|
||||
class DestEndpointForwardingFactory(Factory):
|
||||
|
||||
def __init__(self, sourceProtocol):
|
||||
self._sourceProtocol = sourceProtocol
|
||||
self._proto = None
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
self._proto = DestEndpointForwardingProtocol()
|
||||
self._proto.factory = self
|
||||
return self._proto
|
||||
|
||||
|
||||
class EndpointForwardingProtocol(Protocol):
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
@inlineCallbacks
|
||||
def connectionMade(self):
|
||||
self.log.debug("EndpointForwardingProtocol.connectionMade")
|
||||
self._destFactory = DestEndpointForwardingFactory(self)
|
||||
self._destEndpoint = clientFromString(self.factory.service._reactor,
|
||||
self.factory.service._destEndpointDescriptor)
|
||||
self._destEndpointPort = yield self._destEndpoint.connect(self._destFactory)
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.log.debug(
|
||||
"EndpointForwardingProtocol.dataReceived: {data}",
|
||||
data=data,
|
||||
)
|
||||
if self._destFactory._proto:
|
||||
self._destFactory._proto.transport.write(data)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
self.log.debug("EndpointForwardingProtocol.connectionLost")
|
||||
if self._destFactory._proto:
|
||||
self._destFactory._proto.transport.loseConnection()
|
||||
|
||||
|
||||
class EndpointForwardingService(service.Service):
|
||||
|
||||
def __init__(self, endpointDescriptor, destEndpointDescriptor, reactor=None):
|
||||
if reactor is None:
|
||||
from twisted.internet import reactor
|
||||
self._reactor = reactor
|
||||
self._endpointDescriptor = endpointDescriptor
|
||||
self._destEndpointDescriptor = destEndpointDescriptor
|
||||
|
||||
@inlineCallbacks
|
||||
def startService(self):
|
||||
factory = Factory.forProtocol(EndpointForwardingProtocol)
|
||||
factory.service = self
|
||||
self._endpoint = serverFromString(self._reactor, self._endpointDescriptor)
|
||||
self._endpointPort = yield self._endpoint.listen(factory)
|
||||
|
||||
def stopService(self):
|
||||
return self._endpointPort.stopListening()
|
||||
|
||||
|
||||
class Options(usage.Options):
|
||||
synopsis = "[options]"
|
||||
longdesc = 'Endpoint Forwarder.'
|
||||
optParameters = [
|
||||
["endpoint", "e", None, "Source endpoint."],
|
||||
["dest_endpoint", "d", None, "Destination endpoint."]
|
||||
]
|
||||
|
||||
|
||||
def makeService(config):
|
||||
service = EndpointForwardingService(config['endpoint'], config['dest_endpoint'])
|
||||
return service
|
||||
@@ -0,0 +1,604 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 copy
|
||||
import math
|
||||
from typing import Optional
|
||||
|
||||
import txaio
|
||||
|
||||
from twisted.internet.protocol import Factory
|
||||
from twisted.protocols.basic import Int32StringReceiver
|
||||
from twisted.internet.error import ConnectionDone
|
||||
from twisted.internet.defer import CancelledError
|
||||
|
||||
from autobahn.util import public, _LazyHexFormatter
|
||||
from autobahn.twisted.util import create_transport_details, transport_channel_id
|
||||
from autobahn.wamp.types import TransportDetails
|
||||
from autobahn.wamp.exception import ProtocolError, SerializationError, TransportLost, InvalidUriError
|
||||
from autobahn.exception import PayloadExceededError
|
||||
|
||||
__all__ = (
|
||||
'WampRawSocketServerProtocol',
|
||||
'WampRawSocketClientProtocol',
|
||||
'WampRawSocketServerFactory',
|
||||
'WampRawSocketClientFactory'
|
||||
)
|
||||
|
||||
|
||||
class WampRawSocketProtocol(Int32StringReceiver):
|
||||
"""
|
||||
Base class for Twisted-based WAMP-over-RawSocket protocols.
|
||||
"""
|
||||
log = txaio.make_logger()
|
||||
|
||||
peer: Optional[str] = None
|
||||
is_server: Optional[bool] = None
|
||||
|
||||
def __init__(self):
|
||||
# set the RawSocket maximum message size by default
|
||||
self._max_message_size = 2**24
|
||||
self._transport_details = None
|
||||
|
||||
@property
|
||||
def transport_details(self) -> Optional[TransportDetails]:
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.transport_details`
|
||||
"""
|
||||
return self._transport_details
|
||||
|
||||
def lengthLimitExceeded(self, length):
|
||||
# override hook in Int32StringReceiver base class that is fired when a message is (to be) received
|
||||
# that is larger than what we agreed to handle (by negotiation in the RawSocket opening handshake)
|
||||
emsg = 'RawSocket connection: length of received message exceeded (message was {} bytes, but current maximum is {} bytes)'.format(length, self.MAX_LENGTH)
|
||||
raise PayloadExceededError(emsg)
|
||||
|
||||
def connectionMade(self):
|
||||
# Twisted networking framework entry point, called by Twisted
|
||||
# when the connection is established (either a client or a server)
|
||||
|
||||
# determine preliminary transport details (what is know at this point)
|
||||
self._transport_details = create_transport_details(self.transport, self.is_server)
|
||||
self._transport_details.channel_framing = TransportDetails.CHANNEL_FRAMING_RAWSOCKET
|
||||
|
||||
# backward compatibility
|
||||
self.peer = self._transport_details.peer
|
||||
|
||||
# a Future/Deferred that fires when we hit STATE_CLOSED
|
||||
self.is_closed = txaio.create_future()
|
||||
|
||||
# this will hold an ApplicationSession object
|
||||
# once the RawSocket opening handshake has been
|
||||
# completed
|
||||
#
|
||||
self._session = None
|
||||
|
||||
# Will hold the negotiated serializer once the opening handshake is complete
|
||||
#
|
||||
self._serializer = None
|
||||
|
||||
# Will be set to True once the opening handshake is complete
|
||||
#
|
||||
self._handshake_complete = False
|
||||
|
||||
# Buffer for opening handshake received bytes.
|
||||
#
|
||||
self._handshake_bytes = b''
|
||||
|
||||
# Peer requested to _receive_ this maximum length of serialized messages - hence we must not send larger msgs!
|
||||
#
|
||||
self._max_len_send = None
|
||||
|
||||
def _on_handshake_complete(self):
|
||||
# RawSocket connection established. Now let the user WAMP session factory
|
||||
# create a new WAMP session and fire off session open callback.
|
||||
try:
|
||||
if self._transport_details.is_secure:
|
||||
# now that the TLS opening handshake is complete, the actual TLS channel ID
|
||||
# will be available. make sure to set it!
|
||||
channel_id = {
|
||||
'tls-unique': transport_channel_id(self.transport, self._transport_details.is_server, 'tls-unique'),
|
||||
}
|
||||
self._transport_details.channel_id = channel_id
|
||||
|
||||
self._session = self.factory._factory()
|
||||
self.log.debug('{klass}._on_handshake_complete(): calling {method}', session=self._session,
|
||||
klass=self.__class__.__name__, method=self._session.onOpen)
|
||||
res = self._session.onOpen(self)
|
||||
except Exception as e:
|
||||
# Exceptions raised in onOpen are fatal ..
|
||||
self.log.warn("{klass}._on_handshake_complete(): ApplicationSession constructor / onOpen raised ({err})",
|
||||
klass=self.__class__.__name__, err=e)
|
||||
self.abort()
|
||||
else:
|
||||
self.log.debug('{klass}._on_handshake_complete(): {session} started (res={res}).', klass=self.__class__.__name__,
|
||||
session=self._session, res=res)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
self.log.debug('{klass}.connectionLost(reason="{reason}"', klass=self.__class__.__name__, reason=reason)
|
||||
txaio.resolve(self.is_closed, self)
|
||||
try:
|
||||
wasClean = isinstance(reason.value, ConnectionDone)
|
||||
if self._session:
|
||||
self._session.onClose(wasClean)
|
||||
except Exception as e:
|
||||
# silently ignore exceptions raised here ..
|
||||
self.log.warn('{klass}.connectionLost(): ApplicationSession.onClose raised "{err}"',
|
||||
klass=self.__class__.__name__, err=e)
|
||||
self._session = None
|
||||
|
||||
def stringReceived(self, payload):
|
||||
self.log.trace('{klass}.stringReceived(): RX {octets} octets',
|
||||
klass=self.__class__.__name__, octets=_LazyHexFormatter(payload))
|
||||
try:
|
||||
for msg in self._serializer.unserialize(payload):
|
||||
self.log.trace("{klass}.stringReceived: RX WAMP message: {msg}",
|
||||
klass=self.__class__.__name__, msg=msg)
|
||||
self._session.onMessage(msg)
|
||||
|
||||
except CancelledError as e:
|
||||
self.log.debug("{klass}.stringReceived: WAMP CancelledError - connection will continue!\n{err}",
|
||||
klass=self.__class__.__name__,
|
||||
err=e)
|
||||
|
||||
except InvalidUriError as e:
|
||||
self.log.warn("{klass}.stringReceived: WAMP InvalidUriError - aborting connection!\n{err}",
|
||||
klass=self.__class__.__name__,
|
||||
err=e)
|
||||
self.abort()
|
||||
|
||||
except ProtocolError as e:
|
||||
self.log.warn("{klass}.stringReceived: WAMP ProtocolError - aborting connection!\n{err}",
|
||||
klass=self.__class__.__name__,
|
||||
err=e)
|
||||
self.abort()
|
||||
|
||||
except PayloadExceededError as e:
|
||||
self.log.warn("{klass}.stringReceived: WAMP PayloadExceededError - aborting connection!\n{err}",
|
||||
klass=self.__class__.__name__,
|
||||
err=e)
|
||||
self.abort()
|
||||
|
||||
except SerializationError as e:
|
||||
self.log.warn("{klass}.stringReceived: WAMP SerializationError - aborting connection!\n{err}",
|
||||
klass=self.__class__.__name__,
|
||||
err=e)
|
||||
self.abort()
|
||||
|
||||
except Exception as e:
|
||||
self.log.failure()
|
||||
self.log.warn("{klass}.stringReceived: WAMP Exception - aborting connection!\n{err}",
|
||||
klass=self.__class__.__name__,
|
||||
err=e)
|
||||
self.abort()
|
||||
|
||||
def send(self, msg):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.send`
|
||||
"""
|
||||
if self.isOpen():
|
||||
self.log.trace('{klass}.send() (serializer={serializer}): TX WAMP message: "{msg}"',
|
||||
klass=self.__class__.__name__, msg=msg, serializer=self._serializer)
|
||||
try:
|
||||
payload, _ = self._serializer.serialize(msg)
|
||||
except SerializationError as e:
|
||||
# all exceptions raised from above should be serialization errors ..
|
||||
raise SerializationError("WampRawSocketProtocol: unable to serialize WAMP application payload ({0})".format(e))
|
||||
else:
|
||||
payload_len = len(payload)
|
||||
if 0 < self._max_len_send < payload_len:
|
||||
emsg = 'tried to send RawSocket message with size {} exceeding payload limit of {} octets'.format(
|
||||
payload_len, self._max_len_send)
|
||||
self.log.warn(emsg)
|
||||
raise PayloadExceededError(emsg)
|
||||
else:
|
||||
self.sendString(payload)
|
||||
self.log.trace('{klass}.send(): TX {octets} octets',
|
||||
klass=self.__class__.__name__, octets=_LazyHexFormatter(payload))
|
||||
else:
|
||||
raise TransportLost()
|
||||
|
||||
def isOpen(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.isOpen`
|
||||
"""
|
||||
return self._session is not None
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.close`
|
||||
"""
|
||||
if self.isOpen():
|
||||
self.transport.loseConnection()
|
||||
else:
|
||||
raise TransportLost()
|
||||
|
||||
def abort(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.abort`
|
||||
"""
|
||||
if self.isOpen():
|
||||
if hasattr(self.transport, 'abortConnection'):
|
||||
# ProcessProtocol lacks abortConnection()
|
||||
self.transport.abortConnection()
|
||||
else:
|
||||
self.transport.loseConnection()
|
||||
else:
|
||||
raise TransportLost()
|
||||
|
||||
|
||||
@public
|
||||
class WampRawSocketServerProtocol(WampRawSocketProtocol):
|
||||
"""
|
||||
Twisted-based WAMP-over-RawSocket server protocol.
|
||||
|
||||
Implements:
|
||||
|
||||
* :class:`autobahn.wamp.interfaces.ITransport`
|
||||
"""
|
||||
|
||||
def dataReceived(self, data):
|
||||
|
||||
if self._handshake_complete:
|
||||
WampRawSocketProtocol.dataReceived(self, data)
|
||||
else:
|
||||
remaining = 4 - len(self._handshake_bytes)
|
||||
self._handshake_bytes += data[:remaining]
|
||||
|
||||
if len(self._handshake_bytes) == 4:
|
||||
|
||||
self.log.debug(
|
||||
"WampRawSocketServerProtocol: opening handshake received - 0x{octets}",
|
||||
octets=_LazyHexFormatter(self._handshake_bytes),
|
||||
)
|
||||
|
||||
# first octet must be magic octet 0x7f
|
||||
#
|
||||
_magic = ord(self._handshake_bytes[0:1])
|
||||
if _magic != 127:
|
||||
self.log.warn(
|
||||
"WampRawSocketServerProtocol: invalid magic byte (octet 1) in"
|
||||
" opening handshake: was {magic}, but expected 127",
|
||||
magic=_magic,
|
||||
)
|
||||
self.abort()
|
||||
else:
|
||||
self.log.debug('WampRawSocketServerProtocol: correct magic byte received')
|
||||
|
||||
# peer requests us to send messages of maximum length 2**max_len_exp
|
||||
#
|
||||
self._max_len_send = 2 ** (9 + (ord(self._handshake_bytes[1:2]) >> 4))
|
||||
self.log.debug(
|
||||
"WampRawSocketServerProtocol: client requests us to send out most {max_bytes} bytes per message",
|
||||
max_bytes=self._max_len_send,
|
||||
)
|
||||
|
||||
# client wants to speak this serialization format
|
||||
#
|
||||
ser_id = ord(self._handshake_bytes[1:2]) & 0x0F
|
||||
if ser_id in self.factory._serializers:
|
||||
self._serializer = copy.copy(self.factory._serializers[ser_id])
|
||||
self.log.debug(
|
||||
"WampRawSocketServerProtocol: client wants to use serializer '{serializer}'",
|
||||
serializer=ser_id,
|
||||
)
|
||||
else:
|
||||
self.log.warn(
|
||||
"WampRawSocketServerProtocol: opening handshake - no suitable serializer found (client requested {serializer}, and we have {serializers}",
|
||||
serializer=ser_id,
|
||||
serializers=self.factory._serializers.keys(),
|
||||
)
|
||||
self.abort()
|
||||
|
||||
# we request the client to send message of maximum length 2**reply_max_len_exp
|
||||
#
|
||||
reply_max_len_exp = int(math.ceil(math.log(self._max_message_size, 2)))
|
||||
|
||||
# this is an instance attribute on the Twisted base class for maximum size
|
||||
# of _received_ messages
|
||||
self.MAX_LENGTH = 2**reply_max_len_exp
|
||||
|
||||
# send out handshake reply
|
||||
#
|
||||
reply_octet2 = bytes(bytearray([
|
||||
((reply_max_len_exp - 9) << 4) | self._serializer.RAWSOCKET_SERIALIZER_ID]))
|
||||
self.transport.write(b'\x7F') # magic byte
|
||||
self.transport.write(reply_octet2) # max length / serializer
|
||||
self.transport.write(b'\x00\x00') # reserved octets
|
||||
|
||||
self._handshake_complete = True
|
||||
|
||||
self._on_handshake_complete()
|
||||
|
||||
self.log.debug(
|
||||
"WampRawSocketServerProtocol: opening handshake completed: {serializer}",
|
||||
serializer=self._serializer,
|
||||
)
|
||||
|
||||
# consume any remaining data received already ..
|
||||
#
|
||||
data = data[remaining:]
|
||||
if data:
|
||||
self.dataReceived(data)
|
||||
|
||||
|
||||
@public
|
||||
class WampRawSocketClientProtocol(WampRawSocketProtocol):
|
||||
"""
|
||||
Twisted-based WAMP-over-RawSocket client protocol.
|
||||
|
||||
Implements:
|
||||
|
||||
* :class:`autobahn.wamp.interfaces.ITransport`
|
||||
"""
|
||||
|
||||
def connectionMade(self):
|
||||
WampRawSocketProtocol.connectionMade(self)
|
||||
self._serializer = copy.copy(self.factory._serializer)
|
||||
|
||||
# we request the peer to send messages of maximum length 2**reply_max_len_exp
|
||||
request_max_len_exp = int(math.ceil(math.log(self._max_message_size, 2)))
|
||||
|
||||
# this is an instance attribute on the Twisted base class for maximum size
|
||||
# of _received_ messages
|
||||
self.MAX_LENGTH = 2**request_max_len_exp
|
||||
|
||||
# send out handshake request
|
||||
#
|
||||
request_octet2 = bytes(bytearray([
|
||||
((request_max_len_exp - 9) << 4) | self._serializer.RAWSOCKET_SERIALIZER_ID]))
|
||||
self.transport.write(b'\x7F') # magic byte
|
||||
self.transport.write(request_octet2) # max length / serializer
|
||||
self.transport.write(b'\x00\x00') # reserved octets
|
||||
|
||||
def dataReceived(self, data):
|
||||
|
||||
if self._handshake_complete:
|
||||
WampRawSocketProtocol.dataReceived(self, data)
|
||||
else:
|
||||
remaining = 4 - len(self._handshake_bytes)
|
||||
self._handshake_bytes += data[:remaining]
|
||||
|
||||
if len(self._handshake_bytes) == 4:
|
||||
|
||||
self.log.debug(
|
||||
"WampRawSocketClientProtocol: opening handshake received - {handshake}",
|
||||
handshake=_LazyHexFormatter(self._handshake_bytes),
|
||||
)
|
||||
|
||||
if ord(self._handshake_bytes[0:1]) != 0x7f:
|
||||
self.log.debug(
|
||||
"WampRawSocketClientProtocol: invalid magic byte (octet 1) in opening handshake: was 0x{magic}, but expected 0x7f",
|
||||
magic=_LazyHexFormatter(self._handshake_bytes[0]),
|
||||
)
|
||||
self.abort()
|
||||
|
||||
# peer requests us to _send_ messages of maximum length 2**max_len_exp
|
||||
#
|
||||
self._max_len_send = 2 ** (9 + (ord(self._handshake_bytes[1:2]) >> 4))
|
||||
self.log.debug(
|
||||
"WampRawSocketClientProtocol: server requests us to send out most {max} bytes per message",
|
||||
max=self._max_len_send,
|
||||
)
|
||||
|
||||
# client wants to speak this serialization format
|
||||
#
|
||||
ser_id = ord(self._handshake_bytes[1:2]) & 0x0F
|
||||
if ser_id != self._serializer.RAWSOCKET_SERIALIZER_ID:
|
||||
self.log.error(
|
||||
"WampRawSocketClientProtocol: opening handshake - no suitable serializer found (server replied {serializer}, and we requested {serializers})",
|
||||
serializer=ser_id,
|
||||
serializers=self._serializer.RAWSOCKET_SERIALIZER_ID,
|
||||
)
|
||||
self.abort()
|
||||
|
||||
self._handshake_complete = True
|
||||
|
||||
self._on_handshake_complete()
|
||||
|
||||
self.log.debug(
|
||||
"WampRawSocketClientProtocol: opening handshake completed (using serializer {serializer})",
|
||||
serializer=self._serializer,
|
||||
)
|
||||
|
||||
# consume any remaining data received already ..
|
||||
#
|
||||
data = data[remaining:]
|
||||
if data:
|
||||
self.dataReceived(data)
|
||||
|
||||
|
||||
class WampRawSocketFactory(Factory):
|
||||
"""
|
||||
Base class for Twisted-based WAMP-over-RawSocket factories.
|
||||
"""
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __init__(self, factory):
|
||||
"""
|
||||
|
||||
:param factory: A callable that produces instances that implement
|
||||
:class:`autobahn.wamp.interfaces.ITransportHandler`
|
||||
:type factory: callable
|
||||
"""
|
||||
if callable(factory):
|
||||
self._factory = factory
|
||||
else:
|
||||
self._factory = lambda: factory
|
||||
|
||||
# RawSocket max payload size is 16M (https://wamp-proto.org/_static/gen/wamp_latest_ietf.html#handshake)
|
||||
self._max_message_size = 2**24
|
||||
|
||||
def resetProtocolOptions(self):
|
||||
self._max_message_size = 2**24
|
||||
|
||||
def setProtocolOptions(self, maxMessagePayloadSize=None):
|
||||
self.log.debug('{klass}.setProtocolOptions(maxMessagePayloadSize={maxMessagePayloadSize})',
|
||||
klass=self.__class__.__name__, maxMessagePayloadSize=maxMessagePayloadSize)
|
||||
assert maxMessagePayloadSize is None or (type(maxMessagePayloadSize) == int and maxMessagePayloadSize >= 512 and maxMessagePayloadSize <= 2**24)
|
||||
if maxMessagePayloadSize is not None and maxMessagePayloadSize != self._max_message_size:
|
||||
self._max_message_size = maxMessagePayloadSize
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
self.log.debug('{klass}.buildProtocol(addr={addr})', klass=self.__class__.__name__, addr=addr)
|
||||
p = self.protocol()
|
||||
p.factory = self
|
||||
p.MAX_LENGTH = self._max_message_size
|
||||
p._max_message_size = self._max_message_size
|
||||
self.log.debug('{klass}.buildProtocol() -> proto={proto}, max_message_size={max_message_size}, MAX_LENGTH={MAX_LENGTH}',
|
||||
klass=self.__class__.__name__, proto=p, max_message_size=p._max_message_size, MAX_LENGTH=p.MAX_LENGTH)
|
||||
return p
|
||||
|
||||
|
||||
@public
|
||||
class WampRawSocketServerFactory(WampRawSocketFactory):
|
||||
"""
|
||||
Twisted-based WAMP-over-RawSocket server protocol factory.
|
||||
"""
|
||||
|
||||
protocol = WampRawSocketServerProtocol
|
||||
|
||||
def __init__(self, factory, serializers=None):
|
||||
"""
|
||||
|
||||
:param factory: A callable that produces instances that implement
|
||||
:class:`autobahn.wamp.interfaces.ITransportHandler`
|
||||
:type factory: callable
|
||||
|
||||
:param serializers: A list of WAMP serializers to use (or ``None``
|
||||
for all available serializers).
|
||||
:type serializers: list of objects implementing
|
||||
:class:`autobahn.wamp.interfaces.ISerializer`
|
||||
"""
|
||||
WampRawSocketFactory.__init__(self, factory)
|
||||
|
||||
if serializers is None:
|
||||
serializers = []
|
||||
|
||||
# try CBOR WAMP serializer
|
||||
try:
|
||||
from autobahn.wamp.serializer import CBORSerializer
|
||||
serializers.append(CBORSerializer(batched=True))
|
||||
serializers.append(CBORSerializer())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# try MsgPack WAMP serializer
|
||||
try:
|
||||
from autobahn.wamp.serializer import MsgPackSerializer
|
||||
serializers.append(MsgPackSerializer(batched=True))
|
||||
serializers.append(MsgPackSerializer())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# try UBJSON WAMP serializer
|
||||
try:
|
||||
from autobahn.wamp.serializer import UBJSONSerializer
|
||||
serializers.append(UBJSONSerializer(batched=True))
|
||||
serializers.append(UBJSONSerializer())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# try JSON WAMP serializer
|
||||
try:
|
||||
from autobahn.wamp.serializer import JsonSerializer
|
||||
serializers.append(JsonSerializer(batched=True))
|
||||
serializers.append(JsonSerializer())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if not serializers:
|
||||
raise Exception("could not import any WAMP serializers")
|
||||
|
||||
self._serializers = {}
|
||||
for ser in serializers:
|
||||
self._serializers[ser.RAWSOCKET_SERIALIZER_ID] = ser
|
||||
|
||||
|
||||
@public
|
||||
class WampRawSocketClientFactory(WampRawSocketFactory):
|
||||
"""
|
||||
Twisted-based WAMP-over-RawSocket client protocol factory.
|
||||
"""
|
||||
|
||||
protocol = WampRawSocketClientProtocol
|
||||
|
||||
def __init__(self, factory, serializer=None):
|
||||
"""
|
||||
|
||||
:param factory: A callable that produces instances that implement
|
||||
:class:`autobahn.wamp.interfaces.ITransportHandler`
|
||||
:type factory: callable
|
||||
|
||||
:param serializer: The WAMP serializer to use (or ``None`` for
|
||||
"best" serializer, chosen as the first serializer available from
|
||||
this list: CBOR, MessagePack, UBJSON, JSON).
|
||||
:type serializer: object implementing :class:`autobahn.wamp.interfaces.ISerializer`
|
||||
"""
|
||||
WampRawSocketFactory.__init__(self, factory)
|
||||
|
||||
# Reduce the factory logs noise
|
||||
self.noisy = False
|
||||
|
||||
if serializer is None:
|
||||
|
||||
# try CBOR WAMP serializer
|
||||
try:
|
||||
from autobahn.wamp.serializer import CBORSerializer
|
||||
serializer = CBORSerializer()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if serializer is None:
|
||||
|
||||
# try MsgPack WAMP serializer
|
||||
try:
|
||||
from autobahn.wamp.serializer import MsgPackSerializer
|
||||
serializer = MsgPackSerializer()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if serializer is None:
|
||||
|
||||
# try UBJSON WAMP serializer
|
||||
try:
|
||||
from autobahn.wamp.serializer import UBJSONSerializer
|
||||
serializer = UBJSONSerializer()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if serializer is None:
|
||||
# try JSON WAMP serializer
|
||||
try:
|
||||
from autobahn.wamp.serializer import JsonSerializer
|
||||
serializer = JsonSerializer()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if serializer is None:
|
||||
raise Exception("could not import any WAMP serializer")
|
||||
|
||||
self._serializer = serializer
|
||||
@@ -0,0 +1,182 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 zope.interface import implementer
|
||||
|
||||
from twisted.protocols.policies import ProtocolWrapper
|
||||
try:
|
||||
# starting from Twisted 22.10.0 we have `notFound`
|
||||
from twisted.web.pages import notFound
|
||||
except ImportError:
|
||||
try:
|
||||
# In Twisted < 22.10.0 && > 12.2 this was called `NoResource`
|
||||
from twisted.web.resource import NoResource as notFound
|
||||
except ImportError:
|
||||
# And in Twisted < 12.2 this was in a different place
|
||||
from twisted.web.error import NoResource as notFound
|
||||
|
||||
from twisted.web.resource import IResource, Resource
|
||||
|
||||
# The following triggers an import of reactor at module level!
|
||||
#
|
||||
from twisted.web.server import NOT_DONE_YET
|
||||
|
||||
__all__ = (
|
||||
'WebSocketResource',
|
||||
'WSGIRootResource',
|
||||
)
|
||||
|
||||
|
||||
class WSGIRootResource(Resource):
|
||||
"""
|
||||
Root resource when you want a WSGI resource be the default serving
|
||||
resource for a Twisted Web site, but have subpaths served by
|
||||
different resources.
|
||||
|
||||
This is a hack needed since
|
||||
`twisted.web.wsgi.WSGIResource <http://twistedmatrix.com/documents/current/api/twisted.web.wsgi.WSGIResource.html>`_.
|
||||
does not provide a ``putChild()`` method.
|
||||
|
||||
.. seealso::
|
||||
|
||||
* `Autobahn Twisted Web WSGI example <https://github.com/crossbario/autobahn-python/tree/master/examples/twisted/websocket/echo_wsgi>`_
|
||||
* `Original hack <http://blog.vrplumber.com/index.php?/archives/2426-Making-your-Twisted-resources-a-url-sub-tree-of-your-WSGI-resource....html>`_
|
||||
"""
|
||||
|
||||
def __init__(self, wsgiResource, children):
|
||||
"""
|
||||
|
||||
:param wsgiResource: The WSGI to serve as root resource.
|
||||
:type wsgiResource: Instance of `twisted.web.wsgi.WSGIResource <http://twistedmatrix.com/documents/current/api/twisted.web.wsgi.WSGIResource.html>`_.
|
||||
:param children: A dictionary with string keys constituting URL subpaths, and Twisted Web resources as values.
|
||||
:type children: dict
|
||||
"""
|
||||
Resource.__init__(self)
|
||||
self._wsgiResource = wsgiResource
|
||||
self.children = children
|
||||
|
||||
def getChild(self, path, request):
|
||||
request.prepath.pop()
|
||||
request.postpath.insert(0, path)
|
||||
return self._wsgiResource
|
||||
|
||||
|
||||
@implementer(IResource)
|
||||
class WebSocketResource(object):
|
||||
"""
|
||||
A Twisted Web resource for WebSocket.
|
||||
"""
|
||||
isLeaf = True
|
||||
|
||||
def __init__(self, factory):
|
||||
"""
|
||||
|
||||
:param factory: An instance of :class:`autobahn.twisted.websocket.WebSocketServerFactory`.
|
||||
:type factory: obj
|
||||
"""
|
||||
self._factory = factory
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def getChildWithDefault(self, name, request):
|
||||
"""
|
||||
This resource cannot have children, hence this will always fail.
|
||||
"""
|
||||
return notFound(message="No such child resource.")
|
||||
|
||||
def putChild(self, path, child):
|
||||
"""
|
||||
This resource cannot have children, hence this is always ignored.
|
||||
"""
|
||||
|
||||
def render(self, request):
|
||||
"""
|
||||
Render the resource. This will takeover the transport underlying
|
||||
the request, create a :class:`autobahn.twisted.websocket.WebSocketServerProtocol`
|
||||
and let that do any subsequent communication.
|
||||
"""
|
||||
# for reasons unknown, the transport is already None when the
|
||||
# request is over HTTP2. request.channel.getPeer() is valid at
|
||||
# this point however
|
||||
if request.channel.transport is None:
|
||||
# render an "error, yo're doing HTTPS over WSS" webpage
|
||||
from autobahn.websocket import protocol
|
||||
request.setResponseCode(426, b"Upgrade required")
|
||||
# RFC says MUST set upgrade along with 426 code:
|
||||
# https://tools.ietf.org/html/rfc7231#section-6.5.15
|
||||
request.setHeader(b"Upgrade", b"WebSocket")
|
||||
html = protocol._SERVER_STATUS_TEMPLATE % ("", protocol.__version__)
|
||||
return html.encode('utf8')
|
||||
|
||||
# Create Autobahn WebSocket protocol.
|
||||
#
|
||||
protocol = self._factory.buildProtocol(request.transport.getPeer())
|
||||
if not protocol:
|
||||
# If protocol creation fails, we signal "internal server error"
|
||||
request.setResponseCode(500)
|
||||
return b""
|
||||
|
||||
# Take over the transport from Twisted Web
|
||||
#
|
||||
transport, request.channel.transport = request.channel.transport, None
|
||||
|
||||
# Connect the transport to our protocol. Once #3204 is fixed, there
|
||||
# may be a cleaner way of doing this.
|
||||
# http://twistedmatrix.com/trac/ticket/3204
|
||||
#
|
||||
if isinstance(transport, ProtocolWrapper):
|
||||
# i.e. TLS is a wrapping protocol
|
||||
transport.wrappedProtocol = protocol
|
||||
elif isinstance(transport.protocol, ProtocolWrapper):
|
||||
# this happens in new-TLS
|
||||
transport.protocol.wrappedProtocol = protocol
|
||||
else:
|
||||
transport.protocol = protocol
|
||||
protocol.makeConnection(transport)
|
||||
|
||||
# On Twisted 16+, the transport is paused whilst the existing
|
||||
# request is served; there won't be any requests after us so
|
||||
# we can just resume this ourselves.
|
||||
# 17.1 version
|
||||
if hasattr(transport, "_networkProducer"):
|
||||
transport._networkProducer.resumeProducing()
|
||||
# 16.x version
|
||||
elif hasattr(transport, "resumeProducing"):
|
||||
transport.resumeProducing()
|
||||
|
||||
# We recreate the request and forward the raw data. This is somewhat
|
||||
# silly (since Twisted Web already did the HTTP request parsing
|
||||
# which we will do a 2nd time), but it's totally non-invasive to our
|
||||
# code. Maybe improve this.
|
||||
#
|
||||
data = request.method + b' ' + request.uri + b' HTTP/1.1\x0d\x0a'
|
||||
for h in request.requestHeaders.getAllRawHeaders():
|
||||
data += h[0] + b': ' + b",".join(h[1]) + b'\x0d\x0a'
|
||||
data += b"\x0d\x0a"
|
||||
data += request.content.read()
|
||||
protocol.dataReceived(data)
|
||||
|
||||
return NOT_DONE_YET
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,124 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
# t.i.reactor doesn't exist until we've imported it once, but we
|
||||
# need it to exist so we can @patch it out in the tests ...
|
||||
from twisted.internet import reactor # noqa
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
from twisted.internet.defer import inlineCallbacks, succeed
|
||||
from twisted.trial import unittest
|
||||
|
||||
from autobahn.twisted.wamp import ApplicationRunner
|
||||
|
||||
|
||||
def raise_error(*args, **kw):
|
||||
raise RuntimeError("we always fail")
|
||||
|
||||
|
||||
class TestApplicationRunner(unittest.TestCase):
|
||||
@patch('twisted.internet.reactor')
|
||||
def test_runner_default(self, fakereactor):
|
||||
fakereactor.connectTCP = Mock(side_effect=raise_error)
|
||||
runner = ApplicationRunner('ws://fake:1234/ws', 'dummy realm')
|
||||
|
||||
# we should get "our" RuntimeError when we call run
|
||||
self.assertRaises(RuntimeError, runner.run, raise_error)
|
||||
|
||||
# both reactor.run and reactor.stop should have been called
|
||||
self.assertEqual(fakereactor.run.call_count, 1)
|
||||
self.assertEqual(fakereactor.stop.call_count, 1)
|
||||
|
||||
@patch('twisted.internet.reactor')
|
||||
@inlineCallbacks
|
||||
def test_runner_no_run(self, fakereactor):
|
||||
fakereactor.connectTCP = Mock(side_effect=raise_error)
|
||||
runner = ApplicationRunner('ws://fake:1234/ws', 'dummy realm')
|
||||
|
||||
try:
|
||||
yield runner.run(raise_error, start_reactor=False)
|
||||
self.fail() # should have raise an exception, via Deferred
|
||||
|
||||
except RuntimeError as e:
|
||||
# make sure it's "our" exception
|
||||
self.assertEqual(e.args[0], "we always fail")
|
||||
|
||||
# neither reactor.run() NOR reactor.stop() should have been called
|
||||
# (just connectTCP() will have been called)
|
||||
self.assertEqual(fakereactor.run.call_count, 0)
|
||||
self.assertEqual(fakereactor.stop.call_count, 0)
|
||||
|
||||
@patch('twisted.internet.reactor')
|
||||
def test_runner_no_run_happypath(self, fakereactor):
|
||||
proto = Mock()
|
||||
fakereactor.connectTCP = Mock(return_value=succeed(proto))
|
||||
runner = ApplicationRunner('ws://fake:1234/ws', 'dummy realm')
|
||||
|
||||
d = runner.run(Mock(), start_reactor=False)
|
||||
|
||||
# shouldn't have actually connected to anything
|
||||
# successfully, and the run() call shouldn't have inserted
|
||||
# any of its own call/errbacks. (except the cleanup handler)
|
||||
self.assertFalse(d.called)
|
||||
self.assertEqual(1, len(d.callbacks))
|
||||
|
||||
# neither reactor.run() NOR reactor.stop() should have been called
|
||||
# (just connectTCP() will have been called)
|
||||
self.assertEqual(fakereactor.run.call_count, 0)
|
||||
self.assertEqual(fakereactor.stop.call_count, 0)
|
||||
|
||||
@patch('twisted.internet.reactor')
|
||||
def test_runner_bad_proxy(self, fakereactor):
|
||||
proxy = 'myproxy'
|
||||
|
||||
self.assertRaises(
|
||||
AssertionError,
|
||||
ApplicationRunner,
|
||||
'ws://fake:1234/ws', 'dummy realm',
|
||||
proxy=proxy
|
||||
)
|
||||
|
||||
@patch('twisted.internet.reactor')
|
||||
def test_runner_proxy(self, fakereactor):
|
||||
proto = Mock()
|
||||
fakereactor.connectTCP = Mock(return_value=succeed(proto))
|
||||
|
||||
proxy = {'host': 'myproxy', 'port': 3128}
|
||||
|
||||
runner = ApplicationRunner('ws://fake:1234/ws', 'dummy realm', proxy=proxy)
|
||||
|
||||
d = runner.run(Mock(), start_reactor=False)
|
||||
|
||||
# shouldn't have actually connected to anything
|
||||
# successfully, and the run() call shouldn't have inserted
|
||||
# any of its own call/errbacks. (except the cleanup handler)
|
||||
self.assertFalse(d.called)
|
||||
self.assertEqual(1, len(d.callbacks))
|
||||
|
||||
# neither reactor.run() NOR reactor.stop() should have been called
|
||||
# (just connectTCP() will have been called)
|
||||
self.assertEqual(fakereactor.run.call_count, 0)
|
||||
self.assertEqual(fakereactor.stop.call_count, 0)
|
||||
@@ -0,0 +1,139 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 sys
|
||||
from unittest.mock import Mock
|
||||
|
||||
import twisted.internet
|
||||
from twisted.trial import unittest
|
||||
|
||||
from autobahn.twisted import choosereactor
|
||||
|
||||
|
||||
class ChooseReactorTests(unittest.TestCase):
|
||||
|
||||
def patch_reactor(self, name, new_reactor):
|
||||
"""
|
||||
Patch ``name`` so that Twisted will grab a fake reactor instead of
|
||||
a real one.
|
||||
"""
|
||||
if hasattr(twisted.internet, name):
|
||||
self.patch(twisted.internet, name, new_reactor)
|
||||
else:
|
||||
def _cleanup():
|
||||
delattr(twisted.internet, name)
|
||||
setattr(twisted.internet, name, new_reactor)
|
||||
|
||||
def patch_modules(self):
|
||||
"""
|
||||
Patch ``sys.modules`` so that Twisted believes there is no
|
||||
installed reactor.
|
||||
"""
|
||||
old_modules = dict(sys.modules)
|
||||
|
||||
new_modules = dict(sys.modules)
|
||||
del new_modules["twisted.internet.reactor"]
|
||||
|
||||
def _cleanup():
|
||||
sys.modules = old_modules
|
||||
|
||||
self.addCleanup(_cleanup)
|
||||
sys.modules = new_modules
|
||||
|
||||
def test_unknown(self):
|
||||
"""
|
||||
``install_optimal_reactor`` will use the default reactor if it is
|
||||
unable to detect the platform it is running on.
|
||||
"""
|
||||
reactor_mock = Mock()
|
||||
self.patch_reactor("selectreactor", reactor_mock)
|
||||
self.patch(sys, "platform", "unknown")
|
||||
|
||||
# Emulate that a reactor has not been installed
|
||||
self.patch_modules()
|
||||
|
||||
choosereactor.install_optimal_reactor()
|
||||
reactor_mock.install.assert_called_once_with()
|
||||
|
||||
def test_mac(self):
|
||||
"""
|
||||
``install_optimal_reactor`` will install KQueueReactor on
|
||||
Darwin (OS X).
|
||||
"""
|
||||
reactor_mock = Mock()
|
||||
self.patch_reactor("kqreactor", reactor_mock)
|
||||
self.patch(sys, "platform", "darwin")
|
||||
|
||||
# Emulate that a reactor has not been installed
|
||||
self.patch_modules()
|
||||
|
||||
choosereactor.install_optimal_reactor()
|
||||
reactor_mock.install.assert_called_once_with()
|
||||
|
||||
def test_win(self):
|
||||
"""
|
||||
``install_optimal_reactor`` will install IOCPReactor on Windows.
|
||||
"""
|
||||
if sys.platform != 'win32':
|
||||
raise unittest.SkipTest('unit test requires Windows')
|
||||
|
||||
reactor_mock = Mock()
|
||||
self.patch_reactor("iocpreactor", reactor_mock)
|
||||
self.patch(sys, "platform", "win32")
|
||||
|
||||
# Emulate that a reactor has not been installed
|
||||
self.patch_modules()
|
||||
|
||||
choosereactor.install_optimal_reactor()
|
||||
reactor_mock.install.assert_called_once_with()
|
||||
|
||||
def test_bsd(self):
|
||||
"""
|
||||
``install_optimal_reactor`` will install KQueueReactor on BSD.
|
||||
"""
|
||||
reactor_mock = Mock()
|
||||
self.patch_reactor("kqreactor", reactor_mock)
|
||||
self.patch(sys, "platform", "freebsd11")
|
||||
|
||||
# Emulate that a reactor has not been installed
|
||||
self.patch_modules()
|
||||
|
||||
choosereactor.install_optimal_reactor()
|
||||
reactor_mock.install.assert_called_once_with()
|
||||
|
||||
def test_linux(self):
|
||||
"""
|
||||
``install_optimal_reactor`` will install EPollReactor on Linux.
|
||||
"""
|
||||
reactor_mock = Mock()
|
||||
self.patch_reactor("epollreactor", reactor_mock)
|
||||
self.patch(sys, "platform", "linux")
|
||||
|
||||
# Emulate that a reactor has not been installed
|
||||
self.patch_modules()
|
||||
|
||||
choosereactor.install_optimal_reactor()
|
||||
reactor_mock.install.assert_called_once_with()
|
||||
@@ -0,0 +1,438 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
if os.environ.get('USE_TWISTED', False):
|
||||
from autobahn.twisted.component import Component
|
||||
from zope.interface import directlyProvides
|
||||
from autobahn.wamp.message import Welcome, Goodbye, Hello, Abort
|
||||
from autobahn.wamp.serializer import JsonSerializer
|
||||
from autobahn.testutil import FakeTransport
|
||||
from twisted.internet.interfaces import IStreamClientEndpoint
|
||||
from twisted.internet.defer import inlineCallbacks, succeed, Deferred
|
||||
from twisted.internet.task import Clock
|
||||
from twisted.trial import unittest
|
||||
from txaio.testutil import replace_loop
|
||||
|
||||
class ConnectionTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
@patch('txaio.sleep', return_value=succeed(None))
|
||||
@inlineCallbacks
|
||||
def test_successful_connect(self, fake_sleep):
|
||||
endpoint = Mock()
|
||||
joins = []
|
||||
|
||||
def joined(session, details):
|
||||
joins.append((session, details))
|
||||
return session.leave()
|
||||
directlyProvides(endpoint, IStreamClientEndpoint)
|
||||
component = Component(
|
||||
transports={
|
||||
"type": "websocket",
|
||||
"url": "ws://127.0.0.1/ws",
|
||||
"endpoint": endpoint,
|
||||
}
|
||||
)
|
||||
component.on('join', joined)
|
||||
|
||||
def connect(factory, **kw):
|
||||
proto = factory.buildProtocol('ws://localhost/')
|
||||
transport = FakeTransport()
|
||||
proto.makeConnection(transport)
|
||||
|
||||
from autobahn.websocket.protocol import WebSocketProtocol
|
||||
from base64 import b64encode
|
||||
from hashlib import sha1
|
||||
key = proto.websocket_key + WebSocketProtocol._WS_MAGIC
|
||||
proto.data = (
|
||||
b"HTTP/1.1 101 Switching Protocols\x0d\x0a"
|
||||
b"Upgrade: websocket\x0d\x0a"
|
||||
b"Connection: upgrade\x0d\x0a"
|
||||
b"Sec-Websocket-Protocol: wamp.2.json\x0d\x0a"
|
||||
b"Sec-Websocket-Accept: " + b64encode(sha1(key).digest()) + b"\x0d\x0a\x0d\x0a"
|
||||
)
|
||||
proto.processHandshake()
|
||||
|
||||
from autobahn.wamp import role
|
||||
features = role.RoleBrokerFeatures(
|
||||
publisher_identification=True,
|
||||
pattern_based_subscription=True,
|
||||
session_meta_api=True,
|
||||
subscription_meta_api=True,
|
||||
subscriber_blackwhite_listing=True,
|
||||
publisher_exclusion=True,
|
||||
subscription_revocation=True,
|
||||
payload_transparency=True,
|
||||
payload_encryption_cryptobox=True,
|
||||
)
|
||||
|
||||
msg = Welcome(123456, dict(broker=features), realm='realm')
|
||||
serializer = JsonSerializer()
|
||||
data, is_binary = serializer.serialize(msg)
|
||||
proto.onMessage(data, is_binary)
|
||||
|
||||
msg = Goodbye()
|
||||
proto.onMessage(*serializer.serialize(msg))
|
||||
proto.onClose(True, 100, "some old reason")
|
||||
|
||||
return succeed(proto)
|
||||
endpoint.connect = connect
|
||||
|
||||
# XXX it would actually be nicer if we *could* support
|
||||
# passing a reactor in here, but the _batched_timer =
|
||||
# make_batched_timer() stuff (slash txaio in general)
|
||||
# makes this "hard".
|
||||
reactor = Clock()
|
||||
with replace_loop(reactor):
|
||||
yield component.start(reactor=reactor)
|
||||
self.assertTrue(len(joins), 1)
|
||||
# make sure we fire all our time-outs
|
||||
reactor.advance(3600)
|
||||
|
||||
@patch('txaio.sleep', return_value=succeed(None))
|
||||
def test_successful_proxy_connect(self, fake_sleep):
|
||||
endpoint = Mock()
|
||||
directlyProvides(endpoint, IStreamClientEndpoint)
|
||||
component = Component(
|
||||
transports={
|
||||
"type": "websocket",
|
||||
"url": "ws://127.0.0.1/ws",
|
||||
"endpoint": endpoint,
|
||||
"proxy": {
|
||||
"host": "10.0.0.0",
|
||||
"port": 65000,
|
||||
},
|
||||
"max_retries": 0,
|
||||
},
|
||||
is_fatal=lambda _: True,
|
||||
)
|
||||
|
||||
@component.on_join
|
||||
def joined(session, details):
|
||||
return session.leave()
|
||||
|
||||
def connect(factory, **kw):
|
||||
return succeed(Mock())
|
||||
endpoint.connect = connect
|
||||
|
||||
# XXX it would actually be nicer if we *could* support
|
||||
# passing a reactor in here, but the _batched_timer =
|
||||
# make_batched_timer() stuff (slash txaio in general)
|
||||
# makes this "hard".
|
||||
reactor = Clock()
|
||||
|
||||
got_proxy_connect = Deferred()
|
||||
|
||||
def _tcp(host, port, factory, **kw):
|
||||
self.assertEqual("10.0.0.0", host)
|
||||
self.assertEqual(port, 65000)
|
||||
got_proxy_connect.callback(None)
|
||||
return endpoint.connect(factory._wrappedFactory)
|
||||
reactor.connectTCP = _tcp
|
||||
|
||||
with replace_loop(reactor):
|
||||
d = component.start(reactor=reactor)
|
||||
|
||||
def done(x):
|
||||
if not got_proxy_connect.called:
|
||||
got_proxy_connect.callback(x)
|
||||
# make sure we fire all our time-outs
|
||||
d.addCallbacks(done, done)
|
||||
reactor.advance(3600)
|
||||
return got_proxy_connect
|
||||
|
||||
@patch('txaio.sleep', return_value=succeed(None))
|
||||
@inlineCallbacks
|
||||
def test_cancel(self, fake_sleep):
|
||||
"""
|
||||
if we start a component but call .stop before it connects, ever,
|
||||
it should still exit properly
|
||||
"""
|
||||
endpoint = Mock()
|
||||
directlyProvides(endpoint, IStreamClientEndpoint)
|
||||
component = Component(
|
||||
transports={
|
||||
"type": "websocket",
|
||||
"url": "ws://127.0.0.1/ws",
|
||||
"endpoint": endpoint,
|
||||
}
|
||||
)
|
||||
|
||||
def connect(factory, **kw):
|
||||
return Deferred()
|
||||
endpoint.connect = connect
|
||||
|
||||
# XXX it would actually be nicer if we *could* support
|
||||
# passing a reactor in here, but the _batched_timer =
|
||||
# make_batched_timer() stuff (slash txaio in general)
|
||||
# makes this "hard".
|
||||
reactor = Clock()
|
||||
with replace_loop(reactor):
|
||||
d = component.start(reactor=reactor)
|
||||
component.stop()
|
||||
yield d
|
||||
|
||||
@inlineCallbacks
|
||||
def test_cancel_while_waiting(self):
|
||||
"""
|
||||
if we start a component but call .stop before it connects, ever,
|
||||
it should still exit properly -- even if we're 'between'
|
||||
connection attempts
|
||||
"""
|
||||
endpoint = Mock()
|
||||
directlyProvides(endpoint, IStreamClientEndpoint)
|
||||
component = Component(
|
||||
transports={
|
||||
"type": "websocket",
|
||||
"url": "ws://127.0.0.1/ws",
|
||||
"endpoint": endpoint,
|
||||
"max_retries": 0,
|
||||
"max_retry_delay": 5,
|
||||
"initial_retry_delay": 5,
|
||||
},
|
||||
)
|
||||
|
||||
# XXX it would actually be nicer if we *could* support
|
||||
# passing a reactor in here, but the _batched_timer =
|
||||
# make_batched_timer() stuff (slash txaio in general)
|
||||
# makes this "hard".
|
||||
reactor = Clock()
|
||||
with replace_loop(reactor):
|
||||
|
||||
def connect(factory, **kw):
|
||||
d = Deferred()
|
||||
reactor.callLater(10, d.errback(RuntimeError("no connect for yo")))
|
||||
return d
|
||||
endpoint.connect = connect
|
||||
|
||||
d0 = component.start(reactor=reactor)
|
||||
assert component._delay_f is not None
|
||||
assert not component._done_f.called
|
||||
|
||||
d1 = component.stop()
|
||||
assert component._done_f is None
|
||||
assert d0.called
|
||||
|
||||
yield d1
|
||||
yield d0
|
||||
|
||||
@patch('txaio.sleep', return_value=succeed(None))
|
||||
@inlineCallbacks
|
||||
def test_connect_no_auth_method(self, fake_sleep):
|
||||
endpoint = Mock()
|
||||
|
||||
directlyProvides(endpoint, IStreamClientEndpoint)
|
||||
component = Component(
|
||||
transports={
|
||||
"type": "websocket",
|
||||
"url": "ws://127.0.0.1/ws",
|
||||
"endpoint": endpoint,
|
||||
},
|
||||
is_fatal=lambda e: True,
|
||||
)
|
||||
|
||||
def connect(factory, **kw):
|
||||
proto = factory.buildProtocol('boom')
|
||||
proto.makeConnection(Mock())
|
||||
|
||||
from autobahn.websocket.protocol import WebSocketProtocol
|
||||
from base64 import b64encode
|
||||
from hashlib import sha1
|
||||
key = proto.websocket_key + WebSocketProtocol._WS_MAGIC
|
||||
proto.data = (
|
||||
b"HTTP/1.1 101 Switching Protocols\x0d\x0a"
|
||||
b"Upgrade: websocket\x0d\x0a"
|
||||
b"Connection: upgrade\x0d\x0a"
|
||||
b"Sec-Websocket-Protocol: wamp.2.json\x0d\x0a"
|
||||
b"Sec-Websocket-Accept: " + b64encode(sha1(key).digest()) + b"\x0d\x0a\x0d\x0a"
|
||||
)
|
||||
proto.processHandshake()
|
||||
|
||||
from autobahn.wamp import role
|
||||
subrole = role.RoleSubscriberFeatures()
|
||||
|
||||
msg = Hello("realm", roles=dict(subscriber=subrole), authmethods=["anonymous"])
|
||||
serializer = JsonSerializer()
|
||||
data, is_binary = serializer.serialize(msg)
|
||||
proto.onMessage(data, is_binary)
|
||||
|
||||
msg = Abort(reason="wamp.error.no_auth_method")
|
||||
proto.onMessage(*serializer.serialize(msg))
|
||||
proto.onClose(False, 100, "wamp.error.no_auth_method")
|
||||
|
||||
return succeed(proto)
|
||||
endpoint.connect = connect
|
||||
|
||||
# XXX it would actually be nicer if we *could* support
|
||||
# passing a reactor in here, but the _batched_timer =
|
||||
# make_batched_timer() stuff (slash txaio in general)
|
||||
# makes this "hard".
|
||||
reactor = Clock()
|
||||
with replace_loop(reactor):
|
||||
with self.assertRaises(RuntimeError) as ctx:
|
||||
d = component.start(reactor=reactor)
|
||||
# make sure we fire all our time-outs
|
||||
reactor.advance(3600)
|
||||
yield d
|
||||
self.assertIn(
|
||||
"Exhausted all transport",
|
||||
str(ctx.exception)
|
||||
)
|
||||
|
||||
class InvalidTransportConfigs(unittest.TestCase):
|
||||
|
||||
def test_invalid_key(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
Component(
|
||||
transports=dict(
|
||||
foo='bar', # totally invalid key
|
||||
),
|
||||
)
|
||||
self.assertIn("'foo' is not", str(ctx.exception))
|
||||
|
||||
def test_invalid_key_transport_list(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
Component(
|
||||
transports=[
|
||||
dict(type='websocket', url='ws://127.0.0.1/ws'),
|
||||
dict(foo='bar'), # totally invalid key
|
||||
]
|
||||
)
|
||||
self.assertIn("'foo' is not a valid configuration item", str(ctx.exception))
|
||||
|
||||
def test_invalid_serializer_key(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
Component(
|
||||
transports=[
|
||||
{
|
||||
"url": "ws://127.0.0.1/ws",
|
||||
"serializer": ["quux"],
|
||||
}
|
||||
]
|
||||
)
|
||||
self.assertIn("only for rawsocket", str(ctx.exception))
|
||||
|
||||
def test_invalid_serializer(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
Component(
|
||||
transports=[
|
||||
{
|
||||
"url": "ws://127.0.0.1/ws",
|
||||
"serializers": ["quux"],
|
||||
}
|
||||
]
|
||||
)
|
||||
self.assertIn("Invalid serializer", str(ctx.exception))
|
||||
|
||||
def test_invalid_serializer_type_0(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
Component(
|
||||
transports=[
|
||||
{
|
||||
"url": "ws://127.0.0.1/ws",
|
||||
"serializers": [1, 2],
|
||||
}
|
||||
]
|
||||
)
|
||||
self.assertIn("must be a list", str(ctx.exception))
|
||||
|
||||
def test_invalid_serializer_type_1(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
Component(
|
||||
transports=[
|
||||
{
|
||||
"url": "ws://127.0.0.1/ws",
|
||||
"serializers": 1,
|
||||
}
|
||||
]
|
||||
)
|
||||
self.assertIn("must be a list", str(ctx.exception))
|
||||
|
||||
def test_invalid_type_key(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
Component(
|
||||
transports=[
|
||||
{
|
||||
"type": "bad",
|
||||
}
|
||||
]
|
||||
)
|
||||
self.assertIn("Invalid transport type", str(ctx.exception))
|
||||
|
||||
def test_invalid_type(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
Component(
|
||||
transports=[
|
||||
"foo"
|
||||
]
|
||||
)
|
||||
self.assertIn("invalid WebSocket URL", str(ctx.exception))
|
||||
|
||||
def test_no_url(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
Component(
|
||||
transports=[
|
||||
{
|
||||
"type": "websocket",
|
||||
}
|
||||
]
|
||||
)
|
||||
self.assertIn("Transport requires 'url'", str(ctx.exception))
|
||||
|
||||
def test_endpoint_bogus_object(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
Component(
|
||||
main=lambda r, s: None,
|
||||
transports=[
|
||||
{
|
||||
"type": "websocket",
|
||||
"url": "ws://example.com/ws",
|
||||
"endpoint": ("not", "a", "dict"),
|
||||
}
|
||||
]
|
||||
)
|
||||
self.assertIn("'endpoint' configuration must be", str(ctx.exception))
|
||||
|
||||
def test_endpoint_valid(self):
|
||||
Component(
|
||||
main=lambda r, s: None,
|
||||
transports=[
|
||||
{
|
||||
"type": "websocket",
|
||||
"url": "ws://example.com/ws",
|
||||
"endpoint": {
|
||||
"type": "tcp",
|
||||
"host": "1.2.3.4",
|
||||
"port": "4321",
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class PluginTests(TestCase):
|
||||
if True:
|
||||
skip = "Plugins don't work under Python3 yet"
|
||||
|
||||
def test_import(self):
|
||||
from twisted.plugins import autobahn_endpoints
|
||||
self.assertTrue(hasattr(autobahn_endpoints, 'AutobahnClientParser'))
|
||||
|
||||
def test_parse_client_basic(self):
|
||||
from twisted.plugins import autobahn_endpoints
|
||||
self.assertTrue(hasattr(autobahn_endpoints, 'AutobahnClientParser'))
|
||||
from twisted.internet.endpoints import clientFromString, quoteStringArgument
|
||||
from twisted.internet import reactor
|
||||
|
||||
ep_string = "autobahn:{0}:url={1}".format(
|
||||
quoteStringArgument('tcp:localhost:9000'),
|
||||
quoteStringArgument('ws://localhost:9000'),
|
||||
)
|
||||
# we're just testing that this doesn't fail entirely
|
||||
clientFromString(reactor, ep_string)
|
||||
@@ -0,0 +1,447 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 unittest.mock import Mock
|
||||
|
||||
import txaio
|
||||
txaio.use_twisted()
|
||||
|
||||
from autobahn.util import wildcards2patterns
|
||||
from autobahn.twisted.websocket import WebSocketServerFactory
|
||||
from autobahn.twisted.websocket import WebSocketServerProtocol
|
||||
from autobahn.twisted.websocket import WebSocketClientProtocol
|
||||
from autobahn.wamp.types import TransportDetails
|
||||
from autobahn.websocket.types import ConnectingRequest
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.internet.error import ConnectionDone, ConnectionAborted, \
|
||||
ConnectionLost
|
||||
from twisted.trial import unittest
|
||||
try:
|
||||
from twisted.internet.testing import StringTransport
|
||||
except ImportError:
|
||||
from twisted.test.proto_helpers import StringTransport
|
||||
from autobahn.testutil import FakeTransport
|
||||
|
||||
|
||||
class ExceptionHandlingTests(unittest.TestCase):
|
||||
"""
|
||||
Tests that we format various exception variations properly during
|
||||
connectionLost
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = WebSocketServerFactory()
|
||||
self.proto = WebSocketServerProtocol()
|
||||
self.proto.factory = self.factory
|
||||
self.proto.log = Mock()
|
||||
|
||||
def tearDown(self):
|
||||
for call in [
|
||||
self.proto.autoPingPendingCall,
|
||||
self.proto.autoPingTimeoutCall,
|
||||
self.proto.openHandshakeTimeoutCall,
|
||||
self.proto.closeHandshakeTimeoutCall,
|
||||
]:
|
||||
if call is not None:
|
||||
call.cancel()
|
||||
|
||||
def test_connection_done(self):
|
||||
# pretend we connected
|
||||
self.proto._connectionMade()
|
||||
|
||||
self.proto.connectionLost(Failure(ConnectionDone()))
|
||||
|
||||
messages = ' '.join([str(x[1]) for x in self.proto.log.mock_calls])
|
||||
self.assertTrue('closed cleanly' in messages)
|
||||
|
||||
def test_connection_aborted(self):
|
||||
# pretend we connected
|
||||
self.proto._connectionMade()
|
||||
|
||||
self.proto.connectionLost(Failure(ConnectionAborted()))
|
||||
|
||||
messages = ' '.join([str(x[1]) for x in self.proto.log.mock_calls])
|
||||
self.assertTrue(' aborted ' in messages)
|
||||
|
||||
def test_connection_lost(self):
|
||||
# pretend we connected
|
||||
self.proto._connectionMade()
|
||||
|
||||
self.proto.connectionLost(Failure(ConnectionLost()))
|
||||
|
||||
messages = ' '.join([str(x[1]) for x in self.proto.log.mock_calls])
|
||||
self.assertTrue(' was lost ' in messages)
|
||||
|
||||
def test_connection_lost_arg(self):
|
||||
# pretend we connected
|
||||
self.proto._connectionMade()
|
||||
|
||||
self.proto.connectionLost(Failure(ConnectionLost("greetings")))
|
||||
|
||||
messages = ' '.join([str(x[1]) + str(x[2]) for x in self.proto.log.mock_calls])
|
||||
self.assertTrue(' was lost ' in messages)
|
||||
self.assertTrue('greetings' in messages)
|
||||
|
||||
|
||||
class Hixie76RejectionTests(unittest.TestCase):
|
||||
"""
|
||||
Hixie-76 should not be accepted by an Autobahn server.
|
||||
"""
|
||||
def test_handshake_fails(self):
|
||||
"""
|
||||
A handshake from a client only supporting Hixie-76 will fail.
|
||||
"""
|
||||
t = FakeTransport()
|
||||
f = WebSocketServerFactory()
|
||||
p = WebSocketServerProtocol()
|
||||
p.factory = f
|
||||
p.transport = t
|
||||
|
||||
# from http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
||||
http_request = b"GET /demo HTTP/1.1\r\nHost: example.com\r\nConnection: Upgrade\r\nSec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\nSec-WebSocket-Protocol: sample\r\nUpgrade: WebSocket\r\nSec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\nOrigin: http://example.com\r\n\r\n^n:ds[4U"
|
||||
|
||||
p.openHandshakeTimeout = 0
|
||||
p._connectionMade()
|
||||
p.data = http_request
|
||||
p.processHandshake()
|
||||
self.assertIn(b"HTTP/1.1 400", t._written)
|
||||
self.assertIn(b"Hixie76 protocol not supported", t._written)
|
||||
|
||||
|
||||
class WebSocketOriginMatching(unittest.TestCase):
|
||||
"""
|
||||
Test that we match Origin: headers properly, when asked to
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = WebSocketServerFactory()
|
||||
self.factory.setProtocolOptions(
|
||||
allowedOrigins=['127.0.0.1:*', '*.example.com:*']
|
||||
)
|
||||
self.proto = WebSocketServerProtocol()
|
||||
self.proto.transport = StringTransport()
|
||||
self.proto.factory = self.factory
|
||||
self.proto.failHandshake = Mock()
|
||||
self.proto._connectionMade()
|
||||
|
||||
def tearDown(self):
|
||||
for call in [
|
||||
self.proto.autoPingPendingCall,
|
||||
self.proto.autoPingTimeoutCall,
|
||||
self.proto.openHandshakeTimeoutCall,
|
||||
self.proto.closeHandshakeTimeoutCall,
|
||||
]:
|
||||
if call is not None:
|
||||
call.cancel()
|
||||
|
||||
def test_match_full_origin(self):
|
||||
self.proto.data = b"\r\n".join([
|
||||
b'GET /ws HTTP/1.1',
|
||||
b'Host: www.example.com',
|
||||
b'Sec-WebSocket-Version: 13',
|
||||
b'Origin: http://www.example.com.malicious.com',
|
||||
b'Sec-WebSocket-Extensions: permessage-deflate',
|
||||
b'Sec-WebSocket-Key: tXAxWFUqnhi86Ajj7dRY5g==',
|
||||
b'Connection: keep-alive, Upgrade',
|
||||
b'Upgrade: websocket',
|
||||
b'\r\n', # last string doesn't get a \r\n from join()
|
||||
])
|
||||
self.proto.consumeData()
|
||||
|
||||
self.assertTrue(self.proto.failHandshake.called, "Handshake should have failed")
|
||||
arg = self.proto.failHandshake.mock_calls[0][1][0]
|
||||
self.assertTrue('not allowed' in arg)
|
||||
|
||||
def test_match_wrong_scheme_origin(self):
|
||||
# some monkey-business since we already did this in setUp, but
|
||||
# we want a different set of matching origins
|
||||
self.factory.setProtocolOptions(
|
||||
allowedOrigins=['http://*.example.com:*']
|
||||
)
|
||||
self.proto.allowedOriginsPatterns = self.factory.allowedOriginsPatterns
|
||||
self.proto.allowedOrigins = self.factory.allowedOrigins
|
||||
|
||||
# the actual test
|
||||
self.factory.isSecure = False
|
||||
self.proto.data = b"\r\n".join([
|
||||
b'GET /ws HTTP/1.1',
|
||||
b'Host: www.example.com',
|
||||
b'Sec-WebSocket-Version: 13',
|
||||
b'Origin: https://www.example.com',
|
||||
b'Sec-WebSocket-Extensions: permessage-deflate',
|
||||
b'Sec-WebSocket-Key: tXAxWFUqnhi86Ajj7dRY5g==',
|
||||
b'Connection: keep-alive, Upgrade',
|
||||
b'Upgrade: websocket',
|
||||
b'\r\n', # last string doesn't get a \r\n from join()
|
||||
])
|
||||
self.proto.consumeData()
|
||||
|
||||
self.assertTrue(self.proto.failHandshake.called, "Handshake should have failed")
|
||||
arg = self.proto.failHandshake.mock_calls[0][1][0]
|
||||
self.assertTrue('not allowed' in arg)
|
||||
|
||||
def test_match_origin_secure_scheme(self):
|
||||
self.factory.isSecure = True
|
||||
self.factory.port = 443
|
||||
self.proto.data = b"\r\n".join([
|
||||
b'GET /ws HTTP/1.1',
|
||||
b'Host: www.example.com',
|
||||
b'Sec-WebSocket-Version: 13',
|
||||
b'Origin: https://www.example.com',
|
||||
b'Sec-WebSocket-Extensions: permessage-deflate',
|
||||
b'Sec-WebSocket-Key: tXAxWFUqnhi86Ajj7dRY5g==',
|
||||
b'Connection: keep-alive, Upgrade',
|
||||
b'Upgrade: websocket',
|
||||
b'\r\n', # last string doesn't get a \r\n from join()
|
||||
])
|
||||
self.proto.consumeData()
|
||||
|
||||
self.assertFalse(self.proto.failHandshake.called, "Handshake should have succeeded")
|
||||
|
||||
def test_match_origin_documentation_example(self):
|
||||
"""
|
||||
Test the examples from the docs
|
||||
"""
|
||||
self.factory.setProtocolOptions(
|
||||
allowedOrigins=['*://*.example.com:*']
|
||||
)
|
||||
self.factory.isSecure = True
|
||||
self.factory.port = 443
|
||||
self.proto.data = b"\r\n".join([
|
||||
b'GET /ws HTTP/1.1',
|
||||
b'Host: www.example.com',
|
||||
b'Sec-WebSocket-Version: 13',
|
||||
b'Origin: http://www.example.com',
|
||||
b'Sec-WebSocket-Extensions: permessage-deflate',
|
||||
b'Sec-WebSocket-Key: tXAxWFUqnhi86Ajj7dRY5g==',
|
||||
b'Connection: keep-alive, Upgrade',
|
||||
b'Upgrade: websocket',
|
||||
b'\r\n', # last string doesn't get a \r\n from join()
|
||||
])
|
||||
self.proto.consumeData()
|
||||
|
||||
self.assertFalse(self.proto.failHandshake.called, "Handshake should have succeeded")
|
||||
|
||||
def test_match_origin_examples(self):
|
||||
"""
|
||||
All the example origins from RFC6454 (3.2.1)
|
||||
"""
|
||||
# we're just testing the low-level function here...
|
||||
from autobahn.websocket.protocol import _is_same_origin, _url_to_origin
|
||||
policy = wildcards2patterns(['*example.com:*'])
|
||||
|
||||
# should parametrize test ...
|
||||
for url in ['http://example.com/', 'http://example.com:80/',
|
||||
'http://example.com/path/file',
|
||||
'http://example.com/;semi=true',
|
||||
# 'http://example.com./',
|
||||
'//example.com/',
|
||||
'http://@example.com']:
|
||||
self.assertTrue(_is_same_origin(_url_to_origin(url), 'http', 80, policy), url)
|
||||
|
||||
def test_match_origin_counter_examples(self):
|
||||
"""
|
||||
All the example 'not-same' origins from RFC6454 (3.2.1)
|
||||
"""
|
||||
# we're just testing the low-level function here...
|
||||
from autobahn.websocket.protocol import _is_same_origin, _url_to_origin
|
||||
policy = wildcards2patterns(['example.com'])
|
||||
|
||||
for url in ['http://ietf.org/', 'http://example.org/',
|
||||
'https://example.com/', 'http://example.com:8080/',
|
||||
'http://www.example.com/']:
|
||||
self.assertFalse(_is_same_origin(_url_to_origin(url), 'http', 80, policy))
|
||||
|
||||
def test_match_origin_edge(self):
|
||||
# we're just testing the low-level function here...
|
||||
from autobahn.websocket.protocol import _is_same_origin, _url_to_origin
|
||||
policy = wildcards2patterns(['http://*example.com:80'])
|
||||
|
||||
self.assertTrue(
|
||||
_is_same_origin(_url_to_origin('http://example.com:80'), 'http', 80, policy)
|
||||
)
|
||||
self.assertFalse(
|
||||
_is_same_origin(_url_to_origin('http://example.com:81'), 'http', 81, policy)
|
||||
)
|
||||
self.assertFalse(
|
||||
_is_same_origin(_url_to_origin('https://example.com:80'), 'http', 80, policy)
|
||||
)
|
||||
|
||||
def test_origin_from_url(self):
|
||||
from autobahn.websocket.protocol import _url_to_origin
|
||||
|
||||
# basic function
|
||||
self.assertEqual(
|
||||
_url_to_origin('http://example.com'),
|
||||
('http', 'example.com', 80)
|
||||
)
|
||||
# should lower-case scheme
|
||||
self.assertEqual(
|
||||
_url_to_origin('hTTp://example.com'),
|
||||
('http', 'example.com', 80)
|
||||
)
|
||||
|
||||
def test_origin_file(self):
|
||||
from autobahn.websocket.protocol import _url_to_origin
|
||||
self.assertEqual('null', _url_to_origin('file:///etc/passwd'))
|
||||
|
||||
def test_origin_null(self):
|
||||
from autobahn.websocket.protocol import _is_same_origin, _url_to_origin
|
||||
self.assertEqual('null', _url_to_origin('null'))
|
||||
self.assertFalse(
|
||||
_is_same_origin(_url_to_origin('null'), 'http', 80, [])
|
||||
)
|
||||
self.assertFalse(
|
||||
_is_same_origin(_url_to_origin('null'), 'https', 80, [])
|
||||
)
|
||||
self.assertFalse(
|
||||
_is_same_origin(_url_to_origin('null'), '', 80, [])
|
||||
)
|
||||
self.assertFalse(
|
||||
_is_same_origin(_url_to_origin('null'), None, 80, [])
|
||||
)
|
||||
|
||||
|
||||
class WebSocketXForwardedFor(unittest.TestCase):
|
||||
"""
|
||||
Test that (only) a trusted X-Forwarded-For can replace the peer address.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = WebSocketServerFactory()
|
||||
self.factory.setProtocolOptions(
|
||||
trustXForwardedFor=2
|
||||
)
|
||||
self.proto = WebSocketServerProtocol()
|
||||
self.proto.transport = StringTransport()
|
||||
self.proto.factory = self.factory
|
||||
self.proto.failHandshake = Mock()
|
||||
self.proto._connectionMade()
|
||||
|
||||
def tearDown(self):
|
||||
for call in [
|
||||
self.proto.autoPingPendingCall,
|
||||
self.proto.autoPingTimeoutCall,
|
||||
self.proto.openHandshakeTimeoutCall,
|
||||
self.proto.closeHandshakeTimeoutCall,
|
||||
]:
|
||||
if call is not None:
|
||||
call.cancel()
|
||||
|
||||
def test_trusted_addresses(self):
|
||||
self.proto.data = b"\r\n".join([
|
||||
b'GET /ws HTTP/1.1',
|
||||
b'Host: www.example.com',
|
||||
b'Origin: http://www.example.com',
|
||||
b'Sec-WebSocket-Version: 13',
|
||||
b'Sec-WebSocket-Extensions: permessage-deflate',
|
||||
b'Sec-WebSocket-Key: tXAxWFUqnhi86Ajj7dRY5g==',
|
||||
b'Connection: keep-alive, Upgrade',
|
||||
b'Upgrade: websocket',
|
||||
b'X-Forwarded-For: 1.2.3.4, 2.3.4.5, 111.222.33.44',
|
||||
b'\r\n', # last string doesn't get a \r\n from join()
|
||||
])
|
||||
self.proto.consumeData()
|
||||
|
||||
self.assertEquals(
|
||||
self.proto.peer, "2.3.4.5",
|
||||
"The second address in X-Forwarded-For should have been picked as the peer address")
|
||||
|
||||
|
||||
class OnConnectingTests(unittest.TestCase):
|
||||
"""
|
||||
Tests related to onConnecting callback
|
||||
|
||||
These tests are testing generic behavior, but are somewhat tied to
|
||||
'a framework' so we're just testing using Twisted-specifics here.
|
||||
"""
|
||||
|
||||
def test_on_connecting_client_fails(self):
|
||||
|
||||
MAGIC_STR = 'bad stuff'
|
||||
|
||||
class TestProto(WebSocketClientProtocol):
|
||||
state = None
|
||||
wasClean = True
|
||||
log = Mock()
|
||||
|
||||
def onConnecting(self, transport_details):
|
||||
raise RuntimeError(MAGIC_STR)
|
||||
|
||||
proto = TestProto()
|
||||
proto.transport = FakeTransport()
|
||||
d = proto.startHandshake()
|
||||
self.successResultOf(d) # error is ignored
|
||||
# ... but error should be logged
|
||||
self.assertTrue(len(proto.log.mock_calls) > 0)
|
||||
magic_found = False
|
||||
for i in range(len(proto.log.mock_calls)):
|
||||
if MAGIC_STR in str(proto.log.mock_calls[i]):
|
||||
magic_found = True
|
||||
self.assertTrue(magic_found, 'MAGIC_STR not found when expected')
|
||||
|
||||
def test_on_connecting_client_success(self):
|
||||
|
||||
class TestProto(WebSocketClientProtocol):
|
||||
state = None
|
||||
wasClean = True
|
||||
perMessageCompressionOffers = []
|
||||
version = 18
|
||||
openHandshakeTimeout = 5
|
||||
log = Mock()
|
||||
|
||||
def onConnecting(self, transport_details):
|
||||
return ConnectingRequest(
|
||||
host="example.com",
|
||||
port=443,
|
||||
resource="/ws",
|
||||
)
|
||||
|
||||
proto = TestProto()
|
||||
proto.transport = FakeTransport()
|
||||
proto.factory = Mock()
|
||||
proto._connectionMade()
|
||||
|
||||
d = proto.startHandshake()
|
||||
req = self.successResultOf(d)
|
||||
self.assertEqual("example.com", req.host)
|
||||
self.assertEqual(443, req.port)
|
||||
self.assertEqual("/ws", req.resource)
|
||||
|
||||
def test_str_transport(self):
|
||||
details = TransportDetails(
|
||||
channel_type=TransportDetails.CHANNEL_TYPE_FUNCTION,
|
||||
peer="example.com",
|
||||
is_secure=False,
|
||||
channel_id={},
|
||||
)
|
||||
# we can str() this and it doesn't fail
|
||||
str(details)
|
||||
|
||||
def test_str_connecting(self):
|
||||
req = ConnectingRequest(host="example.com", port="1234", resource="/ws")
|
||||
# we can str() this and it doesn't fail
|
||||
str(req)
|
||||
@@ -0,0 +1,70 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 unittest.mock import Mock
|
||||
|
||||
from autobahn.twisted.rawsocket import (WampRawSocketServerFactory,
|
||||
WampRawSocketServerProtocol,
|
||||
WampRawSocketClientFactory,
|
||||
WampRawSocketClientProtocol)
|
||||
from autobahn.testutil import FakeTransport
|
||||
|
||||
|
||||
class RawSocketHandshakeTests(unittest.TestCase):
|
||||
|
||||
def test_handshake_succeeds(self):
|
||||
"""
|
||||
A client can connect to a server.
|
||||
"""
|
||||
session_mock = Mock()
|
||||
t = FakeTransport()
|
||||
f = WampRawSocketClientFactory(lambda: session_mock)
|
||||
p = WampRawSocketClientProtocol()
|
||||
p.transport = t
|
||||
p.factory = f
|
||||
|
||||
server_session_mock = Mock()
|
||||
st = FakeTransport()
|
||||
sf = WampRawSocketServerFactory(lambda: server_session_mock)
|
||||
sp = WampRawSocketServerProtocol()
|
||||
sp.transport = st
|
||||
sp.factory = sf
|
||||
|
||||
sp.connectionMade()
|
||||
p.connectionMade()
|
||||
|
||||
# Send the server the client handshake
|
||||
sp.dataReceived(t._written[0:1])
|
||||
sp.dataReceived(t._written[1:4])
|
||||
|
||||
# Send the client the server handshake
|
||||
p.dataReceived(st._written)
|
||||
|
||||
# The handshake succeeds, a session on each end is created
|
||||
# onOpen is called on the session
|
||||
session_mock.onOpen.assert_called_once_with(p)
|
||||
server_session_mock.onOpen.assert_called_once_with(sp)
|
||||
@@ -0,0 +1,81 @@
|
||||
from twisted.trial import unittest
|
||||
|
||||
try:
|
||||
from autobahn.twisted.testing import create_memory_agent, MemoryReactorClockResolver, create_pumper
|
||||
HAVE_TESTING = True
|
||||
except ImportError:
|
||||
HAVE_TESTING = False
|
||||
|
||||
from twisted.internet.defer import inlineCallbacks
|
||||
from autobahn.twisted.websocket import WebSocketServerProtocol
|
||||
|
||||
|
||||
class TestAgent(unittest.TestCase):
|
||||
|
||||
skip = not HAVE_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.pumper = create_pumper()
|
||||
self.reactor = MemoryReactorClockResolver()
|
||||
return self.pumper.start()
|
||||
|
||||
def tearDown(self):
|
||||
return self.pumper.stop()
|
||||
|
||||
@inlineCallbacks
|
||||
def test_echo_server(self):
|
||||
|
||||
class EchoServer(WebSocketServerProtocol):
|
||||
def onMessage(self, msg, is_binary):
|
||||
self.sendMessage(msg)
|
||||
|
||||
agent = create_memory_agent(self.reactor, self.pumper, EchoServer)
|
||||
proto = yield agent.open("ws://localhost:1234/ws", dict())
|
||||
|
||||
messages = []
|
||||
|
||||
def got(msg, is_binary):
|
||||
messages.append(msg)
|
||||
proto.on("message", got)
|
||||
|
||||
proto.sendMessage(b"hello")
|
||||
|
||||
if True:
|
||||
# clean close
|
||||
proto.sendClose()
|
||||
else:
|
||||
# unclean close
|
||||
proto.transport.loseConnection()
|
||||
yield proto.is_closed
|
||||
self.assertEqual([b"hello"], messages)
|
||||
|
||||
# FIXME:
|
||||
# /twisted/util.py", line 162, in transport_channel_id channel_id_type, type(transport)))
|
||||
# builtins.RuntimeError: cannot determine TLS channel ID of type "tls-unique" when TLS is not
|
||||
# available on this transport <class 'twisted.test.iosim.FakeTransport'>
|
||||
# @inlineCallbacks
|
||||
# def test_secure_echo_server(self):
|
||||
|
||||
# class EchoServer(WebSocketServerProtocol):
|
||||
# def onMessage(self, msg, is_binary):
|
||||
# self.sendMessage(msg)
|
||||
|
||||
# agent = create_memory_agent(self.reactor, self.pumper, EchoServer)
|
||||
# proto = yield agent.open("wss://localhost:1234/ws", dict())
|
||||
|
||||
# messages = []
|
||||
|
||||
# def got(msg, is_binary):
|
||||
# messages.append(msg)
|
||||
# proto.on("message", got)
|
||||
|
||||
# proto.sendMessage(b"hello")
|
||||
|
||||
# if True:
|
||||
# # clean close
|
||||
# proto.sendClose()
|
||||
# else:
|
||||
# # unclean close
|
||||
# proto.transport.loseConnection()
|
||||
# yield proto.is_closed
|
||||
# self.assertEqual([b"hello"], messages)
|
||||
@@ -0,0 +1,90 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 unittest.mock import patch
|
||||
from zope.interface import implementer
|
||||
from twisted.internet.interfaces import IReactorTime
|
||||
|
||||
|
||||
@implementer(IReactorTime)
|
||||
class FakeReactor(object):
|
||||
"""
|
||||
This just fakes out enough reactor methods so .run() can work.
|
||||
"""
|
||||
stop_called = False
|
||||
|
||||
def __init__(self, to_raise):
|
||||
self.stop_called = False
|
||||
self.to_raise = to_raise
|
||||
self.delayed = []
|
||||
|
||||
def run(self, *args, **kw):
|
||||
raise self.to_raise
|
||||
|
||||
def stop(self):
|
||||
self.stop_called = True
|
||||
|
||||
def callLater(self, delay, func, *args, **kwargs):
|
||||
self.delayed.append((delay, func, args, kwargs))
|
||||
|
||||
def connectTCP(self, *args, **kw):
|
||||
raise RuntimeError("ConnectTCP shouldn't get called")
|
||||
|
||||
|
||||
class TestWampTwistedRunner(unittest.TestCase):
|
||||
# XXX should figure out *why* but the test_protocol timeout
|
||||
# tests fail if we *don't* patch out this txaio stuff. So,
|
||||
# presumably it's messing up some global state that both tests
|
||||
# implicitly depend on ...
|
||||
|
||||
@patch('txaio.use_twisted')
|
||||
@patch('txaio.start_logging')
|
||||
@patch('txaio.config')
|
||||
def test_connect_error(self, *args):
|
||||
"""
|
||||
Ensure the runner doesn't swallow errors and that it exits the
|
||||
reactor properly if there is one.
|
||||
"""
|
||||
try:
|
||||
from autobahn.twisted.wamp import ApplicationRunner
|
||||
from twisted.internet.error import ConnectionRefusedError
|
||||
# the 'reactor' member doesn't exist until we import it
|
||||
from twisted.internet import reactor # noqa: F401
|
||||
except ImportError:
|
||||
raise unittest.SkipTest('No twisted')
|
||||
|
||||
runner = ApplicationRunner('ws://localhost:1', 'realm')
|
||||
exception = ConnectionRefusedError("It's a trap!")
|
||||
|
||||
with patch('twisted.internet.reactor', FakeReactor(exception)) as mockreactor:
|
||||
self.assertRaises(
|
||||
ConnectionRefusedError,
|
||||
# pass a no-op session-creation method
|
||||
runner.run, lambda _: None, start_reactor=True
|
||||
)
|
||||
self.assertTrue(mockreactor.stop_called)
|
||||
@@ -0,0 +1,293 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
# IHostnameResolver et al. were added in Twisted 17.1.0 .. before
|
||||
# that, it was IResolverSimple only.
|
||||
|
||||
try:
|
||||
from twisted.internet.interfaces import IHostnameResolver
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Twisted 17.1.0 or later required for autobahn.twisted.testing"
|
||||
)
|
||||
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.internet.address import IPv4Address
|
||||
from twisted.internet._resolver import HostResolution # "internal" class, but it's simple
|
||||
from twisted.internet.interfaces import ISSLTransport, IReactorPluggableNameResolver
|
||||
try:
|
||||
from twisted.internet.testing import MemoryReactorClock
|
||||
except ImportError:
|
||||
from twisted.test.proto_helpers import MemoryReactorClock
|
||||
from twisted.test import iosim
|
||||
|
||||
from zope.interface import directlyProvides, implementer
|
||||
|
||||
from autobahn.websocket.interfaces import IWebSocketClientAgent
|
||||
from autobahn.twisted.websocket import _TwistedWebSocketClientAgent
|
||||
from autobahn.twisted.websocket import WebSocketServerProtocol
|
||||
from autobahn.twisted.websocket import WebSocketServerFactory
|
||||
|
||||
|
||||
__all__ = (
|
||||
'create_pumper',
|
||||
'create_memory_agent',
|
||||
'MemoryReactorClockResolver',
|
||||
)
|
||||
|
||||
|
||||
@implementer(IHostnameResolver)
|
||||
class _StaticTestResolver(object):
|
||||
def resolveHostName(self, receiver, hostName, portNumber=0):
|
||||
"""
|
||||
Implement IHostnameResolver which always returns 127.0.0.1:31337
|
||||
"""
|
||||
resolution = HostResolution(hostName)
|
||||
receiver.resolutionBegan(resolution)
|
||||
receiver.addressResolved(
|
||||
IPv4Address('TCP', '127.0.0.1', 31337 if portNumber == 0 else portNumber)
|
||||
)
|
||||
receiver.resolutionComplete()
|
||||
|
||||
|
||||
@implementer(IReactorPluggableNameResolver)
|
||||
class _TestNameResolver(object):
|
||||
"""
|
||||
A test version of IReactorPluggableNameResolver
|
||||
"""
|
||||
|
||||
_resolver = None
|
||||
|
||||
@property
|
||||
def nameResolver(self):
|
||||
if self._resolver is None:
|
||||
self._resolver = _StaticTestResolver()
|
||||
return self._resolver
|
||||
|
||||
def installNameResolver(self, resolver):
|
||||
old = self._resolver
|
||||
self._resolver = resolver
|
||||
return old
|
||||
|
||||
|
||||
class MemoryReactorClockResolver(MemoryReactorClock, _TestNameResolver):
|
||||
"""
|
||||
Combine MemoryReactor, Clock and an IReactorPluggableNameResolver
|
||||
together.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class _TwistedWebMemoryAgent(IWebSocketClientAgent):
|
||||
"""
|
||||
A testing agent which will hook up an instance of
|
||||
`server_protocol` for every client that is created via the `open`
|
||||
API call.
|
||||
|
||||
:param reactor: the reactor to use for tests (usually an instance
|
||||
of MemoryReactorClockResolver)
|
||||
|
||||
:param pumper: an implementation IPumper (e.g. as returned by
|
||||
`create_pumper`)
|
||||
|
||||
:param server_protocol: the server-side WebSocket protocol class
|
||||
to instantiate (e.g. a subclass of `WebSocketServerProtocol`
|
||||
"""
|
||||
|
||||
def __init__(self, reactor, pumper, server_protocol):
|
||||
self._reactor = reactor
|
||||
self._server_protocol = server_protocol
|
||||
self._pumper = pumper
|
||||
|
||||
# our "real" underlying agent under test
|
||||
self._agent = _TwistedWebSocketClientAgent(self._reactor)
|
||||
self._pumps = set()
|
||||
self._servers = dict() # client -> server
|
||||
|
||||
def open(self, transport_config, options, protocol_class=None):
|
||||
"""
|
||||
Implement IWebSocketClientAgent with in-memory transports.
|
||||
|
||||
:param transport_config: a string starting with 'wss://' or
|
||||
'ws://'
|
||||
|
||||
:param options: a dict containing options
|
||||
|
||||
:param protocol_class: the client protocol class to
|
||||
instantiate (or `None` for defaults, which is to use
|
||||
`WebSocketClientProtocol`)
|
||||
"""
|
||||
is_secure = transport_config.startswith("wss://")
|
||||
|
||||
# call our "real" agent
|
||||
real_client_protocol = self._agent.open(
|
||||
transport_config, options,
|
||||
protocol_class=protocol_class,
|
||||
)
|
||||
|
||||
if is_secure:
|
||||
host, port, factory, context_factory, timeout, bindAddress = self._reactor.sslClients[-1]
|
||||
else:
|
||||
host, port, factory, timeout, bindAddress = self._reactor.tcpClients[-1]
|
||||
|
||||
server_address = IPv4Address('TCP', '127.0.0.1', port)
|
||||
client_address = IPv4Address('TCP', '127.0.0.1', 31337)
|
||||
|
||||
server_protocol = self._server_protocol()
|
||||
# the protocol could already have a factory
|
||||
if getattr(server_protocol, "factory", None) is None:
|
||||
server_protocol.factory = WebSocketServerFactory()
|
||||
|
||||
server_transport = iosim.FakeTransport(
|
||||
server_protocol, isServer=True,
|
||||
hostAddress=server_address, peerAddress=client_address)
|
||||
clientProtocol = factory.buildProtocol(None)
|
||||
client_transport = iosim.FakeTransport(
|
||||
clientProtocol, isServer=False,
|
||||
hostAddress=client_address, peerAddress=server_address)
|
||||
|
||||
if is_secure:
|
||||
directlyProvides(server_transport, ISSLTransport)
|
||||
directlyProvides(client_transport, ISSLTransport)
|
||||
|
||||
pump = iosim.connect(
|
||||
server_protocol, server_transport, clientProtocol, client_transport)
|
||||
self._pumper.add(pump)
|
||||
|
||||
def add_mapping(proto):
|
||||
self._servers[proto] = server_protocol
|
||||
return proto
|
||||
real_client_protocol.addCallback(add_mapping)
|
||||
return real_client_protocol
|
||||
|
||||
|
||||
class _Kalamazoo(object):
|
||||
"""
|
||||
Feeling whimsical about class names, see https://en.wikipedia.org/wiki/Handcar
|
||||
|
||||
This is 'an IOPump pumper', an object which causes a series of
|
||||
IOPumps it is monitoring to do their I/O operations
|
||||
periodically. This needs the 'real' reactor which trial drives,
|
||||
because reasons:
|
||||
|
||||
- so @inlineCallbacks / async-def functions work
|
||||
(if I could explain exactly why here, I would)
|
||||
|
||||
- we need to 'break the loop' of synchronous calls somewhere and
|
||||
polluting the tests themselves with that is bad
|
||||
|
||||
- get rid of e.g. .flush() calls in tests themselves (thus
|
||||
'teaching' the tests about details of I/O scheduling that they
|
||||
shouldn't know).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._pumps = set()
|
||||
self._pumping = False
|
||||
self._waiting_for_stop = []
|
||||
from twisted.internet import reactor as global_reactor
|
||||
self._global_reactor = global_reactor
|
||||
|
||||
def add(self, p):
|
||||
"""
|
||||
Add a new IOPump. It will be removed when both its client and
|
||||
server are disconnected.
|
||||
"""
|
||||
self._pumps.add(p)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Begin triggering I/O in all IOPump instances we have. We will keep
|
||||
periodically 'pumping' our IOPumps until `.stop()` is
|
||||
called. Call from `setUp()` for example.
|
||||
"""
|
||||
if self._pumping:
|
||||
return
|
||||
self._pumping = True
|
||||
self._global_reactor.callLater(0, self._pump_once)
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
:returns: a Deferred that fires when we have stopped pump()-ing
|
||||
|
||||
Call from `tearDown()`, for example.
|
||||
"""
|
||||
if self._pumping or len(self._waiting_for_stop):
|
||||
d = Deferred()
|
||||
self._waiting_for_stop.append(d)
|
||||
self._pumping = False
|
||||
return d
|
||||
d = Deferred()
|
||||
d.callback(None)
|
||||
return d
|
||||
|
||||
def _pump_once(self):
|
||||
"""
|
||||
flush all data from all our IOPump instances and schedule another
|
||||
iteration on the global reactor
|
||||
"""
|
||||
if self._pumping:
|
||||
self._flush()
|
||||
self._global_reactor.callLater(0.1, self._pump_once)
|
||||
else:
|
||||
for d in self._waiting_for_stop:
|
||||
d.callback(None)
|
||||
self._waiting_for_stop = []
|
||||
|
||||
def _flush(self):
|
||||
"""
|
||||
Flush all data between pending client/server pairs.
|
||||
"""
|
||||
old_pumps = self._pumps
|
||||
new_pumps = self._pumps = set()
|
||||
for p in old_pumps:
|
||||
p.flush()
|
||||
if p.clientIO.disconnected and p.serverIO.disconnected:
|
||||
continue
|
||||
new_pumps.add(p)
|
||||
|
||||
|
||||
def create_pumper():
|
||||
"""
|
||||
return a new instance implementing IPumper
|
||||
"""
|
||||
return _Kalamazoo()
|
||||
|
||||
|
||||
def create_memory_agent(reactor, pumper, server_protocol):
|
||||
"""
|
||||
return a new instance implementing `IWebSocketClientAgent`.
|
||||
|
||||
connection attempts will be satisfied by traversing the Upgrade
|
||||
request path starting at `resource` to find a `WebSocketResource`
|
||||
and then exchange data between client and server using purely
|
||||
in-memory buffers.
|
||||
"""
|
||||
# Note, we currently don't actually do any "resource traversing"
|
||||
# and basically accept any path at all to our websocket resource
|
||||
if server_protocol is None:
|
||||
server_protocol = WebSocketServerProtocol
|
||||
return _TwistedWebMemoryAgent(reactor, pumper, server_protocol)
|
||||
Binary file not shown.
304
backend/lib/python3.12/site-packages/autobahn/twisted/util.py
Normal file
304
backend/lib/python3.12/site-packages/autobahn/twisted/util.py
Normal file
@@ -0,0 +1,304 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 hashlib
|
||||
import threading
|
||||
from typing import Optional, Union, Dict, Any
|
||||
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.internet.address import IPv4Address, UNIXAddress
|
||||
from twisted.internet.interfaces import ITransport, IProcessTransport
|
||||
|
||||
from autobahn.wamp.types import TransportDetails
|
||||
|
||||
try:
|
||||
from twisted.internet.stdio import PipeAddress
|
||||
except ImportError:
|
||||
# stdio.PipeAddress is only avail on Twisted 13.0+
|
||||
PipeAddress = type(None)
|
||||
|
||||
try:
|
||||
from twisted.internet.address import IPv6Address
|
||||
_HAS_IPV6 = True
|
||||
except ImportError:
|
||||
_HAS_IPV6 = False
|
||||
IPv6Address = type(None)
|
||||
|
||||
try:
|
||||
from twisted.internet.interfaces import ISSLTransport
|
||||
from twisted.protocols.tls import TLSMemoryBIOProtocol
|
||||
from OpenSSL.SSL import Connection
|
||||
_HAS_TLS = True
|
||||
except ImportError:
|
||||
_HAS_TLS = False
|
||||
|
||||
__all = (
|
||||
'sleep',
|
||||
'peer2str',
|
||||
'transport_channel_id',
|
||||
'extract_peer_certificate',
|
||||
'create_transport_details',
|
||||
)
|
||||
|
||||
|
||||
def sleep(delay, reactor=None):
|
||||
"""
|
||||
Inline sleep for use in co-routines (Twisted ``inlineCallback`` decorated functions).
|
||||
|
||||
.. seealso::
|
||||
* `twisted.internet.defer.inlineCallbacks <http://twistedmatrix.com/documents/current/api/twisted.internet.defer.html#inlineCallbacks>`__
|
||||
* `twisted.internet.interfaces.IReactorTime <http://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IReactorTime.html>`__
|
||||
|
||||
:param delay: Time to sleep in seconds.
|
||||
:type delay: float
|
||||
:param reactor: The Twisted reactor to use.
|
||||
:type reactor: None or provider of ``IReactorTime``.
|
||||
"""
|
||||
if not reactor:
|
||||
from twisted.internet import reactor
|
||||
d = Deferred()
|
||||
reactor.callLater(delay, d.callback, None)
|
||||
return d
|
||||
|
||||
|
||||
def peer2str(transport: Union[ITransport, IProcessTransport]) -> str:
|
||||
"""
|
||||
Return a *peer descriptor* given a Twisted transport, for example:
|
||||
|
||||
* ``tcp4:127.0.0.1:52914``: a TCPv4 socket
|
||||
* ``unix:/tmp/server.sock``: a Unix domain socket
|
||||
* ``process:142092``: a Pipe originating from a spawning (parent) process
|
||||
* ``pipe``: a Pipe terminating in a spawned (child) process
|
||||
|
||||
:returns: Returns a string representation of the peer of the Twisted transport.
|
||||
"""
|
||||
# IMPORTANT: we need to _first_ test for IProcessTransport
|
||||
if IProcessTransport.providedBy(transport):
|
||||
# note the PID of the forked process in the peer descriptor
|
||||
res = "process:{}".format(transport.pid)
|
||||
elif ITransport.providedBy(transport):
|
||||
addr: Union[IPv4Address, IPv6Address, UNIXAddress, PipeAddress] = transport.getPeer()
|
||||
if isinstance(addr, IPv4Address):
|
||||
res = "tcp4:{0}:{1}".format(addr.host, addr.port)
|
||||
elif _HAS_IPV6 and isinstance(addr, IPv6Address):
|
||||
res = "tcp6:{0}:{1}".format(addr.host, addr.port)
|
||||
elif isinstance(addr, UNIXAddress):
|
||||
if addr.name:
|
||||
res = "unix:{0}".format(addr.name)
|
||||
else:
|
||||
res = "unix"
|
||||
elif isinstance(addr, PipeAddress):
|
||||
# sadly, we don't have a way to get at the PID of the other side of the pipe
|
||||
# res = "pipe"
|
||||
res = "process:{0}".format(os.getppid())
|
||||
else:
|
||||
# gracefully fallback if we can't map the peer's address
|
||||
res = "unknown"
|
||||
else:
|
||||
# gracefully fallback if we can't map the peer's transport
|
||||
res = "unknown"
|
||||
return res
|
||||
|
||||
|
||||
if not _HAS_TLS:
|
||||
def transport_channel_id(transport: object, is_server: bool, channel_id_type: Optional[str] = None) -> Optional[bytes]:
|
||||
if channel_id_type is None:
|
||||
return b'\x00' * 32
|
||||
else:
|
||||
raise RuntimeError('cannot determine TLS channel ID of type "{}" when TLS is not available on this system'.format(channel_id_type))
|
||||
else:
|
||||
def transport_channel_id(transport: object, is_server: bool, channel_id_type: Optional[str] = None) -> Optional[bytes]:
|
||||
"""
|
||||
Return TLS channel ID of WAMP transport of the given TLS channel ID type.
|
||||
|
||||
Application-layer user authentication protocols are vulnerable to generic credential forwarding attacks,
|
||||
where an authentication credential sent by a client C to a server M may then be used by M to impersonate C at
|
||||
another server S.
|
||||
To prevent such credential forwarding attacks, modern authentication protocols rely on channel bindings.
|
||||
For example, WAMP-cryptosign can use the tls-unique channel identifier provided by the TLS layer to strongly
|
||||
bind authentication credentials to the underlying channel, so that a credential received on one TLS channel
|
||||
cannot be forwarded on another.
|
||||
|
||||
:param transport: The Twisted TLS transport to extract the TLS channel ID from. If the transport isn't
|
||||
TLS based, and non-empty ``channel_id_type`` is requested, ``None`` will be returned. If the transport
|
||||
is indeed TLS based, an empty ``channel_id_type`` of ``None`` is requested, 32 NUL bytes will be returned.
|
||||
:param is_server: Flag indicating that the transport is a server transport.
|
||||
:param channel_id_type: TLS channel ID type, if set currently only ``"tls-unique"`` is supported.
|
||||
:returns: The TLS channel ID (32 bytes).
|
||||
"""
|
||||
if channel_id_type is None:
|
||||
return b'\x00' * 32
|
||||
|
||||
if channel_id_type not in ['tls-unique']:
|
||||
raise RuntimeError('invalid TLS channel ID type "{}" requested'.format(channel_id_type))
|
||||
|
||||
if not isinstance(transport, TLSMemoryBIOProtocol):
|
||||
raise RuntimeError(
|
||||
'cannot determine TLS channel ID of type "{}" when TLS is not available on this transport {}'.format(
|
||||
channel_id_type, type(transport)))
|
||||
|
||||
# get access to the OpenSSL connection underlying the Twisted protocol
|
||||
# https://twistedmatrix.com/documents/current/api/twisted.protocols.tls.TLSMemoryBIOProtocol.html#getHandle
|
||||
connection: Connection = transport.getHandle()
|
||||
assert connection and isinstance(connection, Connection)
|
||||
|
||||
# Obtain latest TLS Finished message that we expected from peer, or None if handshake is not completed.
|
||||
# http://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Connection.get_peer_finished
|
||||
is_not_resumed = True
|
||||
|
||||
if channel_id_type == 'tls-unique':
|
||||
# see also: https://bugs.python.org/file22646/tls_channel_binding.patch
|
||||
if is_server != is_not_resumed:
|
||||
# for routers (=servers) XOR new sessions, the channel ID is based on the TLS Finished message we
|
||||
# expected to receive from the client: contents of the message or None if the TLS handshake has
|
||||
# not yet completed.
|
||||
tls_finished_msg = connection.get_peer_finished()
|
||||
else:
|
||||
# for clients XOR resumed sessions, the channel ID is based on the TLS Finished message we sent
|
||||
# to the router (=server): contents of the message or None if the TLS handshake has not yet completed.
|
||||
tls_finished_msg = connection.get_finished()
|
||||
|
||||
if tls_finished_msg is None:
|
||||
# this can occur when:
|
||||
# 1. we made a successful connection (in a TCP sense) but something failed with
|
||||
# the TLS handshake (e.g. invalid certificate)
|
||||
# 2. the TLS handshake has not yet completed
|
||||
return b'\x00' * 32
|
||||
else:
|
||||
m = hashlib.sha256()
|
||||
m.update(tls_finished_msg)
|
||||
return m.digest()
|
||||
else:
|
||||
raise NotImplementedError('should not arrive here (unhandled channel_id_type "{}")'.format(channel_id_type))
|
||||
|
||||
|
||||
if not _HAS_TLS:
|
||||
def extract_peer_certificate(transport: object) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Dummy when no TLS is available.
|
||||
|
||||
:param transport: Ignored.
|
||||
:return: Always return ``None``.
|
||||
"""
|
||||
return None
|
||||
else:
|
||||
def extract_peer_certificate(transport: TLSMemoryBIOProtocol) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Extract TLS x509 client certificate information from a Twisted stream transport, and
|
||||
return a dict with x509 TLS client certificate information (if the client provided a
|
||||
TLS client certificate).
|
||||
|
||||
:param transport: The secure transport from which to extract the peer certificate (if present).
|
||||
:returns: If the peer provided a certificate, the parsed certificate information set.
|
||||
"""
|
||||
# check if the Twisted transport is a TLSMemoryBIOProtocol
|
||||
if not (ISSLTransport.providedBy(transport) and hasattr(transport, 'getPeerCertificate')):
|
||||
return None
|
||||
|
||||
cert = transport.getPeerCertificate()
|
||||
if cert:
|
||||
# extract x509 name components from an OpenSSL X509Name object
|
||||
def maybe_bytes(_value):
|
||||
if isinstance(_value, bytes):
|
||||
return _value.decode('utf8')
|
||||
else:
|
||||
return _value
|
||||
|
||||
result = {
|
||||
'md5': '{}'.format(maybe_bytes(cert.digest('md5'))).upper(),
|
||||
'sha1': '{}'.format(maybe_bytes(cert.digest('sha1'))).upper(),
|
||||
'sha256': '{}'.format(maybe_bytes(cert.digest('sha256'))).upper(),
|
||||
'expired': bool(cert.has_expired()),
|
||||
'hash': maybe_bytes(cert.subject_name_hash()),
|
||||
'serial': int(cert.get_serial_number()),
|
||||
'signature_algorithm': maybe_bytes(cert.get_signature_algorithm()),
|
||||
'version': int(cert.get_version()),
|
||||
'not_before': maybe_bytes(cert.get_notBefore()),
|
||||
'not_after': maybe_bytes(cert.get_notAfter()),
|
||||
'extensions': []
|
||||
}
|
||||
|
||||
for i in range(cert.get_extension_count()):
|
||||
ext = cert.get_extension(i)
|
||||
ext_info = {
|
||||
'name': '{}'.format(maybe_bytes(ext.get_short_name())),
|
||||
'value': '{}'.format(maybe_bytes(ext)),
|
||||
'critical': ext.get_critical() != 0
|
||||
}
|
||||
result['extensions'].append(ext_info)
|
||||
|
||||
for entity, name in [('subject', cert.get_subject()), ('issuer', cert.get_issuer())]:
|
||||
result[entity] = {}
|
||||
for key, value in name.get_components():
|
||||
key = maybe_bytes(key)
|
||||
value = maybe_bytes(value)
|
||||
result[entity]['{}'.format(key).lower()] = '{}'.format(value)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def create_transport_details(transport: Union[ITransport, IProcessTransport], is_server: bool) -> TransportDetails:
|
||||
"""
|
||||
Create transport details from Twisted transport.
|
||||
|
||||
:param transport: The Twisted transport to extract information from.
|
||||
:param is_server: Flag indicating whether this transport side is a "server" (as in TCP server).
|
||||
:return: Transport details object filled with information from the Twisted transport.
|
||||
"""
|
||||
peer = peer2str(transport)
|
||||
|
||||
own_pid = os.getpid()
|
||||
if hasattr(threading, 'get_native_id'):
|
||||
# New in Python 3.8
|
||||
# https://docs.python.org/3/library/threading.html?highlight=get_native_id#threading.get_native_id
|
||||
own_tid = threading.get_native_id()
|
||||
else:
|
||||
own_tid = threading.get_ident()
|
||||
own_fd = -1
|
||||
|
||||
if _HAS_TLS and ISSLTransport.providedBy(transport):
|
||||
channel_id = {
|
||||
# this will only be filled when the TLS opening handshake is complete (!)
|
||||
'tls-unique': transport_channel_id(transport, is_server, 'tls-unique'),
|
||||
}
|
||||
channel_type = TransportDetails.CHANNEL_TYPE_TLS
|
||||
peer_cert = extract_peer_certificate(transport)
|
||||
is_secure = True
|
||||
else:
|
||||
channel_id = {}
|
||||
channel_type = TransportDetails.CHANNEL_TYPE_TCP
|
||||
peer_cert = None
|
||||
is_secure = False
|
||||
|
||||
# FIXME: really set a default (websocket)?
|
||||
channel_framing = TransportDetails.CHANNEL_FRAMING_WEBSOCKET
|
||||
|
||||
td = TransportDetails(channel_type=channel_type, channel_framing=channel_framing, peer=peer,
|
||||
is_server=is_server, own_pid=own_pid, own_tid=own_tid, own_fd=own_fd,
|
||||
is_secure=is_secure, channel_id=channel_id, peer_cert=peer_cert)
|
||||
|
||||
return td
|
||||
902
backend/lib/python3.12/site-packages/autobahn/twisted/wamp.py
Normal file
902
backend/lib/python3.12/site-packages/autobahn/twisted/wamp.py
Normal file
@@ -0,0 +1,902 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 inspect
|
||||
import binascii
|
||||
import random
|
||||
from typing import Optional, Dict, Any, List, Union
|
||||
|
||||
import txaio
|
||||
from autobahn.websocket.protocol import WebSocketProtocol
|
||||
|
||||
txaio.use_twisted() # noqa
|
||||
|
||||
from twisted.internet.defer import inlineCallbacks, succeed, Deferred
|
||||
from twisted.application import service
|
||||
from twisted.internet.interfaces import IReactorCore, IStreamClientEndpoint
|
||||
|
||||
try:
|
||||
from twisted.internet.ssl import CertificateOptions
|
||||
except ImportError:
|
||||
# PyOpenSSL / TLS not available
|
||||
CertificateOptions = Any
|
||||
|
||||
from autobahn.util import public
|
||||
|
||||
from autobahn.websocket.util import parse_url as parse_ws_url
|
||||
from autobahn.rawsocket.util import parse_url as parse_rs_url
|
||||
|
||||
from autobahn.twisted.websocket import WampWebSocketClientFactory
|
||||
from autobahn.twisted.rawsocket import WampRawSocketClientFactory
|
||||
|
||||
from autobahn.websocket.compress import PerMessageDeflateOffer, \
|
||||
PerMessageDeflateResponse, PerMessageDeflateResponseAccept
|
||||
|
||||
from autobahn.wamp import protocol, auth
|
||||
from autobahn.wamp.interfaces import ITransportHandler, ISession, IAuthenticator, ISerializer
|
||||
from autobahn.wamp.types import ComponentConfig
|
||||
|
||||
__all__ = [
|
||||
'ApplicationSession',
|
||||
'ApplicationSessionFactory',
|
||||
'ApplicationRunner',
|
||||
'Application',
|
||||
'Service',
|
||||
|
||||
# new API
|
||||
'Session',
|
||||
# 'run', # should probably move this method to here? instead of component
|
||||
]
|
||||
|
||||
|
||||
@public
|
||||
class ApplicationSession(protocol.ApplicationSession):
|
||||
"""
|
||||
WAMP application session for Twisted-based applications.
|
||||
|
||||
Implements:
|
||||
|
||||
* :class:`autobahn.wamp.interfaces.ITransportHandler`
|
||||
* :class:`autobahn.wamp.interfaces.ISession`
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
|
||||
ITransportHandler.register(ApplicationSession)
|
||||
|
||||
# ISession.register collides with the abc.ABCMeta.register method
|
||||
ISession.abc_register(ApplicationSession)
|
||||
|
||||
|
||||
class ApplicationSessionFactory(protocol.ApplicationSessionFactory):
|
||||
"""
|
||||
WAMP application session factory for Twisted-based applications.
|
||||
"""
|
||||
|
||||
session: ApplicationSession = ApplicationSession
|
||||
"""
|
||||
The application session class this application session factory will use. Defaults to :class:`autobahn.twisted.wamp.ApplicationSession`.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
|
||||
@public
|
||||
class ApplicationRunner(object):
|
||||
"""
|
||||
This class is a convenience tool mainly for development and quick hosting
|
||||
of WAMP application components.
|
||||
|
||||
It can host a WAMP application component in a WAMP-over-WebSocket client
|
||||
connecting to a WAMP router.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __init__(self,
|
||||
url: str,
|
||||
realm: Optional[str] = None,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
serializers: Optional[List[ISerializer]] = None,
|
||||
ssl: Optional[CertificateOptions] = None,
|
||||
proxy: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, Any]] = None,
|
||||
websocket_options: Optional[Dict[str, Any]] = None,
|
||||
max_retries: Optional[int] = None,
|
||||
initial_retry_delay: Optional[float] = None,
|
||||
max_retry_delay: Optional[float] = None,
|
||||
retry_delay_growth: Optional[float] = None,
|
||||
retry_delay_jitter: Optional[float] = None):
|
||||
"""
|
||||
|
||||
:param url: The WebSocket URL of the WAMP router to connect to (e.g. `ws://example.com:8080/mypath`)
|
||||
:param realm: The WAMP realm to join the application session to.
|
||||
:param extra: Optional extra configuration to forward to the application component.
|
||||
:param serializers: A list of WAMP serializers to use (or None for default serializers).
|
||||
Serializers must implement :class:`autobahn.wamp.interfaces.ISerializer`.
|
||||
:type serializers: list
|
||||
:param ssl: (Optional). If specified this should be an
|
||||
instance suitable to pass as ``sslContextFactory`` to
|
||||
:class:`twisted.internet.endpoints.SSL4ClientEndpoint`` such
|
||||
as :class:`twisted.internet.ssl.CertificateOptions`. Leaving
|
||||
it as ``None`` will use the result of calling Twisted
|
||||
:meth:`twisted.internet.ssl.platformTrust` which tries to use
|
||||
your distribution's CA certificates.
|
||||
:param proxy: Explicit proxy server to use; a dict with ``host`` and ``port`` keys.
|
||||
:param headers: Additional headers to send (only applies to WAMP-over-WebSocket).
|
||||
:param websocket_options: Specific WebSocket options to set (only applies to WAMP-over-WebSocket).
|
||||
If not provided, conservative and practical default are chosen.
|
||||
:param max_retries: Maximum number of reconnection attempts. Unlimited if set to -1.
|
||||
:param initial_retry_delay: Initial delay for reconnection attempt in seconds (Default: 1.0s).
|
||||
:param max_retry_delay: Maximum delay for reconnection attempts in seconds (Default: 60s).
|
||||
:param retry_delay_growth: The growth factor applied to the retry delay between reconnection
|
||||
attempts (Default 1.5).
|
||||
:param retry_delay_jitter: A 0-argument callable that introduces noise into the
|
||||
delay (Default ``random.random``).
|
||||
"""
|
||||
# IMPORTANT: keep this, as it is tested in
|
||||
# autobahn.twisted.test.test_tx_application_runner.TestApplicationRunner.test_runner_bad_proxy
|
||||
assert (proxy is None or type(proxy) == dict)
|
||||
|
||||
self.url = url
|
||||
self.realm = realm
|
||||
self.extra = extra or dict()
|
||||
self.serializers = serializers
|
||||
self.ssl = ssl
|
||||
self.proxy = proxy
|
||||
self.headers = headers
|
||||
self.websocket_options = websocket_options
|
||||
self.max_retries = max_retries
|
||||
self.initial_retry_delay = initial_retry_delay
|
||||
self.max_retry_delay = max_retry_delay
|
||||
self.retry_delay_growth = retry_delay_growth
|
||||
self.retry_delay_jitter = retry_delay_jitter
|
||||
|
||||
# this if for auto-reconnection when Twisted ClientService is avail
|
||||
self._client_service = None
|
||||
# total number of successful connections
|
||||
self._connect_successes = 0
|
||||
|
||||
@public
|
||||
def stop(self):
|
||||
"""
|
||||
Stop reconnecting, if auto-reconnecting was enabled.
|
||||
"""
|
||||
self.log.debug('{klass}.stop()', klass=self.__class__.__name__)
|
||||
|
||||
if self._client_service:
|
||||
return self._client_service.stopService()
|
||||
else:
|
||||
return succeed(None)
|
||||
|
||||
@public
|
||||
def run(self, make, start_reactor: bool = True, auto_reconnect: bool = False,
|
||||
log_level: str = 'info', endpoint: Optional[IStreamClientEndpoint] = None,
|
||||
reactor: Optional[IReactorCore] = None) -> Union[type(None), Deferred]:
|
||||
"""
|
||||
Run the application component.
|
||||
|
||||
:param make: A factory that produces instances of :class:`autobahn.twisted.wamp.ApplicationSession`
|
||||
when called with an instance of :class:`autobahn.wamp.types.ComponentConfig`.
|
||||
:param start_reactor: When ``True`` (the default) this method starts
|
||||
the Twisted reactor and doesn't return until the reactor
|
||||
stops. If there are any problems starting the reactor or
|
||||
connect()-ing, we stop the reactor and raise the exception
|
||||
back to the caller.
|
||||
:param auto_reconnect:
|
||||
:param log_level:
|
||||
:param endpoint:
|
||||
:param reactor:
|
||||
:return: None is returned, unless you specify
|
||||
``start_reactor=False`` in which case the Deferred that
|
||||
connect() returns is returned; this will callback() with
|
||||
an IProtocol instance, which will actually be an instance
|
||||
of :class:`WampWebSocketClientProtocol`
|
||||
"""
|
||||
self.log.debug('{klass}.run()', klass=self.__class__.__name__)
|
||||
|
||||
if start_reactor:
|
||||
# only select framework, set loop and start logging when we are asked
|
||||
# start the reactor - otherwise we are running in a program that likely
|
||||
# already tool care of all this.
|
||||
from twisted.internet import reactor
|
||||
txaio.use_twisted()
|
||||
txaio.config.loop = reactor
|
||||
txaio.start_logging(level=log_level)
|
||||
|
||||
if callable(make):
|
||||
# factory for use ApplicationSession
|
||||
def create():
|
||||
cfg = ComponentConfig(self.realm, self.extra, runner=self)
|
||||
try:
|
||||
session = make(cfg)
|
||||
except Exception:
|
||||
self.log.failure('ApplicationSession could not be instantiated: {log_failure.value}')
|
||||
if start_reactor and reactor.running:
|
||||
reactor.stop()
|
||||
raise
|
||||
else:
|
||||
return session
|
||||
else:
|
||||
create = make
|
||||
|
||||
if self.url.startswith('rs'):
|
||||
# try to parse RawSocket URL
|
||||
isSecure, host, port = parse_rs_url(self.url)
|
||||
|
||||
# use the first configured serializer if any (which means, auto-choose "best")
|
||||
serializer = self.serializers[0] if self.serializers else None
|
||||
|
||||
# create a WAMP-over-RawSocket transport client factory
|
||||
transport_factory = WampRawSocketClientFactory(create, serializer=serializer)
|
||||
|
||||
else:
|
||||
# try to parse WebSocket URL
|
||||
isSecure, host, port, resource, path, params = parse_ws_url(self.url)
|
||||
|
||||
# create a WAMP-over-WebSocket transport client factory
|
||||
transport_factory = WampWebSocketClientFactory(create, url=self.url, serializers=self.serializers, proxy=self.proxy, headers=self.headers)
|
||||
|
||||
# client WebSocket settings - similar to:
|
||||
# - http://crossbar.io/docs/WebSocket-Compression/#production-settings
|
||||
# - http://crossbar.io/docs/WebSocket-Options/#production-settings
|
||||
|
||||
# The permessage-deflate extensions offered to the server
|
||||
offers = [PerMessageDeflateOffer()]
|
||||
|
||||
# Function to accept permessage-deflate responses from the server
|
||||
def accept(response):
|
||||
if isinstance(response, PerMessageDeflateResponse):
|
||||
return PerMessageDeflateResponseAccept(response)
|
||||
|
||||
# default WebSocket options for all client connections
|
||||
protocol_options = {
|
||||
'version': WebSocketProtocol.DEFAULT_SPEC_VERSION,
|
||||
'utf8validateIncoming': True,
|
||||
'acceptMaskedServerFrames': False,
|
||||
'maskClientFrames': True,
|
||||
'applyMask': True,
|
||||
'maxFramePayloadSize': 1048576,
|
||||
'maxMessagePayloadSize': 1048576,
|
||||
'autoFragmentSize': 65536,
|
||||
'failByDrop': True,
|
||||
'echoCloseCodeReason': False,
|
||||
'serverConnectionDropTimeout': 1.,
|
||||
'openHandshakeTimeout': 2.5,
|
||||
'closeHandshakeTimeout': 1.,
|
||||
'tcpNoDelay': True,
|
||||
'perMessageCompressionOffers': offers,
|
||||
'perMessageCompressionAccept': accept,
|
||||
'autoPingInterval': 10.,
|
||||
'autoPingTimeout': 5.,
|
||||
'autoPingSize': 12,
|
||||
|
||||
# see: https://github.com/crossbario/autobahn-python/issues/1327 and
|
||||
# _cancelAutoPingTimeoutCall
|
||||
'autoPingRestartOnAnyTraffic': True,
|
||||
}
|
||||
|
||||
# let user override above default options
|
||||
if self.websocket_options:
|
||||
protocol_options.update(self.websocket_options)
|
||||
|
||||
# set websocket protocol options on Autobahn/Twisted protocol factory, from where it will
|
||||
# be applied for every Autobahn/Twisted protocol instance from the factory
|
||||
transport_factory.setProtocolOptions(**protocol_options)
|
||||
|
||||
# supress pointless log noise
|
||||
transport_factory.noisy = False
|
||||
|
||||
if endpoint:
|
||||
client = endpoint
|
||||
else:
|
||||
# if user passed ssl= but isn't using isSecure, we'll never
|
||||
# use the ssl argument which makes no sense.
|
||||
context_factory = None
|
||||
if self.ssl is not None:
|
||||
if not isSecure:
|
||||
raise RuntimeError(
|
||||
'ssl= argument value passed to %s conflicts with the "ws:" '
|
||||
'prefix of the url argument. Did you mean to use "wss:"?' %
|
||||
self.__class__.__name__)
|
||||
context_factory = self.ssl
|
||||
elif isSecure:
|
||||
from twisted.internet.ssl import optionsForClientTLS
|
||||
context_factory = optionsForClientTLS(host)
|
||||
|
||||
from twisted.internet import reactor
|
||||
if self.proxy is not None:
|
||||
from twisted.internet.endpoints import TCP4ClientEndpoint
|
||||
client = TCP4ClientEndpoint(reactor, self.proxy['host'], self.proxy['port'])
|
||||
transport_factory.contextFactory = context_factory
|
||||
elif isSecure:
|
||||
from twisted.internet.endpoints import SSL4ClientEndpoint
|
||||
assert context_factory is not None
|
||||
client = SSL4ClientEndpoint(reactor, host, port, context_factory)
|
||||
else:
|
||||
from twisted.internet.endpoints import TCP4ClientEndpoint
|
||||
client = TCP4ClientEndpoint(reactor, host, port)
|
||||
|
||||
# as the reactor shuts down, we wish to wait until we've sent
|
||||
# out our "Goodbye" message; leave() returns a Deferred that
|
||||
# fires when the transport gets to STATE_CLOSED
|
||||
def cleanup(proto):
|
||||
if hasattr(proto, '_session') and proto._session is not None:
|
||||
if proto._session.is_attached():
|
||||
return proto._session.leave()
|
||||
elif proto._session.is_connected():
|
||||
return proto._session.disconnect()
|
||||
|
||||
# when our proto was created and connected, make sure it's cleaned
|
||||
# up properly later on when the reactor shuts down for whatever reason
|
||||
def init_proto(proto):
|
||||
self._connect_successes += 1
|
||||
reactor.addSystemEventTrigger('before', 'shutdown', cleanup, proto)
|
||||
return proto
|
||||
|
||||
use_service = False
|
||||
if auto_reconnect:
|
||||
try:
|
||||
# since Twisted 16.1.0
|
||||
from twisted.application.internet import ClientService
|
||||
from twisted.application.internet import backoffPolicy
|
||||
use_service = True
|
||||
except ImportError:
|
||||
use_service = False
|
||||
|
||||
if use_service:
|
||||
# this code path is automatically reconnecting ..
|
||||
self.log.debug('using t.a.i.ClientService')
|
||||
|
||||
if (self.max_retries is not None or self.initial_retry_delay is not None or self.max_retry_delay is not None or self.retry_delay_growth is not None or self.retry_delay_jitter is not None):
|
||||
|
||||
if self.max_retry_delay > 0:
|
||||
kwargs = {}
|
||||
|
||||
def _jitter():
|
||||
j = 1 if self.retry_delay_jitter is None else self.retry_delay_jitter
|
||||
return random.random() * j
|
||||
|
||||
for key, val in [('initialDelay', self.initial_retry_delay),
|
||||
('maxDelay', self.max_retry_delay),
|
||||
('factor', self.retry_delay_growth),
|
||||
('jitter', _jitter)]:
|
||||
if val is not None:
|
||||
kwargs[key] = val
|
||||
|
||||
# retry policy that will only try to reconnect if we connected
|
||||
# successfully at least once before (so it fails on host unreachable etc ..)
|
||||
def retry(failed_attempts):
|
||||
if self._connect_successes > 0 and (self.max_retries == -1 or failed_attempts < self.max_retries):
|
||||
return backoffPolicy(**kwargs)(failed_attempts)
|
||||
else:
|
||||
print('hit stop')
|
||||
self.stop()
|
||||
return 100000000000000
|
||||
else:
|
||||
# immediately reconnect (zero delay)
|
||||
def retry(_):
|
||||
return 0
|
||||
else:
|
||||
retry = backoffPolicy()
|
||||
|
||||
# https://twistedmatrix.com/documents/current/api/twisted.application.internet.ClientService.html
|
||||
self._client_service = ClientService(client, transport_factory, retryPolicy=retry)
|
||||
self._client_service.startService()
|
||||
|
||||
d = self._client_service.whenConnected()
|
||||
|
||||
else:
|
||||
# this code path is only connecting once!
|
||||
self.log.debug('using t.i.e.connect()')
|
||||
|
||||
d = client.connect(transport_factory)
|
||||
|
||||
# if we connect successfully, the arg is a WampWebSocketClientProtocol
|
||||
d.addCallback(init_proto)
|
||||
|
||||
# if the user didn't ask us to start the reactor, then they
|
||||
# get to deal with any connect errors themselves.
|
||||
if start_reactor:
|
||||
# if an error happens in the connect(), we save the underlying
|
||||
# exception so that after the event-loop exits we can re-raise
|
||||
# it to the caller.
|
||||
|
||||
class ErrorCollector(object):
|
||||
exception = None
|
||||
|
||||
def __call__(self, failure):
|
||||
self.exception = failure.value
|
||||
reactor.stop()
|
||||
connect_error = ErrorCollector()
|
||||
d.addErrback(connect_error)
|
||||
|
||||
# now enter the Twisted reactor loop
|
||||
reactor.run()
|
||||
|
||||
# if the ApplicationSession sets an "error" key on the self.config.extra dictionary, which
|
||||
# has been set to the self.extra dictionary, extract the Exception from that and re-raise
|
||||
# it as the very last one (see below) exciting back to the caller of self.run()
|
||||
app_error = self.extra.get('error', None)
|
||||
|
||||
# if we exited due to a connection error, raise that to the caller
|
||||
if connect_error.exception:
|
||||
raise connect_error.exception
|
||||
elif app_error:
|
||||
raise app_error
|
||||
|
||||
else:
|
||||
# let the caller handle any errors
|
||||
return d
|
||||
|
||||
|
||||
class _ApplicationSession(ApplicationSession):
|
||||
"""
|
||||
WAMP application session class used internally with :class:`autobahn.twisted.app.Application`.
|
||||
"""
|
||||
|
||||
def __init__(self, config, app):
|
||||
"""
|
||||
|
||||
:param config: The component configuration.
|
||||
:type config: Instance of :class:`autobahn.wamp.types.ComponentConfig`
|
||||
:param app: The application this session is for.
|
||||
:type app: Instance of :class:`autobahn.twisted.wamp.Application`.
|
||||
"""
|
||||
# noinspection PyArgumentList
|
||||
ApplicationSession.__init__(self, config)
|
||||
self.app = app
|
||||
|
||||
@inlineCallbacks
|
||||
def onConnect(self):
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.ISession.onConnect`
|
||||
"""
|
||||
yield self.app._fire_signal('onconnect')
|
||||
self.join(self.config.realm)
|
||||
|
||||
@inlineCallbacks
|
||||
def onJoin(self, details):
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.ISession.onJoin`
|
||||
"""
|
||||
for uri, proc in self.app._procs:
|
||||
yield self.register(proc, uri)
|
||||
|
||||
for uri, handler in self.app._handlers:
|
||||
yield self.subscribe(handler, uri)
|
||||
|
||||
yield self.app._fire_signal('onjoined')
|
||||
|
||||
@inlineCallbacks
|
||||
def onLeave(self, details):
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.ISession.onLeave`
|
||||
"""
|
||||
yield self.app._fire_signal('onleave')
|
||||
self.disconnect()
|
||||
|
||||
@inlineCallbacks
|
||||
def onDisconnect(self):
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.ISession.onDisconnect`
|
||||
"""
|
||||
yield self.app._fire_signal('ondisconnect')
|
||||
|
||||
|
||||
class Application(object):
|
||||
"""
|
||||
A WAMP application. The application object provides a simple way of
|
||||
creating, debugging and running WAMP application components.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __init__(self, prefix=None):
|
||||
"""
|
||||
|
||||
:param prefix: The application URI prefix to use for procedures and topics,
|
||||
e.g. ``"com.example.myapp"``.
|
||||
:type prefix: unicode
|
||||
"""
|
||||
self._prefix = prefix
|
||||
|
||||
# procedures to be registered once the app session has joined the router/realm
|
||||
self._procs = []
|
||||
|
||||
# event handler to be subscribed once the app session has joined the router/realm
|
||||
self._handlers = []
|
||||
|
||||
# app lifecycle signal handlers
|
||||
self._signals = {}
|
||||
|
||||
# once an app session is connected, this will be here
|
||||
self.session = None
|
||||
|
||||
def __call__(self, config):
|
||||
"""
|
||||
Factory creating a WAMP application session for the application.
|
||||
|
||||
:param config: Component configuration.
|
||||
:type config: Instance of :class:`autobahn.wamp.types.ComponentConfig`
|
||||
|
||||
:returns: obj -- An object that derives of
|
||||
:class:`autobahn.twisted.wamp.ApplicationSession`
|
||||
"""
|
||||
assert(self.session is None)
|
||||
self.session = _ApplicationSession(config, self)
|
||||
return self.session
|
||||
|
||||
def run(self, url="ws://localhost:8080/ws", realm="realm1", start_reactor=True):
|
||||
"""
|
||||
Run the application.
|
||||
|
||||
:param url: The URL of the WAMP router to connect to.
|
||||
:type url: unicode
|
||||
:param realm: The realm on the WAMP router to join.
|
||||
:type realm: unicode
|
||||
"""
|
||||
runner = ApplicationRunner(url, realm)
|
||||
return runner.run(self.__call__, start_reactor)
|
||||
|
||||
def register(self, uri=None):
|
||||
"""
|
||||
Decorator exposing a function as a remote callable procedure.
|
||||
|
||||
The first argument of the decorator should be the URI of the procedure
|
||||
to register under.
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@app.register('com.myapp.add2')
|
||||
def add2(a, b):
|
||||
return a + b
|
||||
|
||||
Above function can then be called remotely over WAMP using the URI `com.myapp.add2`
|
||||
the function was registered under.
|
||||
|
||||
If no URI is given, the URI is constructed from the application URI prefix
|
||||
and the Python function name.
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
app = Application('com.myapp')
|
||||
|
||||
# implicit URI will be 'com.myapp.add2'
|
||||
@app.register()
|
||||
def add2(a, b):
|
||||
return a + b
|
||||
|
||||
If the function `yields` (is a co-routine), the `@inlineCallbacks` decorator
|
||||
will be applied automatically to it. In that case, if you wish to return something,
|
||||
you should use `returnValue`:
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from twisted.internet.defer import returnValue
|
||||
|
||||
@app.register('com.myapp.add2')
|
||||
def add2(a, b):
|
||||
res = yield stuff(a, b)
|
||||
returnValue(res)
|
||||
|
||||
:param uri: The URI of the procedure to register under.
|
||||
:type uri: unicode
|
||||
"""
|
||||
def decorator(func):
|
||||
if uri:
|
||||
_uri = uri
|
||||
else:
|
||||
assert(self._prefix is not None)
|
||||
_uri = "{0}.{1}".format(self._prefix, func.__name__)
|
||||
|
||||
if inspect.isgeneratorfunction(func):
|
||||
func = inlineCallbacks(func)
|
||||
|
||||
self._procs.append((_uri, func))
|
||||
return func
|
||||
return decorator
|
||||
|
||||
def subscribe(self, uri=None):
|
||||
"""
|
||||
Decorator attaching a function as an event handler.
|
||||
|
||||
The first argument of the decorator should be the URI of the topic
|
||||
to subscribe to. If no URI is given, the URI is constructed from
|
||||
the application URI prefix and the Python function name.
|
||||
|
||||
If the function yield, it will be assumed that it's an asynchronous
|
||||
process and inlineCallbacks will be applied to it.
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@app.subscribe('com.myapp.topic1')
|
||||
def onevent1(x, y):
|
||||
print("got event on topic1", x, y)
|
||||
|
||||
:param uri: The URI of the topic to subscribe to.
|
||||
:type uri: unicode
|
||||
"""
|
||||
def decorator(func):
|
||||
if uri:
|
||||
_uri = uri
|
||||
else:
|
||||
assert(self._prefix is not None)
|
||||
_uri = "{0}.{1}".format(self._prefix, func.__name__)
|
||||
|
||||
if inspect.isgeneratorfunction(func):
|
||||
func = inlineCallbacks(func)
|
||||
|
||||
self._handlers.append((_uri, func))
|
||||
return func
|
||||
return decorator
|
||||
|
||||
def signal(self, name):
|
||||
"""
|
||||
Decorator attaching a function as handler for application signals.
|
||||
|
||||
Signals are local events triggered internally and exposed to the
|
||||
developer to be able to react to the application lifecycle.
|
||||
|
||||
If the function yield, it will be assumed that it's an asynchronous
|
||||
coroutine and inlineCallbacks will be applied to it.
|
||||
|
||||
Current signals :
|
||||
|
||||
- `onjoined`: Triggered after the application session has joined the
|
||||
realm on the router and registered/subscribed all procedures
|
||||
and event handlers that were setup via decorators.
|
||||
- `onleave`: Triggered when the application session leaves the realm.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@app.signal('onjoined')
|
||||
def _():
|
||||
# do after the app has join a realm
|
||||
|
||||
:param name: The name of the signal to watch.
|
||||
:type name: unicode
|
||||
"""
|
||||
def decorator(func):
|
||||
if inspect.isgeneratorfunction(func):
|
||||
func = inlineCallbacks(func)
|
||||
self._signals.setdefault(name, []).append(func)
|
||||
return func
|
||||
return decorator
|
||||
|
||||
@inlineCallbacks
|
||||
def _fire_signal(self, name, *args, **kwargs):
|
||||
"""
|
||||
Utility method to call all signal handlers for a given signal.
|
||||
|
||||
:param name: The signal name.
|
||||
:type name: str
|
||||
"""
|
||||
for handler in self._signals.get(name, []):
|
||||
try:
|
||||
# FIXME: what if the signal handler is not a coroutine?
|
||||
# Why run signal handlers synchronously?
|
||||
yield handler(*args, **kwargs)
|
||||
except Exception as e:
|
||||
# FIXME
|
||||
self.log.info("Warning: exception in signal handler swallowed: {err}", err=e)
|
||||
|
||||
|
||||
class Service(service.MultiService):
|
||||
"""
|
||||
A WAMP application as a twisted service.
|
||||
The application object provides a simple way of creating, debugging and running WAMP application
|
||||
components inside a traditional twisted application
|
||||
|
||||
This manages application lifecycle of the wamp connection using startService and stopService
|
||||
Using services also allows to create integration tests that properly terminates their connections
|
||||
|
||||
It can host a WAMP application component in a WAMP-over-WebSocket client
|
||||
connecting to a WAMP router.
|
||||
"""
|
||||
factory = WampWebSocketClientFactory
|
||||
|
||||
def __init__(self, url, realm, make, extra=None, context_factory=None):
|
||||
"""
|
||||
|
||||
:param url: The WebSocket URL of the WAMP router to connect to (e.g. `ws://somehost.com:8090/somepath`)
|
||||
:type url: unicode
|
||||
|
||||
:param realm: The WAMP realm to join the application session to.
|
||||
:type realm: unicode
|
||||
|
||||
:param make: A factory that produces instances of :class:`autobahn.asyncio.wamp.ApplicationSession`
|
||||
when called with an instance of :class:`autobahn.wamp.types.ComponentConfig`.
|
||||
:type make: callable
|
||||
|
||||
:param extra: Optional extra configuration to forward to the application component.
|
||||
:type extra: dict
|
||||
|
||||
:param context_factory: optional, only for secure connections. Passed as contextFactory to
|
||||
the ``listenSSL()`` call; see https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IReactorSSL.connectSSL.html
|
||||
:type context_factory: twisted.internet.ssl.ClientContextFactory or None
|
||||
|
||||
You can replace the attribute factory in order to change connectionLost or connectionFailed behaviour.
|
||||
The factory attribute must return a WampWebSocketClientFactory object
|
||||
"""
|
||||
self.url = url
|
||||
self.realm = realm
|
||||
self.extra = extra or dict()
|
||||
self.make = make
|
||||
self.context_factory = context_factory
|
||||
service.MultiService.__init__(self)
|
||||
self.setupService()
|
||||
|
||||
def setupService(self):
|
||||
"""
|
||||
Setup the application component.
|
||||
"""
|
||||
is_secure, host, port, resource, path, params = parse_ws_url(self.url)
|
||||
|
||||
# factory for use ApplicationSession
|
||||
def create():
|
||||
cfg = ComponentConfig(self.realm, self.extra)
|
||||
session = self.make(cfg)
|
||||
return session
|
||||
|
||||
# create a WAMP-over-WebSocket transport client factory
|
||||
transport_factory = self.factory(create, url=self.url)
|
||||
|
||||
# setup the client from a Twisted endpoint
|
||||
|
||||
if is_secure:
|
||||
from twisted.application.internet import SSLClient
|
||||
ctx = self.context_factory
|
||||
if ctx is None:
|
||||
from twisted.internet.ssl import optionsForClientTLS
|
||||
ctx = optionsForClientTLS(host)
|
||||
client = SSLClient(host, port, transport_factory, contextFactory=ctx)
|
||||
else:
|
||||
if self.context_factory is not None:
|
||||
raise Exception("context_factory specified on non-secure URI")
|
||||
from twisted.application.internet import TCPClient
|
||||
client = TCPClient(host, port, transport_factory)
|
||||
|
||||
client.setServiceParent(self)
|
||||
|
||||
|
||||
# new API
|
||||
class Session(protocol._SessionShim):
|
||||
# XXX these methods are redundant, but put here for possibly
|
||||
# better clarity; maybe a bad idea.
|
||||
|
||||
def on_welcome(self, welcome_msg):
|
||||
pass
|
||||
|
||||
def on_join(self, details):
|
||||
pass
|
||||
|
||||
def on_leave(self, details):
|
||||
self.disconnect()
|
||||
|
||||
def on_connect(self):
|
||||
self.join(self.config.realm)
|
||||
|
||||
def on_disconnect(self):
|
||||
pass
|
||||
|
||||
|
||||
# experimental authentication API
|
||||
class AuthCryptoSign(object):
|
||||
|
||||
def __init__(self, **kw):
|
||||
# should put in checkconfig or similar
|
||||
for key in kw.keys():
|
||||
if key not in ['authextra', 'authid', 'authrole', 'privkey']:
|
||||
raise ValueError(
|
||||
"Unexpected key '{}' for {}".format(key, self.__class__.__name__)
|
||||
)
|
||||
for key in ['privkey']:
|
||||
if key not in kw:
|
||||
raise ValueError(
|
||||
"Must provide '{}' for cryptosign".format(key)
|
||||
)
|
||||
for key in kw.get('authextra', dict()):
|
||||
if key not in ['pubkey', 'channel_binding', 'trustroot', 'challenge']:
|
||||
raise ValueError(
|
||||
"Unexpected key '{}' in 'authextra'".format(key)
|
||||
)
|
||||
|
||||
from autobahn.wamp.cryptosign import CryptosignKey
|
||||
self._privkey = CryptosignKey.from_bytes(
|
||||
binascii.a2b_hex(kw['privkey'])
|
||||
)
|
||||
|
||||
if 'pubkey' in kw.get('authextra', dict()):
|
||||
pubkey = kw['authextra']['pubkey']
|
||||
if pubkey != self._privkey.public_key():
|
||||
raise ValueError(
|
||||
"Public key doesn't correspond to private key"
|
||||
)
|
||||
else:
|
||||
kw['authextra'] = kw.get('authextra', dict())
|
||||
kw['authextra']['pubkey'] = self._privkey.public_key()
|
||||
self._args = kw
|
||||
|
||||
def on_challenge(self, session, challenge):
|
||||
# sign the challenge with our private key.
|
||||
channel_id_type = self._args['authextra'].get('channel_binding', None)
|
||||
channel_id = self.transport.transport_details.channel_id.get(channel_id_type, None)
|
||||
signed_challenge = self._privkey.sign_challenge(challenge, channel_id=channel_id,
|
||||
channel_id_type=channel_id_type)
|
||||
return signed_challenge
|
||||
|
||||
|
||||
IAuthenticator.register(AuthCryptoSign)
|
||||
|
||||
|
||||
class AuthWampCra(object):
|
||||
|
||||
def __init__(self, **kw):
|
||||
# should put in checkconfig or similar
|
||||
for key in kw.keys():
|
||||
if key not in ['authextra', 'authid', 'authrole', 'secret']:
|
||||
raise ValueError(
|
||||
"Unexpected key '{}' for {}".format(key, self.__class__.__name__)
|
||||
)
|
||||
for key in ['secret', 'authid']:
|
||||
if key not in kw:
|
||||
raise ValueError(
|
||||
"Must provide '{}' for wampcra".format(key)
|
||||
)
|
||||
|
||||
self._args = kw
|
||||
self._secret = kw.pop('secret')
|
||||
if not isinstance(self._secret, str):
|
||||
self._secret = self._secret.decode('utf8')
|
||||
|
||||
def on_challenge(self, session, challenge):
|
||||
key = self._secret.encode('utf8')
|
||||
if 'salt' in challenge.extra:
|
||||
key = auth.derive_key(
|
||||
key,
|
||||
challenge.extra['salt'],
|
||||
challenge.extra['iterations'],
|
||||
challenge.extra['keylen']
|
||||
)
|
||||
|
||||
signature = auth.compute_wcs(
|
||||
key,
|
||||
challenge.extra['challenge'].encode('utf8')
|
||||
)
|
||||
return signature.decode('ascii')
|
||||
|
||||
|
||||
IAuthenticator.register(AuthWampCra)
|
||||
@@ -0,0 +1,907 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 base64 import b64encode, b64decode
|
||||
from typing import Optional
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
import txaio
|
||||
txaio.use_twisted()
|
||||
|
||||
import twisted.internet.protocol
|
||||
from twisted.internet import endpoints
|
||||
from twisted.internet.interfaces import ITransport
|
||||
|
||||
from twisted.internet.error import ConnectionDone, ConnectionAborted, \
|
||||
ConnectionLost
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.internet.protocol import connectionDone
|
||||
|
||||
from autobahn.util import public, hltype, hlval
|
||||
from autobahn.util import _is_tls_error, _maybe_tls_reason
|
||||
from autobahn.wamp import websocket
|
||||
from autobahn.wamp.types import TransportDetails
|
||||
from autobahn.websocket.types import ConnectionRequest, ConnectionResponse, ConnectionDeny
|
||||
from autobahn.websocket import protocol
|
||||
from autobahn.websocket.interfaces import IWebSocketClientAgent
|
||||
from autobahn.twisted.util import create_transport_details, transport_channel_id
|
||||
|
||||
from autobahn.websocket.compress import PerMessageDeflateOffer, \
|
||||
PerMessageDeflateOfferAccept, \
|
||||
PerMessageDeflateResponse, \
|
||||
PerMessageDeflateResponseAccept
|
||||
|
||||
|
||||
__all__ = (
|
||||
'create_client_agent',
|
||||
|
||||
'WebSocketAdapterProtocol',
|
||||
'WebSocketServerProtocol',
|
||||
'WebSocketClientProtocol',
|
||||
'WebSocketAdapterFactory',
|
||||
'WebSocketServerFactory',
|
||||
'WebSocketClientFactory',
|
||||
|
||||
'WrappingWebSocketAdapter',
|
||||
'WrappingWebSocketServerProtocol',
|
||||
'WrappingWebSocketClientProtocol',
|
||||
'WrappingWebSocketServerFactory',
|
||||
'WrappingWebSocketClientFactory',
|
||||
|
||||
'listenWS',
|
||||
'connectWS',
|
||||
|
||||
'WampWebSocketServerProtocol',
|
||||
'WampWebSocketServerFactory',
|
||||
'WampWebSocketClientProtocol',
|
||||
'WampWebSocketClientFactory',
|
||||
)
|
||||
|
||||
|
||||
def create_client_agent(reactor):
|
||||
"""
|
||||
:returns: an instance implementing IWebSocketClientAgent
|
||||
"""
|
||||
return _TwistedWebSocketClientAgent(reactor)
|
||||
|
||||
|
||||
def check_transport_config(transport_config):
|
||||
"""
|
||||
raises a ValueError if `transport_config` is invalid
|
||||
"""
|
||||
# XXX move me to "autobahn.websocket.util"
|
||||
if not isinstance(transport_config, str):
|
||||
raise ValueError(
|
||||
"'transport_config' must be a string, found {}".format(type(transport_config))
|
||||
)
|
||||
# XXX also accept everything Crossbar has in client transport configs? e.g like:
|
||||
# { "type": "websocket", "endpoint": {"type": "tcp", "host": "example.com", ...}}
|
||||
# XXX what about TLS options? (the above point would address that too)
|
||||
if not transport_config.startswith("ws://") and \
|
||||
not transport_config.startswith("wss://"):
|
||||
raise ValueError(
|
||||
"'transport_config' must start with 'ws://' or 'wss://'"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def check_client_options(options):
|
||||
"""
|
||||
raises a ValueError if `options` is invalid
|
||||
"""
|
||||
# XXX move me to "autobahn.websocket.util"
|
||||
if not isinstance(options, dict):
|
||||
raise ValueError(
|
||||
"'options' must be a dict"
|
||||
)
|
||||
|
||||
# anything that WebSocketClientFactory accepts (at least)
|
||||
valid_keys = [
|
||||
"origin",
|
||||
"protocols",
|
||||
"useragent",
|
||||
"headers",
|
||||
"proxy",
|
||||
]
|
||||
for actual_k in options.keys():
|
||||
if actual_k not in valid_keys:
|
||||
raise ValueError(
|
||||
"'options' may not contain '{}'".format(actual_k)
|
||||
)
|
||||
|
||||
|
||||
def _endpoint_from_config(reactor, factory, transport_config, options):
|
||||
# XXX might want some Crossbar code here? e.g. if we allow
|
||||
# "transport_config" to be a dict etc.
|
||||
|
||||
# ... passing in the Factory is weird, but that's what parses all
|
||||
# the options and the URL currently
|
||||
|
||||
if factory.isSecure:
|
||||
# create default client SSL context factory when none given
|
||||
from twisted.internet import ssl
|
||||
context_factory = ssl.optionsForClientTLS(factory.host)
|
||||
|
||||
if factory.proxy is not None:
|
||||
factory.contextFactory = context_factory
|
||||
endpoint = endpoints.HostnameEndpoint(
|
||||
reactor,
|
||||
factory.proxy['host'],
|
||||
factory.proxy['port'],
|
||||
# timeout, option?
|
||||
)
|
||||
else:
|
||||
if factory.isSecure:
|
||||
from twisted.internet import ssl
|
||||
endpoint = endpoints.SSL4ClientEndpoint(
|
||||
reactor,
|
||||
factory.host,
|
||||
factory.port,
|
||||
context_factory,
|
||||
# timeout, option?
|
||||
)
|
||||
else:
|
||||
endpoint = endpoints.HostnameEndpoint( # XXX right? not TCP4ClientEndpoint
|
||||
reactor,
|
||||
factory.host,
|
||||
factory.port,
|
||||
# timeout, option?
|
||||
# attemptDelay, option?
|
||||
)
|
||||
return endpoint
|
||||
|
||||
|
||||
class _TwistedWebSocketClientAgent(IWebSocketClientAgent):
|
||||
"""
|
||||
This agent creates connections using Twisted
|
||||
"""
|
||||
|
||||
def __init__(self, reactor):
|
||||
self._reactor = reactor
|
||||
|
||||
def open(self, transport_config, options, protocol_class=None):
|
||||
"""
|
||||
Open a new connection.
|
||||
|
||||
:param dict transport_config: valid transport configuration
|
||||
|
||||
:param dict options: additional options for the factory
|
||||
|
||||
:param protocol_class: a callable that returns an instance of
|
||||
the protocol (WebSocketClientProtocol if the default None
|
||||
is passed in)
|
||||
|
||||
:returns: a Deferred that fires with an instance of
|
||||
`protocol_class` (or WebSocketClientProtocol by default)
|
||||
that has successfully shaken hands (completed the
|
||||
handshake).
|
||||
"""
|
||||
check_transport_config(transport_config)
|
||||
check_client_options(options)
|
||||
|
||||
factory = WebSocketClientFactory(
|
||||
url=transport_config,
|
||||
reactor=self._reactor,
|
||||
**options
|
||||
)
|
||||
factory.protocol = WebSocketClientProtocol if protocol_class is None else protocol_class
|
||||
# XXX might want "contextFactory" for TLS ...? (or e.g. CA etc options?)
|
||||
|
||||
endpoint = _endpoint_from_config(self._reactor, factory, transport_config, options)
|
||||
|
||||
rtn_d = Deferred()
|
||||
proto_d = endpoint.connect(factory)
|
||||
|
||||
def failed(f):
|
||||
rtn_d.errback(f)
|
||||
|
||||
def got_proto(proto):
|
||||
|
||||
def handshake_completed(arg):
|
||||
rtn_d.callback(proto)
|
||||
return arg
|
||||
proto.is_open.addCallbacks(handshake_completed, failed)
|
||||
return proto
|
||||
proto_d.addCallbacks(got_proto, failed)
|
||||
return rtn_d
|
||||
|
||||
|
||||
class WebSocketAdapterProtocol(twisted.internet.protocol.Protocol):
|
||||
"""
|
||||
Adapter class for Twisted WebSocket client and server protocols.
|
||||
|
||||
Called from Twisted:
|
||||
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.connectionMade`
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.connectionLost`
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.dataReceived`
|
||||
|
||||
Called from Network-independent Code (WebSocket implementation):
|
||||
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onOpen`
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessageBegin`
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessageFrameData`
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessageFrameEnd`
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessageEnd`
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessage`
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onPing`
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onPong`
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onClose`
|
||||
|
||||
FIXME:
|
||||
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._closeConnection`
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._create_transport_details`
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.registerProducer`
|
||||
* :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.unregisterProducer`
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
peer: Optional[str] = None
|
||||
is_server: Optional[bool] = None
|
||||
|
||||
def connectionMade(self):
|
||||
# Twisted networking framework entry point, called by Twisted
|
||||
# when the connection is established (either a client or a server)
|
||||
|
||||
# determine preliminary transport details (what is know at this point)
|
||||
self._transport_details = create_transport_details(self.transport, self.is_server)
|
||||
self._transport_details.channel_framing = TransportDetails.CHANNEL_FRAMING_WEBSOCKET
|
||||
|
||||
# backward compatibility
|
||||
self.peer = self._transport_details.peer
|
||||
|
||||
# try to set "Nagle" option for TCP sockets
|
||||
try:
|
||||
self.transport.setTcpNoDelay(self.tcpNoDelay)
|
||||
except: # don't touch this! does not work: AttributeError, OSError
|
||||
# eg Unix Domain sockets throw Errno 22 on this
|
||||
pass
|
||||
|
||||
# ok, now forward to the networking framework independent code for websocket
|
||||
self._connectionMade()
|
||||
|
||||
# ok, done!
|
||||
self.log.debug('{func} connection established for peer="{peer}"',
|
||||
func=hltype(self.connectionMade),
|
||||
peer=hlval(self.peer))
|
||||
|
||||
def connectionLost(self, reason: Failure = connectionDone):
|
||||
# Twisted networking framework entry point, called by Twisted
|
||||
# when the connection is lost (either a client or a server)
|
||||
|
||||
was_clean = False
|
||||
if isinstance(reason.value, ConnectionDone):
|
||||
self.log.debug("Connection to/from {peer} was closed cleanly",
|
||||
peer=self.peer)
|
||||
was_clean = True
|
||||
|
||||
elif _is_tls_error(reason.value):
|
||||
self.log.error(_maybe_tls_reason(reason.value))
|
||||
|
||||
elif isinstance(reason.value, ConnectionAborted):
|
||||
self.log.debug("Connection to/from {peer} was aborted locally",
|
||||
peer=self.peer)
|
||||
|
||||
elif isinstance(reason.value, ConnectionLost):
|
||||
message = str(reason.value)
|
||||
if hasattr(reason.value, 'message'):
|
||||
message = reason.value.message
|
||||
self.log.debug(
|
||||
"Connection to/from {peer} was lost in a non-clean fashion: {message}",
|
||||
peer=self.peer,
|
||||
message=message,
|
||||
)
|
||||
|
||||
# at least: FileDescriptorOverrun, ConnectionFdescWentAway - but maybe others as well?
|
||||
else:
|
||||
self.log.debug("Connection to/from {peer} lost ({error_type}): {error})",
|
||||
peer=self.peer, error_type=type(reason.value), error=reason.value)
|
||||
|
||||
# ok, now forward to the networking framework independent code for websocket
|
||||
self._connectionLost(reason)
|
||||
|
||||
# ok, done!
|
||||
if was_clean:
|
||||
self.log.debug('{func} connection lost for peer="{peer}", closed cleanly',
|
||||
func=hltype(self.connectionLost),
|
||||
peer=hlval(self.peer))
|
||||
else:
|
||||
self.log.debug('{func} connection lost for peer="{peer}", closed with error {reason}',
|
||||
func=hltype(self.connectionLost),
|
||||
peer=hlval(self.peer),
|
||||
reason=reason)
|
||||
|
||||
def dataReceived(self, data: bytes):
|
||||
self.log.debug('{func} received {data_len} bytes for peer="{peer}"',
|
||||
func=hltype(self.dataReceived),
|
||||
peer=hlval(self.peer),
|
||||
data_len=hlval(len(data)))
|
||||
|
||||
# bytes received from Twisted, forward to the networking framework independent code for websocket
|
||||
self._dataReceived(data)
|
||||
|
||||
def _closeConnection(self, abort=False):
|
||||
if abort and hasattr(self.transport, 'abortConnection'):
|
||||
self.transport.abortConnection()
|
||||
else:
|
||||
# e.g. ProcessProtocol lacks abortConnection()
|
||||
self.transport.loseConnection()
|
||||
|
||||
def _onOpen(self):
|
||||
if self._transport_details.is_secure:
|
||||
# now that the TLS opening handshake is complete, the actual TLS channel ID
|
||||
# will be available. make sure to set it!
|
||||
channel_id = {
|
||||
'tls-unique': transport_channel_id(self.transport, self._transport_details.is_server, 'tls-unique'),
|
||||
}
|
||||
self._transport_details.channel_id = channel_id
|
||||
|
||||
self.onOpen()
|
||||
|
||||
def _onMessageBegin(self, isBinary):
|
||||
self.onMessageBegin(isBinary)
|
||||
|
||||
def _onMessageFrameBegin(self, length):
|
||||
self.onMessageFrameBegin(length)
|
||||
|
||||
def _onMessageFrameData(self, payload):
|
||||
self.onMessageFrameData(payload)
|
||||
|
||||
def _onMessageFrameEnd(self):
|
||||
self.onMessageFrameEnd()
|
||||
|
||||
def _onMessageFrame(self, payload):
|
||||
self.onMessageFrame(payload)
|
||||
|
||||
def _onMessageEnd(self):
|
||||
self.onMessageEnd()
|
||||
|
||||
def _onMessage(self, payload, isBinary):
|
||||
self.onMessage(payload, isBinary)
|
||||
|
||||
def _onPing(self, payload):
|
||||
self.onPing(payload)
|
||||
|
||||
def _onPong(self, payload):
|
||||
self.onPong(payload)
|
||||
|
||||
def _onClose(self, wasClean, code, reason):
|
||||
self.onClose(wasClean, code, reason)
|
||||
|
||||
def registerProducer(self, producer, streaming):
|
||||
"""
|
||||
Register a Twisted producer with this protocol.
|
||||
|
||||
:param producer: A Twisted push or pull producer.
|
||||
:type producer: object
|
||||
:param streaming: Producer type.
|
||||
:type streaming: bool
|
||||
"""
|
||||
self.transport.registerProducer(producer, streaming)
|
||||
|
||||
def unregisterProducer(self):
|
||||
"""
|
||||
Unregister Twisted producer with this protocol.
|
||||
"""
|
||||
self.transport.unregisterProducer()
|
||||
|
||||
|
||||
@public
|
||||
class WebSocketServerProtocol(WebSocketAdapterProtocol, protocol.WebSocketServerProtocol):
|
||||
"""
|
||||
Base class for Twisted-based WebSocket server protocols.
|
||||
|
||||
Implements :class:`autobahn.websocket.interfaces.IWebSocketChannel`.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
is_server = True
|
||||
|
||||
# def onConnect(self, request: ConnectionRequest) -> Union[Optional[str], Tuple[Optional[str], Dict[str, str]]]:
|
||||
# pass
|
||||
|
||||
|
||||
@public
|
||||
class WebSocketClientProtocol(WebSocketAdapterProtocol, protocol.WebSocketClientProtocol):
|
||||
"""
|
||||
Base class for Twisted-based WebSocket client protocols.
|
||||
|
||||
Implements :class:`autobahn.websocket.interfaces.IWebSocketChannel`.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
is_server = False
|
||||
|
||||
def _onConnect(self, response: ConnectionResponse):
|
||||
self.log.debug('{meth}(response={response})', meth=hltype(self._onConnect), response=response)
|
||||
return self.onConnect(response)
|
||||
|
||||
def startTLS(self):
|
||||
self.log.debug("Starting TLS upgrade")
|
||||
self.transport.startTLS(self.factory.contextFactory)
|
||||
|
||||
|
||||
class WebSocketAdapterFactory(object):
|
||||
"""
|
||||
Adapter class for Twisted-based WebSocket client and server factories.
|
||||
"""
|
||||
|
||||
|
||||
@public
|
||||
class WebSocketServerFactory(WebSocketAdapterFactory, protocol.WebSocketServerFactory, twisted.internet.protocol.ServerFactory):
|
||||
"""
|
||||
Base class for Twisted-based WebSocket server factories.
|
||||
|
||||
Implements :class:`autobahn.websocket.interfaces.IWebSocketServerChannelFactory`
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
|
||||
.. note::
|
||||
In addition to all arguments to the constructor of
|
||||
:meth:`autobahn.websocket.interfaces.IWebSocketServerChannelFactory`,
|
||||
you can supply a ``reactor`` keyword argument to specify the
|
||||
Twisted reactor to be used.
|
||||
"""
|
||||
# lazy import to avoid reactor install upon module import
|
||||
reactor = kwargs.pop('reactor', None)
|
||||
if reactor is None:
|
||||
from twisted.internet import reactor
|
||||
self.reactor = reactor
|
||||
|
||||
protocol.WebSocketServerFactory.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
@public
|
||||
class WebSocketClientFactory(WebSocketAdapterFactory, protocol.WebSocketClientFactory, twisted.internet.protocol.ClientFactory):
|
||||
"""
|
||||
Base class for Twisted-based WebSocket client factories.
|
||||
|
||||
Implements :class:`autobahn.websocket.interfaces.IWebSocketClientChannelFactory`
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
|
||||
.. note::
|
||||
In addition to all arguments to the constructor of
|
||||
:func:`autobahn.websocket.interfaces.IWebSocketClientChannelFactory`,
|
||||
you can supply a ``reactor`` keyword argument to specify the
|
||||
Twisted reactor to be used.
|
||||
"""
|
||||
# lazy import to avoid reactor install upon module import
|
||||
reactor = kwargs.pop('reactor', None)
|
||||
if reactor is None:
|
||||
from twisted.internet import reactor
|
||||
self.reactor = reactor
|
||||
|
||||
protocol.WebSocketClientFactory.__init__(self, *args, **kwargs)
|
||||
# we must up-call *before* we set up the contextFactory
|
||||
# because we need self.host etc to be set properly.
|
||||
if self.isSecure and self.proxy is not None:
|
||||
# if we have a proxy, then our factory will be used to
|
||||
# create the connection after CONNECT and if it's doing
|
||||
# TLS it needs a contextFactory
|
||||
from twisted.internet import ssl
|
||||
self.contextFactory = ssl.optionsForClientTLS(self.host)
|
||||
# NOTE: there's thus no way to send in our own
|
||||
# context-factory, nor any TLS options.
|
||||
|
||||
# Possibly we should allow 'proxy' to contain an actual
|
||||
# IStreamClientEndpoint instance instead of configuration for
|
||||
# how to make one
|
||||
|
||||
|
||||
@implementer(ITransport)
|
||||
class WrappingWebSocketAdapter(object):
|
||||
"""
|
||||
An adapter for stream-based transport over WebSocket.
|
||||
|
||||
This follows `websockify <https://github.com/kanaka/websockify>`_
|
||||
and should be compatible with that.
|
||||
|
||||
It uses WebSocket subprotocol negotiation and supports the
|
||||
following WebSocket subprotocols:
|
||||
|
||||
- ``binary`` (or a compatible subprotocol)
|
||||
- ``base64``
|
||||
|
||||
Octets are either transmitted as the payload of WebSocket binary
|
||||
messages when using the ``binary`` subprotocol (or an alternative
|
||||
binary compatible subprotocol), or encoded with Base64 and then
|
||||
transmitted as the payload of WebSocket text messages when using
|
||||
the ``base64`` subprotocol.
|
||||
"""
|
||||
|
||||
def onConnect(self, requestOrResponse):
|
||||
# Negotiate either the 'binary' or the 'base64' WebSocket subprotocol
|
||||
if isinstance(requestOrResponse, ConnectionRequest):
|
||||
request = requestOrResponse
|
||||
for p in request.protocols:
|
||||
if p in self.factory._subprotocols:
|
||||
self._binaryMode = (p != 'base64')
|
||||
return p
|
||||
raise ConnectionDeny(ConnectionDeny.NOT_ACCEPTABLE, 'this server only speaks {0} WebSocket subprotocols'.format(self.factory._subprotocols))
|
||||
elif isinstance(requestOrResponse, ConnectionResponse):
|
||||
response = requestOrResponse
|
||||
if response.protocol not in self.factory._subprotocols:
|
||||
self._fail_connection(protocol.WebSocketProtocol.CLOSE_STATUS_CODE_PROTOCOL_ERROR, 'this client only speaks {0} WebSocket subprotocols'.format(self.factory._subprotocols))
|
||||
self._binaryMode = (response.protocol != 'base64')
|
||||
else:
|
||||
# should not arrive here
|
||||
raise Exception("logic error")
|
||||
|
||||
def onOpen(self):
|
||||
self._proto.connectionMade()
|
||||
|
||||
def onMessage(self, payload, isBinary):
|
||||
if isBinary != self._binaryMode:
|
||||
self._fail_connection(protocol.WebSocketProtocol.CLOSE_STATUS_CODE_UNSUPPORTED_DATA, 'message payload type does not match the negotiated subprotocol')
|
||||
else:
|
||||
if not isBinary:
|
||||
try:
|
||||
payload = b64decode(payload)
|
||||
except Exception as e:
|
||||
self._fail_connection(protocol.WebSocketProtocol.CLOSE_STATUS_CODE_INVALID_PAYLOAD, 'message payload base64 decoding error: {0}'.format(e))
|
||||
self._proto.dataReceived(payload)
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def onClose(self, wasClean, code, reason):
|
||||
self._proto.connectionLost(None)
|
||||
|
||||
def write(self, data):
|
||||
# part of ITransport
|
||||
assert(type(data) == bytes)
|
||||
if self._binaryMode:
|
||||
self.sendMessage(data, isBinary=True)
|
||||
else:
|
||||
data = b64encode(data)
|
||||
self.sendMessage(data, isBinary=False)
|
||||
|
||||
def writeSequence(self, data):
|
||||
# part of ITransport
|
||||
for d in data:
|
||||
self.write(d)
|
||||
|
||||
def loseConnection(self):
|
||||
# part of ITransport
|
||||
self.sendClose()
|
||||
|
||||
def getPeer(self):
|
||||
# part of ITransport
|
||||
return self.transport.getPeer()
|
||||
|
||||
def getHost(self):
|
||||
# part of ITransport
|
||||
return self.transport.getHost()
|
||||
|
||||
|
||||
class WrappingWebSocketServerProtocol(WrappingWebSocketAdapter, WebSocketServerProtocol):
|
||||
"""
|
||||
Server protocol for stream-based transport over WebSocket.
|
||||
"""
|
||||
|
||||
|
||||
class WrappingWebSocketClientProtocol(WrappingWebSocketAdapter, WebSocketClientProtocol):
|
||||
"""
|
||||
Client protocol for stream-based transport over WebSocket.
|
||||
"""
|
||||
|
||||
|
||||
class WrappingWebSocketServerFactory(WebSocketServerFactory):
|
||||
"""
|
||||
Wrapping server factory for stream-based transport over WebSocket.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
factory,
|
||||
url,
|
||||
reactor=None,
|
||||
enableCompression=True,
|
||||
autoFragmentSize=0,
|
||||
subprotocol=None):
|
||||
"""
|
||||
|
||||
:param factory: Stream-based factory to be wrapped.
|
||||
:type factory: A subclass of ``twisted.internet.protocol.Factory``
|
||||
:param url: WebSocket URL of the server this server factory will work for.
|
||||
:type url: unicode
|
||||
"""
|
||||
self._factory = factory
|
||||
self._subprotocols = ['binary', 'base64']
|
||||
if subprotocol:
|
||||
self._subprotocols.append(subprotocol)
|
||||
|
||||
WebSocketServerFactory.__init__(self,
|
||||
url=url,
|
||||
reactor=reactor,
|
||||
protocols=self._subprotocols)
|
||||
|
||||
# automatically fragment outgoing traffic into WebSocket frames
|
||||
# of this size
|
||||
self.setProtocolOptions(autoFragmentSize=autoFragmentSize)
|
||||
|
||||
# play nice and perform WS closing handshake
|
||||
self.setProtocolOptions(failByDrop=False)
|
||||
|
||||
if enableCompression:
|
||||
# Enable WebSocket extension "permessage-deflate".
|
||||
|
||||
# Function to accept offers from the client ..
|
||||
def accept(offers):
|
||||
for offer in offers:
|
||||
if isinstance(offer, PerMessageDeflateOffer):
|
||||
return PerMessageDeflateOfferAccept(offer)
|
||||
|
||||
self.setProtocolOptions(perMessageCompressionAccept=accept)
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
proto = WrappingWebSocketServerProtocol()
|
||||
proto.factory = self
|
||||
proto._proto = self._factory.buildProtocol(addr)
|
||||
proto._proto.transport = proto
|
||||
return proto
|
||||
|
||||
def startFactory(self):
|
||||
self._factory.startFactory()
|
||||
WebSocketServerFactory.startFactory(self)
|
||||
|
||||
def stopFactory(self):
|
||||
self._factory.stopFactory()
|
||||
WebSocketServerFactory.stopFactory(self)
|
||||
|
||||
|
||||
class WrappingWebSocketClientFactory(WebSocketClientFactory):
|
||||
"""
|
||||
Wrapping client factory for stream-based transport over WebSocket.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
factory,
|
||||
url,
|
||||
reactor=None,
|
||||
enableCompression=True,
|
||||
autoFragmentSize=0,
|
||||
subprotocol=None):
|
||||
"""
|
||||
|
||||
:param factory: Stream-based factory to be wrapped.
|
||||
:type factory: A subclass of ``twisted.internet.protocol.Factory``
|
||||
:param url: WebSocket URL of the server this client factory will connect to.
|
||||
:type url: unicode
|
||||
"""
|
||||
self._factory = factory
|
||||
self._subprotocols = ['binary', 'base64']
|
||||
if subprotocol:
|
||||
self._subprotocols.append(subprotocol)
|
||||
|
||||
WebSocketClientFactory.__init__(self,
|
||||
url=url,
|
||||
reactor=reactor,
|
||||
protocols=self._subprotocols)
|
||||
|
||||
# automatically fragment outgoing traffic into WebSocket frames
|
||||
# of this size
|
||||
self.setProtocolOptions(autoFragmentSize=autoFragmentSize)
|
||||
|
||||
# play nice and perform WS closing handshake
|
||||
self.setProtocolOptions(failByDrop=False)
|
||||
|
||||
if enableCompression:
|
||||
# Enable WebSocket extension "permessage-deflate".
|
||||
|
||||
# The extensions offered to the server ..
|
||||
offers = [PerMessageDeflateOffer()]
|
||||
self.setProtocolOptions(perMessageCompressionOffers=offers)
|
||||
|
||||
# Function to accept responses from the server ..
|
||||
def accept(response):
|
||||
if isinstance(response, PerMessageDeflateResponse):
|
||||
return PerMessageDeflateResponseAccept(response)
|
||||
|
||||
self.setProtocolOptions(perMessageCompressionAccept=accept)
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
proto = WrappingWebSocketClientProtocol()
|
||||
proto.factory = self
|
||||
proto._proto = self._factory.buildProtocol(addr)
|
||||
proto._proto.transport = proto
|
||||
return proto
|
||||
|
||||
|
||||
@public
|
||||
def connectWS(factory, contextFactory=None, timeout=30, bindAddress=None):
|
||||
"""
|
||||
Establish WebSocket connection to a server. The connection parameters like target
|
||||
host, port, resource and others are provided via the factory.
|
||||
|
||||
:param factory: The WebSocket protocol factory to be used for creating client protocol instances.
|
||||
:type factory: An :class:`autobahn.websocket.WebSocketClientFactory` instance.
|
||||
|
||||
:param contextFactory: SSL context factory, required for secure WebSocket connections ("wss").
|
||||
:type contextFactory: A `twisted.internet.ssl.ClientContextFactory <http://twistedmatrix.com/documents/current/api/twisted.internet.ssl.ClientContextFactory.html>`_ instance.
|
||||
|
||||
:param timeout: Number of seconds to wait before assuming the connection has failed.
|
||||
:type timeout: int
|
||||
|
||||
:param bindAddress: A (host, port) tuple of local address to bind to, or None.
|
||||
:type bindAddress: tuple
|
||||
|
||||
:returns: The connector.
|
||||
:rtype: An object which implements `twisted.interface.IConnector <http://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IConnector.html>`_.
|
||||
"""
|
||||
# lazy import to avoid reactor install upon module import
|
||||
if hasattr(factory, 'reactor'):
|
||||
reactor = factory.reactor
|
||||
else:
|
||||
from twisted.internet import reactor
|
||||
|
||||
if factory.isSecure:
|
||||
if contextFactory is None:
|
||||
# create default client SSL context factory when none given
|
||||
from twisted.internet import ssl
|
||||
contextFactory = ssl.ClientContextFactory()
|
||||
|
||||
if factory.proxy is not None:
|
||||
factory.contextFactory = contextFactory
|
||||
conn = reactor.connectTCP(factory.proxy['host'], factory.proxy['port'], factory, timeout, bindAddress)
|
||||
else:
|
||||
if factory.isSecure:
|
||||
conn = reactor.connectSSL(factory.host, factory.port, factory, contextFactory, timeout, bindAddress)
|
||||
else:
|
||||
conn = reactor.connectTCP(factory.host, factory.port, factory, timeout, bindAddress)
|
||||
return conn
|
||||
|
||||
|
||||
@public
|
||||
def listenWS(factory, contextFactory=None, backlog=50, interface=''):
|
||||
"""
|
||||
Listen for incoming WebSocket connections from clients. The connection parameters like
|
||||
listening port and others are provided via the factory.
|
||||
|
||||
:param factory: The WebSocket protocol factory to be used for creating server protocol instances.
|
||||
:type factory: An :class:`autobahn.websocket.WebSocketServerFactory` instance.
|
||||
|
||||
:param contextFactory: SSL context factory, required for secure WebSocket connections ("wss").
|
||||
:type contextFactory: A twisted.internet.ssl.ContextFactory.
|
||||
|
||||
:param backlog: Size of the listen queue.
|
||||
:type backlog: int
|
||||
|
||||
:param interface: The interface (derived from hostname given) to bind to, defaults to '' (all).
|
||||
:type interface: str
|
||||
|
||||
:returns: The listening port.
|
||||
:rtype: An object that implements `twisted.interface.IListeningPort <http://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IListeningPort.html>`_.
|
||||
"""
|
||||
# lazy import to avoid reactor install upon module import
|
||||
if hasattr(factory, 'reactor'):
|
||||
reactor = factory.reactor
|
||||
else:
|
||||
from twisted.internet import reactor
|
||||
|
||||
if factory.isSecure:
|
||||
if contextFactory is None:
|
||||
raise Exception("Secure WebSocket listen requested, but no SSL context factory given")
|
||||
listener = reactor.listenSSL(factory.port, factory, contextFactory, backlog, interface)
|
||||
else:
|
||||
listener = reactor.listenTCP(factory.port, factory, backlog, interface)
|
||||
return listener
|
||||
|
||||
|
||||
@public
|
||||
class WampWebSocketServerProtocol(websocket.WampWebSocketServerProtocol, WebSocketServerProtocol):
|
||||
"""
|
||||
Twisted-based WAMP-over-WebSocket server protocol.
|
||||
|
||||
Implements:
|
||||
|
||||
* :class:`autobahn.wamp.interfaces.ITransport`
|
||||
"""
|
||||
|
||||
|
||||
@public
|
||||
class WampWebSocketServerFactory(websocket.WampWebSocketServerFactory, WebSocketServerFactory):
|
||||
"""
|
||||
Twisted-based WAMP-over-WebSocket server protocol factory.
|
||||
"""
|
||||
|
||||
protocol = WampWebSocketServerProtocol
|
||||
|
||||
def __init__(self, factory, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param factory: A callable that produces instances that implement
|
||||
:class:`autobahn.wamp.interfaces.ITransportHandler`
|
||||
:type factory: callable
|
||||
|
||||
:param serializers: A list of WAMP serializers to use (or ``None``
|
||||
for all available serializers).
|
||||
:type serializers: list of objects implementing
|
||||
:class:`autobahn.wamp.interfaces.ISerializer`
|
||||
"""
|
||||
|
||||
serializers = kwargs.pop('serializers', None)
|
||||
|
||||
websocket.WampWebSocketServerFactory.__init__(self, factory, serializers)
|
||||
|
||||
kwargs['protocols'] = self._protocols
|
||||
|
||||
# noinspection PyCallByClass
|
||||
WebSocketServerFactory.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
@public
|
||||
class WampWebSocketClientProtocol(websocket.WampWebSocketClientProtocol, WebSocketClientProtocol):
|
||||
"""
|
||||
Twisted-based WAMP-over-WebSocket client protocol.
|
||||
|
||||
Implements:
|
||||
|
||||
* :class:`autobahn.wamp.interfaces.ITransport`
|
||||
"""
|
||||
|
||||
|
||||
@public
|
||||
class WampWebSocketClientFactory(websocket.WampWebSocketClientFactory, WebSocketClientFactory):
|
||||
"""
|
||||
Twisted-based WAMP-over-WebSocket client protocol factory.
|
||||
"""
|
||||
|
||||
protocol = WampWebSocketClientProtocol
|
||||
|
||||
def __init__(self, factory, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param factory: A callable that produces instances that implement
|
||||
:class:`autobahn.wamp.interfaces.ITransportHandler`
|
||||
:type factory: callable
|
||||
|
||||
:param serializer: The WAMP serializer to use (or ``None`` for
|
||||
"best" serializer, chosen as the first serializer available from
|
||||
this list: CBOR, MessagePack, UBJSON, JSON).
|
||||
:type serializer: object implementing :class:`autobahn.wamp.interfaces.ISerializer`
|
||||
"""
|
||||
|
||||
serializers = kwargs.pop('serializers', None)
|
||||
|
||||
websocket.WampWebSocketClientFactory.__init__(self, factory, serializers)
|
||||
|
||||
kwargs['protocols'] = self._protocols
|
||||
|
||||
WebSocketClientFactory.__init__(self, *args, **kwargs)
|
||||
|
||||
# Reduce the factory logs noise
|
||||
self.noisy = False
|
||||
@@ -0,0 +1,107 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 sys
|
||||
|
||||
try:
|
||||
from autobahn import xbr # noqa
|
||||
HAS_XBR = True
|
||||
except ImportError as e:
|
||||
sys.stderr.write('WARNING: could not import autobahn.xbr - {}\n'.format(e))
|
||||
HAS_XBR = False
|
||||
|
||||
|
||||
if HAS_XBR:
|
||||
import txaio
|
||||
txaio.use_twisted()
|
||||
from twisted.internet.threads import deferToThread
|
||||
from twisted.internet.task import LoopingCall
|
||||
from twisted.internet.defer import ensureDeferred
|
||||
|
||||
import uuid
|
||||
|
||||
from autobahn.util import hl
|
||||
from autobahn.xbr._interfaces import IProvider, ISeller, IConsumer, IBuyer, IDelegate
|
||||
from autobahn.xbr import _seller, _buyer, _blockchain
|
||||
|
||||
class SimpleBlockchain(_blockchain.SimpleBlockchain):
|
||||
log = txaio.make_logger()
|
||||
backgroundCaller = deferToThread
|
||||
|
||||
class KeySeries(_seller.KeySeries):
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __init__(self, api_id, price, interval=None, count=None, on_rotate=None):
|
||||
super().__init__(api_id, price, interval, count, on_rotate)
|
||||
self.running = False
|
||||
self._run_loop = None
|
||||
self._started = None
|
||||
|
||||
async def start(self):
|
||||
"""
|
||||
Start offering and selling data encryption keys in the background.
|
||||
"""
|
||||
assert self._run_loop is None
|
||||
|
||||
self.log.info('Starting key rotation every {interval} seconds for api_id="{api_id}" ..',
|
||||
interval=hl(self._interval), api_id=hl(uuid.UUID(bytes=self._api_id)))
|
||||
|
||||
self.running = True
|
||||
|
||||
self._run_loop = LoopingCall(lambda: ensureDeferred(self._rotate()))
|
||||
self._started = self._run_loop.start(self._interval)
|
||||
|
||||
return self._started
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop offering/selling data encryption keys.
|
||||
"""
|
||||
if not self._run_loop:
|
||||
raise RuntimeError('cannot stop {} - not currently running'.format(self.__class__.__name__))
|
||||
|
||||
self._run_loop.stop()
|
||||
self._run_loop = None
|
||||
|
||||
return self._started
|
||||
|
||||
class SimpleSeller(_seller.SimpleSeller):
|
||||
"""
|
||||
Simple XBR seller component. This component can be used by a XBR seller delegate to
|
||||
handle the automated selling of data encryption keys to the XBR market maker.
|
||||
"""
|
||||
log = txaio.make_logger()
|
||||
KeySeries = KeySeries
|
||||
|
||||
class SimpleBuyer(_buyer.SimpleBuyer):
|
||||
log = txaio.make_logger()
|
||||
|
||||
ISeller.register(SimpleSeller)
|
||||
IProvider.register(SimpleSeller)
|
||||
IDelegate.register(SimpleSeller)
|
||||
IBuyer.register(SimpleBuyer)
|
||||
IConsumer.register(SimpleBuyer)
|
||||
IDelegate.register(SimpleBuyer)
|
||||
Binary file not shown.
1044
backend/lib/python3.12/site-packages/autobahn/util.py
Normal file
1044
backend/lib/python3.12/site-packages/autobahn/util.py
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user