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', busName='org.cloudeebus'")
301 self.add_stmt("self.bus = bus")
302 self.add_stmt("self.objPath = objPath")
303 self.add_stmt("self.callback = callback")
304 self.add_stmt("dbus.service.Object.__init__(self, conn=bus, bus_name=busName)")
307 ## Create 'add_to_connection' method
308 self.def_method("add_to_connection")
309 self.add_method("connection=None, path=None")
310 self.add_stmt("dbus.service.Object.add_to_connection(self, connection=self.bus, path=self.objPath)")
313 ## Create 'remove_from_connection' method
314 self.def_method("remove_from_connection")
315 self.add_method("connection=None, path=None")
316 self.add_stmt("dbus.service.Object.remove_from_connection(self, connection=None, path=self.objPath)")
319 def createDBusServiceFromXML(self, xml):
320 self.parser = XMLParser(target=self.xmlCB)
321 self.parser.feed(xml)
324 def set_interface(self, ifName):
327 def def_method(self, methodName):
328 self.methodToAdd = methodName
329 self.signalToAdd = None
330 self.args_str = str()
332 self.signature['name'] = str()
333 self.signature['in'] = str()
334 self.signature['out'] = str()
336 def def_signal(self, signalName):
337 self.methodToAdd = None
338 self.signalToAdd = signalName
339 self.args_str = str()
341 self.signature['name'] = str()
342 self.signature['in'] = str()
343 self.signature['out'] = str()
345 def add_signature(self, name, direction, signature):
346 if (direction == 'in'):
347 self.signature['in'] += signature
348 if (self.signature['name'] != str()):
349 self.signature['name'] += ", "
350 self.signature['name'] += name
351 if (direction == 'out'):
352 self.signature['out'] = signature
354 def add_method(self, args = None, async_success_cb = None, async_err_cb = None):
356 if (self.methodToAdd != None):
357 name = self.methodToAdd
359 name = self.signalToAdd
362 if (async_success_cb != None):
363 async_cb_str = async_success_cb
364 if (async_err_cb != None):
365 if (async_cb_str != str()):
367 async_cb_str += async_err_cb
369 parameters = self.args_str
370 if (async_cb_str != str()):
371 if (parameters != str()):
373 parameters +=async_cb_str
375 if (parameters != str()):
376 self.class_code.append_stmt("def " + name + "(self, %s):" % parameters)
378 self.class_code.append_stmt("def " + name + "(self):")
379 self.class_code.indent()
381 def end_method(self):
382 self.class_code.append_stmt("\n")
383 self.class_code.dedent()
385 def add_dbus_method(self):
386 decorator = '@dbus.service.method("' + self.ifName + '"'
387 if (self.signature.has_key('in') and self.signature['in'] != str()):
388 decorator += ", in_signature='" + self.signature['in'] + "'"
389 if (self.signature.has_key('out') and self.signature['out'] != str()):
390 decorator += ", out_signature='" + self.signature['out'] + "'"
391 decorator += ", async_callbacks=('dbus_async_cb', 'dbus_async_err_cb')"
393 self.class_code.append_stmt(decorator)
394 if (self.signature.has_key('name') and self.signature['name'] != str()):
395 self.add_method(self.signature['name'], async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
397 self.add_method(async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
399 def add_dbus_signal(self):
400 decorator = '@dbus.service.signal("' + self.ifName + '"'
401 if (self.signature.has_key('in') and self.signature['in'] != str()):
402 decorator += ", signature='" + self.signature['in'] + "'"
404 self.class_code.append_stmt(decorator)
405 if (self.signature.has_key('name') and self.signature['name'] != str()):
406 self.add_method(self.signature['name'])
410 def add_body_method(self):
411 if (self.methodToAdd != None):
412 if (self.args_str != str()):
413 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', self.objPath, '" + self.ifName + "', " + "dbus_async_cb, dbus_async_err_cb, %s)" % self.args_str)
415 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', self.objPath, '" + self.ifName + "', " + "dbus_async_cb, dbus_async_err_cb)")
417 def add_body_signal(self):
418 self.class_code.append_stmt("return") ## TODO: Remove and fix with code ad hoc
419 self.class_code.append_stmt("\n")
421 def add_stmt(self, stmt) :
422 self.class_code.append_stmt(stmt)
425 self.class_code.execute()
429 ###############################################################################
430 class CloudeebusService:
432 support for sending DBus messages and registering for DBus signals
434 def __init__(self, permissions):
435 self.permissions = {};
436 self.permissions['permissions'] = permissions['permissions']
437 self.permissions['authextra'] = permissions['authextra']
438 self.permissions['services'] = permissions['services']
439 self.proxyObjects = {}
440 self.proxyMethods = {}
441 self.pendingCalls = []
442 self.dynDBusClasses = {} # DBus class source code generated dynamically (a list because one by classname)
443 self.services = {} # DBus service created
444 self.serviceAgents = {} # Instantiated DBus class previously generated dynamically, for now, one by classname
445 self.servicePendingCalls = {} # JS methods called (and waiting for a Success/error response), containing 'methodId', (successCB, errorCB)
446 self.localCtx = locals()
447 self.globalCtx = globals()
450 def proxyObject(self, busName, serviceName, objectName):
452 object hash id as busName#serviceName#objectName
454 id = "#".join([busName, serviceName, objectName])
455 if not self.proxyObjects.has_key(id):
457 # check permissions, array.index throws exception
458 self.permissions['permissions'].index(serviceName)
459 bus = cache.dbusConnexion(busName)
460 self.proxyObjects[id] = bus.get_object(serviceName, objectName)
461 return self.proxyObjects[id]
464 def proxyMethod(self, busName, serviceName, objectName, interfaceName, methodName):
466 method hash id as busName#serviceName#objectName#interfaceName#methodName
468 id = "#".join([busName, serviceName, objectName, interfaceName, methodName])
469 if not self.proxyMethods.has_key(id):
470 obj = self.proxyObject(busName, serviceName, objectName)
471 self.proxyMethods[id] = obj.get_dbus_method(methodName, interfaceName)
472 return self.proxyMethods[id]
476 def dbusRegister(self, list):
478 arguments: bus, sender, object, interface, signal
481 raise Exception("Error: expected arguments: bus, sender, object, interface, signal)")
484 # check permissions, array.index throws exception
485 self.permissions['permissions'].index(list[1])
487 # check if a handler exists
488 sigId = "#".join(list)
489 if cache.signalHandlers.has_key(sigId):
492 # create a handler that will publish the signal
493 dbusSignalHandler = DbusSignalHandler(*list)
494 cache.signalHandlers[sigId] = dbusSignalHandler
496 return dbusSignalHandler.id
500 def dbusSend(self, list):
502 arguments: bus, destination, object, interface, message, [args]
504 # clear pending calls
505 for call in self.pendingCalls:
507 self.pendingCalls.remove(call)
510 raise Exception("Error: expected arguments: bus, destination, object, interface, message, [args])")
512 # parse JSON arg list
515 args = json.loads(list[5])
517 # get dbus proxy method
518 method = self.proxyMethod(*list[0:5])
520 # use a deferred call handler to manage dbus results
521 dbusCallHandler = DbusCallHandler(method, args)
522 self.pendingCalls.append(dbusCallHandler)
523 return dbusCallHandler.callMethod()
527 def emitSignal(self, list):
529 arguments: agentObjectPath, signalName, result (to emit)
532 className = re.sub('/', '_', objectPath[1:])
535 if (self.serviceAgents.has_key(className) == True):
536 exe_str = "self.serviceAgents['"+ className +"']."+ signalName + "(" + str(result) + ")"
537 eval(exe_str, self.globalCtx, self.localCtx)
539 raise Exception("No object path " + objectPath)
542 def returnMethod(self, list):
544 arguments: methodId, callIndex, success (=true, error otherwise), result (to return)
550 if (self.servicePendingCalls.has_key(methodId)):
551 cb = self.servicePendingCalls[methodId]['calls'][callIndex]
553 raise Exception("No pending call " + str(callIndex) + " for methodID " + methodId)
555 successCB = cb["successCB"]
561 errorCB = cb["errorCB"]
566 self.servicePendingCalls[methodId]['calls'][callIndex] = None
567 self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] - 1
568 if self.servicePendingCalls[methodId]['count'] == 0:
569 del self.servicePendingCalls[methodId]
571 raise Exception("No methodID " + methodId)
573 def srvCB(self, name, objPath, ifName, async_succes_cb, async_error_cb, *args):
574 methodId = self.srvName + "#" + objPath + "#" + ifName + "#" + name
575 cb = { 'successCB': async_succes_cb,
576 'errorCB': async_error_cb}
577 if methodId not in self.servicePendingCalls:
578 self.servicePendingCalls[methodId] = {'count': 0, 'calls': []}
581 pendingCallStr = json.dumps({'callIndex': len(self.servicePendingCalls[methodId]['calls']), 'args': args})
583 args = eval( str(args).replace("dbus.Byte", "dbus.Int16") )
584 pendingCallStr = json.dumps({'callIndex': len(self.servicePendingCalls[methodId]['calls']), 'args': args})
586 self.servicePendingCalls[methodId]['calls'].append(cb)
587 self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] + 1
588 factory.dispatch(methodId, pendingCallStr)
591 def serviceAdd(self, list):
593 arguments: busName, srvName
596 self.bus = cache.dbusConnexion( busName )
597 self.srvName = list[1]
598 if not OPENDOOR and (SERVICELIST == [] or SERVICELIST != [] and self.permissions['services'] == None):
599 SERVICELIST.index(self.srvName)
601 if (self.services.has_key(self.srvName) == False):
602 self.services[self.srvName] = dbus.service.BusName(name = self.srvName, bus = self.bus)
606 def serviceRelease(self, list):
608 arguments: busName, srvName
610 self.srvName = list[0]
611 if (self.services.has_key(self.srvName) == True):
612 self.services.pop(self.srvName)
615 raise Exception(self.srvName + " does not exist")
618 def serviceAddAgent(self, list):
620 arguments: objectPath, xmlTemplate
623 agentObjectPath = list[1]
624 xmlTemplate = list[2]
625 className = createClassName(agentObjectPath)
626 if (self.dynDBusClasses.has_key(className) == False):
627 self.dynDBusClasses[className] = DynDBusClass(className, self.globalCtx, self.localCtx)
628 self.dynDBusClasses[className].createDBusServiceFromXML(xmlTemplate)
629 self.dynDBusClasses[className].declare()
631 ## Class already exist, instanciate it if not already instanciated
632 if (self.serviceAgents.has_key(className) == False):
633 self.serviceAgents[className] = eval(className + "(self.bus, callback=self.srvCB, objPath='"+agentObjectPath+"', busName='"+srvName+"')", self.globalCtx, self.localCtx)
635 self.serviceAgents[className].add_to_connection()
636 return (agentObjectPath)
639 def serviceDelAgent(self, list):
641 arguments: objectPath, xmlTemplate
643 agentObjectPath = list[0]
644 className = createClassName(agentObjectPath)
646 print 'PY Try to remove ' + className
648 if (self.serviceAgents.has_key(className)):
649 self.serviceAgents[className].remove_from_connection()
650 self.serviceAgents.pop(className)
652 raise Exception(agentObjectPath + " doesn't exist!")
654 return (agentObjectPath)
657 def getVersion(self):
659 return current version string
665 ###############################################################################
666 class CloudeebusServerProtocol(WampCraServerProtocol):
668 connexion and session authentication management
671 def onSessionOpen(self):
672 # CRA authentication options
673 self.clientAuthTimeout = 0
674 self.clientAuthAllowAnonymous = OPENDOOR
675 # CRA authentication init
676 WampCraServerProtocol.onSessionOpen(self)
679 def getAuthPermissions(self, key, extra):
680 return {'permissions': extra.get("permissions", None),
681 'authextra': extra.get("authextra", None),
682 'services': extra.get("services", None)}
684 def getAuthSecret(self, key):
685 secret = CREDENTIALS.get(key, None)
688 # secret must be of str type to be hashed
692 def onAuthenticated(self, key, permissions):
697 for netfilter in NETMASK:
698 ipHex=ipV4ToHex(self.peer.host)
699 ipAllowed = (ipHex & netfilter['mask']) == netfilter['ipAllowed'] & netfilter['mask']
703 raise Exception("host " + self.peer.host + " is not allowed!")
704 # check authentication key
706 raise Exception("Authentication failed")
707 # check permissions, array.index throws exception
708 if (permissions['permissions'] != None):
709 for req in permissions['permissions']:
710 WHITELIST.index(req);
711 # check allowed service creation, array.index throws exception
712 if (permissions['services'] != None):
713 for req in permissions['services']:
714 SERVICELIST.index(req);
715 # create cloudeebus service instance
716 self.cloudeebusService = CloudeebusService(permissions)
717 # register it for RPC
718 self.registerForRpc(self.cloudeebusService)
719 # register for Publish / Subscribe
720 self.registerForPubSub("", True)
723 def connectionLost(self, reason):
724 WampCraServerProtocol.connectionLost(self, reason)
725 if factory.getConnectionCount() == 0:
730 ###############################################################################
732 if __name__ == '__main__':
736 parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
737 parser.add_argument('-v', '--version', action='store_true',
738 help='print version and exit')
739 parser.add_argument('-d', '--debug', action='store_true',
740 help='log debug info on standard output')
741 parser.add_argument('-o', '--opendoor', action='store_true',
742 help='allow anonymous access to all services')
743 parser.add_argument('-p', '--port', default='9000',
745 parser.add_argument('-c', '--credentials',
746 help='path to credentials file')
747 parser.add_argument('-w', '--whitelist',
748 help='path to whitelist file (DBus services to use)')
749 parser.add_argument('-s', '--servicelist',
750 help='path to servicelist file (DBus services to export)')
751 parser.add_argument('-n', '--netmask',
752 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')
754 args = parser.parse_args(sys.argv[1:])
757 print("Cloudeebus version " + VERSION)
761 log.startLogging(sys.stdout)
763 OPENDOOR = args.opendoor
766 jfile = open(args.credentials)
767 CREDENTIALS = json.load(jfile)
771 jfile = open(args.whitelist)
772 WHITELIST = json.load(jfile)
776 jfile = open(args.servicelist)
777 SERVICELIST = json.load(jfile)
781 iplist = args.netmask.split(",")
783 if ip.rfind("/") != -1:
789 mask = "255.255.255.255"
790 NETMASK.append( {'ipAllowed': ipV4ToHex(ipAllowed), 'mask' : ipV4ToHex(mask)} )
792 uri = "ws://localhost:" + args.port
794 factory = WampServerFactory(uri, debugWamp = args.debug)
795 factory.protocol = CloudeebusServerProtocol
796 factory.setProtocolOptions(allowHixie76 = True)
800 DBusGMainLoop(set_as_default=True)