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 = permissions;
186 self.proxyObjects = {}
187 self.proxyMethods = {}
188 self.pendingCalls = []
191 def proxyObject(self, busName, serviceName, objectName):
193 object hash id as busName#serviceName#objectName
195 id = "#".join([busName, serviceName, objectName])
196 if not self.proxyObjects.has_key(id):
198 # check permissions, array.index throws exception
199 self.permissions.index(serviceName)
200 bus = cache.dbusConnexion(busName)
201 self.proxyObjects[id] = bus.get_object(serviceName, objectName)
202 return self.proxyObjects[id]
205 def proxyMethod(self, busName, serviceName, objectName, interfaceName, methodName):
207 method hash id as busName#serviceName#objectName#interfaceName#methodName
209 id = "#".join([busName, serviceName, objectName, interfaceName, methodName])
210 if not self.proxyMethods.has_key(id):
211 obj = self.proxyObject(busName, serviceName, objectName)
212 self.proxyMethods[id] = obj.get_dbus_method(methodName, interfaceName)
213 return self.proxyMethods[id]
217 def dbusRegister(self, list):
219 arguments: bus, sender, object, interface, signal
222 raise Exception("Error: expected arguments: bus, sender, object, interface, signal)")
225 # check permissions, array.index throws exception
226 self.permissions.index(list[1])
228 # check if a handler exists
229 sigId = "#".join(list)
230 if cache.signalHandlers.has_key(sigId):
233 # create a handler that will publish the signal
234 dbusSignalHandler = DbusSignalHandler(*list)
235 cache.signalHandlers[sigId] = dbusSignalHandler
237 return dbusSignalHandler.id
241 def dbusSend(self, list):
243 arguments: bus, destination, object, interface, message, [args]
245 # clear pending calls
246 for call in self.pendingCalls:
248 self.pendingCalls.remove(call)
251 raise Exception("Error: expected arguments: bus, destination, object, interface, message, [args])")
253 # parse JSON arg list
256 args = json.loads(list[5])
258 # get dbus proxy method
259 method = self.proxyMethod(*list[0:5])
261 # use a deferred call handler to manage dbus results
262 dbusCallHandler = DbusCallHandler(method, args)
263 self.pendingCalls.append(dbusCallHandler)
264 return dbusCallHandler.callMethod()
268 def getVersion(self):
270 return current version string
276 ###############################################################################
277 class CloudeebusServerProtocol(WampCraServerProtocol):
279 connexion and session authentication management
282 def onSessionOpen(self):
283 # CRA authentication options
284 self.clientAuthTimeout = 0
285 self.clientAuthAllowAnonymous = OPENDOOR
286 # CRA authentication init
287 WampCraServerProtocol.onSessionOpen(self)
290 def getAuthPermissions(self, key, extra):
291 return json.loads(extra.get("permissions", "[]"))
294 def getAuthSecret(self, key):
295 secret = CREDENTIALS.get(key, None)
298 # secret must be of str type to be hashed
299 return secret.encode('utf-8')
302 def onAuthenticated(self, key, permissions):
307 for netfilter in NETMASK:
308 ipHex=ipV4ToHex(self.peer.host)
309 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)