This commit is contained in:
2024-12-17 14:36:15 -08:00
parent b2dbf46d28
commit 06d106de53
17731 changed files with 3037186 additions and 144 deletions

View 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

View 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()

View 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'

View File

@@ -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

View File

@@ -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

View File

@@ -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")
"""

View File

@@ -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()

View File

@@ -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

View File

@@ -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.

View File

@@ -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)

View File

@@ -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

View File

@@ -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_')

View 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)

View 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

View File

@@ -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)

View File

@@ -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)

View 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
"""

View 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.
#
###############################################################################
from autobahn.nvx._utf8validator import Utf8Validator # noqa
__all__ = ('Utf8Validator',)

View File

@@ -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);
}
}

View File

@@ -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()

View File

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

View File

@@ -0,0 +1,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])

View File

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

View File

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

View File

@@ -0,0 +1,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'))

View 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

View File

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

View File

@@ -0,0 +1,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)

View 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.
#
###############################################################################
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)

View 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

View File

@@ -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")
"""

View File

@@ -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

View File

@@ -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))

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -0,0 +1,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)

View File

@@ -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()

View File

@@ -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",
}
}
]
)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View 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

View 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)

View File

@@ -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

View File

@@ -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)

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