5 # Copyright 2012 Intel Corporation.
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
11 # http://www.apache.org/licenses/LICENSE-2.0
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
19 # Luc Yriarte <luc.yriarte@intel.com>
20 # Christophe Guiraud <christophe.guiraud@intel.com>
24 import argparse, dbus, json, sys
26 from twisted.internet import glib2reactor
27 # Configure the twisted mainloop to be run inside the glib mainloop.
28 # This must be done before importing the other twisted modules
29 glib2reactor.install()
30 from twisted.internet import reactor, defer
32 from autobahn.websocket import listenWS
33 from autobahn.wamp import exportRpc, WampServerFactory, WampCraServerProtocol
35 from dbus.mainloop.glib import DBusGMainLoop
40 gobject.threads_init()
46 from twisted.python import log
49 from xml.etree.ElementTree import XMLParser
52 ###############################################################################
59 ###############################################################################
62 Global cache of DBus connexions and signal handlers
65 self.dbusConnexions = {}
66 self.signalHandlers = {}
71 Disconnect signal handlers before resetting cache.
73 self.dbusConnexions = {}
74 # disconnect signal handlers
75 for key in self.signalHandlers:
76 self.signalHandlers[key].disconnect()
77 self.signalHandlers = {}
80 def dbusConnexion(self, busName):
81 if not self.dbusConnexions.has_key(busName):
82 if busName == "session":
83 self.dbusConnexions[busName] = dbus.SessionBus()
84 elif busName == "system":
85 self.dbusConnexions[busName] = dbus.SystemBus()
87 raise Exception("Error: invalid bus: %s" % busName)
88 return self.dbusConnexions[busName]
92 ###############################################################################
93 class DbusSignalHandler:
95 signal hash id as busName#senderName#objectName#interfaceName#signalName
97 def __init__(self, busName, senderName, objectName, interfaceName, signalName):
98 self.id = "#".join([busName, senderName, objectName, interfaceName, signalName])
99 # connect handler to signal
100 self.bus = cache.dbusConnexion(busName)
101 self.bus.add_signal_receiver(self.handleSignal, signalName, interfaceName, senderName, objectName)
104 def disconnect(self):
105 names = self.id.split("#")
106 self.bus.remove_signal_receiver(self.handleSignal, names[4], names[3], names[1], names[2])
109 def handleSignal(self, *args):
111 publish dbus args under topic hash id
113 factory.dispatch(self.id, json.dumps(args))
117 ###############################################################################
118 class DbusCallHandler:
120 deferred reply to return dbus results
122 def __init__(self, method, args):
124 self.request = defer.Deferred()
129 def callMethod(self):
131 dbus method async call
134 self.method(*self.args, reply_handler=self.dbusSuccess, error_handler=self.dbusError)
138 def dbusSuccess(self, *result):
140 return JSON string result array
142 self.request.callback(json.dumps(result))
146 def dbusError(self, error):
148 return dbus error message
150 self.request.errback(Exception(error.get_dbus_message()))
155 ################################################################################
157 def __init__(self, globalCtx, localCtx) :
158 self.exec_string = ""
159 self.exec_code = None
160 self.exec_code_valid = 1
161 self.indent_level = 0
162 self.indent_increment = 1
164 self.localCtx = localCtx
165 self.globalCtx = globalCtx
168 def append_stmt(self, stmt) :
169 self.exec_code_valid = 0
171 for x in range(0,self.indent_level):
172 self.exec_string = self.exec_string + ' '
173 self.exec_string = self.exec_string + stmt + '\n'
176 self.indent_level = self.indent_level + self.indent_increment
179 self.indent_level = self.indent_level - self.indent_increment
181 # compile : Compile exec_string into exec_code using the builtin
182 # compile function. Skip if already in sync.
184 if not self.exec_code_valid :
185 self.exec_code = compile(self.exec_string, "<string>", "exec")
186 self.exec_code_valid = True
189 if not self.exec_code_valid :
191 exec(self.exec_code, self.globalCtx, self.localCtx)
195 ################################################################################
196 class XmlCb_Parser: # The target object of the parser
199 def __init__(self, dynDBusClass):
200 self.dynDBusClass = dynDBusClass
202 def start(self, tag, attrib): # Called for each opening tag.
206 if (tag == 'interface'):
207 self.dynDBusClass.set_interface(attrib['name'])
210 if (tag == 'method'):
212 self.dynDBusClass.def_method(attrib['name'])
214 if (tag == 'signal'):
216 self.dynDBusClass.def_signal(attrib['name'])
219 # Set signature (in/out & name) for method
221 if (self.current == 'method'):
222 if (attrib.has_key('direction') == False):
223 attrib['direction'] = "in"
224 self.dynDBusClass.add_signature(attrib['name'],
228 if (self.current == 'signal'):
229 self.dynDBusClass.add_signature(attrib['name'], 'in',
232 def end(self, tag): # Called for each closing tag.
233 if (tag == 'method'):
234 self.dynDBusClass.add_dbus_method()
235 self.dynDBusClass.add_body_method()
236 self.dynDBusClass.end_method()
237 if (tag == 'signal'):
238 self.dynDBusClass.add_dbus_signal()
239 self.dynDBusClass.add_body_signal()
240 self.dynDBusClass.end_method()
242 def data(self, data):
243 pass # We do not need to do anything with data.
244 def close(self): # Called when all data has been parsed.
249 ################################################################################
250 class dynDBusClass():
251 def __init__(self, className, globalCtx, localCtx):
252 self.className = className
253 self.xmlCB = XmlCb_Parser(self)
255 self.class_code = exec_code(globalCtx, localCtx)
256 self.class_code.indent_increment = 4
257 self.class_code.append_stmt("import dbus")
258 self.class_code.append_stmt("\n")
259 self.class_code.append_stmt("class " + self.className + "(dbus.service.Object):")
260 self.class_code.indent()
262 ## Overload of __init__ method
263 self.def_method("__init__")
264 self.add_method("bus, callback=None, objName='/sample', busName='org.cloudeebus'")
265 self.add_stmt("self.bus = bus")
266 self.add_stmt("self.objName = objName")
267 self.add_stmt("self.callback = callback")
268 self.add_stmt("dbus.service.Object.__init__(self, conn=bus, bus_name=busName)")
271 ## Create 'add_to_connection' method
272 self.def_method("add_to_connection")
273 self.add_method("connection=None, path=None")
274 self.add_stmt("dbus.service.Object.add_to_connection(self, connection=self.bus, path=self.objName)")
277 ## Create 'remove_from_connection' method
278 self.def_method("remove_from_connection")
279 self.add_method("connection=None, path=None")
280 self.add_stmt("dbus.service.Object.remove_from_connection(self, connection=None, path=self.objName)")
283 def createDBusServiceFromXML(self, xml):
284 self.parser = XMLParser(target=self.xmlCB)
285 self.parser.feed(xml)
288 def set_interface(self, ifName):
291 def def_method(self, methodName):
292 self.methodToAdd = methodName
293 self.signalToAdd = None
294 self.args_str = str()
296 self.signature['name'] = str()
297 self.signature['in'] = str()
298 self.signature['out'] = str()
300 def def_signal(self, signalName):
301 self.methodToAdd = None
302 self.signalToAdd = signalName
303 self.args_str = str()
305 self.signature['name'] = str()
306 self.signature['in'] = str()
307 self.signature['out'] = str()
309 def add_signature(self, name, direction, signature):
310 if (direction == 'in'):
311 self.signature['in'] += signature
312 if (self.signature['name'] != str()):
313 self.signature['name'] += ", "
314 self.signature['name'] += name
315 if (direction == 'out'):
316 self.signature['out'] = signature
318 def add_method(self, args = None, async_success_cb = None, async_err_cb = None):
320 if (self.methodToAdd != None):
321 name = self.methodToAdd
323 name = self.signalToAdd
326 if (async_success_cb != None):
327 async_cb_str = async_success_cb
328 if (async_err_cb != None):
329 if (async_cb_str != str()):
331 async_cb_str += async_err_cb
333 parameters = self.args_str
334 if (async_cb_str != str()):
335 if (parameters != str()):
337 parameters +=async_cb_str
339 if (parameters != str()):
340 self.class_code.append_stmt("def " + name + "(self, %s):" % parameters)
342 self.class_code.append_stmt("def " + name + "(self):")
343 self.class_code.indent()
345 def end_method(self):
346 self.class_code.append_stmt("\n")
347 self.class_code.dedent()
349 def add_dbus_method(self):
350 decorator = '@dbus.service.method("' + self.ifName + '"'
351 if (self.signature.has_key('in') and self.signature['in'] != str()):
352 decorator += ", in_signature='" + self.signature['in'] + "'"
353 if (self.signature.has_key('out') and self.signature['out'] != str()):
354 decorator += ", out_signature='" + self.signature['out'] + "'"
355 decorator += ", async_callbacks=('dbus_async_cb', 'dbus_async_err_cb')"
357 self.class_code.append_stmt(decorator)
358 if (self.signature.has_key('name') and self.signature['name'] != str()):
359 self.add_method(self.signature['name'], async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
361 self.add_method(async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
363 def add_dbus_signal(self):
364 decorator = '@dbus.service.signal("' + self.ifName + '"'
365 if (self.signature.has_key('in') and self.signature['in'] != str()):
366 decorator += ", signature='" + self.signature['in'] + "'"
368 self.class_code.append_stmt(decorator)
369 if (self.signature.has_key('name') and self.signature['name'] != str()):
370 self.add_method(self.signature['name'])
374 def add_body_method(self):
375 if (self.methodToAdd != None):
376 if (self.args_str != str()):
377 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', dbus_async_cb, dbus_async_err_cb, %s)" % self.args_str)
379 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', dbus_async_cb, dbus_async_err_cb)")
381 def add_body_signal(self):
382 self.class_code.append_stmt("return") ## TODO: Remove and fix with code ad hoc
383 self.class_code.append_stmt("\n")
385 def add_stmt(self, stmt) :
386 self.class_code.append_stmt(stmt)
389 self.class_code.execute()
393 ###############################################################################
394 class CloudeebusService:
396 support for sending DBus messages and registering for DBus signals
398 def __init__(self, permissions):
399 self.permissions = permissions;
400 self.proxyObjects = {}
401 self.proxyMethods = {}
402 self.pendingCalls = []
403 self.dynDBusClasses = {} # DBus class source code generated dynamically (a list because one by classname)
404 self.services = {} # DBus service created
405 self.serviceAgents = {} # Instantiated DBus class previously generated dynamically, for now, one by classname
406 self.servicePendingCalls = {} # JS methods called (and waiting for a Success/error response), containing 'methodId', (successCB, errorCB)
407 self.localCtx = locals()
408 self.globalCtx = globals()
411 def proxyObject(self, busName, serviceName, objectName):
413 object hash id as busName#serviceName#objectName
415 id = "#".join([busName, serviceName, objectName])
416 if not self.proxyObjects.has_key(id):
418 # check permissions, array.index throws exception
419 self.permissions.index(serviceName)
420 bus = cache.dbusConnexion(busName)
421 self.proxyObjects[id] = bus.get_object(serviceName, objectName)
422 return self.proxyObjects[id]
425 def proxyMethod(self, busName, serviceName, objectName, interfaceName, methodName):
427 method hash id as busName#serviceName#objectName#interfaceName#methodName
429 id = "#".join([busName, serviceName, objectName, interfaceName, methodName])
430 if not self.proxyMethods.has_key(id):
431 obj = self.proxyObject(busName, serviceName, objectName)
432 self.proxyMethods[id] = obj.get_dbus_method(methodName, interfaceName)
433 return self.proxyMethods[id]
437 def dbusRegister(self, list):
439 arguments: bus, sender, object, interface, signal
442 raise Exception("Error: expected arguments: bus, sender, object, interface, signal)")
445 # check permissions, array.index throws exception
446 self.permissions.index(list[1])
448 # check if a handler exists
449 sigId = "#".join(list)
450 if cache.signalHandlers.has_key(sigId):
453 # create a handler that will publish the signal
454 dbusSignalHandler = DbusSignalHandler(*list)
455 cache.signalHandlers[sigId] = dbusSignalHandler
457 return dbusSignalHandler.id
461 def dbusSend(self, list):
463 arguments: bus, destination, object, interface, message, [args]
465 # clear pending calls
466 for call in self.pendingCalls:
468 self.pendingCalls.remove(call)
471 raise Exception("Error: expected arguments: bus, destination, object, interface, message, [args])")
473 # parse JSON arg list
476 args = json.loads(list[5])
478 # get dbus proxy method
479 method = self.proxyMethod(*list[0:5])
481 # use a deferred call handler to manage dbus results
482 dbusCallHandler = DbusCallHandler(method, args)
483 self.pendingCalls.append(dbusCallHandler)
484 return dbusCallHandler.callMethod()
488 def returnMethod(self, list):
490 arguments: methodId, success (=true, error otherwise), result (to return)
495 if (self.servicePendingCalls.has_key(methodId)):
496 cb = self.servicePendingCalls[methodId]
498 successCB = cb["successCB"]
504 errorCB = cb["errorCB"]
509 self.servicePendingCalls[methodId] = None
511 raise Exception("No methodID " + methodId)
513 def jsonEncodeTupleKeyDict(self, data):
515 # creates new dictionary with the original tuple converted to json string
518 for index in range(dataLen):
519 for key in data[index]:
520 value = data[index][key]
522 print "value=" + str(value)
525 print "JSON key=" + nkey
526 if (isinstance(value, dbus.Array)):
527 # Searching dbus byte in array...
528 ValueLen = len(value)
530 for indexValue in range(ValueLen):
531 a = value[indexValue]
532 if (isinstance(a, dbus.Byte)):
533 a = int(value[indexValue])
536 nvalue = str(value[indexValue])
538 print "JSON value=" + str(nvalue)
543 def srvCB(self, name, async_succes_cb, async_error_cb, *args):
544 methodId = self.srvName + "#" + self.agentObjectPath + "#" + name
545 cb = { 'successCB': async_succes_cb,
546 'errorCB': async_error_cb}
547 self.servicePendingCalls[methodId] = cb
550 print "Received args=%s" % (str(args))
552 print "No args received"
555 print "factory.dispatch(methodId=%s, args=%s)" % (methodId, json.dumps(args))
556 factory.dispatch(methodId, json.dumps(args))
558 except Exception, e :
559 print "Error=%s" % (str(e))
561 print "Trying to decode dbus.Dictionnary..."
563 params = self.jsonEncodeTupleKeyDict(args)
564 print "factory.dispatch(methodId=%s, args=%s)" % (methodId, params)
565 factory.dispatch(methodId, params)
567 except Exception, e :
568 print "Error=%s" % (str(e))
570 print "Trying to pass args as string..."
572 print "factory.dispatch(methodId=%s, args=%s)" % (methodId, str(args))
573 factory.dispatch(methodId, str(args))
575 except Exception, e :
576 print "Error=%s" % (str(e))
579 def serviceAdd(self, list):
581 arguments: busName, srvName
584 self.bus = cache.dbusConnexion( busName['name'] )
585 self.srvName = list[1]
586 if (self.services.has_key(self.srvName) == False):
587 self.services[self.srvName] = dbus.service.BusName(name = self.srvName, bus = self.bus)
591 def serviceRelease(self, list):
593 arguments: busName, srvName
595 self.srvName = list[0]
596 if (self.services.has_key(self.srvName) == True):
597 self.services.pop(self.srvName)
600 raise Exception(self.srvName + " do not exist")
603 def serviceAddAgent(self, list):
605 arguments: objectPath, xmlTemplate
607 self.agentObjectPath = list[0]
608 xmlTemplate = list[1]
609 self.className = re.sub('/', '_', self.agentObjectPath[1:])
610 if (self.dynDBusClasses.has_key(self.className) == False):
611 self.dynDBusClasses[self.className] = dynDBusClass(self.className, self.globalCtx, self.localCtx)
612 self.dynDBusClasses[self.className].createDBusServiceFromXML(xmlTemplate)
613 self.dynDBusClasses[self.className].declare()
615 ## Class already exist, instanciate it if not already instanciated
616 if (self.serviceAgents.has_key(self.className) == False):
617 self.serviceAgents[self.className] = eval(self.className + "(self.bus, callback=self.srvCB, objName=self.agentObjectPath, busName=self.srvName)", self.globalCtx, self.localCtx)
619 self.serviceAgents[self.className].add_to_connection()
620 return (self.agentObjectPath)
623 def serviceDelAgent(self, list):
625 arguments: objectPath, xmlTemplate
627 agentObjectPath = list[0]
628 className = re.sub('/', '_', agentObjectPath[1:])
630 if (self.serviceAgents.has_key(className)):
631 self.serviceAgents[self.className].remove_from_connection()
632 self.serviceAgents.pop(self.className)
634 raise Exception(agentObjectPath + " doesn't exist!")
636 return (agentObjectPath)
639 def getVersion(self):
641 return current version string
647 ###############################################################################
648 class CloudeebusServerProtocol(WampCraServerProtocol):
650 connexion and session authentication management
653 def onSessionOpen(self):
654 # CRA authentication options
655 self.clientAuthTimeout = 0
656 self.clientAuthAllowAnonymous = OPENDOOR
657 # CRA authentication init
658 WampCraServerProtocol.onSessionOpen(self)
661 def getAuthPermissions(self, key, extra):
662 return json.loads(extra.get("permissions", "[]"))
665 def getAuthSecret(self, key):
666 secret = CREDENTIALS.get(key, None)
669 # secret must be of str type to be hashed
670 return secret.encode('utf-8')
673 def onAuthenticated(self, key, permissions):
675 # check authentication key
677 raise Exception("Authentication failed")
678 # check permissions, array.index throws exception
679 for req in permissions:
681 # create cloudeebus service instance
682 self.cloudeebusService = CloudeebusService(permissions)
683 # register it for RPC
684 self.registerForRpc(self.cloudeebusService)
685 # register for Publish / Subscribe
686 self.registerForPubSub("", True)
689 def connectionLost(self, reason):
690 WampCraServerProtocol.connectionLost(self, reason)
691 if factory.getConnectionCount() == 0:
696 ###############################################################################
698 if __name__ == '__main__':
702 parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
703 parser.add_argument('-v', '--version', action='store_true',
704 help='print version and exit')
705 parser.add_argument('-d', '--debug', action='store_true',
706 help='log debug info on standard output')
707 parser.add_argument('-o', '--opendoor', action='store_true',
708 help='allow anonymous access to all services')
709 parser.add_argument('-p', '--port', default='9000',
711 parser.add_argument('-c', '--credentials',
712 help='path to credentials file')
713 parser.add_argument('-w', '--whitelist',
714 help='path to whitelist file')
716 args = parser.parse_args(sys.argv[1:])
719 print("Cloudeebus version " + VERSION)
723 log.startLogging(sys.stdout)
725 OPENDOOR = args.opendoor
728 jfile = open(args.credentials)
729 CREDENTIALS = json.load(jfile)
733 jfile = open(args.whitelist)
734 WHITELIST = json.load(jfile)
737 uri = "ws://localhost:" + args.port
739 factory = WampServerFactory(uri, debugWamp = args.debug)
740 factory.protocol = CloudeebusServerProtocol
741 factory.setProtocolOptions(allowHixie76 = True)
745 DBusGMainLoop(set_as_default=True)