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 ################################################################################
158 Execute DynDBusClass generated code
160 def __init__(self, globalCtx, localCtx) :
161 self.exec_string = ""
162 self.exec_code = None
163 self.exec_code_valid = 1
164 self.indent_level = 0
165 self.indent_increment = 1
167 self.localCtx = localCtx
168 self.globalCtx = globalCtx
171 def append_stmt(self, stmt) :
172 self.exec_code_valid = 0
174 for x in range(0,self.indent_level):
175 self.exec_string = self.exec_string + ' '
176 self.exec_string = self.exec_string + stmt + '\n'
179 self.indent_level = self.indent_level + self.indent_increment
182 self.indent_level = self.indent_level - self.indent_increment
184 # compile : Compile exec_string into exec_code using the builtin
185 # compile function. Skip if already in sync.
187 if not self.exec_code_valid :
188 self.exec_code = compile(self.exec_string, "<string>", "exec")
189 self.exec_code_valid = True
192 if not self.exec_code_valid :
194 exec(self.exec_code, self.globalCtx, self.localCtx)
198 ################################################################################
199 class XmlCbParser: # The target object of the parser
202 def __init__(self, dynDBusClass):
203 self.dynDBusClass = dynDBusClass
205 def start(self, tag, attrib): # Called for each opening tag.
209 if (tag == 'interface'):
210 self.dynDBusClass.set_interface(attrib['name'])
213 if (tag == 'method'):
215 self.dynDBusClass.def_method(attrib['name'])
217 if (tag == 'signal'):
219 self.dynDBusClass.def_signal(attrib['name'])
222 # Set signature (in/out & name) for method
224 if (self.current == 'method'):
225 if (attrib.has_key('direction') == False):
226 attrib['direction'] = "in"
227 self.dynDBusClass.add_signature(attrib['name'],
231 if (self.current == 'signal'):
232 if (attrib.has_key('name') == False):
233 attrib['name'] = 'value'
234 self.dynDBusClass.add_signature(attrib['name'], 'in',
237 def end(self, tag): # Called for each closing tag.
238 if (tag == 'method'):
239 self.dynDBusClass.add_dbus_method()
240 self.dynDBusClass.add_body_method()
241 self.dynDBusClass.end_method()
242 if (tag == 'signal'):
243 self.dynDBusClass.add_dbus_signal()
244 self.dynDBusClass.add_body_signal()
245 self.dynDBusClass.end_method()
247 def data(self, data):
248 pass # We do not need to do anything with data.
249 def close(self): # Called when all data has been parsed.
254 ################################################################################
255 class DynDBusClass():
256 def __init__(self, className, globalCtx, localCtx):
257 self.className = className
258 self.xmlCB = XmlCbParser(self)
260 self.class_code = ExecCode(globalCtx, localCtx)
261 self.class_code.indent_increment = 4
262 self.class_code.append_stmt("import dbus")
263 self.class_code.append_stmt("\n")
264 self.class_code.append_stmt("class " + self.className + "(dbus.service.Object):")
265 self.class_code.indent()
267 ## Overload of __init__ method
268 self.def_method("__init__")
269 self.add_method("bus, callback=None, objName='/sample', busName='org.cloudeebus'")
270 self.add_stmt("self.bus = bus")
271 self.add_stmt("self.objName = objName")
272 self.add_stmt("self.callback = callback")
273 self.add_stmt("dbus.service.Object.__init__(self, conn=bus, bus_name=busName)")
276 ## Create 'add_to_connection' method
277 self.def_method("add_to_connection")
278 self.add_method("connection=None, path=None")
279 self.add_stmt("dbus.service.Object.add_to_connection(self, connection=self.bus, path=self.objName)")
282 ## Create 'remove_from_connection' method
283 self.def_method("remove_from_connection")
284 self.add_method("connection=None, path=None")
285 self.add_stmt("dbus.service.Object.remove_from_connection(self, connection=None, path=self.objName)")
288 def createDBusServiceFromXML(self, xml):
289 self.parser = XMLParser(target=self.xmlCB)
290 self.parser.feed(xml)
293 def set_interface(self, ifName):
296 def def_method(self, methodName):
297 self.methodToAdd = methodName
298 self.signalToAdd = None
299 self.args_str = str()
301 self.signature['name'] = str()
302 self.signature['in'] = str()
303 self.signature['out'] = str()
305 def def_signal(self, signalName):
306 self.methodToAdd = None
307 self.signalToAdd = signalName
308 self.args_str = str()
310 self.signature['name'] = str()
311 self.signature['in'] = str()
312 self.signature['out'] = str()
314 def add_signature(self, name, direction, signature):
315 if (direction == 'in'):
316 self.signature['in'] += signature
317 if (self.signature['name'] != str()):
318 self.signature['name'] += ", "
319 self.signature['name'] += name
320 if (direction == 'out'):
321 self.signature['out'] = signature
323 def add_method(self, args = None, async_success_cb = None, async_err_cb = None):
325 if (self.methodToAdd != None):
326 name = self.methodToAdd
328 name = self.signalToAdd
331 if (async_success_cb != None):
332 async_cb_str = async_success_cb
333 if (async_err_cb != None):
334 if (async_cb_str != str()):
336 async_cb_str += async_err_cb
338 parameters = self.args_str
339 if (async_cb_str != str()):
340 if (parameters != str()):
342 parameters +=async_cb_str
344 if (parameters != str()):
345 self.class_code.append_stmt("def " + name + "(self, %s):" % parameters)
347 self.class_code.append_stmt("def " + name + "(self):")
348 self.class_code.indent()
350 def end_method(self):
351 self.class_code.append_stmt("\n")
352 self.class_code.dedent()
354 def add_dbus_method(self):
355 decorator = '@dbus.service.method("' + self.ifName + '"'
356 if (self.signature.has_key('in') and self.signature['in'] != str()):
357 decorator += ", in_signature='" + self.signature['in'] + "'"
358 if (self.signature.has_key('out') and self.signature['out'] != str()):
359 decorator += ", out_signature='" + self.signature['out'] + "'"
360 decorator += ", async_callbacks=('dbus_async_cb', 'dbus_async_err_cb')"
362 self.class_code.append_stmt(decorator)
363 if (self.signature.has_key('name') and self.signature['name'] != str()):
364 self.add_method(self.signature['name'], async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
366 self.add_method(async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
368 def add_dbus_signal(self):
369 decorator = '@dbus.service.signal("' + self.ifName + '"'
370 if (self.signature.has_key('in') and self.signature['in'] != str()):
371 decorator += ", signature='" + self.signature['in'] + "'"
373 self.class_code.append_stmt(decorator)
374 if (self.signature.has_key('name') and self.signature['name'] != str()):
375 self.add_method(self.signature['name'])
379 def add_body_method(self):
380 if (self.methodToAdd != None):
381 if (self.args_str != str()):
382 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', dbus_async_cb, dbus_async_err_cb, %s)" % self.args_str)
384 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', dbus_async_cb, dbus_async_err_cb)")
386 def add_body_signal(self):
387 self.class_code.append_stmt("return") ## TODO: Remove and fix with code ad hoc
388 self.class_code.append_stmt("\n")
390 def add_stmt(self, stmt) :
391 self.class_code.append_stmt(stmt)
394 self.class_code.execute()
398 ###############################################################################
399 class CloudeebusService:
401 support for sending DBus messages and registering for DBus signals
403 def __init__(self, permissions):
404 self.permissions = permissions;
405 self.proxyObjects = {}
406 self.proxyMethods = {}
407 self.pendingCalls = []
408 self.dynDBusClasses = {} # DBus class source code generated dynamically (a list because one by classname)
409 self.services = {} # DBus service created
410 self.serviceAgents = {} # Instantiated DBus class previously generated dynamically, for now, one by classname
411 self.servicePendingCalls = {} # JS methods called (and waiting for a Success/error response), containing 'methodId', (successCB, errorCB)
412 self.localCtx = locals()
413 self.globalCtx = globals()
416 def proxyObject(self, busName, serviceName, objectName):
418 object hash id as busName#serviceName#objectName
420 id = "#".join([busName, serviceName, objectName])
421 if not self.proxyObjects.has_key(id):
423 # check permissions, array.index throws exception
424 self.permissions.index(serviceName)
425 bus = cache.dbusConnexion(busName)
426 self.proxyObjects[id] = bus.get_object(serviceName, objectName)
427 return self.proxyObjects[id]
430 def proxyMethod(self, busName, serviceName, objectName, interfaceName, methodName):
432 method hash id as busName#serviceName#objectName#interfaceName#methodName
434 id = "#".join([busName, serviceName, objectName, interfaceName, methodName])
435 if not self.proxyMethods.has_key(id):
436 obj = self.proxyObject(busName, serviceName, objectName)
437 self.proxyMethods[id] = obj.get_dbus_method(methodName, interfaceName)
438 return self.proxyMethods[id]
442 def dbusRegister(self, list):
444 arguments: bus, sender, object, interface, signal
447 raise Exception("Error: expected arguments: bus, sender, object, interface, signal)")
450 # check permissions, array.index throws exception
451 self.permissions.index(list[1])
453 # check if a handler exists
454 sigId = "#".join(list)
455 if cache.signalHandlers.has_key(sigId):
458 # create a handler that will publish the signal
459 dbusSignalHandler = DbusSignalHandler(*list)
460 cache.signalHandlers[sigId] = dbusSignalHandler
462 return dbusSignalHandler.id
466 def dbusSend(self, list):
468 arguments: bus, destination, object, interface, message, [args]
470 # clear pending calls
471 for call in self.pendingCalls:
473 self.pendingCalls.remove(call)
476 raise Exception("Error: expected arguments: bus, destination, object, interface, message, [args])")
478 # parse JSON arg list
481 args = json.loads(list[5])
483 # get dbus proxy method
484 method = self.proxyMethod(*list[0:5])
486 # use a deferred call handler to manage dbus results
487 dbusCallHandler = DbusCallHandler(method, args)
488 self.pendingCalls.append(dbusCallHandler)
489 return dbusCallHandler.callMethod()
493 def emitSignal(self, list):
495 arguments: agentObjectPath, signalName, result (to emit)
498 className = re.sub('/', '_', objectPath[1:])
501 if (self.serviceAgents.has_key(className) == True):
502 exe_str = "self.serviceAgents['"+ className +"']."+ signalName + "(" + str(result) + ")"
503 eval(exe_str, self.globalCtx, self.localCtx)
505 raise Exception("No object path " + objectPath)
508 def returnMethod(self, list):
510 arguments: methodId, callIndex, success (=true, error otherwise), result (to return)
516 if (self.servicePendingCalls.has_key(methodId)):
517 cb = self.servicePendingCalls[methodId]['calls'][callIndex]
519 raise Exception("No pending call " + str(callIndex) + " for methodID " + methodId)
521 successCB = cb["successCB"]
527 errorCB = cb["errorCB"]
532 self.servicePendingCalls[methodId]['calls'][callIndex] = None
533 self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] - 1
534 if self.servicePendingCalls[methodId]['count'] == 0:
535 del self.servicePendingCalls[methodId]
537 raise Exception("No methodID " + methodId)
539 def srvCB(self, name, async_succes_cb, async_error_cb, *args):
540 methodId = self.srvName + "#" + self.agentObjectPath + "#" + name
541 cb = { 'successCB': async_succes_cb,
542 'errorCB': async_error_cb}
543 if methodId not in self.servicePendingCalls:
544 self.servicePendingCalls[methodId] = {'count': 0, 'calls': []}
545 pendingCallStr = json.dumps({'callIndex': len(self.servicePendingCalls[methodId]['calls']), 'args': args})
546 self.servicePendingCalls[methodId]['calls'].append(cb)
547 self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] + 1
548 factory.dispatch(methodId, pendingCallStr)
551 def serviceAdd(self, list):
553 arguments: busName, srvName
556 self.bus = cache.dbusConnexion( busName['name'] )
557 self.srvName = list[1]
558 if (self.services.has_key(self.srvName) == False):
559 self.services[self.srvName] = dbus.service.BusName(name = self.srvName, bus = self.bus)
563 def serviceRelease(self, list):
565 arguments: busName, srvName
567 self.srvName = list[0]
568 if (self.services.has_key(self.srvName) == True):
569 self.services.pop(self.srvName)
572 raise Exception(self.srvName + " do not exist")
575 def serviceAddAgent(self, list):
577 arguments: objectPath, xmlTemplate
579 self.agentObjectPath = list[0]
580 xmlTemplate = list[1]
581 self.className = re.sub('/', '_', self.agentObjectPath[1:])
582 if (self.dynDBusClasses.has_key(self.className) == False):
583 self.dynDBusClasses[self.className] = DynDBusClass(self.className, self.globalCtx, self.localCtx)
584 self.dynDBusClasses[self.className].createDBusServiceFromXML(xmlTemplate)
585 self.dynDBusClasses[self.className].declare()
587 ## Class already exist, instanciate it if not already instanciated
588 if (self.serviceAgents.has_key(self.className) == False):
589 self.serviceAgents[self.className] = eval(self.className + "(self.bus, callback=self.srvCB, objName=self.agentObjectPath, busName=self.srvName)", self.globalCtx, self.localCtx)
591 self.serviceAgents[self.className].add_to_connection()
592 return (self.agentObjectPath)
595 def serviceDelAgent(self, list):
597 arguments: objectPath, xmlTemplate
599 agentObjectPath = list[0]
600 className = re.sub('/', '_', agentObjectPath[1:])
602 if (self.serviceAgents.has_key(className)):
603 self.serviceAgents[self.className].remove_from_connection()
604 self.serviceAgents.pop(self.className)
606 raise Exception(agentObjectPath + " doesn't exist!")
608 return (agentObjectPath)
611 def getVersion(self):
613 return current version string
619 ###############################################################################
620 class CloudeebusServerProtocol(WampCraServerProtocol):
622 connexion and session authentication management
625 def onSessionOpen(self):
626 # CRA authentication options
627 self.clientAuthTimeout = 0
628 self.clientAuthAllowAnonymous = OPENDOOR
629 # CRA authentication init
630 WampCraServerProtocol.onSessionOpen(self)
633 def getAuthPermissions(self, key, extra):
634 return json.loads(extra.get("permissions", "[]"))
637 def getAuthSecret(self, key):
638 secret = CREDENTIALS.get(key, None)
641 # secret must be of str type to be hashed
642 return secret.encode('utf-8')
645 def onAuthenticated(self, key, permissions):
647 # check authentication key
649 raise Exception("Authentication failed")
650 # check permissions, array.index throws exception
651 for req in permissions:
653 # create cloudeebus service instance
654 self.cloudeebusService = CloudeebusService(permissions)
655 # register it for RPC
656 self.registerForRpc(self.cloudeebusService)
657 # register for Publish / Subscribe
658 self.registerForPubSub("", True)
661 def connectionLost(self, reason):
662 WampCraServerProtocol.connectionLost(self, reason)
663 if factory.getConnectionCount() == 0:
668 ###############################################################################
670 if __name__ == '__main__':
674 parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
675 parser.add_argument('-v', '--version', action='store_true',
676 help='print version and exit')
677 parser.add_argument('-d', '--debug', action='store_true',
678 help='log debug info on standard output')
679 parser.add_argument('-o', '--opendoor', action='store_true',
680 help='allow anonymous access to all services')
681 parser.add_argument('-p', '--port', default='9000',
683 parser.add_argument('-c', '--credentials',
684 help='path to credentials file')
685 parser.add_argument('-w', '--whitelist',
686 help='path to whitelist file')
688 args = parser.parse_args(sys.argv[1:])
691 print("Cloudeebus version " + VERSION)
695 log.startLogging(sys.stdout)
697 OPENDOOR = args.opendoor
700 jfile = open(args.credentials)
701 CREDENTIALS = json.load(jfile)
705 jfile = open(args.whitelist)
706 WHITELIST = json.load(jfile)
709 uri = "ws://localhost:" + args.port
711 factory = WampServerFactory(uri, debugWamp = args.debug)
712 factory.protocol = CloudeebusServerProtocol
713 factory.setProtocolOptions(allowHixie76 = True)
717 DBusGMainLoop(set_as_default=True)