diff -r 7f59aa398669 -r 2a9549e4380e runtime/WampClient.py --- a/runtime/WampClient.py Fri Jun 15 09:48:05 2018 +0200 +++ b/runtime/WampClient.py Fri Jul 20 11:05:17 2018 +0200 @@ -26,31 +26,53 @@ from __future__ import print_function import time import json +import os +import re 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.defer import inlineCallbacks from twisted.internet.protocol import ReconnectingClientFactory - - +from twisted.python.components import registerAdapter + +from formless import annotate, webform +import formless +from nevow import tags, url, static + +mandatoryConfigItems = ["ID", "active", "realm", "url"] + +_transportFactory = None _WampSession = None _PySrv = None +WorkingDir = None + +# Find pre-existing project WAMP config file +_WampConf = None +_WampSecret = None ExposedCalls = [ - "StartPLC", - "StopPLC", - "ForceReload", - "GetPLCstatus", - "NewPLC", - "MatchMD5", - "SetTraceVariablesList", - "GetTraceVariables", - "RemoteExec", - "GetLogMessage", - "ResetLogCount", + ("StartPLC", {}), + ("StopPLC", {}), + ("ForceReload", {}), + ("GetPLCstatus", {}), + ("NewPLC", {}), + ("MatchMD5", {}), + ("SetTraceVariablesList", {}), + ("GetTraceVariables", {}), + ("RemoteExec", {}), + ("GetLogMessage", {}), + ("ResetLogCount", {}) ] +# de-activated dumb wamp config +defaultWampConfig = { + "ID": "wamptest", + "active": False, + "realm": "Automation", + "url": "ws://127.0.0.1:8888" +} + # Those two lists are meant to be filled by customized runtime # or User python code. @@ -60,6 +82,7 @@ """ things to do on join (callables) """ DoOnJoin = [] +lastKnownConfig = None def GetCallee(name): """ Get Callee or Subscriber corresponding to '.' spearated object path """ @@ -73,16 +96,19 @@ class WampSession(wamp.ApplicationSession): def onConnect(self): if "secret" in self.config.extra: - user = self.config.extra["ID"].encode('utf8') + user = self.config.extra["ID"] self.join(u"Automation", [u"wampcra"], user) else: self.join(u"Automation") def onChallenge(self, challenge): if challenge.method == u"wampcra": - secret = self.config.extra["secret"].encode('utf8') - signature = auth.compute_wcs(secret, challenge.extra['challenge'].encode('utf8')) - return signature.decode("ascii") + 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") + else: + raise Exception("no secret given for authentication") else: raise Exception("don't know how to handle authmethod {}".format(challenge.method)) @@ -91,10 +117,15 @@ global _WampSession _WampSession = self ID = self.config.extra["ID"] - print('WAMP session joined by :', ID) - for name in ExposedCalls: - regoption = types.RegisterOptions(u'exact', u'last') - yield self.register(GetCallee(name), u'.'.join((ID, name)), regoption) + + for name, kwargs in ExposedCalls: + try: + registerOptions = types.RegisterOptions(**kwargs) + except TypeError as e: + registerOptions = None + print(_("TypeError register option: {}".format(e))) + + yield self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions) for name in SubscribedEvents: yield self.subscribe(GetCallee(name), unicode(name)) @@ -102,81 +133,283 @@ for func in DoOnJoin: yield func(self) + print(_('WAMP session joined (%s) by:' % time.ctime()), ID) + def onLeave(self, details): - global _WampSession + global _WampSession, _transportFactory + super(WampSession, self).onLeave(details) _WampSession = None + _transportFactory = None print(_('WAMP session left')) class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory): + def __init__(self, config, *args, **kwargs): + global _transportFactory + WampWebSocketClientFactory.__init__(self, *args, **kwargs) + + try: + protocolOptions = config.extra.get('protocolOptions', None) + if protocolOptions: + self.setProtocolOptions(**protocolOptions) + _transportFactory = self + except Exception, e: + print(_("Custom protocol options failed :"), e) + _transportFactory = None + + def buildProtocol(self, addr): + self.resetDelay() + return ReconnectingClientFactory.buildProtocol(self, addr) + def clientConnectionFailed(self, connector, reason): - print(_("WAMP Client connection failed (%s) .. retrying .." % time.ctime())) - ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) + if self.continueTrying: + print(_("WAMP Client connection failed (%s) .. retrying ..") % time.ctime()) + super(ReconnectingWampWebSocketClientFactory, self).clientConnectionFailed(connector, reason) + else: + del connector def clientConnectionLost(self, connector, reason): - print(_("WAMP Client connection lost (%s) .. retrying .." % time.ctime())) - ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) - - -def LoadWampClientConf(wampconf): - try: - WSClientConf = json.load(open(wampconf)) - return WSClientConf - except ValueError, ve: - print(_("WAMP load error: "), ve) - return None - except Exception: - return None + if self.continueTrying: + print(_("WAMP Client connection lost (%s) .. retrying ..") % time.ctime()) + super(ReconnectingWampWebSocketClientFactory, self).clientConnectionFailed(connector, reason) + else: + del connector + + +def CheckConfiguration(WampClientConf): + url = WampClientConf["url"] + if not IsCorrectUri(url): + raise annotate.ValidateError( + {"url":"Invalid URL: {}".format(url)}, + _("WAMP configuration error:")) + +def GetConfiguration(): + global lastKnownConfig + + if os.path.exists(_WampConf): + WampClientConf = json.load(open(_WampConf)) + else: + WampClientConf = defaultWampConfig.copy() + + for itemName in mandatoryConfigItems: + if WampClientConf.get(itemName, None) is None : + raise Exception(_("WAMP configuration error : missing '{}' parameter.").format(itemName)) + + CheckConfiguration(WampClientConf) + + lastKnownConfig = WampClientConf.copy() + return WampClientConf + + +def SetWampSecret(wampSecret): + with open(os.path.realpath(_WampSecret), 'w') as f: + f.write(wampSecret) + +def SetConfiguration(WampClientConf): + global lastKnownConfig + + CheckConfiguration(WampClientConf) + + lastKnownConfig = WampClientConf.copy() + + with open(os.path.realpath(_WampConf), 'w') as f: + json.dump(WampClientConf, f, sort_keys=True, indent=4) + if 'active' in WampClientConf and WampClientConf['active']: + if _transportFactory and _WampSession: + StopReconnectWampClient() + StartReconnectWampClient() + else: + StopReconnectWampClient() + + return WampClientConf def LoadWampSecret(secretfname): - try: - WSClientWampSecret = open(secretfname, 'rb').read() - return WSClientWampSecret - except ValueError, ve: - print(_("Wamp secret load error:"), ve) - return None - except Exception: - return None - - -def RegisterWampClient(wampconf, secretfname): - - WSClientConf = LoadWampClientConf(wampconf) - - if not WSClientConf: - print(_("WAMP client connection not established!")) + WSClientWampSecret = open(secretfname, 'rb').read() + if len(WSClientWampSecret) == 0 : + raise Exception(_("WAMP secret empty")) + return WSClientWampSecret + + +def IsCorrectUri(uri): + 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 + if _WampConf is None: + # default project's wampconf has precedance over commandline given + if os.path.exists(_WampConfDefault) or wampconf is None: + _WampConf = _WampConfDefault + else: + _WampConf = wampconf + + WampClientConf = GetConfiguration() + + # set secret file path only if not already set + if _WampSecret is None: + # default project's wamp secret also has precedance over commandline given + if os.path.exists(_WampSecretDefault): + _WampSecret = _WampSecretDefault + else: + _WampSecret = wampsecret + + if _WampSecret is not None: + WampClientConf["secret"] = LoadWampSecret(_WampSecret) + else : + print(_("WAMP authentication has no secret configured")) + _WampSecret = _WampSecretDefault + + if not WampClientConf["active"]: + print(_("WAMP deactivated in configuration")) return - WampSecret = LoadWampSecret(secretfname) - - if WampSecret is not None: - WSClientConf["secret"] = WampSecret - # create a WAMP application session factory component_config = types.ComponentConfig( - realm=WSClientConf["realm"], - extra=WSClientConf) + realm=WampClientConf["realm"], + extra=WampClientConf) session_factory = wamp.ApplicationSessionFactory( config=component_config) session_factory.session = WampSession # create a WAMP-over-WebSocket transport client factory - transport_factory = ReconnectingWampWebSocketClientFactory( + ReconnectingWampWebSocketClientFactory( + component_config, session_factory, - url=WSClientConf["url"], + url=WampClientConf["url"], serializers=[MsgPackSerializer()]) # start the client from a Twisted endpoint - conn = connectWS(transport_factory) - print(_("WAMP client connecting to :"), WSClientConf["url"]) - return conn + if _transportFactory: + conn = connectWS(_transportFactory) + print(_("WAMP client connecting to :"), WampClientConf["url"]) + return True + else: + print(_("WAMP client can not connect to :"), WampClientConf["url"]) + return False + + +def StopReconnectWampClient(): + if _transportFactory is not None : + _transportFactory.stopTrying() + if _WampSession is not None : + _WampSession.leave() + + +def StartReconnectWampClient(): + if _WampSession: + # do reconnect + _WampSession.disconnect() + return True + else: + # do connect + RegisterWampClient() + return True def GetSession(): return _WampSession +def getWampStatus(): + if _transportFactory is not None : + if _WampSession is not None : + if _WampSession.is_attached() : + return "Attached" + return "Established" + return "Connecting" + return "Disconnected" + def SetServer(pysrv): - global _PySrv _PySrv = pysrv + + +#### WEB CONFIGURATION INTERFACE #### +WAMP_SECRET_URL = "secret" +webExposedConfigItems = ['active', 'url', 'ID'] + +def wampConfigDefault(ctx,argument): + if lastKnownConfig is not None : + return lastKnownConfig.get(argument.name, 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() + SetWampSecret(secret) + + newConfig = lastKnownConfig.copy() + for argname in webExposedConfigItems: + arg = kwargs.get(argname, None) + if arg is not None : + newConfig[argname] = arg + + SetConfiguration(newConfig) + +class FileUploadDownload(annotate.FileUpload): + pass + + +class FileUploadDownloadRenderer(webform.FileUploadRenderer): + def input(self, context, slot, data, name, value): + slot[_("Upload:")] + 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(WAMP_SECRET_URL).\ + child(lastKnownConfig["ID"]+".secret") + +webFormInterface = [ + ("status", + annotate.String(label=_("Current status"), + immutable = True, + default = lambda *k:getWampStatus())), + ("ID", + annotate.String(label=_("ID"), + default = wampConfigDefault)), + ("secretfile", + FileUploadDownload( + label = _("File containing secret for that ID"), + download_url = getDownloadUrl, + )), + ("active", + annotate.Boolean(label=_("Enable WAMP connection"), + default=wampConfigDefault)), + ("url", + annotate.String(label=_("WAMP Server URL"), + 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 beutifull 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): + NS.ConfigurableSettings.addExtension( + "wamp", + _("Wamp Settings"), + webFormInterface, + _("Set"), + wampConfig) + + + NS.customSettingsURLs[WAMP_SECRET_URL] = deliverWampSecret +