Edouard@1439: #!/usr/bin/env python Edouard@1439: # -*- coding: utf-8 -*- Edouard@1439: andrej@1667: # This file is part of Beremiz runtime. andrej@1511: # andrej@1511: # Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD andrej@1511: # andrej@1667: # See COPYING.Runtime file for copyrights details. andrej@1511: # andrej@1667: # This library is free software; you can redistribute it and/or andrej@1667: # modify it under the terms of the GNU Lesser General Public andrej@1667: # License as published by the Free Software Foundation; either andrej@1667: # version 2.1 of the License, or (at your option) any later version. andrej@1667: andrej@1667: # This library is distributed in the hope that it will be useful, andrej@1511: # but WITHOUT ANY WARRANTY; without even the implied warranty of andrej@1667: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU andrej@1667: # Lesser General Public License for more details. andrej@1667: andrej@1667: # You should have received a copy of the GNU Lesser General Public andrej@1667: # License along with this library; if not, write to the Free Software andrej@1667: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA andrej@1511: andrej@1826: andrej@1881: from __future__ import absolute_import andrej@1826: from __future__ import print_function Edouard@1899: import time Edouard@1893: import json denis@2203: import os denis@2201: import re Edouard@1439: from autobahn.twisted import wamp Edouard@1439: from autobahn.twisted.websocket import WampWebSocketClientFactory, connectWS Edouard@1893: from autobahn.wamp import types, auth Edouard@1440: from autobahn.wamp.serializer import MsgPackSerializer andrej@1834: from twisted.internet.defer import inlineCallbacks Edouard@1441: from twisted.internet.protocol import ReconnectingClientFactory andrej@1832: Edouard@2218: import runtime.NevowServer as NS Edouard@2218: Edouard@2218: from formless import annotate Edouard@1439: Edouard@2212: mandatoryConfigItems = ["ID", "active", "realm", "url"] Edouard@2212: denis@2201: _transportFactory = None Edouard@1439: _WampSession = None Edouard@1439: _PySrv = None denis@2202: _WampConf = None denis@2202: _WampSecret = None Edouard@1439: andrej@1878: ExposedCalls = [ denis@2206: ("StartPLC", {}), denis@2206: ("StopPLC", {}), denis@2206: ("ForceReload", {}), denis@2206: ("GetPLCstatus", {}), denis@2206: ("NewPLC", {}), denis@2206: ("MatchMD5", {}), denis@2206: ("SetTraceVariablesList", {}), denis@2206: ("GetTraceVariables", {}), denis@2206: ("RemoteExec", {}), denis@2206: ("GetLogMessage", {}), denis@2206: ("ResetLogCount", {}) andrej@1878: ] Edouard@1440: Edouard@1898: # Those two lists are meant to be filled by customized runtime Edouard@1901: # or User python code. Edouard@1898: Edouard@1901: """ crossbar Events to register to """ Edouard@1446: SubscribedEvents = [] Edouard@1446: Edouard@1901: """ things to do on join (callables) """ Edouard@1446: DoOnJoin = [] Edouard@1446: Edouard@2218: lastKnownConfig = None andrej@1736: Edouard@1445: def GetCallee(name): Edouard@1446: """ Get Callee or Subscriber corresponding to '.' spearated object path """ Edouard@1445: names = name.split('.') Edouard@1445: obj = _PySrv.plcobj andrej@1756: while names: andrej@1756: obj = getattr(obj, names.pop(0)) Edouard@1445: return obj Edouard@1440: andrej@1736: Edouard@1439: class WampSession(wamp.ApplicationSession): Edouard@1893: def onConnect(self): Edouard@1901: if "secret" in self.config.extra: denis@2194: user = self.config.extra["ID"] Edouard@1893: self.join(u"Automation", [u"wampcra"], user) Edouard@1893: else: Edouard@1893: self.join(u"Automation") Edouard@1893: Edouard@1893: def onChallenge(self, challenge): Edouard@1893: if challenge.method == u"wampcra": Edouard@2218: if "secret" in self.config.extra: Edouard@2218: secret = self.config.extra["secret"].encode('utf8') Edouard@2218: signature = auth.compute_wcs(secret, challenge.extra['challenge'].encode('utf8')) Edouard@2218: return signature.decode("ascii") Edouard@2218: else: Edouard@2218: raise Exception("no secret given for authentication") Edouard@1893: else: Edouard@1893: raise Exception("don't know how to handle authmethod {}".format(challenge.method)) Edouard@1440: Edouard@1440: @inlineCallbacks Edouard@1439: def onJoin(self, details): Edouard@1439: global _WampSession Edouard@1439: _WampSession = self Edouard@1901: ID = self.config.extra["ID"] denis@2206: denis@2206: for name, kwargs in ExposedCalls: denis@2206: try: denis@2206: registerOptions = types.RegisterOptions(**kwargs) denis@2206: except TypeError as e: denis@2206: registerOptions = None denis@2206: print(_("TypeError register option: {}".format(e))) denis@2206: denis@2197: yield self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions) Edouard@1439: Edouard@1446: for name in SubscribedEvents: Edouard@1898: yield self.subscribe(GetCallee(name), unicode(name)) Edouard@1446: Edouard@1446: for func in DoOnJoin: Edouard@1446: yield func(self) Edouard@1446: denis@2194: print(_('WAMP session joined (%s) by:' % time.ctime()), ID) denis@2194: Edouard@1439: def onLeave(self, details): denis@2201: global _WampSession, _transportFactory denis@2201: super(WampSession, self).onLeave(details) Edouard@1439: _WampSession = None denis@2201: _transportFactory = None Edouard@1890: print(_('WAMP session left')) Edouard@1439: andrej@1736: Edouard@1441: class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory): denis@2195: def __init__(self, config, *args, **kwargs): denis@2201: global _transportFactory denis@2195: WampWebSocketClientFactory.__init__(self, *args, **kwargs) denis@2195: denis@2207: try: denis@2207: protocolOptions = config.extra.get('protocolOptions', None) denis@2207: if protocolOptions: denis@2207: self.setProtocolOptions(**protocolOptions) denis@2207: _transportFactory = self denis@2207: except Exception, e: denis@2207: print(_("Custom protocol options failed :"), e) denis@2207: _transportFactory = None denis@2195: denis@2193: def buildProtocol(self, addr): denis@2193: self.resetDelay() denis@2193: return ReconnectingClientFactory.buildProtocol(self, addr) denis@2193: Edouard@1441: def clientConnectionFailed(self, connector, reason): denis@2201: if self.continueTrying: Edouard@2215: print(_("WAMP Client connection failed (%s) .. retrying ..") % time.ctime()) denis@2201: super(ReconnectingWampWebSocketClientFactory, self).clientConnectionFailed(connector, reason) denis@2201: else: denis@2201: del connector andrej@1751: Edouard@1441: def clientConnectionLost(self, connector, reason): denis@2201: if self.continueTrying: Edouard@2215: print(_("WAMP Client connection lost (%s) .. retrying ..") % time.ctime()) denis@2201: super(ReconnectingWampWebSocketClientFactory, self).clientConnectionFailed(connector, reason) denis@2201: else: denis@2201: del connector Edouard@1441: andrej@1736: Edouard@2218: def CheckConfiguration(WSClientConf): Edouard@2218: url = WSClientConf["url"] Edouard@2218: if not IsCorrectUri(url): Edouard@2218: raise annotate.ValidateError( Edouard@2218: {"url":"Invalid URL: {}".format(url)}, Edouard@2218: _("WAMP confiuration error:")) Edouard@2218: Edouard@2212: def GetConfiguration(): Edouard@2218: global lastKnownConfig Edouard@2218: Edouard@2212: WSClientConf = json.load(open(_WampConf)) Edouard@2212: for itemName in mandatoryConfigItems: Edouard@2212: if WSClientConf.get(itemName, None) is None : Edouard@2212: raise Exception(_("WAMP configuration error : missing '{}' parameter.").format(itemName)) Edouard@2212: Edouard@2218: CheckConfiguration(WSClientConf) Edouard@2218: Edouard@2218: lastKnownConfig = WSClientConf.copy() Edouard@2212: return WSClientConf Edouard@2212: Edouard@2212: Edouard@2212: def SetConfiguration(WSClientConf): Edouard@2218: global lastKnownConfig Edouard@2218: Edouard@2218: CheckConfiguration(WSClientConf) Edouard@2218: Edouard@2218: lastKnownConfig = WSClientConf.copy() Edouard@2218: Edouard@2218: with open(os.path.realpath(_WampConf), 'w') as f: Edouard@2218: json.dump(WSClientConf, f, sort_keys=True, indent=4) Edouard@2218: if 'active' in WSClientConf and WSClientConf['active']: Edouard@2218: if _transportFactory and _WampSession: Edouard@2212: StopReconnectWampClient() Edouard@2218: StartReconnectWampClient() Edouard@2218: else: Edouard@2218: StopReconnectWampClient() Edouard@2218: Edouard@2218: return WSClientConf Edouard@1901: Edouard@2212: Edouard@1893: def LoadWampSecret(secretfname): Edouard@2218: WSClientWampSecret = open(secretfname, 'rb').read() Edouard@2218: if len(WSClientWampSecret) == 0 : Edouard@2218: raise Exception(_("WAMP secret empty")) Edouard@2218: return WSClientWampSecret Edouard@1446: andrej@1736: denis@2201: def IsCorrectUri(uri): Edouard@2218: return re.match(r'wss?://[^\s?:#-]+(:[0-9]+)?(/[^\s]*)?$', uri) is not None denis@2201: denis@2201: Edouard@2212: def RegisterWampClient(wampconf=None, wampsecret=None): Edouard@2212: global _WampConf, _WampSecret Edouard@2212: if wampsecret: Edouard@2212: _WampSecret = wampsecret denis@2202: if wampconf: denis@2204: _WampConf = wampconf Edouard@2212: Edouard@2212: WSClientConf = GetConfiguration() denis@2201: Edouard@2212: if not WSClientConf["active"]: Edouard@2212: print(_("WAMP deactivated in configuration")) Edouard@2212: return Edouard@2212: Edouard@2218: if _WampSecret is not None: Edouard@2218: WSClientConf["secret"] = LoadWampSecret(_WampSecret) Edouard@1900: Edouard@1440: # create a WAMP application session factory Edouard@1439: component_config = types.ComponentConfig( andrej@1744: realm=WSClientConf["realm"], Edouard@1901: extra=WSClientConf) Edouard@1439: session_factory = wamp.ApplicationSessionFactory( andrej@1744: config=component_config) Edouard@1439: session_factory.session = WampSession Edouard@1439: Edouard@1440: # create a WAMP-over-WebSocket transport client factory denis@2207: ReconnectingWampWebSocketClientFactory( denis@2195: component_config, Edouard@1439: session_factory, andrej@1744: url=WSClientConf["url"], Edouard@1893: serializers=[MsgPackSerializer()]) Edouard@1439: Edouard@1440: # start the client from a Twisted endpoint denis@2207: if _transportFactory: denis@2207: conn = connectWS(_transportFactory) denis@2207: print(_("WAMP client connecting to :"), WSClientConf["url"]) denis@2207: return True denis@2207: else: denis@2207: print(_("WAMP client can not connect to :"), WSClientConf["url"]) denis@2207: return False denis@2204: denis@2201: denis@2203: def StopReconnectWampClient(): Edouard@2218: if _transportFactory is not None : Edouard@2218: _transportFactory.stopTrying() Edouard@2218: if _WampSession is not None : Edouard@2218: _WampSession.leave() denis@2203: denis@2204: denis@2203: def StartReconnectWampClient(): denis@2203: if _WampSession: denis@2203: # do reconnect denis@2202: _WampSession.disconnect() denis@2202: return True denis@2204: else: denis@2203: # do connect denis@2202: RegisterWampClient() denis@2202: return True denis@2203: andrej@1736: Edouard@1439: def GetSession(): Edouard@1439: return _WampSession Edouard@1439: Edouard@2218: def getWampStatus(): Edouard@2218: if _transportFactory is not None : Edouard@2218: if _WampSession is not None : Edouard@2218: if _WampSession.is_attached() : Edouard@2218: return "Attached" Edouard@2218: return "Established" Edouard@2218: return "Connecting" Edouard@2218: return "Disconnected" denis@2202: denis@2204: Edouard@2212: def SetServer(pysrv): Edouard@1439: _PySrv = pysrv Edouard@2218: Edouard@2218: Edouard@2218: #### WEB CONFIGURATION INTERFACE #### Edouard@2218: Edouard@2218: webExposedConfigItems = ['active', 'url', 'ID'] Edouard@2218: Edouard@2218: def wampConfigDefault(ctx,argument): Edouard@2218: if lastKnownConfig is not None : Edouard@2218: return lastKnownConfig.get(argument.name, None) Edouard@2218: Edouard@2218: def wampConfig(**kwargs): Edouard@2218: newConfig = lastKnownConfig.copy() Edouard@2218: for argname in webExposedConfigItems: Edouard@2218: newConfig[argname] = kwargs[argname] Edouard@2218: Edouard@2218: SetConfiguration(newConfig) Edouard@2218: Edouard@2218: webFormInterface = [ Edouard@2218: ("status", Edouard@2218: annotate.String(label=_("Current status"), Edouard@2218: immutable = True, Edouard@2218: default = lambda *k:getWampStatus())), Edouard@2218: ("ID", Edouard@2218: annotate.String(label=_("ID"), Edouard@2218: default = wampConfigDefault)), Edouard@2218: ("active", Edouard@2218: annotate.Boolean(label=_("Enable WAMP connection"), Edouard@2218: default=wampConfigDefault)), Edouard@2218: ("url", Edouard@2218: annotate.String(label=_("WAMP Server URL"), Edouard@2218: default=wampConfigDefault))] Edouard@2218: Edouard@2218: Edouard@2218: NS.ConfigurableSettings.addExtension( Edouard@2218: "wamp", Edouard@2218: _("Wamp Settings"), Edouard@2218: webFormInterface, Edouard@2218: _("Set"), Edouard@2218: wampConfig)