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 self.class_code.append_stmt("print 'In " + self.methodToAdd + "()'")
377 if (self.args_str != str()):
378 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', dbus_async_cb, dbus_async_err_cb, %s)" % self.args_str)
380 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', dbus_async_cb, dbus_async_err_cb)")
382 def add_body_signal(self):
383 self.class_code.append_stmt("return") ## TODO: Remove and fix with code ad hoc
384 self.class_code.append_stmt("\n")
386 def add_stmt(self, stmt) :
387 self.class_code.append_stmt(stmt)
390 self.class_code.execute()
394 ###############################################################################
395 class CloudeebusService:
397 support for sending DBus messages and registering for DBus signals
399 def __init__(self, permissions):
400 self.permissions = permissions;
401 self.proxyObjects = {}
402 self.proxyMethods = {}
403 self.pendingCalls = []
404 self.dynDBusClasses = {} # DBus class source code generated dynamically (a list because one by classname)
405 self.services = {} # DBus service created
406 self.serviceAgents = {} # Instantiated DBus class previously generated dynamically, for now, one by classname
407 self.servicePendingCalls = {} # JS methods called (and waiting for a Success/error response), containing 'methodId', (successCB, errorCB)
408 self.localCtx = locals()
409 self.globalCtx = globals()
412 def proxyObject(self, busName, serviceName, objectName):
414 object hash id as busName#serviceName#objectName
416 id = "#".join([busName, serviceName, objectName])
417 if not self.proxyObjects.has_key(id):
419 # check permissions, array.index throws exception
420 self.permissions.index(serviceName)
421 bus = cache.dbusConnexion(busName)
422 self.proxyObjects[id] = bus.get_object(serviceName, objectName)
423 return self.proxyObjects[id]
426 def proxyMethod(self, busName, serviceName, objectName, interfaceName, methodName):
428 method hash id as busName#serviceName#objectName#interfaceName#methodName
430 id = "#".join([busName, serviceName, objectName, interfaceName, methodName])
431 if not self.proxyMethods.has_key(id):
432 obj = self.proxyObject(busName, serviceName, objectName)
433 self.proxyMethods[id] = obj.get_dbus_method(methodName, interfaceName)
434 return self.proxyMethods[id]
438 def dbusRegister(self, list):
440 arguments: bus, sender, object, interface, signal
443 raise Exception("Error: expected arguments: bus, sender, object, interface, signal)")
446 # check permissions, array.index throws exception
447 self.permissions.index(list[1])
449 # check if a handler exists
450 sigId = "#".join(list)
451 if cache.signalHandlers.has_key(sigId):
454 # create a handler that will publish the signal
455 dbusSignalHandler = DbusSignalHandler(*list)
456 cache.signalHandlers[sigId] = dbusSignalHandler
458 return dbusSignalHandler.id
462 def dbusSend(self, list):
464 arguments: bus, destination, object, interface, message, [args]
466 # clear pending calls
467 for call in self.pendingCalls:
469 self.pendingCalls.remove(call)
472 raise Exception("Error: expected arguments: bus, destination, object, interface, message, [args])")
474 # parse JSON arg list
477 args = json.loads(list[5])
479 # get dbus proxy method
480 method = self.proxyMethod(*list[0:5])
482 # use a deferred call handler to manage dbus results
483 dbusCallHandler = DbusCallHandler(method, args)
484 self.pendingCalls.append(dbusCallHandler)
485 return dbusCallHandler.callMethod()
489 def returnMethod(self, list):
491 arguments: methodId, success (=true, error otherwise), result (to return)
496 if (self.servicePendingCalls.has_key(methodId)):
497 cb = self.servicePendingCalls[methodId]
499 successCB = cb["successCB"]
505 errorCB = cb["errorCB"]
510 self.servicePendingCalls[methodId] = None
512 print "No methodID %s !!" % (methodId)
514 def jsonEncodeTupleKeyDict(self, data):
516 # creates new dictionary with the original tuple converted to json string
519 for index in range(dataLen):
520 for key in data[index]:
521 value = data[index][key]
523 print "value=" + str(value)
526 print "JSON key=" + nkey
527 if (isinstance(value, dbus.Array)):
528 # Searching dbus byte in array...
529 ValueLen = len(value)
531 for indexValue in range(ValueLen):
532 a = value[indexValue]
533 if (isinstance(a, dbus.Byte)):
534 a = int(value[indexValue])
537 nvalue = str(value[indexValue])
539 print "JSON value=" + str(nvalue)
544 def srvCB(self, name, async_succes_cb, async_error_cb, *args):
545 methodId = self.srvName + "#" + self.agentObjectPath + "#" + name
546 cb = { 'successCB': async_succes_cb,
547 'errorCB': async_error_cb}
548 self.servicePendingCalls[methodId] = cb
551 print "Received args=%s" % (str(args))
553 print "No args received"
556 print "factory.dispatch(methodId=%s, args=%s)" % (methodId, json.dumps(args))
557 factory.dispatch(methodId, json.dumps(args))
559 except Exception, e :
560 print "Error=%s" % (str(e))
562 print "Trying to decode dbus.Dictionnary..."
564 params = self.jsonEncodeTupleKeyDict(args)
565 print "factory.dispatch(methodId=%s, args=%s)" % (methodId, params)
566 factory.dispatch(methodId, params)
568 except Exception, e :
569 print "Error=%s" % (str(e))
571 print "Trying to pass args as string..."
573 print "factory.dispatch(methodId=%s, args=%s)" % (methodId, str(args))
574 factory.dispatch(methodId, str(args))
576 except Exception, e :
577 print "Error=%s" % (str(e))
580 def serviceAdd(self, list):
582 arguments: busName, srvName
585 self.bus = cache.dbusConnexion( busName['name'] )
586 self.srvName = list[1]
587 if (self.services.has_key(self.srvName) == False):
588 self.services[self.srvName] = dbus.service.BusName(name = self.srvName, bus = self.bus)
592 def serviceRelease(self, list):
594 arguments: busName, srvName
596 self.srvName = list[0]
597 if (self.services.has_key(self.srvName) == True):
598 self.services.pop(self.srvName)
601 raise Exception(self.srvName + " do not exist")
604 def serviceAddAgent(self, list):
606 arguments: objectPath, xmlTemplate
608 self.agentObjectPath = list[0]
609 xmlTemplate = list[1]
610 self.className = re.sub('/', '_', self.agentObjectPath[1:])
611 if (self.dynDBusClasses.has_key(self.className) == False):
612 self.dynDBusClasses[self.className] = dynDBusClass(self.className, self.globalCtx, self.localCtx)
613 self.dynDBusClasses[self.className].createDBusServiceFromXML(xmlTemplate)
614 self.dynDBusClasses[self.className].declare()
616 ## Class already exist, instanciate it if not already instanciated
617 if (self.serviceAgents.has_key(self.className) == False):
618 self.serviceAgents[self.className] = eval(self.className + "(self.bus, callback=self.srvCB, objName=self.agentObjectPath, busName=self.srvName)", self.globalCtx, self.localCtx)
620 self.serviceAgents[self.className].add_to_connection()
621 return (self.agentObjectPath)
624 def serviceDelAgent(self, list):
626 arguments: objectPath, xmlTemplate
628 agentObjectPath = list[0]
629 className = re.sub('/', '_', agentObjectPath[1:])
631 if (self.serviceAgents.has_key(className)):
632 self.serviceAgents[self.className].remove_from_connection()
633 self.serviceAgents.pop(self.className)
635 raise Exception(agentObjectPath + " doesn't exist!")
637 return (agentObjectPath)
640 def getVersion(self):
642 return current version string
648 ###############################################################################
649 class CloudeebusServerProtocol(WampCraServerProtocol):
651 connexion and session authentication management
654 def onSessionOpen(self):
655 # CRA authentication options
656 self.clientAuthTimeout = 0
657 self.clientAuthAllowAnonymous = OPENDOOR
658 # CRA authentication init
659 WampCraServerProtocol.onSessionOpen(self)
662 def getAuthPermissions(self, key, extra):
663 return json.loads(extra.get("permissions", "[]"))
666 def getAuthSecret(self, key):
667 secret = CREDENTIALS.get(key, None)
670 # secret must be of str type to be hashed
671 return secret.encode('utf-8')
674 def onAuthenticated(self, key, permissions):
676 # check authentication key
678 raise Exception("Authentication failed")
679 # check permissions, array.index throws exception
680 for req in permissions:
682 # create cloudeebus service instance
683 self.cloudeebusService = CloudeebusService(permissions)
684 # register it for RPC
685 self.registerForRpc(self.cloudeebusService)
686 # register for Publish / Subscribe
687 self.registerForPubSub("", True)
690 def connectionLost(self, reason):
691 WampCraServerProtocol.connectionLost(self, reason)
692 if factory.getConnectionCount() == 0:
697 ###############################################################################
699 if __name__ == '__main__':
703 parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
704 parser.add_argument('-v', '--version', action='store_true',
705 help='print version and exit')
706 parser.add_argument('-d', '--debug', action='store_true',
707 help='log debug info on standard output')
708 parser.add_argument('-o', '--opendoor', action='store_true',
709 help='allow anonymous access to all services')
710 parser.add_argument('-p', '--port', default='9000',
712 parser.add_argument('-c', '--credentials',
713 help='path to credentials file')
714 parser.add_argument('-w', '--whitelist',
715 help='path to whitelist file')
717 args = parser.parse_args(sys.argv[1:])
720 print("Cloudeebus version " + VERSION)
724 log.startLogging(sys.stdout)
726 OPENDOOR = args.opendoor
729 jfile = open(args.credentials)
730 CREDENTIALS = json.load(jfile)
734 jfile = open(args.whitelist)
735 WHITELIST = json.load(jfile)
738 uri = "ws://localhost:" + args.port
740 factory = WampServerFactory(uri, debugWamp = args.debug)
741 factory.protocol = CloudeebusServerProtocol
742 factory.setProtocolOptions(allowHixie76 = True)
746 DBusGMainLoop(set_as_default=True)