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>
21 # Frederic Paut <frederic.paut@intel.com>
25 import argparse, dbus, json, sys
27 from twisted.internet import glib2reactor
28 # Configure the twisted mainloop to be run inside the glib mainloop.
29 # This must be done before importing the other twisted modules
30 glib2reactor.install()
31 from twisted.internet import reactor, defer
33 from autobahn.websocket import listenWS
34 from autobahn.wamp import exportRpc, WampServerFactory, WampCraServerProtocol
36 from dbus.mainloop.glib import DBusGMainLoop
41 gobject.threads_init()
47 from twisted.python import log
50 from xml.etree.ElementTree import XMLParser
52 ###############################################################################
61 ###############################################################################
63 ## Convert an ip or an IP mask (such as ip/24 or ip/255.255.255.0) in hex value (32bits)
66 if mask.rfind(".") == -1:
68 maskHex = (2**(int(mask))-1)
69 maskHex = maskHex << (32-int(mask))
71 raise Exception("Illegal mask (larger than 32 bits) " + mask)
73 maskField = mask.split(".")
74 # Check if mask has four fields (byte)
75 if len(maskField) != 4:
76 raise Exception("Illegal ip address / mask (should be 4 bytes) " + mask)
77 for maskQuartet in maskField:
78 byte = int(maskQuartet)
79 # Check if each field is really a byte
81 raise Exception("Illegal ip address / mask (digit larger than a byte) " + mask)
83 maskHex = maskHex << 8
84 maskHex = maskHex >> 8
87 ###############################################################################
90 Global cache of DBus connexions and signal handlers
93 self.dbusConnexions = {}
94 self.signalHandlers = {}
99 Disconnect signal handlers before resetting cache.
101 self.dbusConnexions = {}
102 # disconnect signal handlers
103 for key in self.signalHandlers:
104 self.signalHandlers[key].disconnect()
105 self.signalHandlers = {}
108 def dbusConnexion(self, busName):
109 if not self.dbusConnexions.has_key(busName):
110 if busName == "session":
111 self.dbusConnexions[busName] = dbus.SessionBus()
112 elif busName == "system":
113 self.dbusConnexions[busName] = dbus.SystemBus()
115 raise Exception("Error: invalid bus: %s" % busName)
116 return self.dbusConnexions[busName]
120 ###############################################################################
121 class DbusSignalHandler:
123 signal hash id as busName#senderName#objectName#interfaceName#signalName
125 def __init__(self, busName, senderName, objectName, interfaceName, signalName):
126 self.id = "#".join([busName, senderName, objectName, interfaceName, signalName])
127 # connect handler to signal
128 self.bus = cache.dbusConnexion(busName)
129 self.bus.add_signal_receiver(self.handleSignal, signalName, interfaceName, senderName, objectName)
132 def disconnect(self):
133 names = self.id.split("#")
134 self.bus.remove_signal_receiver(self.handleSignal, names[4], names[3], names[1], names[2])
137 def handleSignal(self, *args):
139 publish dbus args under topic hash id
141 factory.dispatch(self.id, json.dumps(args))
145 ###############################################################################
146 class DbusCallHandler:
148 deferred reply to return dbus results
150 def __init__(self, method, args):
152 self.request = defer.Deferred()
157 def callMethod(self):
159 dbus method async call
162 self.method(*self.args, reply_handler=self.dbusSuccess, error_handler=self.dbusError)
166 def dbusSuccess(self, *result):
168 return JSON string result array
170 self.request.callback(json.dumps(result))
174 def dbusError(self, error):
176 return dbus error message
178 self.request.errback(Exception(error.get_dbus_message()))
183 ################################################################################
186 Execute DynDBusClass generated code
188 def __init__(self, globalCtx, localCtx) :
189 self.exec_string = ""
190 self.exec_code = None
191 self.exec_code_valid = 1
192 self.indent_level = 0
193 self.indent_increment = 1
195 self.localCtx = localCtx
196 self.globalCtx = globalCtx
199 def append_stmt(self, stmt) :
200 self.exec_code_valid = 0
202 for x in range(0,self.indent_level):
203 self.exec_string = self.exec_string + ' '
204 self.exec_string = self.exec_string + stmt + '\n'
207 self.indent_level = self.indent_level + self.indent_increment
210 self.indent_level = self.indent_level - self.indent_increment
212 # compile : Compile exec_string into exec_code using the builtin
213 # compile function. Skip if already in sync.
215 if not self.exec_code_valid :
216 self.exec_code = compile(self.exec_string, "<string>", "exec")
217 self.exec_code_valid = True
220 if not self.exec_code_valid :
222 exec(self.exec_code, self.globalCtx, self.localCtx)
226 ################################################################################
227 class XmlCbParser: # The target object of the parser
230 def __init__(self, dynDBusClass):
231 self.dynDBusClass = dynDBusClass
233 def start(self, tag, attrib): # Called for each opening tag.
237 if (tag == 'interface'):
238 self.dynDBusClass.set_interface(attrib['name'])
241 if (tag == 'method'):
243 self.dynDBusClass.def_method(attrib['name'])
245 if (tag == 'signal'):
247 self.dynDBusClass.def_signal(attrib['name'])
250 # Set signature (in/out & name) for method
252 if (self.current == 'method'):
253 if (attrib.has_key('direction') == False):
254 attrib['direction'] = "in"
255 self.dynDBusClass.add_signature(attrib['name'],
259 if (self.current == 'signal'):
260 if (attrib.has_key('name') == False):
261 attrib['name'] = 'value'
262 self.dynDBusClass.add_signature(attrib['name'], 'in',
265 def end(self, tag): # Called for each closing tag.
266 if (tag == 'method'):
267 self.dynDBusClass.add_dbus_method()
268 self.dynDBusClass.add_body_method()
269 self.dynDBusClass.end_method()
270 if (tag == 'signal'):
271 self.dynDBusClass.add_dbus_signal()
272 self.dynDBusClass.add_body_signal()
273 self.dynDBusClass.end_method()
275 def data(self, data):
276 pass # We do not need to do anything with data.
277 def close(self): # Called when all data has been parsed.
282 ###############################################################################
283 def createClassName(objectPath):
284 return re.sub('/', '_', objectPath[1:])
286 ################################################################################
287 class DynDBusClass():
288 def __init__(self, className, globalCtx, localCtx):
289 self.xmlCB = XmlCbParser(self)
291 self.class_code = ExecCode(globalCtx, localCtx)
292 self.class_code.indent_increment = 4
293 self.class_code.append_stmt("import dbus")
294 self.class_code.append_stmt("\n")
295 self.class_code.append_stmt("class " + className + "(dbus.service.Object):")
296 self.class_code.indent()
298 ## Overload of __init__ method
299 self.def_method("__init__")
300 self.add_method("bus, callback=None, objPath='/sample', srvName='org.cloudeebus'")
301 self.add_stmt("self.bus = bus")
302 self.add_stmt("self.objPath = objPath")
303 self.add_stmt("self.srvName = srvName")
304 self.add_stmt("self.callback = callback")
305 self.add_stmt("dbus.service.Object.__init__(self, conn=bus, bus_name=srvName)")
308 ## Create 'add_to_connection' method
309 self.def_method("add_to_connection")
310 self.add_method("connection=None, path=None")
311 self.add_stmt("dbus.service.Object.add_to_connection(self, connection=self.bus, path=self.objPath)")
314 ## Create 'remove_from_connection' method
315 self.def_method("remove_from_connection")
316 self.add_method("connection=None, path=None")
317 self.add_stmt("dbus.service.Object.remove_from_connection(self, connection=None, path=self.objPath)")
320 def createDBusServiceFromXML(self, xml):
321 self.parser = XMLParser(target=self.xmlCB)
322 self.parser.feed(xml)
325 def set_interface(self, ifName):
328 def def_method(self, methodName):
329 self.methodToAdd = methodName
330 self.signalToAdd = None
331 self.args_str = str()
333 self.signature['name'] = str()
334 self.signature['in'] = str()
335 self.signature['out'] = str()
337 def def_signal(self, signalName):
338 self.methodToAdd = None
339 self.signalToAdd = signalName
340 self.args_str = str()
342 self.signature['name'] = str()
343 self.signature['in'] = str()
344 self.signature['out'] = str()
346 def add_signature(self, name, direction, signature):
347 if (direction == 'in'):
348 self.signature['in'] += signature
349 if (self.signature['name'] != str()):
350 self.signature['name'] += ", "
351 self.signature['name'] += name
352 if (direction == 'out'):
353 self.signature['out'] = signature
355 def add_method(self, args = None, async_success_cb = None, async_err_cb = None):
357 if (self.methodToAdd != None):
358 name = self.methodToAdd
360 name = self.signalToAdd
363 if (async_success_cb != None):
364 async_cb_str = async_success_cb
365 if (async_err_cb != None):
366 if (async_cb_str != str()):
368 async_cb_str += async_err_cb
370 parameters = self.args_str
371 if (async_cb_str != str()):
372 if (parameters != str()):
374 parameters +=async_cb_str
376 if (parameters != str()):
377 self.class_code.append_stmt("def " + name + "(self, %s):" % parameters)
379 self.class_code.append_stmt("def " + name + "(self):")
380 self.class_code.indent()
382 def end_method(self):
383 self.class_code.append_stmt("\n")
384 self.class_code.dedent()
386 def add_dbus_method(self):
387 decorator = '@dbus.service.method("' + self.ifName + '"'
388 if (self.signature.has_key('in') and self.signature['in'] != str()):
389 decorator += ", in_signature='" + self.signature['in'] + "'"
390 if (self.signature.has_key('out') and self.signature['out'] != str()):
391 decorator += ", out_signature='" + self.signature['out'] + "'"
392 decorator += ", async_callbacks=('dbus_async_cb', 'dbus_async_err_cb')"
394 self.class_code.append_stmt(decorator)
395 if (self.signature.has_key('name') and self.signature['name'] != str()):
396 self.add_method(self.signature['name'], async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
398 self.add_method(async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
400 def add_dbus_signal(self):
401 decorator = '@dbus.service.signal("' + self.ifName + '"'
402 if (self.signature.has_key('in') and self.signature['in'] != str()):
403 decorator += ", signature='" + self.signature['in'] + "'"
405 self.class_code.append_stmt(decorator)
406 if (self.signature.has_key('name') and self.signature['name'] != str()):
407 self.add_method(self.signature['name'])
411 def add_body_method(self):
412 if (self.methodToAdd != None):
413 if (self.args_str != str()):
414 self.class_code.append_stmt("self.callback(self.srvName,'" + self.methodToAdd + "', self.objPath, '" + self.ifName + "', " + "dbus_async_cb, dbus_async_err_cb, %s)" % self.args_str)
416 self.class_code.append_stmt("self.callback(self.srvName,'" + self.methodToAdd + "', self.objPath, '" + self.ifName + "', " + "dbus_async_cb, dbus_async_err_cb)")
418 def add_body_signal(self):
419 self.class_code.append_stmt("return") ## TODO: Remove and fix with code ad hoc
420 self.class_code.append_stmt("\n")
422 def add_stmt(self, stmt) :
423 self.class_code.append_stmt(stmt)
426 self.class_code.execute()
430 ###############################################################################
431 class CloudeebusService:
433 support for sending DBus messages and registering for DBus signals
435 def __init__(self, permissions):
436 self.permissions = {};
437 self.permissions['permissions'] = permissions['permissions']
438 self.permissions['authextra'] = permissions['authextra']
439 self.permissions['services'] = permissions['services']
440 self.proxyObjects = {}
441 self.proxyMethods = {}
442 self.pendingCalls = []
443 self.dynDBusClasses = {} # DBus class source code generated dynamically (a list because one by classname)
444 self.services = {} # DBus service created
445 self.serviceAgents = {} # Instantiated DBus class previously generated dynamically, for now, one by classname
446 self.servicePendingCalls = {} # JS methods called (and waiting for a Success/error response), containing 'methodId', (successCB, errorCB)
447 self.localCtx = locals()
448 self.globalCtx = globals()
451 def proxyObject(self, busName, serviceName, objectName):
453 object hash id as busName#serviceName#objectName
455 id = "#".join([busName, serviceName, objectName])
456 if not self.proxyObjects.has_key(id):
458 # check permissions, array.index throws exception
459 self.permissions['permissions'].index(serviceName)
460 bus = cache.dbusConnexion(busName)
461 self.proxyObjects[id] = bus.get_object(serviceName, objectName)
462 return self.proxyObjects[id]
465 def proxyMethod(self, busName, serviceName, objectName, interfaceName, methodName):
467 method hash id as busName#serviceName#objectName#interfaceName#methodName
469 id = "#".join([busName, serviceName, objectName, interfaceName, methodName])
470 if not self.proxyMethods.has_key(id):
471 obj = self.proxyObject(busName, serviceName, objectName)
472 self.proxyMethods[id] = obj.get_dbus_method(methodName, interfaceName)
473 return self.proxyMethods[id]
477 def dbusRegister(self, list):
479 arguments: bus, sender, object, interface, signal
482 raise Exception("Error: expected arguments: bus, sender, object, interface, signal)")
485 # check permissions, array.index throws exception
486 self.permissions['permissions'].index(list[1])
488 # check if a handler exists
489 sigId = "#".join(list)
490 if cache.signalHandlers.has_key(sigId):
493 # create a handler that will publish the signal
494 dbusSignalHandler = DbusSignalHandler(*list)
495 cache.signalHandlers[sigId] = dbusSignalHandler
497 return dbusSignalHandler.id
501 def dbusSend(self, list):
503 arguments: bus, destination, object, interface, message, [args]
505 # clear pending calls
506 for call in self.pendingCalls:
508 self.pendingCalls.remove(call)
511 raise Exception("Error: expected arguments: bus, destination, object, interface, message, [args])")
513 # parse JSON arg list
516 args = json.loads(list[5])
518 # get dbus proxy method
519 method = self.proxyMethod(*list[0:5])
521 # use a deferred call handler to manage dbus results
522 dbusCallHandler = DbusCallHandler(method, args)
523 self.pendingCalls.append(dbusCallHandler)
524 return dbusCallHandler.callMethod()
528 def emitSignal(self, list):
530 arguments: agentObjectPath, signalName, result (to emit)
533 className = re.sub('/', '_', objectPath[1:])
536 if (self.serviceAgents.has_key(className) == True):
538 exe_str = "self.serviceAgents['"+ className +"']."+ signalName + "(" + str(result) + ")"
540 exe_str = "self.serviceAgents['"+ className +"']."+ signalName + "()"
541 eval(exe_str, self.globalCtx, self.localCtx)
543 raise Exception("No object path " + objectPath)
546 def returnMethod(self, list):
548 arguments: methodId, callIndex, success (=true, error otherwise), result (to return)
554 if (self.servicePendingCalls.has_key(methodId)):
555 cb = self.servicePendingCalls[methodId]['calls'][callIndex]
557 raise Exception("No pending call " + str(callIndex) + " for methodID " + methodId)
559 successCB = cb["successCB"]
565 errorCB = cb["errorCB"]
570 self.servicePendingCalls[methodId]['calls'][callIndex] = None
571 self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] - 1
572 if self.servicePendingCalls[methodId]['count'] == 0:
573 del self.servicePendingCalls[methodId]
575 raise Exception("No methodID " + methodId)
577 def srvCB(self, srvName, name, objPath, ifName, async_succes_cb, async_error_cb, *args):
578 methodId = srvName + "#" + objPath + "#" + ifName + "#" + name
579 cb = { 'successCB': async_succes_cb,
580 'errorCB': async_error_cb}
581 if methodId not in self.servicePendingCalls:
582 self.servicePendingCalls[methodId] = {'count': 0, 'calls': []}
585 pendingCallStr = json.dumps({'callIndex': len(self.servicePendingCalls[methodId]['calls']), 'args': args})
587 args = eval( str(args).replace("dbus.Byte", "dbus.Int16") )
588 pendingCallStr = json.dumps({'callIndex': len(self.servicePendingCalls[methodId]['calls']), 'args': args})
590 self.servicePendingCalls[methodId]['calls'].append(cb)
591 self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] + 1
592 factory.dispatch(methodId, pendingCallStr)
595 def serviceAdd(self, list):
597 arguments: busName, srvName
600 self.bus = cache.dbusConnexion( busName )
602 if not OPENDOOR and (SERVICELIST == [] or SERVICELIST != [] and self.permissions['services'] == None):
603 SERVICELIST.index(srvName)
605 if (self.services.has_key(srvName) == False):
606 self.services[srvName] = dbus.service.BusName(name = srvName, bus = self.bus)
610 def serviceRelease(self, list):
612 arguments: busName, srvName
615 if (self.services.has_key(srvName) == True):
616 self.services.pop(srvName)
619 raise Exception(srvName + " does not exist")
622 def serviceAddAgent(self, list):
624 arguments: objectPath, xmlTemplate
627 agentObjectPath = list[1]
628 xmlTemplate = list[2]
629 className = createClassName(agentObjectPath)
630 if (self.dynDBusClasses.has_key(className) == False):
631 self.dynDBusClasses[className] = DynDBusClass(className, self.globalCtx, self.localCtx)
632 self.dynDBusClasses[className].createDBusServiceFromXML(xmlTemplate)
633 self.dynDBusClasses[className].declare()
635 ## Class already exist, instanciate it if not already instanciated
636 if (self.serviceAgents.has_key(className) == False):
637 self.serviceAgents[className] = eval(className + "(self.bus, callback=self.srvCB, objPath='" + agentObjectPath + "', srvName='" + srvName + "')", self.globalCtx, self.localCtx)
639 self.serviceAgents[className].add_to_connection()
640 return (agentObjectPath)
643 def serviceDelAgent(self, list):
645 arguments: objectPath, xmlTemplate
647 agentObjectPath = list[0]
648 className = createClassName(agentObjectPath)
650 if (self.serviceAgents.has_key(className)):
651 self.serviceAgents[className].remove_from_connection()
652 self.serviceAgents.pop(className)
654 raise Exception(agentObjectPath + " doesn't exist!")
656 return (agentObjectPath)
659 def getVersion(self):
661 return current version string
667 ###############################################################################
668 class CloudeebusServerProtocol(WampCraServerProtocol):
670 connexion and session authentication management
673 def onSessionOpen(self):
674 # CRA authentication options
675 self.clientAuthTimeout = 0
676 self.clientAuthAllowAnonymous = OPENDOOR
677 # CRA authentication init
678 WampCraServerProtocol.onSessionOpen(self)
681 def getAuthPermissions(self, key, extra):
682 return {'permissions': extra.get("permissions", None),
683 'authextra': extra.get("authextra", None),
684 'services': extra.get("services", None)}
686 def getAuthSecret(self, key):
687 secret = CREDENTIALS.get(key, None)
690 # secret must be of str type to be hashed
694 def onAuthenticated(self, key, permissions):
699 for netfilter in NETMASK:
700 ipHex=ipV4ToHex(self.peer.host)
701 ipAllowed = (ipHex & netfilter['mask']) == netfilter['ipAllowed'] & netfilter['mask']
705 raise Exception("host " + self.peer.host + " is not allowed!")
706 # check authentication key
708 raise Exception("Authentication failed")
709 # check permissions, array.index throws exception
710 if (permissions['permissions'] != None):
711 for req in permissions['permissions']:
712 WHITELIST.index(req);
713 # check allowed service creation, array.index throws exception
714 if (permissions['services'] != None):
715 for req in permissions['services']:
716 SERVICELIST.index(req);
717 # create cloudeebus service instance
718 self.cloudeebusService = CloudeebusService(permissions)
719 # register it for RPC
720 self.registerForRpc(self.cloudeebusService)
721 # register for Publish / Subscribe
722 self.registerForPubSub("", True)
725 def connectionLost(self, reason):
726 WampCraServerProtocol.connectionLost(self, reason)
727 if factory.getConnectionCount() == 0:
732 ###############################################################################
734 if __name__ == '__main__':
738 parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
739 parser.add_argument('-v', '--version', action='store_true',
740 help='print version and exit')
741 parser.add_argument('-d', '--debug', action='store_true',
742 help='log debug info on standard output')
743 parser.add_argument('-o', '--opendoor', action='store_true',
744 help='allow anonymous access to all services')
745 parser.add_argument('-p', '--port', default='9000',
747 parser.add_argument('-c', '--credentials',
748 help='path to credentials file')
749 parser.add_argument('-w', '--whitelist',
750 help='path to whitelist file (DBus services to use)')
751 parser.add_argument('-s', '--servicelist',
752 help='path to servicelist file (DBus services to export)')
753 parser.add_argument('-n', '--netmask',
754 help='netmask,IP filter (comma separated.) eg. : -n 127.0.0.1,192.168.2.0/24,10.12.16.0/255.255.255.0')
756 args = parser.parse_args(sys.argv[1:])
759 print("Cloudeebus version " + VERSION)
763 log.startLogging(sys.stdout)
765 OPENDOOR = args.opendoor
768 jfile = open(args.credentials)
769 CREDENTIALS = json.load(jfile)
773 jfile = open(args.whitelist)
774 WHITELIST = json.load(jfile)
778 jfile = open(args.servicelist)
779 SERVICELIST = json.load(jfile)
783 iplist = args.netmask.split(",")
785 if ip.rfind("/") != -1:
791 mask = "255.255.255.255"
792 NETMASK.append( {'ipAllowed': ipV4ToHex(ipAllowed), 'mask' : ipV4ToHex(mask)} )
794 uri = "ws://localhost:" + args.port
796 factory = WampServerFactory(uri, debugWamp = args.debug)
797 factory.protocol = CloudeebusServerProtocol
798 factory.setProtocolOptions(allowHixie76 = True)
802 DBusGMainLoop(set_as_default=True)