okay fine

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

View File

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

View File

@@ -0,0 +1,81 @@
###############################################################################
#
# 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.types import \
ComponentConfig, \
SessionDetails, \
CloseDetails, \
RegisterOptions, \
CallOptions, \
CallDetails, \
CallResult, \
SubscribeOptions, \
PublishOptions, \
EventDetails
from autobahn.wamp.exception import \
Error, \
SessionNotReady, \
SerializationError, \
ProtocolError, \
TransportLost, \
ApplicationError, \
InvalidUri
from autobahn.wamp.interfaces import ISession
from autobahn.wamp.uri import \
error, \
register, \
subscribe
__all__ = (
'ComponentConfig',
'SessionDetails',
'CloseDetails',
'RegisterOptions',
'CallOptions',
'CallDetails',
'CallResult',
'SubscribeOptions',
'PublishOptions',
'EventDetails',
'Error',
'SessionNotReady',
'SerializationError',
'ProtocolError',
'TransportLost',
'ApplicationError',
'InvalidUri',
'ISession',
'error',
'register',
'subscribe',
)

View File

@@ -0,0 +1,694 @@
###############################################################################
#
# 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 base64
import struct
import time
import binascii
import hmac
import hashlib
import random
from typing import Optional, Dict
from autobahn.util import public
from autobahn.util import xor as xor_array
from autobahn.wamp.interfaces import IAuthenticator
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
# if we don't have argon2/passlib (see "authentication" extra) then
# you don't get AuthScram and variants
try:
from argon2.low_level import hash_secret
from argon2 import Type
from passlib.utils import saslprep
HAS_ARGON = True
except ImportError:
HAS_ARGON = False
__all__ = (
'AuthAnonymous',
'AuthScram',
'AuthCryptoSign',
'AuthWampCra',
'AuthTicket',
'create_authenticator',
'pbkdf2',
'generate_totp_secret',
'compute_totp',
'check_totp',
'qrcode_from_totp',
'derive_key',
'generate_wcs',
'compute_wcs',
'derive_scram_credential',
)
def create_authenticator(name, **kwargs):
"""
Accepts various keys and values to configure an authenticator. The
valid keys depend on the kind of authenticator but all can
understand: `authextra`, `authid` and `authrole`
:return: an instance implementing IAuthenticator with the given
configuration.
"""
try:
klass = {
AuthScram.name: AuthScram,
AuthCryptoSign.name: AuthCryptoSign,
AuthCryptoSignProxy.name: AuthCryptoSignProxy,
AuthWampCra.name: AuthWampCra,
AuthAnonymous.name: AuthAnonymous,
AuthAnonymousProxy.name: AuthAnonymousProxy,
AuthTicket.name: AuthTicket,
}[name]
except KeyError:
raise ValueError(
"Unknown authenticator '{}'".format(name)
)
# this may raise further ValueErrors if the kwargs are wrong
authenticator = klass(**kwargs)
return authenticator
# experimental authentication API
class AuthAnonymous(object):
name = 'anonymous'
def __init__(self, **kw):
self._args = kw
@property
def authextra(self):
return self._args.get('authextra', dict())
def on_challenge(self, session, challenge):
raise RuntimeError(
"on_challenge called on anonymous authentication"
)
def on_welcome(self, msg, authextra):
return None
IAuthenticator.register(AuthAnonymous)
class AuthAnonymousProxy(AuthAnonymous):
name = 'anonymous-proxy'
IAuthenticator.register(AuthAnonymousProxy)
class AuthTicket(object):
name = 'ticket'
def __init__(self, **kw):
self._args = kw
try:
self._ticket = self._args.pop('ticket')
except KeyError:
raise ValueError(
"ticket authentication requires 'ticket=' kwarg"
)
@property
def authextra(self):
return self._args.get('authextra', dict())
def on_challenge(self, session, challenge):
assert challenge.method == "ticket"
return self._ticket
def on_welcome(self, msg, authextra):
return None
IAuthenticator.register(AuthTicket)
class AuthCryptoSign(object):
name = 'cryptosign'
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)
)
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._channel_binding = kw.get('authextra', dict()).get('channel_binding', None)
self._args = kw
@property
def authextra(self):
return self._args.get('authextra', dict())
def on_challenge(self, session, challenge):
channel_id = session._transport.transport_details.channel_id.get(self._channel_binding, None)
return self._privkey.sign_challenge(challenge,
channel_id=channel_id,
channel_id_type=self._channel_binding)
def on_welcome(self, msg, authextra):
return None
IAuthenticator.register(AuthCryptoSign)
class AuthCryptoSignProxy(AuthCryptoSign):
name = 'cryptosign-proxy'
IAuthenticator.register(AuthCryptoSignProxy)
def _hash_argon2id13_secret(password, salt, iterations, memory):
"""
Internal helper. Returns the salted/hashed password using the
argon2id-13 algorithm. The return value is base64-encoded.
"""
rawhash = hash_secret(
secret=password,
salt=base64.b64decode(salt),
time_cost=iterations,
memory_cost=memory,
parallelism=1, # hard-coded by WAMP-SCRAM spec
hash_len=32,
type=Type.ID,
version=0x13, # note this is decimal "19" which appears in places
)
# spits out stuff like:
# '$argon2i$v=19$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ'
_, tag, ver, options, salt_data, hash_data = rawhash.split(b'$')
return hash_data
def _hash_pbkdf2_secret(password, salt, iterations):
"""
Internal helper for SCRAM authentication
"""
return pbkdf2(password, salt, iterations, keylen=32)
class AuthScram(object):
"""
Implements "wamp-scram" authentication for components.
NOTE: This is a prototype of a draft spec; see
https://github.com/wamp-proto/wamp-proto/issues/135
"""
name = 'scram'
def __init__(self, **kw):
if not HAS_ARGON:
raise RuntimeError(
"Cannot support WAMP-SCRAM without argon2_cffi and "
"passlib libraries; install autobahn['scram']"
)
self._args = kw
self._client_nonce = None
@property
def authextra(self):
# is authextra() called exactly once per authentication?
if self._client_nonce is None:
self._client_nonce = base64.b64encode(os.urandom(16)).decode('ascii')
return {
"nonce": self._client_nonce,
}
def on_challenge(self, session, challenge):
assert challenge.method == "scram"
assert self._client_nonce is not None
required_args = ['nonce', 'kdf', 'salt', 'iterations']
optional_args = ['memory', 'channel_binding']
for k in required_args:
if k not in challenge.extra:
raise RuntimeError(
"WAMP-SCRAM challenge option '{}' is "
" required but not specified".format(k)
)
for k in challenge.extra:
if k not in optional_args + required_args:
raise RuntimeError(
"WAMP-SCRAM challenge has unknown attribute '{}'".format(k)
)
channel_binding = challenge.extra.get('channel_binding', '')
server_nonce = challenge.extra['nonce'] # base64
salt = challenge.extra['salt'] # base64
iterations = int(challenge.extra['iterations'])
memory = int(challenge.extra.get('memory', -1))
password = self._args['password'].encode('utf8') # supplied by user
authid = saslprep(self._args['authid'])
algorithm = challenge.extra['kdf']
client_nonce = self._client_nonce
self._auth_message = (
"{client_first_bare},{server_first},{client_final_no_proof}".format(
client_first_bare="n={},r={}".format(authid, client_nonce),
server_first="r={},s={},i={}".format(server_nonce, salt, iterations),
client_final_no_proof="c={},r={}".format(channel_binding, server_nonce),
)
).encode('ascii')
if algorithm == 'argon2id-13':
if memory == -1:
raise ValueError(
"WAMP-SCRAM 'argon2id-13' challenge requires 'memory' parameter"
)
self._salted_password = _hash_argon2id13_secret(password, salt, iterations, memory)
elif algorithm == 'pbkdf2':
self._salted_password = _hash_pbkdf2_secret(password, salt, iterations)
else:
raise RuntimeError(
"WAMP-SCRAM specified unknown KDF '{}'".format(algorithm)
)
client_key = hmac.new(self._salted_password, b"Client Key", hashlib.sha256).digest()
stored_key = hashlib.new('sha256', client_key).digest()
client_signature = hmac.new(stored_key, self._auth_message, hashlib.sha256).digest()
client_proof = xor_array(client_key, client_signature)
return base64.b64encode(client_proof)
def on_welcome(self, session, authextra):
"""
When the server is satisfied, it sends a 'WELCOME' message.
This hook allows us an opportunity to deny the session right
before it gets set up -- we check the server-signature thus
authorizing the server and if it fails we drop the connection.
"""
alleged_server_sig = base64.b64decode(authextra['scram_server_signature'])
server_key = hmac.new(self._salted_password, b"Server Key", hashlib.sha256).digest()
server_signature = hmac.new(server_key, self._auth_message, hashlib.sha256).digest()
if not hmac.compare_digest(server_signature, alleged_server_sig):
session.log.error("Verification of server SCRAM signature failed")
return "Verification of server SCRAM signature failed"
session.log.info(
"Verification of server SCRAM signature successful"
)
return None
IAuthenticator.register(AuthScram)
class AuthWampCra(object):
name = 'wampcra'
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')
@property
def authextra(self):
return self._args.get('authextra', dict())
def on_challenge(self, session, challenge):
key = self._secret.encode('utf8')
if 'salt' in challenge.extra:
key = derive_key(
key,
challenge.extra['salt'],
challenge.extra['iterations'],
challenge.extra['keylen']
)
signature = compute_wcs(
key,
challenge.extra['challenge'].encode('utf8')
)
return signature.decode('ascii')
def on_welcome(self, msg, authextra):
return None
IAuthenticator.register(AuthWampCra)
@public
def generate_totp_secret(length=10):
"""
Generates a new Base32 encoded, random secret.
.. seealso:: http://en.wikipedia.org/wiki/Base32
:param length: The length of the entropy used to generate the secret.
:type length: int
:returns: The generated secret in Base32 (letters ``A-Z`` and digits ``2-7``).
The length of the generated secret is ``length * 8 / 5`` octets.
:rtype: unicode
"""
assert(type(length) == int)
return base64.b32encode(os.urandom(length)).decode('ascii')
@public
def compute_totp(secret, offset=0):
"""
Computes the current TOTP code.
:param secret: Base32 encoded secret.
:type secret: unicode
:param offset: Time offset (in steps, use eg -1, 0, +1 for compliance with RFC6238)
for which to compute TOTP.
:type offset: int
:returns: TOTP for current time (+/- offset).
:rtype: unicode
"""
assert(type(secret) == str)
assert(type(offset) == int)
try:
key = base64.b32decode(secret)
except TypeError:
raise Exception('invalid secret')
interval = offset + int(time.time()) // 30
msg = struct.pack('>Q', interval)
digest = hmac.new(key, msg, hashlib.sha1).digest()
o = 15 & (digest[19])
token = (struct.unpack('>I', digest[o:o + 4])[0] & 0x7fffffff) % 1000000
return '{0:06d}'.format(token)
@public
def check_totp(secret, ticket):
"""
Check a TOTP value received from a principal trying to authenticate against
the expected value computed from the secret shared between the principal and
the authenticating entity.
The Internet can be slow, and clocks might not match exactly, so some
leniency is allowed. RFC6238 recommends looking an extra time step in either
direction, which essentially opens the window from 30 seconds to 90 seconds.
:param secret: The secret shared between the principal (eg a client) that
is authenticating, and the authenticating entity (eg a server).
:type secret: unicode
:param ticket: The TOTP value to be checked.
:type ticket: unicode
:returns: ``True`` if the TOTP value is correct, else ``False``.
:rtype: bool
"""
for offset in [0, 1, -1]:
if ticket == compute_totp(secret, offset):
return True
return False
@public
def qrcode_from_totp(secret, label, issuer):
if type(secret) != str:
raise Exception('secret must be of type unicode, not {}'.format(type(secret)))
if type(label) != str:
raise Exception('label must be of type unicode, not {}'.format(type(label)))
try:
import qrcode
import qrcode.image.svg
except ImportError:
raise Exception('qrcode not installed')
return qrcode.make(
'otpauth://totp/{}?secret={}&issuer={}'.format(label, secret, issuer),
box_size=3,
image_factory=qrcode.image.svg.SvgImage).to_string()
@public
def pbkdf2(data, salt, iterations=1000, keylen=32, hashfunc=None):
"""
Returns a binary digest for the PBKDF2 hash algorithm of ``data``
with the given ``salt``. It iterates ``iterations`` time and produces a
key of ``keylen`` bytes. By default SHA-256 is used as hash function,
a different hashlib ``hashfunc`` can be provided.
:param data: The data for which to compute the PBKDF2 derived key.
:type data: bytes
:param salt: The salt to use for deriving the key.
:type salt: bytes
:param iterations: The number of iterations to perform in PBKDF2.
:type iterations: int
:param keylen: The length of the cryptographic key to derive.
:type keylen: int
:param hashfunc: Name of the hash algorithm to use
:type hashfunc: str
:returns: The derived cryptographic key.
:rtype: bytes
"""
if not (type(data) == bytes) or \
not (type(salt) == bytes) or \
not (type(iterations) == int) or \
not (type(keylen) == int):
raise ValueError("Invalid argument types")
# justification: WAMP-CRA uses SHA256 and users shouldn't have any
# other reason to call this particular pbkdf2 function (arguably,
# it should be private maybe?)
if hashfunc is None:
hashfunc = 'sha256'
if hashfunc is callable:
# used to take stuff from hashlib; translate?
raise ValueError(
"pbkdf2 now takes the name of a hash algorithm for 'hashfunc='"
)
backend = default_backend()
# https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#pbkdf2
kdf = PBKDF2HMAC(
algorithm=getattr(hashes, hashfunc.upper())(),
length=keylen,
salt=salt,
iterations=iterations,
backend=backend,
)
return kdf.derive(data)
@public
def derive_key(secret, salt, iterations=1000, keylen=32):
"""
Computes a derived cryptographic key from a password according to PBKDF2.
.. seealso:: http://en.wikipedia.org/wiki/PBKDF2
:param secret: The secret.
:type secret: bytes or unicode
:param salt: The salt to be used.
:type salt: bytes or unicode
:param iterations: Number of iterations of derivation algorithm to run.
:type iterations: int
:param keylen: Length of the key to derive in bytes.
:type keylen: int
:return: The derived key in Base64 encoding.
:rtype: bytes
"""
if not (type(secret) in [str, bytes]):
raise ValueError("'secret' must be bytes")
if not (type(salt) in [str, bytes]):
raise ValueError("'salt' must be bytes")
if not (type(iterations) == int):
raise ValueError("'iterations' must be an integer")
if not (type(keylen) == int):
raise ValueError("'keylen' must be an integer")
if type(secret) == str:
secret = secret.encode('utf8')
if type(salt) == str:
salt = salt.encode('utf8')
key = pbkdf2(secret, salt, iterations, keylen)
return binascii.b2a_base64(key).strip()
WCS_SECRET_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
"""
The characters from which :func:`autobahn.wamp.auth.generate_wcs` generates secrets.
"""
@public
def generate_wcs(length=14):
"""
Generates a new random secret for use with WAMP-CRA.
The secret generated is a random character sequence drawn from
- upper and lower case latin letters
- digits
-
:param length: The length of the secret to generate.
:type length: int
:return: The generated secret. The length of the generated is ``length`` octets.
:rtype: bytes
"""
assert(type(length) == int)
return "".join(random.choice(WCS_SECRET_CHARSET) for _ in range(length)).encode('ascii')
@public
def compute_wcs(key, challenge):
"""
Compute an WAMP-CRA authentication signature from an authentication
challenge and a (derived) key.
:param key: The key derived (via PBKDF2) from the secret.
:type key: bytes
:param challenge: The authentication challenge to sign.
:type challenge: bytes
:return: The authentication signature.
:rtype: bytes
"""
assert(type(key) in [str, bytes])
assert(type(challenge) in [str, bytes])
if type(key) == str:
key = key.encode('utf8')
if type(challenge) == str:
challenge = challenge.encode('utf8')
sig = hmac.new(key, challenge, hashlib.sha256).digest()
return binascii.b2a_base64(sig).strip()
def derive_scram_credential(email: str, password: str, salt: Optional[bytes] = None) -> Dict:
"""
Derive WAMP-SCRAM credentials from user email and password. The SCRAM parameters used
are the following (these are also contained in the returned credentials):
* kdf ``argon2id-13``
* time cost ``4096``
* memory cost ``512``
* parallelism ``1``
See `draft-irtf-cfrg-argon2 <https://datatracker.ietf.org/doc/draft-irtf-cfrg-argon2/>`__ and
`argon2-cffi <https://argon2-cffi.readthedocs.io/en/stable/>`__.
:param email: User email.
:param password: User password.
:param salt: Optional salt to use (must be 16 bytes long). If none is given, compute salt
from email as ``salt = SHA256(email)[:16]``.
:return: WAMP-SCRAM credentials. When serialized, the returned credentials can be copy-pasted
into the ``config.json`` node configuration for a Crossbar.io node.
"""
assert HAS_ARGON, 'missing dependency argon2'
from argon2.low_level import hash_secret
from argon2.low_level import Type
# derive salt from email
if not salt:
m = hashlib.sha256()
m.update(email.encode('utf8'))
salt = m.digest()[:16]
assert len(salt) == 16
hash_data = hash_secret(
secret=password.encode('utf8'),
salt=salt,
time_cost=4096,
memory_cost=512,
parallelism=1,
hash_len=32,
type=Type.ID,
version=19,
)
_, tag, v, params, _, salted_password = hash_data.decode('ascii').split('$')
assert tag == 'argon2id'
assert v == 'v=19' # argon's version 1.3 is represented as 0x13, which is 19 decimal...
params = {
k: v
for k, v in
[x.split('=') for x in params.split(',')]
}
salted_password = salted_password.encode('ascii')
client_key = hmac.new(salted_password, b"Client Key", hashlib.sha256).digest()
stored_key = hashlib.new('sha256', client_key).digest()
server_key = hmac.new(salted_password, b"Server Key", hashlib.sha256).digest()
credential = {
"kdf": "argon2id-13",
"memory": int(params['m']),
"iterations": int(params['t']),
"salt": binascii.b2a_hex(salt).decode('ascii'),
"stored-key": binascii.b2a_hex(stored_key).decode('ascii'),
"server-key": binascii.b2a_hex(server_key).decode('ascii'),
}
return credential

View File

@@ -0,0 +1,983 @@
###############################################################################
#
# 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 itertools
import random
from functools import partial
import txaio
from autobahn.util import ObservableMixin
from autobahn.websocket.util import parse_url as parse_ws_url
from autobahn.rawsocket.util import parse_url as parse_rs_url
from autobahn.wamp.types import ComponentConfig, SubscribeOptions, RegisterOptions
from autobahn.wamp.exception import SessionNotReady, ApplicationError
from autobahn.wamp.auth import create_authenticator, IAuthenticator
from autobahn.wamp.serializer import SERID_TO_SER
__all__ = (
'Component'
)
def _validate_endpoint(endpoint, check_native_endpoint=None):
"""
Check a WAMP connecting endpoint configuration.
"""
if check_native_endpoint:
check_native_endpoint(endpoint)
elif not isinstance(endpoint, dict):
raise ValueError(
"'endpoint' must be a dict"
)
# note, we're falling through here -- check_native_endpoint can
# disallow or allow dict-based config as it likes, but if it
# *does* allow a dict through, we want to check "base options"
# here so that both Twisted and asyncio don't have to check these
# things as well.
if isinstance(endpoint, dict):
# XXX what about filling in anything missing from the URL? Or
# is that only for when *nothing* is provided for endpoint?
if 'type' not in endpoint:
# could maybe just make tcp the default?
raise ValueError("'type' required in endpoint configuration")
if endpoint['type'] not in ['tcp', 'unix']:
raise ValueError('invalid type "{}" in endpoint'.format(endpoint['type']))
for k in endpoint.keys():
if k not in ['type', 'host', 'port', 'path', 'tls', 'timeout', 'version']:
raise ValueError(
"Invalid key '{}' in endpoint configuration".format(k)
)
if endpoint['type'] == 'tcp':
for k in ['host', 'port']:
if k not in endpoint:
raise ValueError(
"'{}' required in 'tcp' endpoint config".format(k)
)
for k in ['path']:
if k in endpoint:
raise ValueError(
"'{}' not valid in 'tcp' endpoint config".format(k)
)
elif endpoint['type'] == 'unix':
for k in ['path']:
if k not in endpoint:
raise ValueError(
"'{}' required for 'unix' endpoint config".format(k)
)
for k in ['host', 'port', 'tls']:
if k in endpoint:
raise ValueError(
"'{}' not valid in 'unix' endpoint config".format(k)
)
else:
assert False, 'should not arrive here'
def _create_transport(index, transport, check_native_endpoint=None):
"""
Internal helper to insert defaults and create _Transport instances.
:param transport: a (possibly valid) transport configuration
:type transport: dict
:returns: a _Transport instance
:raises: ValueError on invalid configuration
"""
if type(transport) != dict:
raise ValueError('invalid type {} for transport configuration - must be a dict'.format(type(transport)))
valid_transport_keys = [
'type', 'url', 'endpoint', 'serializer', 'serializers', 'options',
'max_retries', 'max_retry_delay', 'initial_retry_delay',
'retry_delay_growth', 'retry_delay_jitter', 'proxy', 'headers'
]
for k in transport.keys():
if k not in valid_transport_keys:
raise ValueError(
"'{}' is not a valid configuration item".format(k)
)
kind = 'websocket'
if 'type' in transport:
if transport['type'] not in ['websocket', 'rawsocket']:
raise ValueError('Invalid transport type {}'.format(transport['type']))
kind = transport['type']
else:
transport['type'] = 'websocket'
if 'proxy' in transport and kind != 'websocket':
raise ValueError(
"proxy= only supported for type=websocket transports"
)
proxy = transport.get("proxy", None)
if proxy is not None:
for k in proxy.keys():
if k not in ['host', 'port']:
raise ValueError(
"Unknown key '{}' in proxy config".format(k)
)
for k in ['host', 'port']:
if k not in proxy:
raise ValueError(
"Proxy config requires '{}'".formaT(k)
)
options = dict()
if 'options' in transport:
options = transport['options']
if not isinstance(options, dict):
raise ValueError(
'options must be a dict, not {}'.format(type(options))
)
headers = transport.get("headers")
if kind == 'websocket':
for key in ['url']:
if key not in transport:
raise ValueError("Transport requires '{}' key".format(key))
# endpoint not required; we will deduce from URL if it's not provided
# XXX not in the branch I rebased; can this go away? (is it redundant??)
if 'endpoint' not in transport:
is_secure, host, port, resource, path, params = parse_ws_url(transport['url'])
endpoint_config = {
'type': 'tcp',
'host': host,
'port': port,
'tls': is_secure,
}
else:
# note: we're avoiding mutating the incoming "configuration"
# dict, so this should avoid that too...
endpoint_config = transport['endpoint']
_validate_endpoint(endpoint_config, check_native_endpoint)
if 'serializer' in transport:
raise ValueError("'serializer' is only for rawsocket; use 'serializers'")
if 'serializers' in transport:
if not isinstance(transport['serializers'], (list, tuple)):
raise ValueError("'serializers' must be a list of strings")
if not all([
isinstance(s, (str, str))
for s in transport['serializers']]):
raise ValueError("'serializers' must be a list of strings")
valid_serializers = SERID_TO_SER.keys()
for serial in transport['serializers']:
if serial not in valid_serializers:
raise ValueError(
"Invalid serializer '{}' (expected one of: {})".format(
serial,
', '.join([repr(s) for s in valid_serializers]),
)
)
serializer_config = transport.get('serializers', ['cbor', 'json'])
elif kind == 'rawsocket':
if 'endpoint' not in transport:
if transport['url'].startswith('rs'):
# # try to parse RawSocket URL ..
isSecure, host, port = parse_rs_url(transport['url'])
elif transport['url'].startswith('ws'):
# try to parse WebSocket URL ..
isSecure, host, port, resource, path, params = parse_ws_url(transport['url'])
else:
raise RuntimeError()
if host == 'unix':
# here, "port" is actually holding the path on the host, eg "/tmp/file.sock"
endpoint_config = {
'type': 'unix',
'path': port,
}
else:
endpoint_config = {
'type': 'tcp',
'host': host,
'port': port,
}
else:
endpoint_config = transport['endpoint']
if 'serializers' in transport:
raise ValueError("'serializers' is only for websocket; use 'serializer'")
if headers is not None:
raise ValueError("'headers' not supported for rawsocket transport")
# always a list; len == 1 for rawsocket
if 'serializer' in transport:
if not isinstance(transport['serializer'], (str, str)):
raise ValueError("'serializer' must be a string")
serializer_config = [transport['serializer']]
else:
serializer_config = ['cbor']
else:
assert False, 'should not arrive here'
kw = {}
for key in ['max_retries', 'max_retry_delay', 'initial_retry_delay',
'retry_delay_growth', 'retry_delay_jitter']:
if key in transport:
kw[key] = transport[key]
return _Transport(
index,
kind=kind,
url=transport.get('url', None),
endpoint=endpoint_config,
serializers=serializer_config,
proxy=proxy,
options=options,
headers=headers,
**kw
)
class _Transport(object):
"""
Thin-wrapper for WAMP transports used by a Connection.
"""
def __init__(self, idx, kind, url, endpoint, serializers,
max_retries=-1,
max_retry_delay=300,
initial_retry_delay=1.5,
retry_delay_growth=1.5,
retry_delay_jitter=0.1,
proxy=None,
options=None,
headers=None):
"""
"""
if options is None:
options = dict()
self.idx = idx
self.type = kind
self.url = url
self.endpoint = endpoint
self.options = options
self.headers = headers
self.serializers = serializers
if self.type == 'rawsocket' and len(serializers) != 1:
raise ValueError(
"'rawsocket' transport requires exactly one serializer"
)
self.max_retries = max_retries
self.max_retry_delay = max_retry_delay
self.initial_retry_delay = initial_retry_delay
self.retry_delay_growth = retry_delay_growth
self.retry_delay_jitter = retry_delay_jitter
self.proxy = proxy # this is a dict of proxy config
# used via can_reconnect() and failed() to record this
# transport is never going to work
self._permanent_failure = False
self.reset()
def reset(self):
"""
set connection failure rates and retry-delay to initial values
"""
self.connect_attempts = 0
self.connect_sucesses = 0
self.connect_failures = 0
self.retry_delay = self.initial_retry_delay
def failed(self):
"""
Mark this transport as failed, meaning we won't try to connect to
it any longer (that is: can_reconnect() will always return
False afer calling this).
"""
self._permanent_failure = True
def can_reconnect(self):
if self._permanent_failure:
return False
if self.max_retries == -1:
return True
return self.connect_attempts < self.max_retries + 1
def next_delay(self):
if self.connect_attempts == 0:
# if we never tried before, try immediately
return 0
elif self.max_retries != -1 and self.connect_attempts >= self.max_retries + 1:
raise RuntimeError('max reconnects reached')
else:
self.retry_delay = self.retry_delay * self.retry_delay_growth
self.retry_delay = random.normalvariate(self.retry_delay, self.retry_delay * self.retry_delay_jitter)
if self.retry_delay > self.max_retry_delay:
self.retry_delay = self.max_retry_delay
return self.retry_delay
def describe_endpoint(self):
"""
returns a human-readable description of the endpoint
"""
if isinstance(self.endpoint, dict):
return self.endpoint['type']
return repr(self.endpoint)
# this could probably implement twisted.application.service.IService
# if we wanted; or via an adapter...which just adds a startService()
# and stopService() [latter can be async]
class Component(ObservableMixin):
"""
A WAMP application component. A component holds configuration for
(and knows how to create) transports and sessions.
"""
session_factory = None
"""
The factory of the session we will instantiate.
"""
def subscribe(self, topic, options=None, check_types=False):
"""
A decorator as a shortcut for subscribing during on-join
For example::
@component.subscribe(
"some.topic",
options=SubscribeOptions(match='prefix'),
)
def topic(*args, **kw):
print("some.topic({}, {}): event received".format(args, kw))
"""
assert options is None or isinstance(options, SubscribeOptions)
def decorator(fn):
def do_subscription(session, details):
return session.subscribe(fn, topic=topic, options=options, check_types=check_types)
self.on('join', do_subscription)
return fn
return decorator
def register(self, uri, options=None, check_types=False):
"""
A decorator as a shortcut for registering during on-join
For example::
@component.register(
"com.example.add",
options=RegisterOptions(invoke='roundrobin'),
)
def add(*args, **kw):
print("add({}, {}): event received".format(args, kw))
"""
assert options is None or isinstance(options, RegisterOptions)
def decorator(fn):
def do_registration(session, details):
return session.register(fn, procedure=uri, options=options, check_types=check_types)
self.on('join', do_registration)
return fn
return decorator
def __init__(self, main=None, transports=None, config=None, realm='realm1', extra=None,
authentication=None, session_factory=None, is_fatal=None):
"""
:param main: After a transport has been connected and a session
has been established and joined to a realm, this (async)
procedure will be run until it finishes -- which signals that
the component has run to completion. In this case, it usually
doesn't make sense to use the ``on_*`` kwargs. If you do not
pass a main() procedure, the session will not be closed
(unless you arrange for .leave() to be called).
:type main: callable taking two args ``reactor`` and ``ISession``
:param transports: Transport configurations for creating
transports. Each transport can be a WAMP URL, or a dict
containing the following configuration keys:
- ``type`` (optional): ``websocket`` (default) or ``rawsocket``
- ``url``: the router URL
- ``endpoint`` (optional, derived from URL if not provided):
- ``type``: "tcp" or "unix"
- ``host``, ``port``: only for TCP
- ``path``: only for unix
- ``timeout``: in seconds
- ``tls``: ``True`` or (under Twisted) an
``twisted.internet.ssl.IOpenSSLClientComponentCreator``
instance (such as returned from
``twisted.internet.ssl.optionsForClientTLS``) or
``CertificateOptions`` instance.
- ``max_retries``: Maximum number of reconnection attempts. Unlimited if set to -1.
- ``initial_retry_delay``: Initial delay for reconnection attempt in seconds (Default: 1.0s).
- ``max_retry_delay``: Maximum delay for reconnection attempts in seconds (Default: 60s).
- ``retry_delay_growth``: The growth factor applied to the retry delay between reconnection attempts (Default 1.5).
- ``retry_delay_jitter``: A 0-argument callable that introduces nose into the delay. (Default random.random)
- ``serializer`` (only for raw socket): Specify an accepted serializer (e.g. 'json', 'msgpack', 'cbor', 'ubjson', 'flatbuffers')
- ``serializers``: Specify list of accepted serializers
- ``options``: tbd
- ``proxy``: tbd
:type transports: None or str or list
:param realm: the realm to join
:type realm: str
:param authentication: configuration of authenticators
:type authentication: dict
:param session_factory: if None, ``ApplicationSession`` is
used, otherwise a callable taking a single ``config`` argument
that is used to create a new `ApplicationSession` instance.
:param is_fatal: a callable taking a single argument, an
``Exception`` instance. The callable should return ``True`` if
this error is "fatal", meaning we should not try connecting to
the current transport again. The default behavior (on None) is
to always return ``False``
"""
self.set_valid_events(
[
'start', # fired by base class
'connect', # fired by ApplicationSession
'join', # fired by ApplicationSession
'ready', # fired by ApplicationSession
'leave', # fired by ApplicationSession
'disconnect', # fired by ApplicationSession
'connectfailure', # fired by base class
]
)
if is_fatal is not None and not callable(is_fatal):
raise ValueError('"is_fatal" must be a callable or None')
self._is_fatal = is_fatal
if main is not None and not callable(main):
raise ValueError('"main" must be a callable if given')
self._entry = main
# use WAMP-over-WebSocket to localhost when no transport is specified at all
if transports is None:
transports = 'ws://127.0.0.1:8080/ws'
# allows to provide a URL instead of a list of transports
if isinstance(transports, (str, str)):
url = transports
# 'endpoint' will get filled in by parsing the 'url'
transport = {
'type': 'websocket',
'url': url,
}
transports = [transport]
# allows single transport instead of a list (convenience)
elif isinstance(transports, dict):
transports = [transports]
# XXX do we want to be able to provide an infinite iterable of
# transports here? e.g. a generator that makes new transport
# to try?
# now check and save list of transports
self._transports = []
for idx, transport in enumerate(transports):
# allows to provide a URL instead of transport dict
if type(transport) == str:
_transport = {
'type': 'websocket',
'url': transport,
}
else:
_transport = transport
self._transports.append(
_create_transport(idx, _transport, self._check_native_endpoint)
)
# XXX should have some checkconfig support
self._authentication = authentication or {}
if session_factory:
self.session_factory = session_factory
self._realm = realm
self._extra = extra
self._delay_f = None
self._done_f = None
self._session = None
self._stopping = False
def _can_reconnect(self):
# check if any of our transport has any reconnect attempt left
for transport in self._transports:
if transport.can_reconnect():
return True
return False
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/Deferred which will resolve (to ``None``) when we are
"done" or with an error if something went wrong.
"""
# we can only be "start()ed" once before we stop .. but that
# doesn't have to be an error we can give back another future
# that fires when our "real" _done_f is completed.
if self._done_f is not None:
d = txaio.create_future()
def _cb(arg):
txaio.resolve(d, arg)
txaio.add_callbacks(self._done_f, _cb, _cb)
return d
# this future will be returned, and thus has the semantics
# specified in the docstring.
self._done_f = txaio.create_future()
def _reset(arg):
"""
if the _done_f future is resolved (good or bad), we want to set it
to None in our class
"""
self._done_f = None
return arg
txaio.add_callbacks(self._done_f, _reset, _reset)
# Create a generator of transports that .can_reconnect()
transport_gen = itertools.cycle(self._transports)
# this is a 1-element list so we can set it from closures in
# this function
transport_candidate = [0]
def error(fail):
self._delay_f = None
if self._stopping:
# might be better to add framework-specific checks in
# subclasses to see if this is CancelledError (for
# Twisted) and whatever asyncio does .. but tracking
# if we're in the shutdown path is fine too
txaio.resolve(self._done_f, None)
else:
self.log.info("Internal error {msg}", msg=txaio.failure_message(fail))
self.log.debug("{tb}", tb=txaio.failure_format_traceback(fail))
txaio.reject(self._done_f, fail)
def attempt_connect(_):
self._delay_f = None
def handle_connect_error(fail):
# FIXME - make txaio friendly
# Can connect_f ever be in a cancelled state?
# if txaio.using_asyncio and isinstance(fail.value, asyncio.CancelledError):
# unrecoverable_error = True
self.log.debug('component failed: {error}', error=txaio.failure_message(fail))
self.log.debug('{tb}', tb=txaio.failure_format_traceback(fail))
# If this is a "fatal error" that will never work,
# we bail out now
if isinstance(fail.value, ApplicationError):
self.log.error("{msg}", msg=fail.value.error_message())
elif isinstance(fail.value, OSError):
# failed to connect entirely, like nobody
# listening etc.
self.log.info("Connection failed with OS error: {msg}", msg=txaio.failure_message(fail))
elif self._is_ssl_error(fail.value):
# Quoting pyOpenSSL docs: "Whenever
# [SSL.Error] is raised directly, it has a
# list of error messages from the OpenSSL
# error queue, where each item is a tuple
# (lib, function, reason). Here lib, function
# and reason are all strings, describing where
# and what the problem is. See err(3) for more
# information."
# (and 'args' is a 1-tuple containing the above
# 3-tuple...)
ssl_lib, ssl_func, ssl_reason = fail.value.args[0][0]
self.log.error("TLS failure: {reason}", reason=ssl_reason)
else:
self.log.error(
'Connection failed: {error}',
error=txaio.failure_message(fail),
)
if self._is_fatal is None:
is_fatal = False
else:
is_fatal = self._is_fatal(fail.value)
if is_fatal:
self.log.info("Error was fatal; failing transport")
transport_candidate[0].failed()
txaio.call_later(0, transport_check, None)
return
def notify_connect_error(fail):
chain_f = txaio.create_future()
# hmm, if connectfailure took a _Transport instead of
# (or in addition to?) self it could .failed() the
# transport and we could do away with the is_fatal
# listener?
handler_f = self.fire('connectfailure', self, fail.value)
txaio.add_callbacks(
handler_f,
lambda _: txaio.reject(chain_f, fail),
lambda _: txaio.reject(chain_f, fail)
)
return chain_f
def connect_error(fail):
notify_f = notify_connect_error(fail)
txaio.add_callbacks(notify_f, None, handle_connect_error)
def session_done(x):
txaio.resolve(self._done_f, None)
connect_f = txaio.as_future(
self._connect_once,
loop, transport_candidate[0],
)
txaio.add_callbacks(connect_f, session_done, connect_error)
def transport_check(_):
self.log.debug('Entering re-connect loop')
if not self._can_reconnect():
err_msg = "Component failed: Exhausted all transport connect attempts"
self.log.info(err_msg)
try:
raise RuntimeError(err_msg)
except RuntimeError as e:
txaio.reject(self._done_f, e)
return
while True:
transport = next(transport_gen)
if transport.can_reconnect():
transport_candidate[0] = transport
break
delay = transport.next_delay()
self.log.warn(
'trying transport {transport_idx} ("{transport_url}") using connect delay {transport_delay}',
transport_idx=transport.idx,
transport_url=transport.url,
transport_delay=delay,
)
self._delay_f = txaio.sleep(delay)
txaio.add_callbacks(self._delay_f, attempt_connect, error)
# issue our first event, then start reconnect loop
start_f = self.fire('start', loop, self)
txaio.add_callbacks(start_f, transport_check, error)
return self._done_f
def stop(self):
self._stopping = True
if self._session and self._session.is_attached():
return self._session.leave()
elif self._delay_f:
# This cancel request will actually call the "error" callback of
# the _delay_f future. Nothing to worry about.
return txaio.as_future(txaio.cancel, self._delay_f)
# if (for some reason -- should we log warning here to figure
# out if this can evern happen?) we've not fired _done_f, we
# do that now (causing our "main" to exit, and thus react() to
# quit)
if not txaio.is_called(self._done_f):
txaio.resolve(self._done_f, None)
return txaio.create_future_success(None)
def _connect_once(self, reactor, transport):
self.log.info(
'connecting once using transport type "{transport_type}" '
'over endpoint "{endpoint_desc}"',
transport_type=transport.type,
endpoint_desc=transport.describe_endpoint(),
)
done = txaio.create_future()
# factory for ISession objects
def create_session():
cfg = ComponentConfig(self._realm, self._extra)
try:
self._session = session = self.session_factory(cfg)
for auth_name, auth_config in self._authentication.items():
if isinstance(auth_config, IAuthenticator):
session.add_authenticator(auth_config)
else:
authenticator = create_authenticator(auth_name, **auth_config)
session.add_authenticator(authenticator)
except Exception as e:
# couldn't instantiate session calls, which is fatal.
# let the reconnection logic deal with that
f = txaio.create_failure(e)
txaio.reject(done, f)
raise
else:
# hook up the listener to the parent so we can bubble
# up events happning on the session onto the
# connection. This lets you do component.on('join',
# cb) which will work just as if you called
# session.on('join', cb) for every session created.
session._parent = self
# listen on leave events; if we get errors
# (e.g. no_such_realm), an on_leave can happen without
# an on_join before
def on_leave(session, details):
self.log.info(
"session leaving '{details.reason}'",
details=details,
)
if not txaio.is_called(done):
if details.reason in ["wamp.close.normal", "wamp.close.goodbye_and_out"]:
txaio.resolve(done, None)
else:
f = txaio.create_failure(
ApplicationError(details.reason, details.message)
)
txaio.reject(done, f)
session.on('leave', on_leave)
# if we were given a "main" procedure, we run through
# it completely (i.e. until its Deferred fires) and
# then disconnect this session
def on_join(session, details):
transport.reset()
transport.connect_sucesses += 1
self.log.debug("session on_join: {details}", details=details)
d = txaio.as_future(self._entry, reactor, session)
def main_success(_):
self.log.debug("main_success")
def leave():
try:
session.leave()
except SessionNotReady:
# someone may have already called
# leave()
pass
txaio.call_later(0, leave)
def main_error(err):
self.log.debug("main_error: {err}", err=err)
txaio.reject(done, err)
session.disconnect()
txaio.add_callbacks(d, main_success, main_error)
if self._entry is not None:
session.on('join', on_join)
# listen on disconnect events. Note that in case we
# had a "main" procedure, we could have already
# resolve()'d our "done" future
def on_disconnect(session, was_clean):
self.log.debug(
"session on_disconnect: was_clean={was_clean}",
was_clean=was_clean,
)
if not txaio.is_called(done):
if not was_clean:
self.log.warn(
"Session disconnected uncleanly"
)
else:
# eg the session has left the realm, and the transport was properly
# shut down. successfully finish the connection
txaio.resolve(done, None)
session.on('disconnect', on_disconnect)
# return the fresh session object
return session
transport.connect_attempts += 1
d = txaio.as_future(
self._connect_transport,
reactor, transport, create_session, done,
)
def on_error(err):
"""
this may seem redundant after looking at _connect_transport, but
it will handle a case where something goes wrong in
_connect_transport itself -- as the only connect our
caller has is the 'done' future
"""
transport.connect_failures += 1
# something bad has happened, and maybe didn't get caught
# upstream yet
if not txaio.is_called(done):
txaio.reject(done, err)
txaio.add_callbacks(d, None, on_error)
return done
def on_join(self, fn):
"""
A decorator as a shortcut for listening for 'join' events.
For example::
@component.on_join
def joined(session, details):
print("Session {} joined: {}".format(session, details))
"""
self.on('join', fn)
def on_leave(self, fn):
"""
A decorator as a shortcut for listening for 'leave' events.
"""
self.on('leave', fn)
def on_connect(self, fn):
"""
A decorator as a shortcut for listening for 'connect' events.
"""
self.on('connect', fn)
def on_disconnect(self, fn):
"""
A decorator as a shortcut for listening for 'disconnect' events.
"""
self.on('disconnect', fn)
def on_ready(self, fn):
"""
A decorator as a shortcut for listening for 'ready' events.
"""
self.on('ready', fn)
def on_connectfailure(self, fn):
"""
A decorator as a shortcut for listening for 'connectfailure' events.
"""
self.on('connectfailure', fn)
def _run(reactor, components, done_callback=None):
"""
Internal helper. Use "run" method from autobahn.twisted.wamp or
autobahn.asyncio.wamp
This is the generic parts of the run() method so that there's very
little code in the twisted/asyncio specific run() methods.
This is called by react() (or run_until_complete() so any errors
coming out of this should be handled properly. Logging will
already be started.
"""
# let user pass a single component to run, too
# XXX probably want IComponent? only demand it, here and below?
if isinstance(components, Component):
components = [components]
if type(components) != list:
raise ValueError(
'"components" must be a list of Component objects - encountered'
' {0}'.format(type(components))
)
for c in components:
if not isinstance(c, Component):
raise ValueError(
'"components" must be a list of Component objects - encountered'
'item of type {0}'.format(type(c))
)
# validation complete; proceed with startup
log = txaio.make_logger()
def component_success(comp, arg):
log.debug("Component '{c}' successfully completed: {arg}", c=comp, arg=arg)
return arg
def component_failure(comp, f):
log.error("Component '{c}' error: {msg}", c=comp, msg=txaio.failure_message(f))
log.debug("Component error: {tb}", tb=txaio.failure_format_traceback(f))
# double-check: is a component-failure still fatal to the
# startup process (because we passed consume_exception=False
# to gather() below?)
return None
def component_start(comp):
# the future from start() errbacks if we fail, or callbacks
# when the component is considered "done" (so maybe never)
d = txaio.as_future(comp.start, reactor)
txaio.add_callbacks(
d,
partial(component_success, comp),
partial(component_failure, comp),
)
return d
# note that these are started in parallel -- maybe we want to add
# a "connected" signal to components so we could start them in the
# order they're given to run() as "a" solution to dependencies.
dl = []
for comp in components:
d = component_start(comp)
dl.append(d)
done_d = txaio.gather(dl, consume_exceptions=False)
if done_callback:
def all_done(arg):
log.debug("All components ended; stopping reactor")
done_callback(reactor, arg)
txaio.add_callbacks(done_d, all_done, all_done)
return done_d

View File

@@ -0,0 +1,268 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from autobahn.util import public
from autobahn.wamp.interfaces import IPayloadCodec
from autobahn.wamp.types import EncodedPayload
from autobahn.wamp.serializer import _dumps as _json_dumps
from autobahn.wamp.serializer import _loads as _json_loads
__all__ = [
'HAS_CRYPTOBOX',
'EncodedPayload'
]
try:
# try to import everything we need for WAMP-cryptobox
from nacl.encoding import Base64Encoder, RawEncoder, HexEncoder
from nacl.public import PrivateKey, PublicKey, Box
from nacl.utils import random
from pytrie import StringTrie
except ImportError:
HAS_CRYPTOBOX = False
else:
HAS_CRYPTOBOX = True
__all__.extend(['Key', 'KeyRing'])
if HAS_CRYPTOBOX:
@public
class Key(object):
"""
Holds originator and responder keys for an URI.
The originator is either a caller or a publisher. The responder is either a callee or subscriber.
"""
def __init__(self, originator_priv=None, originator_pub=None, responder_priv=None, responder_pub=None):
# the originator private and public keys, as available
if originator_priv:
self.originator_priv = PrivateKey(originator_priv, encoder=Base64Encoder)
else:
self.originator_priv = None
if self.originator_priv:
self.originator_pub = self.originator_priv.public_key
assert(originator_pub is None or originator_pub == self.originator_pub)
else:
self.originator_pub = PublicKey(originator_pub, encoder=Base64Encoder)
# the responder private and public keys, as available
if responder_priv:
self.responder_priv = PrivateKey(responder_priv, encoder=Base64Encoder)
else:
self.responder_priv = None
if self.responder_priv:
self.responder_pub = self.responder_priv.public_key
assert(responder_pub is None or responder_pub == self.responder_pub)
else:
self.responder_pub = PublicKey(responder_pub, encoder=Base64Encoder)
# this crypto box is for originators (callers, publishers):
#
# 1. _encrypting_ WAMP messages outgoing from originators: CALL*, PUBLISH*
# 2. _decrypting_ WAMP messages incoming to originators: RESULT*, ERROR
#
if self.originator_priv and self.responder_pub:
self.originator_box = Box(self.originator_priv, self.responder_pub)
else:
self.originator_box = None
# this crypto box is for responders (callees, subscribers):
#
# 1. _decrypting_ WAMP messages incoming to responders: INVOCATION*, EVENT*
# 2. _encrypting_ WAMP messages outgoing from responders: YIELD*, ERROR
#
if self.responder_priv and self.originator_pub:
self.responder_box = Box(self.responder_priv, self.originator_pub)
else:
self.responder_box = None
if not (self.originator_box or self.responder_box):
raise Exception("insufficient keys provided for at least originator or responder role")
@public
class SymKey(object):
"""
Holds a symmetric key for an URI.
"""
def __init__(self, raw=None):
pass
@public
class KeyRing(object):
"""
A keyring holds (cryptobox) public-private key pairs for use with WAMP-cryptobox payload
encryption. The keyring can be set on a WAMP session and then transparently will get used
for encrypting and decrypting WAMP message payloads.
"""
@public
def __init__(self, default_key=None):
"""
Create a new key ring to hold public and private keys mapped from an URI space.
"""
assert(default_key is None or isinstance(default_key, Key) or type(default_key == str))
self._uri_to_key = StringTrie()
if type(default_key) == str:
default_key = Key(originator_priv=default_key, responder_priv=default_key)
self._default_key = default_key
@public
def generate_key(self):
"""
Generate a new private key and return a pair with the base64 encodings
of (priv_key, pub_key).
"""
key = PrivateKey.generate()
priv_key = key.encode(encoder=Base64Encoder)
pub_key = key.public_key.encode(encoder=Base64Encoder)
return priv_key.decode('ascii'), pub_key.decode('ascii')
@public
def generate_key_hex(self):
"""
Generate a new private key and return a pair with the hex encodings
of (priv_key, pub_key).
"""
key = PrivateKey.generate()
priv_key = key.encode(encoder=HexEncoder)
pub_key = key.public_key.encode(encoder=HexEncoder)
return priv_key.decode('ascii'), pub_key.decode('ascii')
@public
def set_key(self, uri, key):
"""
Add a key set for a given URI.
"""
assert(type(uri) == str)
assert(key is None or isinstance(key, Key) or type(key) == str)
if type(key) == str:
key = Key(originator_priv=key, responder_priv=key)
if uri == '':
self._default_key = key
else:
if key is None:
if uri in self._uri_to_key:
del self._uri_to_key[uri]
else:
self._uri_to_key[uri] = key
@public
def rotate_key(self, uri):
assert(type(uri) == str)
if uri in self._uri_to_key:
self._uri_to_key[uri].rotate()
else:
self._uri_to_key[uri].rotate()
def _get_box(self, is_originating, uri, match_exact=False):
try:
if match_exact:
key = self._uri_to_key[uri]
else:
key = self._uri_to_key.longest_prefix_value(uri)
except KeyError:
if self._default_key:
key = self._default_key
else:
return None
if is_originating:
return key.originator_box
else:
return key.responder_box
@public
def encode(self, is_originating, uri, args=None, kwargs=None):
"""
Encrypt the given WAMP URI, args and kwargs into an EncodedPayload instance, or None
if the URI should not be encrypted.
"""
assert(type(is_originating) == bool)
assert(type(uri) == str)
assert(args is None or type(args) in (list, tuple))
assert(kwargs is None or type(kwargs) == dict)
box = self._get_box(is_originating, uri)
if not box:
# if we didn't find a crypto box, then return None, which
# signals that the payload travel unencrypted (normal)
return None
payload = {
'uri': uri,
'args': args,
'kwargs': kwargs
}
nonce = random(Box.NONCE_SIZE)
payload_ser = _json_dumps(payload).encode('utf8')
payload_encr = box.encrypt(payload_ser, nonce, encoder=RawEncoder)
# above returns an instance of http://pynacl.readthedocs.io/en/latest/utils/#nacl.utils.EncryptedMessage
# which is a bytes _subclass_! hence we apply bytes() to get at the underlying plain
# bytes "scalar", which is the concatenation of `payload_encr.nonce + payload_encr.ciphertext`
payload_bytes = bytes(payload_encr)
payload_key = None
return EncodedPayload(payload_bytes, 'cryptobox', 'json', enc_key=payload_key)
@public
def decode(self, is_originating, uri, encoded_payload):
"""
Decrypt the given WAMP URI and EncodedPayload into a tuple ``(uri, args, kwargs)``.
"""
assert(type(uri) == str)
assert(isinstance(encoded_payload, EncodedPayload))
assert(encoded_payload.enc_algo == 'cryptobox')
box = self._get_box(is_originating, uri)
if not box:
raise Exception("received encrypted payload, but can't find key!")
payload_ser = box.decrypt(encoded_payload.payload, encoder=RawEncoder)
if encoded_payload.enc_serializer != 'json':
raise Exception("received encrypted payload, but don't know how to process serializer '{}'".format(encoded_payload.enc_serializer))
payload = _json_loads(payload_ser.decode('utf8'))
uri = payload.get('uri', None)
args = payload.get('args', None)
kwargs = payload.get('kwargs', None)
return uri, args, kwargs
# A WAMP-cryptobox keyring can work as a codec for
# payload transparency
IPayloadCodec.register(KeyRing)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,326 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from autobahn.util import public
from autobahn.wamp.uri import error
__all__ = (
'Error',
'SessionNotReady',
'SerializationError',
'InvalidUriError',
'ProtocolError',
'TransportLost',
'ApplicationError',
'NotAuthorized',
'InvalidUri',
'InvalidPayload',
'TypeCheckError',
)
@public
class Error(RuntimeError):
"""
Base class for all exceptions related to WAMP.
"""
@public
class SessionNotReady(Error):
"""
The application tried to perform a WAMP interaction, but the
session is not yet fully established.
"""
@public
class SerializationError(Error):
"""
Exception raised when the WAMP serializer could not serialize the
application payload (``args`` or ``kwargs`` for ``CALL``, ``PUBLISH``, etc).
"""
@public
class InvalidUriError(Error):
"""
Exception raised when an invalid WAMP URI was used.
"""
@public
class ProtocolError(Error):
"""
Exception raised when WAMP protocol was violated. Protocol errors
are fatal and are handled by the WAMP implementation. They are
not supposed to be handled at the application level.
"""
@public
class TransportLost(Error):
"""
Exception raised when the transport underlying the WAMP session
was lost or is not connected.
"""
@public
class ApplicationError(Error):
"""
Base class for all exceptions that can/may be handled
at the application level.
"""
INVALID_URI = "wamp.error.invalid_uri"
"""
Peer provided an incorrect URI for a URI-based attribute of a WAMP message
such as a realm, topic or procedure.
"""
INVALID_PAYLOAD = "wamp.error.invalid_payload"
"""
The application payload could not be serialized.
"""
PAYLOAD_SIZE_EXCEEDED = "wamp.error.payload_size_exceeded"
"""
The application payload could not be transported becuase the serialized/framed payload
exceeds the transport limits.
"""
NO_SUCH_PROCEDURE = "wamp.error.no_such_procedure"
"""
A Dealer could not perform a call, since not procedure is currently registered
under the given URI.
"""
PROCEDURE_ALREADY_EXISTS = "wamp.error.procedure_already_exists"
"""
A procedure could not be registered, since a procedure with the given URI is
already registered.
"""
PROCEDURE_EXISTS_INVOCATION_POLICY_CONFLICT = "wamp.error.procedure_exists_with_different_invocation_policy"
"""
A procedure could not be registered, since a procedure with the given URI is
already registered, and the registration has a conflicting invocation policy.
"""
NO_SUCH_REGISTRATION = "wamp.error.no_such_registration"
"""
A Dealer could not perform a unregister, since the given registration is not active.
"""
NO_SUCH_SUBSCRIPTION = "wamp.error.no_such_subscription"
"""
A Broker could not perform a unsubscribe, since the given subscription is not active.
"""
NO_SUCH_SESSION = "wamp.error.no_such_session"
"""
A router could not perform an operation, since a session ID specified was non-existant.
"""
INVALID_ARGUMENT = "wamp.error.invalid_argument"
"""
A call failed, since the given argument types or values are not acceptable to the
called procedure - in which case the *Callee* may throw this error. Or a Router
performing *payload validation* checked the payload (``args`` / ``kwargs``) of a call,
call result, call error or publish, and the payload did not conform.
"""
# FIXME: this currently isn't used neither in Autobahn nor Crossbar. Check!
SYSTEM_SHUTDOWN = "wamp.error.system_shutdown"
"""
The *Peer* is shutting down completely - used as a ``GOODBYE`` (or ``ABORT``) reason.
"""
# FIXME: this currently isn't used neither in Autobahn nor Crossbar. Check!
CLOSE_REALM = "wamp.error.close_realm"
"""
The *Peer* want to leave the realm - used as a ``GOODBYE`` reason.
"""
# FIXME: this currently isn't used neither in Autobahn nor Crossbar. Check!
GOODBYE_AND_OUT = "wamp.error.goodbye_and_out"
"""
A *Peer* acknowledges ending of a session - used as a ``GOOBYE`` reply reason.
"""
NOT_AUTHORIZED = "wamp.error.not_authorized"
"""
A call, register, publish or subscribe failed, since the session is not authorized
to perform the operation.
"""
AUTHORIZATION_FAILED = "wamp.error.authorization_failed"
"""
A Dealer or Broker could not determine if the *Peer* is authorized to perform
a join, call, register, publish or subscribe, since the authorization operation
*itself* failed. E.g. a custom authorizer did run into an error.
"""
AUTHENTICATION_FAILED = "wamp.error.authentication_failed"
"""
Something failed with the authentication itself, that is, authentication could
not run to end.
"""
NO_AUTH_METHOD = "wamp.error.no_auth_method"
"""
No authentication method the peer offered is available or active.
"""
NO_SUCH_REALM = "wamp.error.no_such_realm"
"""
Peer wanted to join a non-existing realm (and the *Router* did not allow to auto-create
the realm).
"""
NO_SUCH_ROLE = "wamp.error.no_such_role"
"""
A *Peer* was to be authenticated under a Role that does not (or no longer) exists on the Router.
For example, the *Peer* was successfully authenticated, but the Role configured does not
exists - hence there is some misconfiguration in the Router.
"""
NO_SUCH_PRINCIPAL = "wamp.error.no_such_principal"
"""
A *Peer* was authenticated for an authid that does not or longer exists.
"""
CANCELED = "wamp.error.canceled"
"""
A Dealer or Callee canceled a call previously issued (WAMP AP).
"""
TIMEOUT = "wamp.error.timeout"
"""
A pending (in-flight) call was timed out.
"""
# FIXME: this currently isn't used neither in Autobahn nor Crossbar. Check!
NO_ELIGIBLE_CALLEE = "wamp.error.no_eligible_callee"
"""
A *Dealer* could not perform a call, since a procedure with the given URI is registered,
but *Callee Black- and Whitelisting* and/or *Caller Exclusion* lead to the
exclusion of (any) *Callee* providing the procedure (WAMP AP).
"""
ENC_NO_PAYLOAD_CODEC = "wamp.error.no_payload_codec"
"""
WAMP message in payload transparency mode received, but no codec set
or codec did not decode the payload.
"""
ENC_TRUSTED_URI_MISMATCH = "wamp.error.encryption.trusted_uri_mismatch"
"""
WAMP-cryptobox application payload end-to-end encryption error.
"""
ENC_DECRYPT_ERROR = "wamp.error.encryption.decrypt_error"
"""
WAMP-cryptobox application payload end-to-end encryption error.
"""
TYPE_CHECK_ERROR = "wamp.error.type_check_error"
"""
WAMP procedure called with wrong argument types or subscription published
with wrong argument types.
"""
def __init__(self, error, *args, **kwargs):
"""
:param error: The URI of the error that occurred, e.g. ``wamp.error.not_authorized``.
:type error: str
"""
Exception.__init__(self, *args)
self.kwargs = kwargs
self.error = error
self.enc_algo = kwargs.pop('enc_algo', None)
self.callee = kwargs.pop('callee', None)
self.callee_authid = kwargs.pop('callee_authid', None)
self.callee_authrole = kwargs.pop('callee_authrole', None)
self.forward_for = kwargs.pop('forward_for', None)
@public
def error_message(self):
"""
Get the error message of this exception.
:returns: The error message.
:rtype: str
"""
return '{0}: {1}'.format(
self.error,
' '.join([str(a) for a in self.args]),
)
def __unicode__(self):
if self.kwargs and 'traceback' in self.kwargs:
tb = ':\n' + self.kwargs.pop('traceback') + '\n'
self.kwargs['traceback'] = '...'
else:
tb = ''
return "ApplicationError(error=<{0}>, args={1}, kwargs={2}, enc_algo={3}, callee={4}, callee_authid={5}, callee_authrole={6}, forward_for={7}){8}".format(
self.error, list(self.args), self.kwargs, self.enc_algo, self.callee, self.callee_authid, self.callee_authrole, self.forward_for, tb)
def __str__(self):
return self.__unicode__()
@error(ApplicationError.NOT_AUTHORIZED)
class NotAuthorized(Exception):
"""
Not authorized to perform the respective action.
"""
@error(ApplicationError.INVALID_URI)
class InvalidUri(Exception):
"""
The URI for a topic, procedure or error is not a valid WAMP URI.
"""
@error(ApplicationError.INVALID_PAYLOAD)
class InvalidPayload(Exception):
"""
The URI for a topic, procedure or error is not a valid WAMP URI.
"""
class TypeCheckError(ApplicationError):
"""
The URI for a topic published with invalid argument types or a
procedure called with invalid arguments types.
"""
def __init__(self, *args, **kwargs):
super().__init__(ApplicationError.TYPE_CHECK_ERROR, *args, **kwargs)

View File

@@ -0,0 +1,52 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: wamp
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class Map(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = Map()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsMap(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# Map
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# Map
def Key(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# Map
def Value(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
def MapStart(builder): builder.StartObject(2)
def Start(builder):
return MapStart(builder)
def MapAddKey(builder, key): builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(key), 0)
def AddKey(builder, key):
return MapAddKey(builder, key)
def MapAddValue(builder, value): builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(value), 0)
def AddValue(builder, value):
return MapAddValue(builder, value)
def MapEnd(builder): return builder.EndObject()
def End(builder):
return MapEnd(builder)

View File

@@ -0,0 +1,32 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: wamp
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class Void(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = Void()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsVoid(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# Void
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
def VoidStart(builder): builder.StartObject(0)
def Start(builder):
return VoidStart(builder)
def VoidEnd(builder): return builder.EndObject()
def End(builder):
return VoidEnd(builder)

View File

@@ -0,0 +1,62 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class Abort(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = Abort()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAbort(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# Abort
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# Abort
def Session(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Abort
def Reason(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# Abort
def Message(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
def AbortStart(builder): builder.StartObject(3)
def Start(builder):
return AbortStart(builder)
def AbortAddSession(builder, session): builder.PrependUint64Slot(0, session, 0)
def AddSession(builder, session):
return AbortAddSession(builder, session)
def AbortAddReason(builder, reason): builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(reason), 0)
def AddReason(builder, reason):
return AbortAddReason(builder, reason)
def AbortAddMessage(builder, message): builder.PrependUOffsetTRelativeSlot(2, flatbuffers.number_types.UOffsetTFlags.py_type(message), 0)
def AddMessage(builder, message):
return AbortAddMessage(builder, message)
def AbortEnd(builder): return builder.EndObject()
def End(builder):
return AbortEnd(builder)

View File

@@ -0,0 +1,31 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
class AnyMessage(object):
NONE = 0
Hello = 1
Welcome = 2
Abort = 3
Challenge = 4
Authenticate = 5
Goodbye = 6
Error = 7
Publish = 8
Published = 9
Subscribe = 10
Subscribed = 11
Unsubscribe = 12
Unsubscribed = 13
Event = 14
EventReceived = 15
Call = 16
Cancel = 17
Result = 18
Register = 19
Registered = 20
Unregister = 21
Unregistered = 22
Invocation = 23
Interrupt = 24
Yield = 25

View File

@@ -0,0 +1,72 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class AuthCraChallenge(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = AuthCraChallenge()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAuthCraChallenge(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# AuthCraChallenge
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# AuthCraChallenge
def Challenge(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# AuthCraChallenge
def Salt(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# AuthCraChallenge
def Iterations(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos)
return 1000
# AuthCraChallenge
def Keylen(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 32
def AuthCraChallengeStart(builder): builder.StartObject(4)
def Start(builder):
return AuthCraChallengeStart(builder)
def AuthCraChallengeAddChallenge(builder, challenge): builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(challenge), 0)
def AddChallenge(builder, challenge):
return AuthCraChallengeAddChallenge(builder, challenge)
def AuthCraChallengeAddSalt(builder, salt): builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(salt), 0)
def AddSalt(builder, salt):
return AuthCraChallengeAddSalt(builder, salt)
def AuthCraChallengeAddIterations(builder, iterations): builder.PrependUint32Slot(2, iterations, 1000)
def AddIterations(builder, iterations):
return AuthCraChallengeAddIterations(builder, iterations)
def AuthCraChallengeAddKeylen(builder, keylen): builder.PrependUint8Slot(3, keylen, 32)
def AddKeylen(builder, keylen):
return AuthCraChallengeAddKeylen(builder, keylen)
def AuthCraChallengeEnd(builder): return builder.EndObject()
def End(builder):
return AuthCraChallengeEnd(builder)

View File

@@ -0,0 +1,32 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class AuthCraRequest(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = AuthCraRequest()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAuthCraRequest(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# AuthCraRequest
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
def AuthCraRequestStart(builder): builder.StartObject(0)
def Start(builder):
return AuthCraRequestStart(builder)
def AuthCraRequestEnd(builder): return builder.EndObject()
def End(builder):
return AuthCraRequestEnd(builder)

View File

@@ -0,0 +1,32 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class AuthCraWelcome(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = AuthCraWelcome()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAuthCraWelcome(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# AuthCraWelcome
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
def AuthCraWelcomeStart(builder): builder.StartObject(0)
def Start(builder):
return AuthCraWelcomeStart(builder)
def AuthCraWelcomeEnd(builder): return builder.EndObject()
def End(builder):
return AuthCraWelcomeEnd(builder)

View File

@@ -0,0 +1,42 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class AuthCryptosignChallenge(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = AuthCryptosignChallenge()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAuthCryptosignChallenge(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# AuthCryptosignChallenge
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# AuthCryptosignChallenge
def ChannelBinding(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
def AuthCryptosignChallengeStart(builder): builder.StartObject(1)
def Start(builder):
return AuthCryptosignChallengeStart(builder)
def AuthCryptosignChallengeAddChannelBinding(builder, channelBinding): builder.PrependUint8Slot(0, channelBinding, 0)
def AddChannelBinding(builder, channelBinding):
return AuthCryptosignChallengeAddChannelBinding(builder, channelBinding)
def AuthCryptosignChallengeEnd(builder): return builder.EndObject()
def End(builder):
return AuthCryptosignChallengeEnd(builder)

View File

@@ -0,0 +1,52 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class AuthCryptosignRequest(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = AuthCryptosignRequest()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAuthCryptosignRequest(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# AuthCryptosignRequest
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# AuthCryptosignRequest
def Pubkey(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# AuthCryptosignRequest
def ChannelBinding(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
def AuthCryptosignRequestStart(builder): builder.StartObject(2)
def Start(builder):
return AuthCryptosignRequestStart(builder)
def AuthCryptosignRequestAddPubkey(builder, pubkey): builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(pubkey), 0)
def AddPubkey(builder, pubkey):
return AuthCryptosignRequestAddPubkey(builder, pubkey)
def AuthCryptosignRequestAddChannelBinding(builder, channelBinding): builder.PrependUint8Slot(1, channelBinding, 0)
def AddChannelBinding(builder, channelBinding):
return AuthCryptosignRequestAddChannelBinding(builder, channelBinding)
def AuthCryptosignRequestEnd(builder): return builder.EndObject()
def End(builder):
return AuthCryptosignRequestEnd(builder)

View File

@@ -0,0 +1,32 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class AuthCryptosignWelcome(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = AuthCryptosignWelcome()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAuthCryptosignWelcome(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# AuthCryptosignWelcome
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
def AuthCryptosignWelcomeStart(builder): builder.StartObject(0)
def Start(builder):
return AuthCryptosignWelcomeStart(builder)
def AuthCryptosignWelcomeEnd(builder): return builder.EndObject()
def End(builder):
return AuthCryptosignWelcomeEnd(builder)

View File

@@ -0,0 +1,10 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
class AuthFactor(object):
NONE = 0
AuthTicketRequest = 1
AuthCraRequest = 2
AuthScramRequest = 3
AuthCryptosignRequest = 4

View File

@@ -0,0 +1,12 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
class AuthMethod(object):
ANONYMOUS = 0
COOKIE = 1
TLS = 2
TICKET = 3
CRA = 4
SCRAM = 5
CRYPTOSIGN = 6

View File

@@ -0,0 +1,7 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
class AuthMode(object):
FIRST = 0
MULTIFACTOR = 1

View File

@@ -0,0 +1,92 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class AuthScramChallenge(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = AuthScramChallenge()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAuthScramChallenge(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# AuthScramChallenge
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# AuthScramChallenge
def Nonce(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# AuthScramChallenge
def Salt(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# AuthScramChallenge
def Kdf(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 2
# AuthScramChallenge
def Iterations(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos)
return 0
# AuthScramChallenge
def Memory(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos)
return 0
# AuthScramChallenge
def ChannelBinding(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
def AuthScramChallengeStart(builder): builder.StartObject(6)
def Start(builder):
return AuthScramChallengeStart(builder)
def AuthScramChallengeAddNonce(builder, nonce): builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(nonce), 0)
def AddNonce(builder, nonce):
return AuthScramChallengeAddNonce(builder, nonce)
def AuthScramChallengeAddSalt(builder, salt): builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(salt), 0)
def AddSalt(builder, salt):
return AuthScramChallengeAddSalt(builder, salt)
def AuthScramChallengeAddKdf(builder, kdf): builder.PrependUint8Slot(2, kdf, 2)
def AddKdf(builder, kdf):
return AuthScramChallengeAddKdf(builder, kdf)
def AuthScramChallengeAddIterations(builder, iterations): builder.PrependUint32Slot(3, iterations, 0)
def AddIterations(builder, iterations):
return AuthScramChallengeAddIterations(builder, iterations)
def AuthScramChallengeAddMemory(builder, memory): builder.PrependUint32Slot(4, memory, 0)
def AddMemory(builder, memory):
return AuthScramChallengeAddMemory(builder, memory)
def AuthScramChallengeAddChannelBinding(builder, channelBinding): builder.PrependUint8Slot(5, channelBinding, 0)
def AddChannelBinding(builder, channelBinding):
return AuthScramChallengeAddChannelBinding(builder, channelBinding)
def AuthScramChallengeEnd(builder): return builder.EndObject()
def End(builder):
return AuthScramChallengeEnd(builder)

View File

@@ -0,0 +1,52 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class AuthScramRequest(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = AuthScramRequest()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAuthScramRequest(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# AuthScramRequest
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# AuthScramRequest
def Nonce(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# AuthScramRequest
def ChannelBinding(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
def AuthScramRequestStart(builder): builder.StartObject(2)
def Start(builder):
return AuthScramRequestStart(builder)
def AuthScramRequestAddNonce(builder, nonce): builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(nonce), 0)
def AddNonce(builder, nonce):
return AuthScramRequestAddNonce(builder, nonce)
def AuthScramRequestAddChannelBinding(builder, channelBinding): builder.PrependUint8Slot(1, channelBinding, 0)
def AddChannelBinding(builder, channelBinding):
return AuthScramRequestAddChannelBinding(builder, channelBinding)
def AuthScramRequestEnd(builder): return builder.EndObject()
def End(builder):
return AuthScramRequestEnd(builder)

View File

@@ -0,0 +1,42 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class AuthScramWelcome(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = AuthScramWelcome()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAuthScramWelcome(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# AuthScramWelcome
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# AuthScramWelcome
def Verifier(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
def AuthScramWelcomeStart(builder): builder.StartObject(1)
def Start(builder):
return AuthScramWelcomeStart(builder)
def AuthScramWelcomeAddVerifier(builder, verifier): builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(verifier), 0)
def AddVerifier(builder, verifier):
return AuthScramWelcomeAddVerifier(builder, verifier)
def AuthScramWelcomeEnd(builder): return builder.EndObject()
def End(builder):
return AuthScramWelcomeEnd(builder)

View File

@@ -0,0 +1,32 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class AuthTicketChallenge(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = AuthTicketChallenge()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAuthTicketChallenge(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# AuthTicketChallenge
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
def AuthTicketChallengeStart(builder): builder.StartObject(0)
def Start(builder):
return AuthTicketChallengeStart(builder)
def AuthTicketChallengeEnd(builder): return builder.EndObject()
def End(builder):
return AuthTicketChallengeEnd(builder)

View File

@@ -0,0 +1,32 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class AuthTicketRequest(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = AuthTicketRequest()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAuthTicketRequest(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# AuthTicketRequest
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
def AuthTicketRequestStart(builder): builder.StartObject(0)
def Start(builder):
return AuthTicketRequestStart(builder)
def AuthTicketRequestEnd(builder): return builder.EndObject()
def End(builder):
return AuthTicketRequestEnd(builder)

View File

@@ -0,0 +1,32 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class AuthTicketWelcome(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = AuthTicketWelcome()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAuthTicketWelcome(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# AuthTicketWelcome
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
def AuthTicketWelcomeStart(builder): builder.StartObject(0)
def Start(builder):
return AuthTicketWelcomeStart(builder)
def AuthTicketWelcomeEnd(builder): return builder.EndObject()
def End(builder):
return AuthTicketWelcomeEnd(builder)

View File

@@ -0,0 +1,66 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class Authenticate(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = Authenticate()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsAuthenticate(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# Authenticate
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# Authenticate
def Session(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Authenticate
def Signature(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# Authenticate
def Extra(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
x = self._tab.Indirect(o + self._tab.Pos)
from wamp.Map import Map
obj = Map()
obj.Init(self._tab.Bytes, x)
return obj
return None
def AuthenticateStart(builder): builder.StartObject(3)
def Start(builder):
return AuthenticateStart(builder)
def AuthenticateAddSession(builder, session): builder.PrependUint64Slot(0, session, 0)
def AddSession(builder, session):
return AuthenticateAddSession(builder, session)
def AuthenticateAddSignature(builder, signature): builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(signature), 0)
def AddSignature(builder, signature):
return AuthenticateAddSignature(builder, signature)
def AuthenticateAddExtra(builder, extra): builder.PrependUOffsetTRelativeSlot(2, flatbuffers.number_types.UOffsetTFlags.py_type(extra), 0)
def AddExtra(builder, extra):
return AuthenticateAddExtra(builder, extra)
def AuthenticateEnd(builder): return builder.EndObject()
def End(builder):
return AuthenticateEnd(builder)

View File

@@ -0,0 +1,172 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class BrokerFeatures(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = BrokerFeatures()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsBrokerFeatures(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# BrokerFeatures
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# BrokerFeatures
def PublisherIdentification(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# BrokerFeatures
def PublisherExclusion(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# BrokerFeatures
def SubscriberBlackwhiteListing(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# BrokerFeatures
def PatternBasedSubscription(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# BrokerFeatures
def PublicationTrustlevels(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# BrokerFeatures
def SubscriptionRevocation(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# BrokerFeatures
def SessionMetaApi(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# BrokerFeatures
def SubscriptionMetaApi(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# BrokerFeatures
def EventRetention(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# BrokerFeatures
def EventHistory(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(22))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# BrokerFeatures
def AcknowledgeEventReceived(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(24))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# BrokerFeatures
def AcknowledgeSubscriberReceived(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(26))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# BrokerFeatures
def PayloadTransparency(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(28))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# BrokerFeatures
def PayloadEncryptionCryptobox(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(30))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
def BrokerFeaturesStart(builder): builder.StartObject(14)
def Start(builder):
return BrokerFeaturesStart(builder)
def BrokerFeaturesAddPublisherIdentification(builder, publisherIdentification): builder.PrependBoolSlot(0, publisherIdentification, 0)
def AddPublisherIdentification(builder, publisherIdentification):
return BrokerFeaturesAddPublisherIdentification(builder, publisherIdentification)
def BrokerFeaturesAddPublisherExclusion(builder, publisherExclusion): builder.PrependBoolSlot(1, publisherExclusion, 0)
def AddPublisherExclusion(builder, publisherExclusion):
return BrokerFeaturesAddPublisherExclusion(builder, publisherExclusion)
def BrokerFeaturesAddSubscriberBlackwhiteListing(builder, subscriberBlackwhiteListing): builder.PrependBoolSlot(2, subscriberBlackwhiteListing, 0)
def AddSubscriberBlackwhiteListing(builder, subscriberBlackwhiteListing):
return BrokerFeaturesAddSubscriberBlackwhiteListing(builder, subscriberBlackwhiteListing)
def BrokerFeaturesAddPatternBasedSubscription(builder, patternBasedSubscription): builder.PrependBoolSlot(3, patternBasedSubscription, 0)
def AddPatternBasedSubscription(builder, patternBasedSubscription):
return BrokerFeaturesAddPatternBasedSubscription(builder, patternBasedSubscription)
def BrokerFeaturesAddPublicationTrustlevels(builder, publicationTrustlevels): builder.PrependBoolSlot(4, publicationTrustlevels, 0)
def AddPublicationTrustlevels(builder, publicationTrustlevels):
return BrokerFeaturesAddPublicationTrustlevels(builder, publicationTrustlevels)
def BrokerFeaturesAddSubscriptionRevocation(builder, subscriptionRevocation): builder.PrependBoolSlot(5, subscriptionRevocation, 0)
def AddSubscriptionRevocation(builder, subscriptionRevocation):
return BrokerFeaturesAddSubscriptionRevocation(builder, subscriptionRevocation)
def BrokerFeaturesAddSessionMetaApi(builder, sessionMetaApi): builder.PrependBoolSlot(6, sessionMetaApi, 0)
def AddSessionMetaApi(builder, sessionMetaApi):
return BrokerFeaturesAddSessionMetaApi(builder, sessionMetaApi)
def BrokerFeaturesAddSubscriptionMetaApi(builder, subscriptionMetaApi): builder.PrependBoolSlot(7, subscriptionMetaApi, 0)
def AddSubscriptionMetaApi(builder, subscriptionMetaApi):
return BrokerFeaturesAddSubscriptionMetaApi(builder, subscriptionMetaApi)
def BrokerFeaturesAddEventRetention(builder, eventRetention): builder.PrependBoolSlot(8, eventRetention, 0)
def AddEventRetention(builder, eventRetention):
return BrokerFeaturesAddEventRetention(builder, eventRetention)
def BrokerFeaturesAddEventHistory(builder, eventHistory): builder.PrependBoolSlot(9, eventHistory, 0)
def AddEventHistory(builder, eventHistory):
return BrokerFeaturesAddEventHistory(builder, eventHistory)
def BrokerFeaturesAddAcknowledgeEventReceived(builder, acknowledgeEventReceived): builder.PrependBoolSlot(10, acknowledgeEventReceived, 0)
def AddAcknowledgeEventReceived(builder, acknowledgeEventReceived):
return BrokerFeaturesAddAcknowledgeEventReceived(builder, acknowledgeEventReceived)
def BrokerFeaturesAddAcknowledgeSubscriberReceived(builder, acknowledgeSubscriberReceived): builder.PrependBoolSlot(11, acknowledgeSubscriberReceived, 0)
def AddAcknowledgeSubscriberReceived(builder, acknowledgeSubscriberReceived):
return BrokerFeaturesAddAcknowledgeSubscriberReceived(builder, acknowledgeSubscriberReceived)
def BrokerFeaturesAddPayloadTransparency(builder, payloadTransparency): builder.PrependBoolSlot(12, payloadTransparency, 0)
def AddPayloadTransparency(builder, payloadTransparency):
return BrokerFeaturesAddPayloadTransparency(builder, payloadTransparency)
def BrokerFeaturesAddPayloadEncryptionCryptobox(builder, payloadEncryptionCryptobox): builder.PrependBoolSlot(13, payloadEncryptionCryptobox, 0)
def AddPayloadEncryptionCryptobox(builder, payloadEncryptionCryptobox):
return BrokerFeaturesAddPayloadEncryptionCryptobox(builder, payloadEncryptionCryptobox)
def BrokerFeaturesEnd(builder): return builder.EndObject()
def End(builder):
return BrokerFeaturesEnd(builder)

View File

@@ -0,0 +1,238 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class Call(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = Call()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsCall(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# Call
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# Call
def Session(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Call
def Request(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Call
def Procedure(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# Call
def Payload(self, j):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
a = self._tab.Vector(o)
return self._tab.Get(flatbuffers.number_types.Uint8Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 1))
return 0
# Call
def PayloadAsNumpy(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint8Flags, o)
return 0
# Call
def PayloadLength(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
return self._tab.VectorLen(o)
return 0
# Call
def PayloadIsNone(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
return o == 0
# Call
def EncAlgo(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
# Call
def EncSerializer(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
# Call
def EncKey(self, j):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16))
if o != 0:
a = self._tab.Vector(o)
return self._tab.Get(flatbuffers.number_types.Uint8Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 1))
return 0
# Call
def EncKeyAsNumpy(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16))
if o != 0:
return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint8Flags, o)
return 0
# Call
def EncKeyLength(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16))
if o != 0:
return self._tab.VectorLen(o)
return 0
# Call
def EncKeyIsNone(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16))
return o == 0
# Call
def Timeout(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos)
return 0
# Call
def ReceiveProgress(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# Call
def TransactionHash(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(22))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# Call
def Caller(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(24))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Call
def CallerAuthid(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(26))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# Call
def CallerAuthrole(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(28))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# Call
def ForwardFor(self, j):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(30))
if o != 0:
x = self._tab.Vector(o)
x += flatbuffers.number_types.UOffsetTFlags.py_type(j) * 8
from wamp.proto.Principal import Principal
obj = Principal()
obj.Init(self._tab.Bytes, x)
return obj
return None
# Call
def ForwardForLength(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(30))
if o != 0:
return self._tab.VectorLen(o)
return 0
# Call
def ForwardForIsNone(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(30))
return o == 0
def CallStart(builder): builder.StartObject(14)
def Start(builder):
return CallStart(builder)
def CallAddSession(builder, session): builder.PrependUint64Slot(0, session, 0)
def AddSession(builder, session):
return CallAddSession(builder, session)
def CallAddRequest(builder, request): builder.PrependUint64Slot(1, request, 0)
def AddRequest(builder, request):
return CallAddRequest(builder, request)
def CallAddProcedure(builder, procedure): builder.PrependUOffsetTRelativeSlot(2, flatbuffers.number_types.UOffsetTFlags.py_type(procedure), 0)
def AddProcedure(builder, procedure):
return CallAddProcedure(builder, procedure)
def CallAddPayload(builder, payload): builder.PrependUOffsetTRelativeSlot(3, flatbuffers.number_types.UOffsetTFlags.py_type(payload), 0)
def AddPayload(builder, payload):
return CallAddPayload(builder, payload)
def CallStartPayloadVector(builder, numElems): return builder.StartVector(1, numElems, 1)
def StartPayloadVector(builder, numElems):
return CallStartPayloadVector(builder, numElems)
def CallAddEncAlgo(builder, encAlgo): builder.PrependUint8Slot(4, encAlgo, 0)
def AddEncAlgo(builder, encAlgo):
return CallAddEncAlgo(builder, encAlgo)
def CallAddEncSerializer(builder, encSerializer): builder.PrependUint8Slot(5, encSerializer, 0)
def AddEncSerializer(builder, encSerializer):
return CallAddEncSerializer(builder, encSerializer)
def CallAddEncKey(builder, encKey): builder.PrependUOffsetTRelativeSlot(6, flatbuffers.number_types.UOffsetTFlags.py_type(encKey), 0)
def AddEncKey(builder, encKey):
return CallAddEncKey(builder, encKey)
def CallStartEncKeyVector(builder, numElems): return builder.StartVector(1, numElems, 1)
def StartEncKeyVector(builder, numElems):
return CallStartEncKeyVector(builder, numElems)
def CallAddTimeout(builder, timeout): builder.PrependUint32Slot(7, timeout, 0)
def AddTimeout(builder, timeout):
return CallAddTimeout(builder, timeout)
def CallAddReceiveProgress(builder, receiveProgress): builder.PrependBoolSlot(8, receiveProgress, 0)
def AddReceiveProgress(builder, receiveProgress):
return CallAddReceiveProgress(builder, receiveProgress)
def CallAddTransactionHash(builder, transactionHash): builder.PrependUOffsetTRelativeSlot(9, flatbuffers.number_types.UOffsetTFlags.py_type(transactionHash), 0)
def AddTransactionHash(builder, transactionHash):
return CallAddTransactionHash(builder, transactionHash)
def CallAddCaller(builder, caller): builder.PrependUint64Slot(10, caller, 0)
def AddCaller(builder, caller):
return CallAddCaller(builder, caller)
def CallAddCallerAuthid(builder, callerAuthid): builder.PrependUOffsetTRelativeSlot(11, flatbuffers.number_types.UOffsetTFlags.py_type(callerAuthid), 0)
def AddCallerAuthid(builder, callerAuthid):
return CallAddCallerAuthid(builder, callerAuthid)
def CallAddCallerAuthrole(builder, callerAuthrole): builder.PrependUOffsetTRelativeSlot(12, flatbuffers.number_types.UOffsetTFlags.py_type(callerAuthrole), 0)
def AddCallerAuthrole(builder, callerAuthrole):
return CallAddCallerAuthrole(builder, callerAuthrole)
def CallAddForwardFor(builder, forwardFor): builder.PrependUOffsetTRelativeSlot(13, flatbuffers.number_types.UOffsetTFlags.py_type(forwardFor), 0)
def AddForwardFor(builder, forwardFor):
return CallAddForwardFor(builder, forwardFor)
def CallStartForwardForVector(builder, numElems): return builder.StartVector(8, numElems, 8)
def StartForwardForVector(builder, numElems):
return CallStartForwardForVector(builder, numElems)
def CallEnd(builder): return builder.EndObject()
def End(builder):
return CallEnd(builder)

View File

@@ -0,0 +1,132 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class CalleeFeatures(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = CalleeFeatures()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsCalleeFeatures(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# CalleeFeatures
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# CalleeFeatures
def CallerIdentification(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CalleeFeatures
def CallTrustlevels(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CalleeFeatures
def CallTimeout(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CalleeFeatures
def CallCanceling(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CalleeFeatures
def ProgressiveCallResults(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CalleeFeatures
def RegistrationRevocation(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CalleeFeatures
def PatternBasedRegistration(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CalleeFeatures
def SharedRegistration(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CalleeFeatures
def PayloadTransparency(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CalleeFeatures
def PayloadEncryptionCryptobox(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(22))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
def CalleeFeaturesStart(builder): builder.StartObject(10)
def Start(builder):
return CalleeFeaturesStart(builder)
def CalleeFeaturesAddCallerIdentification(builder, callerIdentification): builder.PrependBoolSlot(0, callerIdentification, 0)
def AddCallerIdentification(builder, callerIdentification):
return CalleeFeaturesAddCallerIdentification(builder, callerIdentification)
def CalleeFeaturesAddCallTrustlevels(builder, callTrustlevels): builder.PrependBoolSlot(1, callTrustlevels, 0)
def AddCallTrustlevels(builder, callTrustlevels):
return CalleeFeaturesAddCallTrustlevels(builder, callTrustlevels)
def CalleeFeaturesAddCallTimeout(builder, callTimeout): builder.PrependBoolSlot(2, callTimeout, 0)
def AddCallTimeout(builder, callTimeout):
return CalleeFeaturesAddCallTimeout(builder, callTimeout)
def CalleeFeaturesAddCallCanceling(builder, callCanceling): builder.PrependBoolSlot(3, callCanceling, 0)
def AddCallCanceling(builder, callCanceling):
return CalleeFeaturesAddCallCanceling(builder, callCanceling)
def CalleeFeaturesAddProgressiveCallResults(builder, progressiveCallResults): builder.PrependBoolSlot(4, progressiveCallResults, 0)
def AddProgressiveCallResults(builder, progressiveCallResults):
return CalleeFeaturesAddProgressiveCallResults(builder, progressiveCallResults)
def CalleeFeaturesAddRegistrationRevocation(builder, registrationRevocation): builder.PrependBoolSlot(5, registrationRevocation, 0)
def AddRegistrationRevocation(builder, registrationRevocation):
return CalleeFeaturesAddRegistrationRevocation(builder, registrationRevocation)
def CalleeFeaturesAddPatternBasedRegistration(builder, patternBasedRegistration): builder.PrependBoolSlot(6, patternBasedRegistration, 0)
def AddPatternBasedRegistration(builder, patternBasedRegistration):
return CalleeFeaturesAddPatternBasedRegistration(builder, patternBasedRegistration)
def CalleeFeaturesAddSharedRegistration(builder, sharedRegistration): builder.PrependBoolSlot(7, sharedRegistration, 0)
def AddSharedRegistration(builder, sharedRegistration):
return CalleeFeaturesAddSharedRegistration(builder, sharedRegistration)
def CalleeFeaturesAddPayloadTransparency(builder, payloadTransparency): builder.PrependBoolSlot(8, payloadTransparency, 0)
def AddPayloadTransparency(builder, payloadTransparency):
return CalleeFeaturesAddPayloadTransparency(builder, payloadTransparency)
def CalleeFeaturesAddPayloadEncryptionCryptobox(builder, payloadEncryptionCryptobox): builder.PrependBoolSlot(9, payloadEncryptionCryptobox, 0)
def AddPayloadEncryptionCryptobox(builder, payloadEncryptionCryptobox):
return CalleeFeaturesAddPayloadEncryptionCryptobox(builder, payloadEncryptionCryptobox)
def CalleeFeaturesEnd(builder): return builder.EndObject()
def End(builder):
return CalleeFeaturesEnd(builder)

View File

@@ -0,0 +1,92 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class CallerFeatures(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = CallerFeatures()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsCallerFeatures(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# CallerFeatures
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# CallerFeatures
def CallerIdentification(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CallerFeatures
def CallTimeout(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CallerFeatures
def CallCanceling(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CallerFeatures
def ProgressiveCallResults(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CallerFeatures
def PayloadTransparency(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# CallerFeatures
def PayloadEncryptionCryptobox(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
def CallerFeaturesStart(builder): builder.StartObject(6)
def Start(builder):
return CallerFeaturesStart(builder)
def CallerFeaturesAddCallerIdentification(builder, callerIdentification): builder.PrependBoolSlot(0, callerIdentification, 0)
def AddCallerIdentification(builder, callerIdentification):
return CallerFeaturesAddCallerIdentification(builder, callerIdentification)
def CallerFeaturesAddCallTimeout(builder, callTimeout): builder.PrependBoolSlot(1, callTimeout, 0)
def AddCallTimeout(builder, callTimeout):
return CallerFeaturesAddCallTimeout(builder, callTimeout)
def CallerFeaturesAddCallCanceling(builder, callCanceling): builder.PrependBoolSlot(2, callCanceling, 0)
def AddCallCanceling(builder, callCanceling):
return CallerFeaturesAddCallCanceling(builder, callCanceling)
def CallerFeaturesAddProgressiveCallResults(builder, progressiveCallResults): builder.PrependBoolSlot(3, progressiveCallResults, 0)
def AddProgressiveCallResults(builder, progressiveCallResults):
return CallerFeaturesAddProgressiveCallResults(builder, progressiveCallResults)
def CallerFeaturesAddPayloadTransparency(builder, payloadTransparency): builder.PrependBoolSlot(4, payloadTransparency, 0)
def AddPayloadTransparency(builder, payloadTransparency):
return CallerFeaturesAddPayloadTransparency(builder, payloadTransparency)
def CallerFeaturesAddPayloadEncryptionCryptobox(builder, payloadEncryptionCryptobox): builder.PrependBoolSlot(5, payloadEncryptionCryptobox, 0)
def AddPayloadEncryptionCryptobox(builder, payloadEncryptionCryptobox):
return CallerFeaturesAddPayloadEncryptionCryptobox(builder, payloadEncryptionCryptobox)
def CallerFeaturesEnd(builder): return builder.EndObject()
def End(builder):
return CallerFeaturesEnd(builder)

View File

@@ -0,0 +1,92 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class Cancel(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = Cancel()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsCancel(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# Cancel
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# Cancel
def Session(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Cancel
def Request(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Cancel
def Mode(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
# Cancel
def ForwardFor(self, j):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
x = self._tab.Vector(o)
x += flatbuffers.number_types.UOffsetTFlags.py_type(j) * 8
from wamp.proto.Principal import Principal
obj = Principal()
obj.Init(self._tab.Bytes, x)
return obj
return None
# Cancel
def ForwardForLength(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
return self._tab.VectorLen(o)
return 0
# Cancel
def ForwardForIsNone(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
return o == 0
def CancelStart(builder): builder.StartObject(4)
def Start(builder):
return CancelStart(builder)
def CancelAddSession(builder, session): builder.PrependUint64Slot(0, session, 0)
def AddSession(builder, session):
return CancelAddSession(builder, session)
def CancelAddRequest(builder, request): builder.PrependUint64Slot(1, request, 0)
def AddRequest(builder, request):
return CancelAddRequest(builder, request)
def CancelAddMode(builder, mode): builder.PrependUint8Slot(2, mode, 0)
def AddMode(builder, mode):
return CancelAddMode(builder, mode)
def CancelAddForwardFor(builder, forwardFor): builder.PrependUOffsetTRelativeSlot(3, flatbuffers.number_types.UOffsetTFlags.py_type(forwardFor), 0)
def AddForwardFor(builder, forwardFor):
return CancelAddForwardFor(builder, forwardFor)
def CancelStartForwardForVector(builder, numElems): return builder.StartVector(8, numElems, 8)
def StartForwardForVector(builder, numElems):
return CancelStartForwardForVector(builder, numElems)
def CancelEnd(builder): return builder.EndObject()
def End(builder):
return CancelEnd(builder)

View File

@@ -0,0 +1,8 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
class CancelMode(object):
SKIP = 0
ABORT = 1
KILL = 2

View File

@@ -0,0 +1,66 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class Challenge(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = Challenge()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsChallenge(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# Challenge
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# Challenge
def Session(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Challenge
def Method(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
# Challenge
def Extra(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
x = self._tab.Indirect(o + self._tab.Pos)
from wamp.Map import Map
obj = Map()
obj.Init(self._tab.Bytes, x)
return obj
return None
def ChallengeStart(builder): builder.StartObject(3)
def Start(builder):
return ChallengeStart(builder)
def ChallengeAddSession(builder, session): builder.PrependUint64Slot(0, session, 0)
def AddSession(builder, session):
return ChallengeAddSession(builder, session)
def ChallengeAddMethod(builder, method): builder.PrependUint8Slot(1, method, 0)
def AddMethod(builder, method):
return ChallengeAddMethod(builder, method)
def ChallengeAddExtra(builder, extra): builder.PrependUOffsetTRelativeSlot(2, flatbuffers.number_types.UOffsetTFlags.py_type(extra), 0)
def AddExtra(builder, extra):
return ChallengeAddExtra(builder, extra)
def ChallengeEnd(builder): return builder.EndObject()
def End(builder):
return ChallengeEnd(builder)

View File

@@ -0,0 +1,7 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
class ChannelBinding(object):
NONE = 0
TLS_UNIQUE = 1

View File

@@ -0,0 +1,88 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class ClientRoles(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = ClientRoles()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsClientRoles(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# ClientRoles
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# ClientRoles
def Publisher(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
x = self._tab.Indirect(o + self._tab.Pos)
from wamp.proto.PublisherFeatures import PublisherFeatures
obj = PublisherFeatures()
obj.Init(self._tab.Bytes, x)
return obj
return None
# ClientRoles
def Subscriber(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
x = self._tab.Indirect(o + self._tab.Pos)
from wamp.proto.SubscriberFeatures import SubscriberFeatures
obj = SubscriberFeatures()
obj.Init(self._tab.Bytes, x)
return obj
return None
# ClientRoles
def Caller(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
x = self._tab.Indirect(o + self._tab.Pos)
from wamp.proto.CallerFeatures import CallerFeatures
obj = CallerFeatures()
obj.Init(self._tab.Bytes, x)
return obj
return None
# ClientRoles
def Callee(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
x = self._tab.Indirect(o + self._tab.Pos)
from wamp.proto.CalleeFeatures import CalleeFeatures
obj = CalleeFeatures()
obj.Init(self._tab.Bytes, x)
return obj
return None
def ClientRolesStart(builder): builder.StartObject(4)
def Start(builder):
return ClientRolesStart(builder)
def ClientRolesAddPublisher(builder, publisher): builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(publisher), 0)
def AddPublisher(builder, publisher):
return ClientRolesAddPublisher(builder, publisher)
def ClientRolesAddSubscriber(builder, subscriber): builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(subscriber), 0)
def AddSubscriber(builder, subscriber):
return ClientRolesAddSubscriber(builder, subscriber)
def ClientRolesAddCaller(builder, caller): builder.PrependUOffsetTRelativeSlot(2, flatbuffers.number_types.UOffsetTFlags.py_type(caller), 0)
def AddCaller(builder, caller):
return ClientRolesAddCaller(builder, caller)
def ClientRolesAddCallee(builder, callee): builder.PrependUOffsetTRelativeSlot(3, flatbuffers.number_types.UOffsetTFlags.py_type(callee), 0)
def AddCallee(builder, callee):
return ClientRolesAddCallee(builder, callee)
def ClientRolesEnd(builder): return builder.EndObject()
def End(builder):
return ClientRolesEnd(builder)

View File

@@ -0,0 +1,162 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class DealerFeatures(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = DealerFeatures()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsDealerFeatures(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# DealerFeatures
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# DealerFeatures
def CallerIdentification(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# DealerFeatures
def CallTrustlevels(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# DealerFeatures
def CallTimeout(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# DealerFeatures
def CallCanceling(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# DealerFeatures
def ProgressiveCallResults(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# DealerFeatures
def RegistrationRevocation(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# DealerFeatures
def PatternBasedRegistration(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# DealerFeatures
def SharedRegistration(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# DealerFeatures
def SessionMetaApi(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# DealerFeatures
def RegistrationMetaApi(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(22))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# DealerFeatures
def TestamentMetaApi(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(24))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# DealerFeatures
def PayloadTransparency(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(26))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# DealerFeatures
def PayloadEncryptionCryptobox(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(28))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
def DealerFeaturesStart(builder): builder.StartObject(13)
def Start(builder):
return DealerFeaturesStart(builder)
def DealerFeaturesAddCallerIdentification(builder, callerIdentification): builder.PrependBoolSlot(0, callerIdentification, 0)
def AddCallerIdentification(builder, callerIdentification):
return DealerFeaturesAddCallerIdentification(builder, callerIdentification)
def DealerFeaturesAddCallTrustlevels(builder, callTrustlevels): builder.PrependBoolSlot(1, callTrustlevels, 0)
def AddCallTrustlevels(builder, callTrustlevels):
return DealerFeaturesAddCallTrustlevels(builder, callTrustlevels)
def DealerFeaturesAddCallTimeout(builder, callTimeout): builder.PrependBoolSlot(2, callTimeout, 0)
def AddCallTimeout(builder, callTimeout):
return DealerFeaturesAddCallTimeout(builder, callTimeout)
def DealerFeaturesAddCallCanceling(builder, callCanceling): builder.PrependBoolSlot(3, callCanceling, 0)
def AddCallCanceling(builder, callCanceling):
return DealerFeaturesAddCallCanceling(builder, callCanceling)
def DealerFeaturesAddProgressiveCallResults(builder, progressiveCallResults): builder.PrependBoolSlot(4, progressiveCallResults, 0)
def AddProgressiveCallResults(builder, progressiveCallResults):
return DealerFeaturesAddProgressiveCallResults(builder, progressiveCallResults)
def DealerFeaturesAddRegistrationRevocation(builder, registrationRevocation): builder.PrependBoolSlot(5, registrationRevocation, 0)
def AddRegistrationRevocation(builder, registrationRevocation):
return DealerFeaturesAddRegistrationRevocation(builder, registrationRevocation)
def DealerFeaturesAddPatternBasedRegistration(builder, patternBasedRegistration): builder.PrependBoolSlot(6, patternBasedRegistration, 0)
def AddPatternBasedRegistration(builder, patternBasedRegistration):
return DealerFeaturesAddPatternBasedRegistration(builder, patternBasedRegistration)
def DealerFeaturesAddSharedRegistration(builder, sharedRegistration): builder.PrependBoolSlot(7, sharedRegistration, 0)
def AddSharedRegistration(builder, sharedRegistration):
return DealerFeaturesAddSharedRegistration(builder, sharedRegistration)
def DealerFeaturesAddSessionMetaApi(builder, sessionMetaApi): builder.PrependBoolSlot(8, sessionMetaApi, 0)
def AddSessionMetaApi(builder, sessionMetaApi):
return DealerFeaturesAddSessionMetaApi(builder, sessionMetaApi)
def DealerFeaturesAddRegistrationMetaApi(builder, registrationMetaApi): builder.PrependBoolSlot(9, registrationMetaApi, 0)
def AddRegistrationMetaApi(builder, registrationMetaApi):
return DealerFeaturesAddRegistrationMetaApi(builder, registrationMetaApi)
def DealerFeaturesAddTestamentMetaApi(builder, testamentMetaApi): builder.PrependBoolSlot(10, testamentMetaApi, 0)
def AddTestamentMetaApi(builder, testamentMetaApi):
return DealerFeaturesAddTestamentMetaApi(builder, testamentMetaApi)
def DealerFeaturesAddPayloadTransparency(builder, payloadTransparency): builder.PrependBoolSlot(11, payloadTransparency, 0)
def AddPayloadTransparency(builder, payloadTransparency):
return DealerFeaturesAddPayloadTransparency(builder, payloadTransparency)
def DealerFeaturesAddPayloadEncryptionCryptobox(builder, payloadEncryptionCryptobox): builder.PrependBoolSlot(12, payloadEncryptionCryptobox, 0)
def AddPayloadEncryptionCryptobox(builder, payloadEncryptionCryptobox):
return DealerFeaturesAddPayloadEncryptionCryptobox(builder, payloadEncryptionCryptobox)
def DealerFeaturesEnd(builder): return builder.EndObject()
def End(builder):
return DealerFeaturesEnd(builder)

View File

@@ -0,0 +1,158 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class Error(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = Error()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsError(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# Error
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# Error
def Session(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Error
def RequestType(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint16Flags, o + self._tab.Pos)
return 0
# Error
def Request(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Error
def Error(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# Error
def Payload(self, j):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
if o != 0:
a = self._tab.Vector(o)
return self._tab.Get(flatbuffers.number_types.Uint8Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 1))
return 0
# Error
def PayloadAsNumpy(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
if o != 0:
return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint8Flags, o)
return 0
# Error
def PayloadLength(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
if o != 0:
return self._tab.VectorLen(o)
return 0
# Error
def PayloadIsNone(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
return o == 0
# Error
def EncAlgo(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
# Error
def EncSerializer(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
# Error
def EncKey(self, j):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18))
if o != 0:
a = self._tab.Vector(o)
return self._tab.Get(flatbuffers.number_types.Uint8Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 1))
return 0
# Error
def EncKeyAsNumpy(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18))
if o != 0:
return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint8Flags, o)
return 0
# Error
def EncKeyLength(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18))
if o != 0:
return self._tab.VectorLen(o)
return 0
# Error
def EncKeyIsNone(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18))
return o == 0
def ErrorStart(builder): builder.StartObject(8)
def Start(builder):
return ErrorStart(builder)
def ErrorAddSession(builder, session): builder.PrependUint64Slot(0, session, 0)
def AddSession(builder, session):
return ErrorAddSession(builder, session)
def ErrorAddRequestType(builder, requestType): builder.PrependUint16Slot(1, requestType, 0)
def AddRequestType(builder, requestType):
return ErrorAddRequestType(builder, requestType)
def ErrorAddRequest(builder, request): builder.PrependUint64Slot(2, request, 0)
def AddRequest(builder, request):
return ErrorAddRequest(builder, request)
def ErrorAddError(builder, error): builder.PrependUOffsetTRelativeSlot(3, flatbuffers.number_types.UOffsetTFlags.py_type(error), 0)
def AddError(builder, error):
return ErrorAddError(builder, error)
def ErrorAddPayload(builder, payload): builder.PrependUOffsetTRelativeSlot(4, flatbuffers.number_types.UOffsetTFlags.py_type(payload), 0)
def AddPayload(builder, payload):
return ErrorAddPayload(builder, payload)
def ErrorStartPayloadVector(builder, numElems): return builder.StartVector(1, numElems, 1)
def StartPayloadVector(builder, numElems):
return ErrorStartPayloadVector(builder, numElems)
def ErrorAddEncAlgo(builder, encAlgo): builder.PrependUint8Slot(5, encAlgo, 0)
def AddEncAlgo(builder, encAlgo):
return ErrorAddEncAlgo(builder, encAlgo)
def ErrorAddEncSerializer(builder, encSerializer): builder.PrependUint8Slot(6, encSerializer, 0)
def AddEncSerializer(builder, encSerializer):
return ErrorAddEncSerializer(builder, encSerializer)
def ErrorAddEncKey(builder, encKey): builder.PrependUOffsetTRelativeSlot(7, flatbuffers.number_types.UOffsetTFlags.py_type(encKey), 0)
def AddEncKey(builder, encKey):
return ErrorAddEncKey(builder, encKey)
def ErrorStartEncKeyVector(builder, numElems): return builder.StartVector(1, numElems, 1)
def StartEncKeyVector(builder, numElems):
return ErrorStartEncKeyVector(builder, numElems)
def ErrorEnd(builder): return builder.EndObject()
def End(builder):
return ErrorEnd(builder)

View File

@@ -0,0 +1,317 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class Event(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = Event()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsEvent(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# Event
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# Event
def Session(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Event
def Subscription(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Event
def Publication(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Positional values for application-defined event payload.
# Event
def Args(self, j):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
a = self._tab.Vector(o)
return self._tab.Get(flatbuffers.number_types.Uint8Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 1))
return 0
# Event
def ArgsAsNumpy(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint8Flags, o)
return 0
# Event
def ArgsLength(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
return self._tab.VectorLen(o)
return 0
# Event
def ArgsIsNone(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
return o == 0
# Keyword values for application-defined event payload.
# Event
def Kwargs(self, j):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
if o != 0:
a = self._tab.Vector(o)
return self._tab.Get(flatbuffers.number_types.Uint8Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 1))
return 0
# Event
def KwargsAsNumpy(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
if o != 0:
return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint8Flags, o)
return 0
# Event
def KwargsLength(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
if o != 0:
return self._tab.VectorLen(o)
return 0
# Event
def KwargsIsNone(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
return o == 0
# Alternative, transparent payload. If given, ``args`` and ``kwargs`` must be left unset.
# Event
def Payload(self, j):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
if o != 0:
a = self._tab.Vector(o)
return self._tab.Get(flatbuffers.number_types.Uint8Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 1))
return 0
# Event
def PayloadAsNumpy(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
if o != 0:
return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint8Flags, o)
return 0
# Event
def PayloadLength(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
if o != 0:
return self._tab.VectorLen(o)
return 0
# Event
def PayloadIsNone(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
return o == 0
# Event
def EncAlgo(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
# Event
def EncSerializer(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
# Event
def EncKey(self, j):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20))
if o != 0:
a = self._tab.Vector(o)
return self._tab.Get(flatbuffers.number_types.Uint8Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 1))
return 0
# Event
def EncKeyAsNumpy(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20))
if o != 0:
return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint8Flags, o)
return 0
# Event
def EncKeyLength(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20))
if o != 0:
return self._tab.VectorLen(o)
return 0
# Event
def EncKeyIsNone(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20))
return o == 0
# Event
def Publisher(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(22))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# Event
def PublisherAuthid(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(24))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# Event
def PublisherAuthrole(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(26))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# Event
def Topic(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(28))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# Event
def Retained(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(30))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# Event
def TransactionHash(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(32))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None
# Event
def Acknowledge(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(34))
if o != 0:
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
return False
# Event
def ForwardFor(self, j):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(36))
if o != 0:
x = self._tab.Vector(o)
x += flatbuffers.number_types.UOffsetTFlags.py_type(j) * 8
from wamp.proto.Principal import Principal
obj = Principal()
obj.Init(self._tab.Bytes, x)
return obj
return None
# Event
def ForwardForLength(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(36))
if o != 0:
return self._tab.VectorLen(o)
return 0
# Event
def ForwardForIsNone(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(36))
return o == 0
def EventStart(builder): builder.StartObject(17)
def Start(builder):
return EventStart(builder)
def EventAddSession(builder, session): builder.PrependUint64Slot(0, session, 0)
def AddSession(builder, session):
return EventAddSession(builder, session)
def EventAddSubscription(builder, subscription): builder.PrependUint64Slot(1, subscription, 0)
def AddSubscription(builder, subscription):
return EventAddSubscription(builder, subscription)
def EventAddPublication(builder, publication): builder.PrependUint64Slot(2, publication, 0)
def AddPublication(builder, publication):
return EventAddPublication(builder, publication)
def EventAddArgs(builder, args): builder.PrependUOffsetTRelativeSlot(3, flatbuffers.number_types.UOffsetTFlags.py_type(args), 0)
def AddArgs(builder, args):
return EventAddArgs(builder, args)
def EventStartArgsVector(builder, numElems): return builder.StartVector(1, numElems, 1)
def StartArgsVector(builder, numElems):
return EventStartArgsVector(builder, numElems)
def EventAddKwargs(builder, kwargs): builder.PrependUOffsetTRelativeSlot(4, flatbuffers.number_types.UOffsetTFlags.py_type(kwargs), 0)
def AddKwargs(builder, kwargs):
return EventAddKwargs(builder, kwargs)
def EventStartKwargsVector(builder, numElems): return builder.StartVector(1, numElems, 1)
def StartKwargsVector(builder, numElems):
return EventStartKwargsVector(builder, numElems)
def EventAddPayload(builder, payload): builder.PrependUOffsetTRelativeSlot(5, flatbuffers.number_types.UOffsetTFlags.py_type(payload), 0)
def AddPayload(builder, payload):
return EventAddPayload(builder, payload)
def EventStartPayloadVector(builder, numElems): return builder.StartVector(1, numElems, 1)
def StartPayloadVector(builder, numElems):
return EventStartPayloadVector(builder, numElems)
def EventAddEncAlgo(builder, encAlgo): builder.PrependUint8Slot(6, encAlgo, 0)
def AddEncAlgo(builder, encAlgo):
return EventAddEncAlgo(builder, encAlgo)
def EventAddEncSerializer(builder, encSerializer): builder.PrependUint8Slot(7, encSerializer, 0)
def AddEncSerializer(builder, encSerializer):
return EventAddEncSerializer(builder, encSerializer)
def EventAddEncKey(builder, encKey): builder.PrependUOffsetTRelativeSlot(8, flatbuffers.number_types.UOffsetTFlags.py_type(encKey), 0)
def AddEncKey(builder, encKey):
return EventAddEncKey(builder, encKey)
def EventStartEncKeyVector(builder, numElems): return builder.StartVector(1, numElems, 1)
def StartEncKeyVector(builder, numElems):
return EventStartEncKeyVector(builder, numElems)
def EventAddPublisher(builder, publisher): builder.PrependUint64Slot(9, publisher, 0)
def AddPublisher(builder, publisher):
return EventAddPublisher(builder, publisher)
def EventAddPublisherAuthid(builder, publisherAuthid): builder.PrependUOffsetTRelativeSlot(10, flatbuffers.number_types.UOffsetTFlags.py_type(publisherAuthid), 0)
def AddPublisherAuthid(builder, publisherAuthid):
return EventAddPublisherAuthid(builder, publisherAuthid)
def EventAddPublisherAuthrole(builder, publisherAuthrole): builder.PrependUOffsetTRelativeSlot(11, flatbuffers.number_types.UOffsetTFlags.py_type(publisherAuthrole), 0)
def AddPublisherAuthrole(builder, publisherAuthrole):
return EventAddPublisherAuthrole(builder, publisherAuthrole)
def EventAddTopic(builder, topic): builder.PrependUOffsetTRelativeSlot(12, flatbuffers.number_types.UOffsetTFlags.py_type(topic), 0)
def AddTopic(builder, topic):
return EventAddTopic(builder, topic)
def EventAddRetained(builder, retained): builder.PrependBoolSlot(13, retained, 0)
def AddRetained(builder, retained):
return EventAddRetained(builder, retained)
def EventAddTransactionHash(builder, transactionHash): builder.PrependUOffsetTRelativeSlot(14, flatbuffers.number_types.UOffsetTFlags.py_type(transactionHash), 0)
def AddTransactionHash(builder, transactionHash):
return EventAddTransactionHash(builder, transactionHash)
def EventAddAcknowledge(builder, acknowledge): builder.PrependBoolSlot(15, acknowledge, 0)
def AddAcknowledge(builder, acknowledge):
return EventAddAcknowledge(builder, acknowledge)
def EventAddForwardFor(builder, forwardFor): builder.PrependUOffsetTRelativeSlot(16, flatbuffers.number_types.UOffsetTFlags.py_type(forwardFor), 0)
def AddForwardFor(builder, forwardFor):
return EventAddForwardFor(builder, forwardFor)
def EventStartForwardForVector(builder, numElems): return builder.StartVector(8, numElems, 8)
def StartForwardForVector(builder, numElems):
return EventStartForwardForVector(builder, numElems)
def EventEnd(builder): return builder.EndObject()
def End(builder):
return EventEnd(builder)

View File

@@ -0,0 +1,138 @@
# automatically generated by the FlatBuffers compiler, do not modify
# namespace: proto
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
class EventReceived(object):
__slots__ = ['_tab']
@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = EventReceived()
x.Init(buf, n + offset)
return x
@classmethod
def GetRootAsEventReceived(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)
# EventReceived
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)
# EventReceived
def Session(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# EventReceived
def Publication(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
return 0
# EventReceived
def Payload(self, j):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
a = self._tab.Vector(o)
return self._tab.Get(flatbuffers.number_types.Uint8Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 1))
return 0
# EventReceived
def PayloadAsNumpy(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint8Flags, o)
return 0
# EventReceived
def PayloadLength(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
if o != 0:
return self._tab.VectorLen(o)
return 0
# EventReceived
def PayloadIsNone(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
return o == 0
# EventReceived
def EncAlgo(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
# EventReceived
def EncSerializer(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
if o != 0:
return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
return 0
# EventReceived
def EncKey(self, j):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
if o != 0:
a = self._tab.Vector(o)
return self._tab.Get(flatbuffers.number_types.Uint8Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 1))
return 0
# EventReceived
def EncKeyAsNumpy(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
if o != 0:
return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint8Flags, o)
return 0
# EventReceived
def EncKeyLength(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
if o != 0:
return self._tab.VectorLen(o)
return 0
# EventReceived
def EncKeyIsNone(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
return o == 0
def EventReceivedStart(builder): builder.StartObject(6)
def Start(builder):
return EventReceivedStart(builder)
def EventReceivedAddSession(builder, session): builder.PrependUint64Slot(0, session, 0)
def AddSession(builder, session):
return EventReceivedAddSession(builder, session)
def EventReceivedAddPublication(builder, publication): builder.PrependUint64Slot(1, publication, 0)
def AddPublication(builder, publication):
return EventReceivedAddPublication(builder, publication)
def EventReceivedAddPayload(builder, payload): builder.PrependUOffsetTRelativeSlot(2, flatbuffers.number_types.UOffsetTFlags.py_type(payload), 0)
def AddPayload(builder, payload):
return EventReceivedAddPayload(builder, payload)
def EventReceivedStartPayloadVector(builder, numElems): return builder.StartVector(1, numElems, 1)
def StartPayloadVector(builder, numElems):
return EventReceivedStartPayloadVector(builder, numElems)
def EventReceivedAddEncAlgo(builder, encAlgo): builder.PrependUint8Slot(3, encAlgo, 0)
def AddEncAlgo(builder, encAlgo):
return EventReceivedAddEncAlgo(builder, encAlgo)
def EventReceivedAddEncSerializer(builder, encSerializer): builder.PrependUint8Slot(4, encSerializer, 0)
def AddEncSerializer(builder, encSerializer):
return EventReceivedAddEncSerializer(builder, encSerializer)
def EventReceivedAddEncKey(builder, encKey): builder.PrependUOffsetTRelativeSlot(5, flatbuffers.number_types.UOffsetTFlags.py_type(encKey), 0)
def AddEncKey(builder, encKey):
return EventReceivedAddEncKey(builder, encKey)
def EventReceivedStartEncKeyVector(builder, numElems): return builder.StartVector(1, numElems, 1)
def StartEncKeyVector(builder, numElems):
return EventReceivedStartEncKeyVector(builder, numElems)
def EventReceivedEnd(builder): return builder.EndObject()
def End(builder):
return EventReceivedEnd(builder)

Some files were not shown because too many files have changed in this diff Show More