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
622 self.agentObjectPath = list[0]
623 xmlTemplate = list[1]
624 className = createClassName(self.agentObjectPath)
625 if (self.dynDBusClasses.has_key(className) == False):
626 self.dynDBusClasses[className] = DynDBusClass(className, self.globalCtx, self.localCtx)
627 self.dynDBusClasses[className].createDBusServiceFromXML(xmlTemplate)
628 self.dynDBusClasses[className].declare()
630 ## Class already exist, instanciate it if not already instanciated
631 if (self.serviceAgents.has_key(className) == False):
632 self.serviceAgents[className] = eval(className + "(self.bus, callback=self.srvCB, objPath=self.agentObjectPath, busName=self.srvName)", self.globalCtx, self.localCtx)
634 self.serviceAgents[className].add_to_connection()
635 return (self.agentObjectPath)
638 def serviceDelAgent(self, list):
640 arguments: objectPath, xmlTemplate
642 agentObjectPath = list[0]
643 className = createClassName(agentObjectPath)
645 print 'PY Try to remove ' + className
647 if (self.serviceAgents.has_key(className)):
648 self.serviceAgents[className].remove_from_connection()
649 self.serviceAgents.pop(className)
651 raise Exception(agentObjectPath + " doesn't exist!")
653 return (agentObjectPath)
656 def getVersion(self):
658 return current version string
664 ###############################################################################
665 class CloudeebusServerProtocol(WampCraServerProtocol):
667 connexion and session authentication management
670 def onSessionOpen(self):
671 # CRA authentication options
672 self.clientAuthTimeout = 0
673 self.clientAuthAllowAnonymous = OPENDOOR
674 # CRA authentication init
675 WampCraServerProtocol.onSessionOpen(self)
678 def getAuthPermissions(self, key, extra):
679 return {'permissions': extra.get("permissions", None),
680 'authextra': extra.get("authextra", None),
681 'services': extra.get("services", None)}
683 def getAuthSecret(self, key):
684 secret = CREDENTIALS.get(key, None)
687 # secret must be of str type to be hashed
691 def onAuthenticated(self, key, permissions):
696 for netfilter in NETMASK:
697 ipHex=ipV4ToHex(self.peer.host)
698 ipAllowed = (ipHex & netfilter['mask']) == netfilter['ipAllowed'] & netfilter['mask']
702 raise Exception("host " + self.peer.host + " is not allowed!")
703 # check authentication key
705 raise Exception("Authentication failed")
706 # check permissions, array.index throws exception
707 if (permissions['permissions'] != None):
708 for req in permissions['permissions']:
709 WHITELIST.index(req);
710 # check allowed service creation, array.index throws exception
711 if (permissions['services'] != None):
712 for req in permissions['services']:
713 SERVICELIST.index(req);
714 # create cloudeebus service instance
715 self.cloudeebusService = CloudeebusService(permissions)
716 # register it for RPC
717 self.registerForRpc(self.cloudeebusService)
718 # register for Publish / Subscribe
719 self.registerForPubSub("", True)
722 def connectionLost(self, reason):
723 WampCraServerProtocol.connectionLost(self, reason)
724 if factory.getConnectionCount() == 0:
729 ###############################################################################
731 if __name__ == '__main__':
735 parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
736 parser.add_argument('-v', '--version', action='store_true',
737 help='print version and exit')
738 parser.add_argument('-d', '--debug', action='store_true',
739 help='log debug info on standard output')
740 parser.add_argument('-o', '--opendoor', action='store_true',
741 help='allow anonymous access to all services')
742 parser.add_argument('-p', '--port', default='9000',
744 parser.add_argument('-c', '--credentials',
745 help='path to credentials file')
746 parser.add_argument('-w', '--whitelist',
747 help='path to whitelist file (DBus services to use)')
748 parser.add_argument('-s', '--servicelist',
749 help='path to servicelist file (DBus services to export)')
750 parser.add_argument('-n', '--netmask',
751 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')
753 args = parser.parse_args(sys.argv[1:])
756 print("Cloudeebus version " + VERSION)
760 log.startLogging(sys.stdout)
762 OPENDOOR = args.opendoor
765 jfile = open(args.credentials)
766 CREDENTIALS = json.load(jfile)
770 jfile = open(args.whitelist)
771 WHITELIST = json.load(jfile)
775 jfile = open(args.servicelist)
776 SERVICELIST = json.load(jfile)
780 iplist = args.netmask.split(",")
782 if ip.rfind("/") != -1:
788 mask = "255.255.255.255"
789 NETMASK.append( {'ipAllowed': ipV4ToHex(ipAllowed), 'mask' : ipV4ToHex(mask)} )
791 uri = "ws://localhost:" + args.port
793 factory = WampServerFactory(uri, debugWamp = args.debug)
794 factory.protocol = CloudeebusServerProtocol
795 factory.setProtocolOptions(allowHixie76 = True)
799 DBusGMainLoop(set_as_default=True)