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