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):
537 exe_str = "self.serviceAgents['"+ className +"']."+ signalName + "(" + str(result) + ")"
538 eval(exe_str, self.globalCtx, self.localCtx)
540 raise Exception("No object path " + objectPath)
543 def returnMethod(self, list):
545 arguments: methodId, callIndex, success (=true, error otherwise), result (to return)
551 if (self.servicePendingCalls.has_key(methodId)):
552 cb = self.servicePendingCalls[methodId]['calls'][callIndex]
554 raise Exception("No pending call " + str(callIndex) + " for methodID " + methodId)
556 successCB = cb["successCB"]
562 errorCB = cb["errorCB"]
567 self.servicePendingCalls[methodId]['calls'][callIndex] = None
568 self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] - 1
569 if self.servicePendingCalls[methodId]['count'] == 0:
570 del self.servicePendingCalls[methodId]
572 raise Exception("No methodID " + methodId)
574 def srvCB(self, srvName, name, objPath, ifName, async_succes_cb, async_error_cb, *args):
575 methodId = srvName + "#" + objPath + "#" + ifName + "#" + name
576 cb = { 'successCB': async_succes_cb,
577 'errorCB': async_error_cb}
578 if methodId not in self.servicePendingCalls:
579 self.servicePendingCalls[methodId] = {'count': 0, 'calls': []}
582 pendingCallStr = json.dumps({'callIndex': len(self.servicePendingCalls[methodId]['calls']), 'args': args})
584 args = eval( str(args).replace("dbus.Byte", "dbus.Int16") )
585 pendingCallStr = json.dumps({'callIndex': len(self.servicePendingCalls[methodId]['calls']), 'args': args})
587 self.servicePendingCalls[methodId]['calls'].append(cb)
588 self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] + 1
589 factory.dispatch(methodId, pendingCallStr)
592 def serviceAdd(self, list):
594 arguments: busName, srvName
597 self.bus = cache.dbusConnexion( busName )
599 if not OPENDOOR and (SERVICELIST == [] or SERVICELIST != [] and self.permissions['services'] == None):
600 SERVICELIST.index(srvName)
602 if (self.services.has_key(srvName) == False):
603 self.services[srvName] = dbus.service.BusName(name = srvName, bus = self.bus)
607 def serviceRelease(self, list):
609 arguments: busName, srvName
612 if (self.services.has_key(srvName) == True):
613 self.services.pop(srvName)
616 raise Exception(srvName + " does not exist")
619 def serviceAddAgent(self, list):
621 arguments: objectPath, xmlTemplate
624 agentObjectPath = list[1]
625 xmlTemplate = list[2]
626 className = createClassName(agentObjectPath)
627 if (self.dynDBusClasses.has_key(className) == False):
628 self.dynDBusClasses[className] = DynDBusClass(className, self.globalCtx, self.localCtx)
629 self.dynDBusClasses[className].createDBusServiceFromXML(xmlTemplate)
630 self.dynDBusClasses[className].declare()
632 ## Class already exist, instanciate it if not already instanciated
633 if (self.serviceAgents.has_key(className) == False):
634 self.serviceAgents[className] = eval(className + "(self.bus, callback=self.srvCB, objPath='" + agentObjectPath + "', srvName='" + srvName + "')", self.globalCtx, self.localCtx)
636 self.serviceAgents[className].add_to_connection()
637 return (agentObjectPath)
640 def serviceDelAgent(self, list):
642 arguments: objectPath, xmlTemplate
644 agentObjectPath = list[0]
645 className = createClassName(agentObjectPath)
647 print 'PY Try to remove ' + className
649 if (self.serviceAgents.has_key(className)):
650 self.serviceAgents[className].remove_from_connection()
651 self.serviceAgents.pop(className)
653 raise Exception(agentObjectPath + " doesn't exist!")
655 return (agentObjectPath)
658 def getVersion(self):
660 return current version string
666 ###############################################################################
667 class CloudeebusServerProtocol(WampCraServerProtocol):
669 connexion and session authentication management
672 def onSessionOpen(self):
673 # CRA authentication options
674 self.clientAuthTimeout = 0
675 self.clientAuthAllowAnonymous = OPENDOOR
676 # CRA authentication init
677 WampCraServerProtocol.onSessionOpen(self)
680 def getAuthPermissions(self, key, extra):
681 return {'permissions': extra.get("permissions", None),
682 'authextra': extra.get("authextra", None),
683 'services': extra.get("services", None)}
685 def getAuthSecret(self, key):
686 secret = CREDENTIALS.get(key, None)
689 # secret must be of str type to be hashed
693 def onAuthenticated(self, key, permissions):
698 for netfilter in NETMASK:
699 ipHex=ipV4ToHex(self.peer.host)
700 ipAllowed = (ipHex & netfilter['mask']) == netfilter['ipAllowed'] & netfilter['mask']
704 raise Exception("host " + self.peer.host + " is not allowed!")
705 # check authentication key
707 raise Exception("Authentication failed")
708 # check permissions, array.index throws exception
709 if (permissions['permissions'] != None):
710 for req in permissions['permissions']:
711 WHITELIST.index(req);
712 # check allowed service creation, array.index throws exception
713 if (permissions['services'] != None):
714 for req in permissions['services']:
715 SERVICELIST.index(req);
716 # create cloudeebus service instance
717 self.cloudeebusService = CloudeebusService(permissions)
718 # register it for RPC
719 self.registerForRpc(self.cloudeebusService)
720 # register for Publish / Subscribe
721 self.registerForPubSub("", True)
724 def connectionLost(self, reason):
725 WampCraServerProtocol.connectionLost(self, reason)
726 if factory.getConnectionCount() == 0:
731 ###############################################################################
733 if __name__ == '__main__':
737 parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
738 parser.add_argument('-v', '--version', action='store_true',
739 help='print version and exit')
740 parser.add_argument('-d', '--debug', action='store_true',
741 help='log debug info on standard output')
742 parser.add_argument('-o', '--opendoor', action='store_true',
743 help='allow anonymous access to all services')
744 parser.add_argument('-p', '--port', default='9000',
746 parser.add_argument('-c', '--credentials',
747 help='path to credentials file')
748 parser.add_argument('-w', '--whitelist',
749 help='path to whitelist file (DBus services to use)')
750 parser.add_argument('-s', '--servicelist',
751 help='path to servicelist file (DBus services to export)')
752 parser.add_argument('-n', '--netmask',
753 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')
755 args = parser.parse_args(sys.argv[1:])
758 print("Cloudeebus version " + VERSION)
762 log.startLogging(sys.stdout)
764 OPENDOOR = args.opendoor
767 jfile = open(args.credentials)
768 CREDENTIALS = json.load(jfile)
772 jfile = open(args.whitelist)
773 WHITELIST = json.load(jfile)
777 jfile = open(args.servicelist)
778 SERVICELIST = json.load(jfile)
782 iplist = args.netmask.split(",")
784 if ip.rfind("/") != -1:
790 mask = "255.255.255.255"
791 NETMASK.append( {'ipAllowed': ipV4ToHex(ipAllowed), 'mask' : ipV4ToHex(mask)} )
793 uri = "ws://localhost:" + args.port
795 factory = WampServerFactory(uri, debugWamp = args.debug)
796 factory.protocol = CloudeebusServerProtocol
797 factory.setProtocolOptions(allowHixie76 = True)
801 DBusGMainLoop(set_as_default=True)