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>
24 import argparse, dbus, json, sys
26 from twisted.internet import glib2reactor
27 # Configure the twisted mainloop to be run inside the glib mainloop.
28 # This must be done before importing the other twisted modules
29 glib2reactor.install()
30 from twisted.internet import reactor, defer
32 from autobahn.websocket import listenWS
33 from autobahn.wamp import exportRpc, WampServerFactory, WampCraServerProtocol
35 from dbus.mainloop.glib import DBusGMainLoop
38 gobject.threads_init()
44 from twisted.python import log
48 ###############################################################################
56 ###############################################################################
58 ## Convert an ip or an IP mask (such as ip/24 or ip/255.255.255.0) in hex value (32bits)
61 if mask.rfind(".") == -1:
63 maskHex = (2**(int(mask))-1)
64 maskHex = maskHex << (32-int(mask))
66 raise Exception("Illegal mask (larger than 32 bits) " + mask)
68 maskField = mask.split(".")
69 # Check if mask has four fields (byte)
70 if len(maskField) != 4:
71 raise Exception("Illegal ip address / mask (should be 4 bytes) " + mask)
72 for maskQuartet in maskField:
73 byte = int(maskQuartet)
74 # Check if each field is really a byte
76 raise Exception("Illegal ip address / mask (digit larger than a byte) " + mask)
78 maskHex = maskHex << 8
79 maskHex = maskHex >> 8
82 ###############################################################################
85 Global cache of DBus connexions and signal handlers
88 self.dbusConnexions = {}
89 self.signalHandlers = {}
94 Disconnect signal handlers before resetting cache.
96 self.dbusConnexions = {}
97 # disconnect signal handlers
98 for key in self.signalHandlers:
99 self.signalHandlers[key].disconnect()
100 self.signalHandlers = {}
103 def dbusConnexion(self, busName):
104 if not self.dbusConnexions.has_key(busName):
105 if busName == "session":
106 self.dbusConnexions[busName] = dbus.SessionBus()
107 elif busName == "system":
108 self.dbusConnexions[busName] = dbus.SystemBus()
110 raise Exception("Error: invalid bus: %s" % busName)
111 return self.dbusConnexions[busName]
115 ###############################################################################
116 class DbusSignalHandler:
118 signal hash id as busName#senderName#objectName#interfaceName#signalName
120 def __init__(self, busName, senderName, objectName, interfaceName, signalName):
121 self.id = "#".join([busName, senderName, objectName, interfaceName, signalName])
122 # connect handler to signal
123 self.bus = cache.dbusConnexion(busName)
124 self.bus.add_signal_receiver(self.handleSignal, signalName, interfaceName, senderName, objectName)
127 def disconnect(self):
128 names = self.id.split("#")
129 self.bus.remove_signal_receiver(self.handleSignal, names[4], names[3], names[1], names[2])
132 def handleSignal(self, *args):
134 publish dbus args under topic hash id
136 factory.dispatch(self.id, json.dumps(args))
140 ###############################################################################
141 class DbusCallHandler:
143 deferred reply to return dbus results
145 def __init__(self, method, args):
147 self.request = defer.Deferred()
152 def callMethod(self):
154 dbus method async call
157 self.method(*self.args, reply_handler=self.dbusSuccess, error_handler=self.dbusError)
161 def dbusSuccess(self, *result):
163 return JSON string result array
165 self.request.callback(json.dumps(result))
169 def dbusError(self, error):
171 return dbus error message
173 self.request.errback(Exception(error.get_dbus_message()))
178 ###############################################################################
179 class CloudeebusService:
181 support for sending DBus messages and registering for DBus signals
183 def __init__(self, permissions):
184 self.permissions = permissions;
185 self.proxyObjects = {}
186 self.proxyMethods = {}
187 self.pendingCalls = []
190 def proxyObject(self, busName, serviceName, objectName):
192 object hash id as busName#serviceName#objectName
194 id = "#".join([busName, serviceName, objectName])
195 if not self.proxyObjects.has_key(id):
197 # check permissions, array.index throws exception
198 self.permissions.index(serviceName)
199 bus = cache.dbusConnexion(busName)
200 self.proxyObjects[id] = bus.get_object(serviceName, objectName)
201 return self.proxyObjects[id]
204 def proxyMethod(self, busName, serviceName, objectName, interfaceName, methodName):
206 method hash id as busName#serviceName#objectName#interfaceName#methodName
208 id = "#".join([busName, serviceName, objectName, interfaceName, methodName])
209 if not self.proxyMethods.has_key(id):
210 obj = self.proxyObject(busName, serviceName, objectName)
211 self.proxyMethods[id] = obj.get_dbus_method(methodName, interfaceName)
212 return self.proxyMethods[id]
216 def dbusRegister(self, list):
218 arguments: bus, sender, object, interface, signal
221 raise Exception("Error: expected arguments: bus, sender, object, interface, signal)")
224 # check permissions, array.index throws exception
225 self.permissions.index(list[1])
227 # check if a handler exists
228 sigId = "#".join(list)
229 if cache.signalHandlers.has_key(sigId):
232 # create a handler that will publish the signal
233 dbusSignalHandler = DbusSignalHandler(*list)
234 cache.signalHandlers[sigId] = dbusSignalHandler
236 return dbusSignalHandler.id
240 def dbusSend(self, list):
242 arguments: bus, destination, object, interface, message, [args]
244 # clear pending calls
245 for call in self.pendingCalls:
247 self.pendingCalls.remove(call)
250 raise Exception("Error: expected arguments: bus, destination, object, interface, message, [args])")
252 # parse JSON arg list
255 args = json.loads(list[5])
257 # get dbus proxy method
258 method = self.proxyMethod(*list[0:5])
260 # use a deferred call handler to manage dbus results
261 dbusCallHandler = DbusCallHandler(method, args)
262 self.pendingCalls.append(dbusCallHandler)
263 return dbusCallHandler.callMethod()
267 def getVersion(self):
269 return current version string
275 ###############################################################################
276 class CloudeebusServerProtocol(WampCraServerProtocol):
278 connexion and session authentication management
281 def onSessionOpen(self):
282 # CRA authentication options
283 self.clientAuthTimeout = 0
284 self.clientAuthAllowAnonymous = OPENDOOR
285 # CRA authentication init
286 WampCraServerProtocol.onSessionOpen(self)
289 def getAuthPermissions(self, key, extra):
290 return json.loads(extra.get("permissions", "[]"))
293 def getAuthSecret(self, key):
294 secret = CREDENTIALS.get(key, None)
297 # secret must be of str type to be hashed
298 return secret.encode('utf-8')
301 def onAuthenticated(self, key, permissions):
306 for netfilter in NETMASK:
307 ipHex=ipV4ToHex(self.peer.host)
308 ipAllowed = (ipHex & netfilter['mask']) == netfilter['ipAllowed'] & netfilter['mask']
313 raise Exception("host " + self.peer.host + " is not allowed!")
314 # check authentication key
316 raise Exception("Authentication failed")
317 # check permissions, array.index throws exception
318 for req in permissions:
320 # create cloudeebus service instance
321 self.cloudeebusService = CloudeebusService(permissions)
322 # register it for RPC
323 self.registerForRpc(self.cloudeebusService)
324 # register for Publish / Subscribe
325 self.registerForPubSub("", True)
328 def connectionLost(self, reason):
329 WampCraServerProtocol.connectionLost(self, reason)
330 if factory.getConnectionCount() == 0:
335 ###############################################################################
337 if __name__ == '__main__':
341 parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
342 parser.add_argument('-v', '--version', action='store_true',
343 help='print version and exit')
344 parser.add_argument('-d', '--debug', action='store_true',
345 help='log debug info on standard output')
346 parser.add_argument('-o', '--opendoor', action='store_true',
347 help='allow anonymous access to all services')
348 parser.add_argument('-p', '--port', default='9000',
350 parser.add_argument('-c', '--credentials',
351 help='path to credentials file')
352 parser.add_argument('-w', '--whitelist',
353 help='path to whitelist file')
354 parser.add_argument('-n', '--netmask',
355 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')
357 args = parser.parse_args(sys.argv[1:])
360 print("Cloudeebus version " + VERSION)
364 log.startLogging(sys.stdout)
366 OPENDOOR = args.opendoor
369 jfile = open(args.credentials)
370 CREDENTIALS = json.load(jfile)
374 jfile = open(args.whitelist)
375 WHITELIST = json.load(jfile)
379 iplist = args.netmask.split(",")
381 if ip.rfind("/") != -1:
387 mask = "255.255.255.255"
388 NETMASK.append( {'ipAllowed': ipV4ToHex(ipAllowed), 'mask' : ipV4ToHex(mask)} )
390 uri = "ws://localhost:" + args.port
392 factory = WampServerFactory(uri, debugWamp = args.debug)
393 factory.protocol = CloudeebusServerProtocol
394 factory.setProtocolOptions(allowHixie76 = True)
398 DBusGMainLoop(set_as_default=True)