runtime/WampClient.py
changeset 2279 70143c20d2c0
parent 2260 74205edac761
child 2262 4195545e2d17
child 2286 f0a49a662870
equal deleted inserted replaced
2278:a3ac46366b86 2279:70143c20d2c0
    24 
    24 
    25 from __future__ import absolute_import
    25 from __future__ import absolute_import
    26 from __future__ import print_function
    26 from __future__ import print_function
    27 import time
    27 import time
    28 import json
    28 import json
       
    29 import os
       
    30 import re
    29 from autobahn.twisted import wamp
    31 from autobahn.twisted import wamp
    30 from autobahn.twisted.websocket import WampWebSocketClientFactory, connectWS
    32 from autobahn.twisted.websocket import WampWebSocketClientFactory, connectWS
    31 from autobahn.wamp import types, auth
    33 from autobahn.wamp import types, auth
    32 from autobahn.wamp.serializer import MsgPackSerializer
    34 from autobahn.wamp.serializer import MsgPackSerializer
    33 from twisted.internet.defer import inlineCallbacks
    35 from twisted.internet.defer import inlineCallbacks
    34 from twisted.internet.protocol import ReconnectingClientFactory
    36 from twisted.internet.protocol import ReconnectingClientFactory
    35 
    37 from twisted.python.components import registerAdapter
    36 
    38 
       
    39 from formless import annotate, webform
       
    40 import formless
       
    41 from nevow import tags, url, static
       
    42 
       
    43 mandatoryConfigItems = ["ID", "active", "realm", "url"]
       
    44 
       
    45 _transportFactory = None
    37 _WampSession = None
    46 _WampSession = None
    38 _PySrv = None
    47 _PySrv = None
       
    48 WorkingDir = None
       
    49 
       
    50 # Find pre-existing project WAMP config file
       
    51 _WampConf = None
       
    52 _WampSecret = None
    39 
    53 
    40 ExposedCalls = [
    54 ExposedCalls = [
    41     "StartPLC",
    55     ("StartPLC", {}),
    42     "StopPLC",
    56     ("StopPLC", {}),
    43     "ForceReload",
    57     ("ForceReload", {}),
    44     "GetPLCstatus",
    58     ("GetPLCstatus", {}),
    45     "NewPLC",
    59     ("NewPLC", {}),
    46     "MatchMD5",
    60     ("MatchMD5", {}),
    47     "SetTraceVariablesList",
    61     ("SetTraceVariablesList", {}),
    48     "GetTraceVariables",
    62     ("GetTraceVariables", {}),
    49     "RemoteExec",
    63     ("RemoteExec", {}),
    50     "GetLogMessage",
    64     ("GetLogMessage", {}),
    51     "ResetLogCount",
    65     ("ResetLogCount", {})
    52 ]
    66 ]
       
    67 
       
    68 # de-activated dumb wamp config
       
    69 defaultWampConfig = {
       
    70     "ID": "wamptest",
       
    71     "active": False,
       
    72     "realm": "Automation",
       
    73     "url": "ws://127.0.0.1:8888"
       
    74 }
    53 
    75 
    54 # Those two lists are meant to be filled by customized runtime
    76 # Those two lists are meant to be filled by customized runtime
    55 # or User python code.
    77 # or User python code.
    56 
    78 
    57 """ crossbar Events to register to """
    79 """ crossbar Events to register to """
    58 SubscribedEvents = []
    80 SubscribedEvents = []
    59 
    81 
    60 """ things to do on join (callables) """
    82 """ things to do on join (callables) """
    61 DoOnJoin = []
    83 DoOnJoin = []
       
    84 
       
    85 lastKnownConfig = None
    62 
    86 
    63 
    87 
    64 def GetCallee(name):
    88 def GetCallee(name):
    65     """ Get Callee or Subscriber corresponding to '.' spearated object path """
    89     """ Get Callee or Subscriber corresponding to '.' spearated object path """
    66     names = name.split('.')
    90     names = name.split('.')
    69         obj = getattr(obj, names.pop(0))
    93         obj = getattr(obj, names.pop(0))
    70     return obj
    94     return obj
    71 
    95 
    72 
    96 
    73 class WampSession(wamp.ApplicationSession):
    97 class WampSession(wamp.ApplicationSession):
       
    98 
    74     def onConnect(self):
    99     def onConnect(self):
    75         if "secret" in self.config.extra:
   100         if "secret" in self.config.extra:
    76             user = self.config.extra["ID"].encode('utf8')
   101             user = self.config.extra["ID"]
    77             self.join(u"Automation", [u"wampcra"], user)
   102             self.join(u"Automation", [u"wampcra"], user)
    78         else:
   103         else:
    79             self.join(u"Automation")
   104             self.join(u"Automation")
    80 
   105 
    81     def onChallenge(self, challenge):
   106     def onChallenge(self, challenge):
    82         if challenge.method == u"wampcra":
   107         if challenge.method == u"wampcra":
    83             secret = self.config.extra["secret"].encode('utf8')
   108             if "secret" in self.config.extra:
    84             signature = auth.compute_wcs(secret, challenge.extra['challenge'].encode('utf8'))
   109                 secret = self.config.extra["secret"].encode('utf8')
    85             return signature.decode("ascii")
   110                 signature = auth.compute_wcs(
    86         else:
   111                     secret, challenge.extra['challenge'].encode('utf8'))
    87             raise Exception("don't know how to handle authmethod {}".format(challenge.method))
   112                 return signature.decode("ascii")
       
   113             else:
       
   114                 raise Exception("no secret given for authentication")
       
   115         else:
       
   116             raise Exception(
       
   117                 "don't know how to handle authmethod {}".format(challenge.method))
    88 
   118 
    89     @inlineCallbacks
   119     @inlineCallbacks
    90     def onJoin(self, details):
   120     def onJoin(self, details):
    91         global _WampSession
   121         global _WampSession
    92         _WampSession = self
   122         _WampSession = self
    93         ID = self.config.extra["ID"]
   123         ID = self.config.extra["ID"]
    94         print('WAMP session joined by :', ID)
   124 
    95         for name in ExposedCalls:
   125         for name, kwargs in ExposedCalls:
    96             regoption = types.RegisterOptions(u'exact', u'last')
   126             try:
    97             yield self.register(GetCallee(name), u'.'.join((ID, name)), regoption)
   127                 registerOptions = types.RegisterOptions(**kwargs)
       
   128             except TypeError as e:
       
   129                 registerOptions = None
       
   130                 print(_("TypeError register option: {}".format(e)))
       
   131 
       
   132             yield self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions)
    98 
   133 
    99         for name in SubscribedEvents:
   134         for name in SubscribedEvents:
   100             yield self.subscribe(GetCallee(name), unicode(name))
   135             yield self.subscribe(GetCallee(name), unicode(name))
   101 
   136 
   102         for func in DoOnJoin:
   137         for func in DoOnJoin:
   103             yield func(self)
   138             yield func(self)
   104 
   139 
       
   140         print(_('WAMP session joined (%s) by:' % time.ctime()), ID)
       
   141 
   105     def onLeave(self, details):
   142     def onLeave(self, details):
   106         global _WampSession
   143         global _WampSession, _transportFactory
       
   144         super(WampSession, self).onLeave(details)
   107         _WampSession = None
   145         _WampSession = None
       
   146         _transportFactory = None
   108         print(_('WAMP session left'))
   147         print(_('WAMP session left'))
   109 
   148 
   110 
   149 
   111 class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory):
   150 class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory):
       
   151 
       
   152     def __init__(self, config, *args, **kwargs):
       
   153         global _transportFactory
       
   154         WampWebSocketClientFactory.__init__(self, *args, **kwargs)
       
   155 
       
   156         try:
       
   157             protocolOptions = config.extra.get('protocolOptions', None)
       
   158             if protocolOptions:
       
   159                 self.setProtocolOptions(**protocolOptions)
       
   160             _transportFactory = self
       
   161         except Exception, e:
       
   162             print(_("Custom protocol options failed :"), e)
       
   163             _transportFactory = None
       
   164 
       
   165     def buildProtocol(self, addr):
       
   166         self.resetDelay()
       
   167         return ReconnectingClientFactory.buildProtocol(self, addr)
       
   168 
   112     def clientConnectionFailed(self, connector, reason):
   169     def clientConnectionFailed(self, connector, reason):
   113         print(_("WAMP Client connection failed (%s) .. retrying .." % time.ctime()))
   170         if self.continueTrying:
   114         ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
   171             print(_("WAMP Client connection failed (%s) .. retrying ..") %
       
   172                   time.ctime())
       
   173             super(ReconnectingWampWebSocketClientFactory,
       
   174                   self).clientConnectionFailed(connector, reason)
       
   175         else:
       
   176             del connector
   115 
   177 
   116     def clientConnectionLost(self, connector, reason):
   178     def clientConnectionLost(self, connector, reason):
   117         print(_("WAMP Client connection lost (%s) .. retrying .." % time.ctime()))
   179         if self.continueTrying:
   118         ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
   180             print(_("WAMP Client connection lost (%s) .. retrying ..") %
   119 
   181                   time.ctime())
   120 
   182             super(ReconnectingWampWebSocketClientFactory,
   121 def LoadWampClientConf(wampconf):
   183                   self).clientConnectionFailed(connector, reason)
   122     try:
   184         else:
   123         WSClientConf = json.load(open(wampconf))
   185             del connector
   124         return WSClientConf
   186 
   125     except ValueError, ve:
   187 
   126         print(_("WAMP load error: "), ve)
   188 def CheckConfiguration(WampClientConf):
   127         return None
   189     url = WampClientConf["url"]
   128     except Exception:
   190     if not IsCorrectUri(url):
   129         return None
   191         raise annotate.ValidateError(
       
   192             {"url": "Invalid URL: {}".format(url)},
       
   193             _("WAMP configuration error:"))
       
   194 
       
   195 
       
   196 def GetConfiguration():
       
   197     global lastKnownConfig
       
   198 
       
   199     if os.path.exists(_WampConf):
       
   200         WampClientConf = json.load(open(_WampConf))
       
   201     else:
       
   202         WampClientConf = defaultWampConfig.copy()
       
   203 
       
   204     for itemName in mandatoryConfigItems:
       
   205         if WampClientConf.get(itemName, None) is None:
       
   206             raise Exception(
       
   207                 _("WAMP configuration error : missing '{}' parameter.").format(itemName))
       
   208 
       
   209     CheckConfiguration(WampClientConf)
       
   210 
       
   211     lastKnownConfig = WampClientConf.copy()
       
   212     return WampClientConf
       
   213 
       
   214 
       
   215 def SetWampSecret(wampSecret):
       
   216     with open(os.path.realpath(_WampSecret), 'w') as f:
       
   217         f.write(wampSecret)
       
   218 
       
   219 
       
   220 def SetConfiguration(WampClientConf):
       
   221     global lastKnownConfig
       
   222 
       
   223     CheckConfiguration(WampClientConf)
       
   224 
       
   225     lastKnownConfig = WampClientConf.copy()
       
   226 
       
   227     with open(os.path.realpath(_WampConf), 'w') as f:
       
   228         json.dump(WampClientConf, f, sort_keys=True, indent=4)
       
   229     if 'active' in WampClientConf and WampClientConf['active']:
       
   230         if _transportFactory and _WampSession:
       
   231             StopReconnectWampClient()
       
   232         StartReconnectWampClient()
       
   233     else:
       
   234         StopReconnectWampClient()
       
   235 
       
   236     return WampClientConf
   130 
   237 
   131 
   238 
   132 def LoadWampSecret(secretfname):
   239 def LoadWampSecret(secretfname):
   133     try:
   240     WSClientWampSecret = open(secretfname, 'rb').read()
   134         WSClientWampSecret = open(secretfname, 'rb').read()
   241     if len(WSClientWampSecret) == 0:
   135         return WSClientWampSecret
   242         raise Exception(_("WAMP secret empty"))
   136     except ValueError, ve:
   243     return WSClientWampSecret
   137         print(_("Wamp secret load error:"), ve)
   244 
   138         return None
   245 
   139     except Exception:
   246 def IsCorrectUri(uri):
   140         return None
   247     return re.match(r'wss?://[^\s?:#-]+(:[0-9]+)?(/[^\s]*)?$', uri) is not None
   141 
   248 
   142 
   249 
   143 def RegisterWampClient(wampconf, secretfname):
   250 def RegisterWampClient(wampconf=None, wampsecret=None):
   144 
   251     global _WampConf, _WampSecret
   145     WSClientConf = LoadWampClientConf(wampconf)
   252     _WampConfDefault = os.path.join(WorkingDir, "wampconf.json")
   146 
   253     _WampSecretDefault = os.path.join(WorkingDir, "wamp.secret")
   147     if not WSClientConf:
   254 
   148         print(_("WAMP client connection not established!"))
   255     # set config file path only if not already set
       
   256     if _WampConf is None:
       
   257         # default project's wampconf has precedance over commandline given
       
   258         if os.path.exists(_WampConfDefault) or wampconf is None:
       
   259             _WampConf = _WampConfDefault
       
   260         else:
       
   261             _WampConf = wampconf
       
   262 
       
   263     WampClientConf = GetConfiguration()
       
   264 
       
   265     # set secret file path only if not already set
       
   266     if _WampSecret is None:
       
   267         # default project's wamp secret also
       
   268         # has precedance over commandline given
       
   269         if os.path.exists(_WampSecretDefault):
       
   270             _WampSecret = _WampSecretDefault
       
   271         else:
       
   272             _WampSecret = wampsecret
       
   273 
       
   274     if _WampSecret is not None:
       
   275         WampClientConf["secret"] = LoadWampSecret(_WampSecret)
       
   276     else:
       
   277         print(_("WAMP authentication has no secret configured"))
       
   278         _WampSecret = _WampSecretDefault
       
   279 
       
   280     if not WampClientConf["active"]:
       
   281         print(_("WAMP deactivated in configuration"))
   149         return
   282         return
   150 
       
   151     WampSecret = LoadWampSecret(secretfname)
       
   152 
       
   153     if WampSecret is not None:
       
   154         WSClientConf["secret"] = WampSecret
       
   155 
   283 
   156     # create a WAMP application session factory
   284     # create a WAMP application session factory
   157     component_config = types.ComponentConfig(
   285     component_config = types.ComponentConfig(
   158         realm=WSClientConf["realm"],
   286         realm=WampClientConf["realm"],
   159         extra=WSClientConf)
   287         extra=WampClientConf)
   160     session_factory = wamp.ApplicationSessionFactory(
   288     session_factory = wamp.ApplicationSessionFactory(
   161         config=component_config)
   289         config=component_config)
   162     session_factory.session = WampSession
   290     session_factory.session = WampSession
   163 
   291 
   164     # create a WAMP-over-WebSocket transport client factory
   292     # create a WAMP-over-WebSocket transport client factory
   165     transport_factory = ReconnectingWampWebSocketClientFactory(
   293     ReconnectingWampWebSocketClientFactory(
       
   294         component_config,
   166         session_factory,
   295         session_factory,
   167         url=WSClientConf["url"],
   296         url=WampClientConf["url"],
   168         serializers=[MsgPackSerializer()])
   297         serializers=[MsgPackSerializer()])
   169 
   298 
   170     # start the client from a Twisted endpoint
   299     # start the client from a Twisted endpoint
   171     conn = connectWS(transport_factory)
   300     if _transportFactory:
   172     print(_("WAMP client connecting to :"), WSClientConf["url"])
   301         connectWS(_transportFactory)
   173     return conn
   302         print(_("WAMP client connecting to :"), WampClientConf["url"])
       
   303         return True
       
   304     else:
       
   305         print(_("WAMP client can not connect to :"), WampClientConf["url"])
       
   306         return False
       
   307 
       
   308 
       
   309 def StopReconnectWampClient():
       
   310     if _transportFactory is not None:
       
   311         _transportFactory.stopTrying()
       
   312     if _WampSession is not None:
       
   313         _WampSession.leave()
       
   314 
       
   315 
       
   316 def StartReconnectWampClient():
       
   317     if _WampSession:
       
   318         # do reconnect
       
   319         _WampSession.disconnect()
       
   320         return True
       
   321     else:
       
   322         # do connect
       
   323         RegisterWampClient()
       
   324         return True
   174 
   325 
   175 
   326 
   176 def GetSession():
   327 def GetSession():
   177     return _WampSession
   328     return _WampSession
   178 
   329 
   179 
   330 
       
   331 def getWampStatus():
       
   332     if _transportFactory is not None:
       
   333         if _WampSession is not None:
       
   334             if _WampSession.is_attached():
       
   335                 return "Attached"
       
   336             return "Established"
       
   337         return "Connecting"
       
   338     return "Disconnected"
       
   339 
       
   340 
   180 def SetServer(pysrv):
   341 def SetServer(pysrv):
   181     global _PySrv
       
   182     _PySrv = pysrv
   342     _PySrv = pysrv
       
   343 
       
   344 
       
   345 # WEB CONFIGURATION INTERFACE
       
   346 WAMP_SECRET_URL = "secret"
       
   347 webExposedConfigItems = ['active', 'url', 'ID']
       
   348 
       
   349 
       
   350 def wampConfigDefault(ctx, argument):
       
   351     if lastKnownConfig is not None:
       
   352         return lastKnownConfig.get(argument.name, None)
       
   353 
       
   354 
       
   355 def wampConfig(**kwargs):
       
   356     secretfile_field = kwargs["secretfile"]
       
   357     if secretfile_field is not None:
       
   358         secretfile = getattr(secretfile_field, "file", None)
       
   359         if secretfile is not None:
       
   360             secret = secretfile_field.file.read()
       
   361             SetWampSecret(secret)
       
   362 
       
   363     newConfig = lastKnownConfig.copy()
       
   364     for argname in webExposedConfigItems:
       
   365         arg = kwargs.get(argname, None)
       
   366         if arg is not None:
       
   367             newConfig[argname] = arg
       
   368 
       
   369     SetConfiguration(newConfig)
       
   370 
       
   371 
       
   372 class FileUploadDownload(annotate.FileUpload):
       
   373     pass
       
   374 
       
   375 
       
   376 class FileUploadDownloadRenderer(webform.FileUploadRenderer):
       
   377 
       
   378     def input(self, context, slot, data, name, value):
       
   379         # pylint: disable=expression-not-assigned
       
   380         slot[_("Upload:")]
       
   381         slot = webform.FileUploadRenderer.input(
       
   382             self, context, slot, data, name, value)
       
   383         download_url = data.typedValue.getAttribute('download_url')
       
   384         return slot[tags.a(href=download_url)[_("Download")]]
       
   385 
       
   386 
       
   387 registerAdapter(FileUploadDownloadRenderer, FileUploadDownload,
       
   388                 formless.iformless.ITypedRenderer)
       
   389 
       
   390 
       
   391 def getDownloadUrl(ctx, argument):
       
   392     if lastKnownConfig is not None:
       
   393         return url.URL.fromContext(ctx).\
       
   394             child(WAMP_SECRET_URL).\
       
   395             child(lastKnownConfig["ID"] + ".secret")
       
   396 
       
   397 
       
   398 webFormInterface = [
       
   399     ("status",
       
   400      annotate.String(label=_("Current status"),
       
   401                      immutable=True,
       
   402                      default=lambda *k:getWampStatus())),
       
   403     ("ID",
       
   404      annotate.String(label=_("ID"),
       
   405                      default=wampConfigDefault)),
       
   406     ("secretfile",
       
   407      FileUploadDownload(label=_("File containing secret for that ID"),
       
   408                         download_url=getDownloadUrl)),
       
   409     ("active",
       
   410      annotate.Boolean(label=_("Enable WAMP connection"),
       
   411                       default=wampConfigDefault)),
       
   412     ("url",
       
   413      annotate.String(label=_("WAMP Server URL"),
       
   414                      default=wampConfigDefault))]
       
   415 
       
   416 
       
   417 def deliverWampSecret(ctx, segments):
       
   418     # filename = segments[1].decode('utf-8')
       
   419 
       
   420     # FIXME: compare filename to ID+".secret"
       
   421     # for now all url under /secret returns the secret
       
   422 
       
   423     # TODO: make beautifull message in case of exception
       
   424     # while loading secret (if empty or dont exist)
       
   425     secret = LoadWampSecret(_WampSecret)
       
   426     return static.Data(secret, 'application/octet-stream'), ()
       
   427 
       
   428 
       
   429 def RegisterWebSettings(NS):
       
   430     NS.ConfigurableSettings.addExtension(
       
   431         "wamp",
       
   432         _("Wamp Settings"),
       
   433         webFormInterface,
       
   434         _("Set"),
       
   435         wampConfig)
       
   436 
       
   437     NS.customSettingsURLs[WAMP_SECRET_URL] = deliverWampSecret