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
39 gobject.threads_init()
45 from twisted.python import log
49 ###############################################################################
57 ###############################################################################
59 ## Convert an ip or an IP mask (such as ip/24 or ip/255.255.255.0) in hex value (32bits)
62 if mask.rfind(".") == -1:
64 maskHex = (2**(int(mask))-1)
65 maskHex = maskHex << (32-int(mask))
67 raise Exception("Illegal mask (larger than 32 bits) " + mask)
69 maskField = mask.split(".")
70 # Check if mask has four fields (byte)
71 if len(maskField) != 4:
72 raise Exception("Illegal ip address / mask (should be 4 bytes) " + mask)
73 for maskQuartet in maskField:
74 byte = int(maskQuartet)
75 # Check if each field is really a byte
77 raise Exception("Illegal ip address / mask (digit larger than a byte) " + mask)
79 maskHex = maskHex << 8
80 maskHex = maskHex >> 8
83 ###############################################################################
86 Global cache of DBus connexions and signal handlers
89 self.dbusConnexions = {}
90 self.signalHandlers = {}
95 Disconnect signal handlers before resetting cache.
97 self.dbusConnexions = {}
98 # disconnect signal handlers
99 for key in self.signalHandlers:
100 self.signalHandlers[key].disconnect()
101 self.signalHandlers = {}
104 def dbusConnexion(self, busName):
105 if not self.dbusConnexions.has_key(busName):
106 if busName == "session":
107 self.dbusConnexions[busName] = dbus.SessionBus()
108 elif busName == "system":
109 self.dbusConnexions[busName] = dbus.SystemBus()
111 raise Exception("Error: invalid bus: %s" % busName)
112 return self.dbusConnexions[busName]
116 ###############################################################################
117 class DbusSignalHandler:
119 signal hash id as busName#senderName#objectName#interfaceName#signalName
121 def __init__(self, busName, senderName, objectName, interfaceName, signalName):
122 self.id = "#".join([busName, senderName, objectName, interfaceName, signalName])
123 # connect handler to signal
124 self.bus = cache.dbusConnexion(busName)
125 self.bus.add_signal_receiver(self.handleSignal, signalName, interfaceName, senderName, objectName)
128 def disconnect(self):
129 names = self.id.split("#")
130 self.bus.remove_signal_receiver(self.handleSignal, names[4], names[3], names[1], names[2])
133 def handleSignal(self, *args):
135 publish dbus args under topic hash id
137 factory.dispatch(self.id, json.dumps(args))
141 ###############################################################################
142 class DbusCallHandler:
144 deferred reply to return dbus results
146 def __init__(self, method, args):
148 self.request = defer.Deferred()
153 def callMethod(self):
155 dbus method async call
158 self.method(*self.args, reply_handler=self.dbusSuccess, error_handler=self.dbusError)
162 def dbusSuccess(self, *result):
164 return JSON string result array
166 self.request.callback(json.dumps(result))
170 def dbusError(self, error):
172 return dbus error message
174 self.request.errback(Exception(error.get_dbus_message()))
179 ###############################################################################
180 class CloudeebusService:
182 support for sending DBus messages and registering for DBus signals
184 def __init__(self, permissions):
185 self.permissions = {};
186 self.permissions['permissions'] = permissions['permissions']
187 self.permissions['authextra'] = permissions['authextra']
188 self.proxyObjects = {}
189 self.proxyMethods = {}
190 self.pendingCalls = []
193 def proxyObject(self, busName, serviceName, objectName):
195 object hash id as busName#serviceName#objectName
197 id = "#".join([busName, serviceName, objectName])
198 if not self.proxyObjects.has_key(id):
200 # check permissions, array.index throws exception
201 self.permissions['permissions'].index(serviceName)
202 bus = cache.dbusConnexion(busName)
203 self.proxyObjects[id] = bus.get_object(serviceName, objectName)
204 return self.proxyObjects[id]
207 def proxyMethod(self, busName, serviceName, objectName, interfaceName, methodName):
209 method hash id as busName#serviceName#objectName#interfaceName#methodName
211 id = "#".join([busName, serviceName, objectName, interfaceName, methodName])
212 if not self.proxyMethods.has_key(id):
213 obj = self.proxyObject(busName, serviceName, objectName)
214 self.proxyMethods[id] = obj.get_dbus_method(methodName, interfaceName)
215 return self.proxyMethods[id]
219 def dbusRegister(self, list):
221 arguments: bus, sender, object, interface, signal
224 raise Exception("Error: expected arguments: bus, sender, object, interface, signal)")
227 # check permissions, array.index throws exception
228 self.permissions['permissions'].index(list[1])
230 # check if a handler exists
231 sigId = "#".join(list)
232 if cache.signalHandlers.has_key(sigId):
235 # create a handler that will publish the signal
236 dbusSignalHandler = DbusSignalHandler(*list)
237 cache.signalHandlers[sigId] = dbusSignalHandler
239 return dbusSignalHandler.id
243 def dbusSend(self, list):
245 arguments: bus, destination, object, interface, message, [args]
247 # clear pending calls
248 for call in self.pendingCalls:
250 self.pendingCalls.remove(call)
253 raise Exception("Error: expected arguments: bus, destination, object, interface, message, [args])")
255 # parse JSON arg list
258 args = json.loads(list[5])
260 # get dbus proxy method
261 method = self.proxyMethod(*list[0:5])
263 # use a deferred call handler to manage dbus results
264 dbusCallHandler = DbusCallHandler(method, args)
265 self.pendingCalls.append(dbusCallHandler)
266 return dbusCallHandler.callMethod()
270 def getVersion(self):
272 return current version string
278 ###############################################################################
279 class CloudeebusServerProtocol(WampCraServerProtocol):
281 connexion and session authentication management
284 def onSessionOpen(self):
285 # CRA authentication options
286 self.clientAuthTimeout = 0
287 self.clientAuthAllowAnonymous = OPENDOOR
288 # CRA authentication init
289 WampCraServerProtocol.onSessionOpen(self)
292 def getAuthPermissions(self, key, extra):
293 return {'permissions': extra.get("permissions", None),
294 'authextra': extra.get("authextra", None)}
296 def getAuthSecret(self, key):
297 secret = CREDENTIALS.get(key, None)
300 # secret must be of str type to be hashed
304 def onAuthenticated(self, key, permissions):
309 for netfilter in NETMASK:
310 ipHex=ipV4ToHex(self.peer.host)
311 ipAllowed = (ipHex & netfilter['mask']) == netfilter['ipAllowed'] & netfilter['mask']
315 raise Exception("host " + self.peer.host + " is not allowed!")
316 # check authentication key
318 raise Exception("Authentication failed")
319 # check permissions, array.index throws exception
320 for req in permissions['permissions']:
321 WHITELIST.index(req);
322 # create cloudeebus service instance
323 self.cloudeebusService = CloudeebusService(permissions)
324 # register it for RPC
325 self.registerForRpc(self.cloudeebusService)
326 # register for Publish / Subscribe
327 self.registerForPubSub("", True)
330 def connectionLost(self, reason):
331 WampCraServerProtocol.connectionLost(self, reason)
332 if factory.getConnectionCount() == 0:
337 ###############################################################################
339 if __name__ == '__main__':
343 parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
344 parser.add_argument('-v', '--version', action='store_true',
345 help='print version and exit')
346 parser.add_argument('-d', '--debug', action='store_true',
347 help='log debug info on standard output')
348 parser.add_argument('-o', '--opendoor', action='store_true',
349 help='allow anonymous access to all services')
350 parser.add_argument('-p', '--port', default='9000',
352 parser.add_argument('-c', '--credentials',
353 help='path to credentials file')
354 parser.add_argument('-w', '--whitelist',
355 help='path to whitelist file')
356 parser.add_argument('-n', '--netmask',
357 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')
359 args = parser.parse_args(sys.argv[1:])
362 print("Cloudeebus version " + VERSION)
366 log.startLogging(sys.stdout)
368 OPENDOOR = args.opendoor
371 jfile = open(args.credentials)
372 CREDENTIALS = json.load(jfile)
376 jfile = open(args.whitelist)
377 WHITELIST = json.load(jfile)
381 iplist = args.netmask.split(",")
383 if ip.rfind("/") != -1:
389 mask = "255.255.255.255"
390 NETMASK.append( {'ipAllowed': ipV4ToHex(ipAllowed), 'mask' : ipV4ToHex(mask)} )
392 uri = "ws://localhost:" + args.port
394 factory = WampServerFactory(uri, debugWamp = args.debug)
395 factory.protocol = CloudeebusServerProtocol
396 factory.setProtocolOptions(allowHixie76 = True)
400 DBusGMainLoop(set_as_default=True)