# This file is part of Beremiz runtime.
# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
# See COPYING.Runtime file for copyrights details.
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from __future__ import absolute_import
from __future__ import print_function
from six import text_type as text
from autobahn.twisted import wamp
from autobahn.twisted.websocket import WampWebSocketClientFactory, connectWS
from autobahn.wamp import types, auth
from autobahn.wamp.serializer import MsgPackSerializer
from twisted.internet.protocol import ReconnectingClientFactory
from twisted.python.components import registerAdapter
from formless import annotate, webform
from nevow import tags, url, static
from runtime import GetPLCObjectSingleton
mandatoryConfigItems = ["ID", "active", "realm", "url"]
# Find pre-existing project WAMP config file
("AppendChunkToBlob", {}),
("SetTraceVariablesList", {}),
("GetTraceVariables", {}),
# de-activated dumb wamp config
"url": "ws://127.0.0.1:8888",
"clientFactoryOptions": {
# Those two lists are meant to be filled by customized runtime
""" crossbar Events to register to """
""" things to do on join (callables) """
""" Get Callee or Subscriber corresponding to '.' spearated object path """
obj = GetPLCObjectSingleton()
obj = getattr(obj, names.pop(0))
class WampSession(wamp.ApplicationSession):
if "secret" in self.config.extra:
user = self.config.extra["ID"]
self.join(u"Automation", [u"wampcra"], user)
def onChallenge(self, challenge):
if challenge.method == u"wampcra":
if "secret" in self.config.extra:
secret = self.config.extra["secret"].encode('utf8')
signature = auth.compute_wcs(
secret, challenge.extra['challenge'].encode('utf8'))
return signature.decode("ascii")
raise Exception("no secret given for authentication")
"don't know how to handle authmethod {}".format(challenge.method))
def onJoin(self, details):
ID = self.config.extra["ID"]
for name, kwargs in ExposedCalls:
registerOptions = types.RegisterOptions(**kwargs)
print(_("TypeError register option: {}".format(e)))
self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions)
for name in SubscribedEvents:
self.subscribe(GetCallee(name), text(name))
print(_('WAMP session joined (%s) by:' % time.ctime()), ID)
def onLeave(self, details):
global _WampSession, _transportFactory
super(WampSession, self).onLeave(details)
print(_('WAMP session left'))
def publishWithOwnID(self, eventID, value):
ID = self.config.extra["ID"]
self.publish(text(ID+'.'+eventID), value)
class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory):
def __init__(self, config, *args, **kwargs):
WampWebSocketClientFactory.__init__(self, *args, **kwargs)
clientFactoryOptions = config.extra.get("clientFactoryOptions")
self.setClientFactoryOptions(clientFactoryOptions)
print(_("Custom client factory options failed : "), e)
protocolOptions = config.extra.get('protocolOptions', None)
self.setProtocolOptions(**protocolOptions)
print(_("Custom protocol options failed :"), e)
def setClientFactoryOptions(self, options):
for key, value in options.items():
if key in ["maxDelay", "initialDelay", "maxRetries", "factor", "jitter"]:
setattr(self, key, value)
def buildProtocol(self, addr):
return ReconnectingClientFactory.buildProtocol(self, addr)
def clientConnectionFailed(self, connector, reason):
print(_("WAMP Client connection failed (%s) .. retrying ..") %
super(ReconnectingWampWebSocketClientFactory,
self).clientConnectionFailed(connector, reason)
def clientConnectionLost(self, connector, reason):
print(_("WAMP Client connection lost (%s) .. retrying ..") %
super(ReconnectingWampWebSocketClientFactory,
self).clientConnectionFailed(connector, reason)
def CheckConfiguration(WampClientConf):
url = WampClientConf["url"]
if not IsCorrectUri(url):
raise annotate.ValidateError(
{"url": "Invalid URL: {}".format(url)},
_("WAMP configuration error:"))
def UpdateWithDefault(d1, d2):
if os.path.exists(_WampConf):
WampClientConf = json.load(open(_WampConf))
UpdateWithDefault(WampClientConf, defaultWampConfig)
if WampClientConf is None:
WampClientConf = defaultWampConfig.copy()
for itemName in mandatoryConfigItems:
if WampClientConf.get(itemName, None) is None:
_("WAMP configuration error : missing '{}' parameter.").format(itemName))
CheckConfiguration(WampClientConf)
lastKnownConfig = WampClientConf.copy()
def SetWampSecret(wampSecret):
with open(os.path.realpath(_WampSecret), 'w') as f:
def SetConfiguration(WampClientConf):
CheckConfiguration(WampClientConf)
lastKnownConfig = WampClientConf.copy()
with open(os.path.realpath(_WampConf), 'w') as f:
json.dump(WampClientConf, f, sort_keys=True, indent=4)
StopReconnectWampClient()
if 'active' in WampClientConf and WampClientConf['active']:
StartReconnectWampClient()
def LoadWampSecret(secretfname):
WSClientWampSecret = open(secretfname, 'rb').read()
if len(WSClientWampSecret) == 0:
raise Exception(_("WAMP secret empty"))
return WSClientWampSecret
return re.match(r'wss?://[^\s?:#-]+(:[0-9]+)?(/[^\s]*)?$', uri) is not None
def RegisterWampClient(wampconf=None, wampsecret=None):
global _WampConf, _WampSecret
_WampConfDefault = os.path.join(WorkingDir, "wampconf.json")
_WampSecretDefault = os.path.join(WorkingDir, "wamp.secret")
# set config file path only if not already set
# default project's wampconf has precedance over commandline given
if os.path.exists(_WampConfDefault) or wampconf is None:
_WampConf = _WampConfDefault
WampClientConf = GetConfiguration()
# set secret file path only if not already set
# default project's wamp secret also
# has precedance over commandline given
if os.path.exists(_WampSecretDefault):
_WampSecret = _WampSecretDefault
if _WampSecret is not None:
WampClientConf["secret"] = LoadWampSecret(_WampSecret)
print(_("WAMP authentication has no secret configured"))
_WampSecret = _WampSecretDefault
if not WampClientConf["active"]:
print(_("WAMP deactivated in configuration"))
# create a WAMP application session factory
component_config = types.ComponentConfig(
realm=WampClientConf["realm"],
session_factory = wamp.ApplicationSessionFactory(
session_factory.session = WampSession
# create a WAMP-over-WebSocket transport client factory
ReconnectingWampWebSocketClientFactory(
url=WampClientConf["url"],
serializers=[MsgPackSerializer()])
# start the client from a Twisted endpoint
connectWS(_transportFactory)
print(_("WAMP client connecting to :"), WampClientConf["url"])
print(_("WAMP client can not connect to :"), WampClientConf["url"])
def StopReconnectWampClient():
if _transportFactory is not None:
_transportFactory.stopTrying()
if _WampSession is not None:
def StartReconnectWampClient():
# do reconnect and reset continueTrying and initialDelay parameter
if _transportFactory is not None:
_transportFactory.resetDelay()
_WampSession.disconnect()
if _transportFactory is not None:
if _WampSession is not None:
if _WampSession.is_attached():
def PublishEvent(eventID, value):
if getWampStatus() == "Attached":
_WampSession.publish(text(eventID), value)
def PublishEventWithOwnID(eventID, value):
if getWampStatus() == "Attached":
_WampSession.publishWithOwnID(text(eventID), value)
# WEB CONFIGURATION INTERFACE
WAMP_SECRET_URL = "secret"
webExposedConfigItems = [
"clientFactoryOptions.maxDelay",
"protocolOptions.autoPingInterval",
"protocolOptions.autoPingTimeout"
def wampConfigDefault(ctx, argument):
if lastKnownConfig is not None:
# Check if name is composed with an intermediate dot symbol and go deep in lastKnownConfig if it is
argument_name_path = argument.name.split(".")
searchValue = lastKnownConfig
while argument_name_path:
searchValue = searchValue.get(argument_name_path.pop(0), None)
def wampConfig(**kwargs):
secretfile_field = kwargs["secretfile"]
if secretfile_field is not None:
secretfile = getattr(secretfile_field, "file", None)
if secretfile is not None:
secret = secretfile_field.file.read()
newConfig = lastKnownConfig.copy()
for argname in webExposedConfigItems:
# Check if name is composed with an intermediate dot symbol and go deep in lastKnownConfig if it is
# and then set a new value.
argname_path = argname.split(".")
arg_last = argname_path.pop()
arg = kwargs.get(argname, None)
tmpConf = tmpConf.setdefault(argname_path.pop(0), {})
SetConfiguration(newConfig)
class FileUploadDownload(annotate.FileUpload):
class FileUploadDownloadRenderer(webform.FileUploadRenderer):
def input(self, context, slot, data, name, value):
# pylint: disable=expression-not-assigned
slot = webform.FileUploadRenderer.input(
self, context, slot, data, name, value)
download_url = data.typedValue.getAttribute('download_url')
return slot[tags.a(href=download_url)[_("Download")]]
registerAdapter(FileUploadDownloadRenderer, FileUploadDownload,
formless.iformless.ITypedRenderer)
def getDownloadUrl(ctx, argument):
if lastKnownConfig is not None:
return url.URL.fromContext(ctx).\
child(lastKnownConfig["ID"] + ".secret")
annotate.String(label=_("Current status"),
default=lambda *k:getWampStatus())),
annotate.String(label=_("ID"),
default=wampConfigDefault)),
FileUploadDownload(label=_("File containing secret for that ID"),
download_url=getDownloadUrl)),
annotate.Boolean(label=_("Enable WAMP connection"),
default=wampConfigDefault)),
annotate.String(label=_("WAMP Server URL"),
default=wampConfigDefault)),
("clientFactoryOptions.maxDelay",
annotate.Integer(label=_("Max reconnection delay (s)"),
default=wampConfigDefault)),
("protocolOptions.autoPingInterval",
annotate.Integer(label=_("Auto ping interval (s)"),
default=wampConfigDefault)),
("protocolOptions.autoPingTimeout",
annotate.Integer(label=_("Auto ping timeout (s)"),
default=wampConfigDefault))
def deliverWampSecret(ctx, segments):
# filename = segments[1].decode('utf-8')
# FIXME: compare filename to ID+".secret"
# for now all url under /secret returns the secret
# TODO: make beautifull message in case of exception
# while loading secret (if empty or dont exist)
secret = LoadWampSecret(_WampSecret)
return static.Data(secret, 'application/octet-stream'), ()
def RegisterWebSettings(NS):
WebSettings = NS.newExtensionSetting("Wamp Extension Settings", "wamp_settings")
WebSettings.addCustomURL(WAMP_SECRET_URL, deliverWampSecret)