xaal.lib 0.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
xaal/lib/engine.py ADDED
@@ -0,0 +1,231 @@
1
+ #
2
+ # Copyright 2014, Jérôme Colin, Jérôme Kerdreux, Philippe Tanguy,
3
+ # Telecom Bretagne.
4
+ #
5
+ # This file is part of xAAL.
6
+ #
7
+ # xAAL is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # xAAL is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with xAAL. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ from . import core
22
+ from .network import NetworkConnector
23
+ from .exceptions import *
24
+ from . import config
25
+
26
+ import time
27
+ import collections
28
+ from enum import Enum
29
+
30
+ import logging
31
+ logger = logging.getLogger(__name__)
32
+
33
+ class EngineState(Enum):
34
+ started = 1
35
+ running = 2
36
+ halted = 3
37
+
38
+ class Engine(core.EngineMixin):
39
+
40
+ __slots__ = ['__last_timer','__txFifo','state','network']
41
+
42
+ def __init__(self,address=config.address,port=config.port,hops=config.hops,key=config.key):
43
+ core.EngineMixin.__init__(self,address,port,hops,key)
44
+
45
+ self.__last_timer = 0 # last timer check
46
+ self.__txFifo = collections.deque() # tx msg fifo
47
+
48
+ # message receive workflow
49
+ self.subscribe(self.handle_request)
50
+ # ready to go
51
+ self.state = EngineState.halted
52
+ # start network
53
+ self.network = NetworkConnector(address, port, hops)
54
+
55
+ #####################################################
56
+ # xAAL messages Tx handling
57
+ #####################################################
58
+ # Fifo for msg to send
59
+ def queue_msg(self, msg):
60
+ """queue an encoded / cyphered message"""
61
+ self.__txFifo.append(msg)
62
+
63
+ def send_msg(self, msg):
64
+ """Send an encoded message to the bus, use queue_msg instead"""
65
+ self.network.send(msg)
66
+
67
+ def process_tx_msg(self):
68
+ """ Process (send) message in tx queue called from the loop()"""
69
+ cnt = 0
70
+ while self.__txFifo:
71
+ temp = self.__txFifo.popleft()
72
+ self.send_msg(temp)
73
+ # try to limit rate
74
+ cnt = cnt + 1
75
+ if cnt > config.queue_size:
76
+ time.sleep(0.2)
77
+ break
78
+
79
+ #####################################################
80
+ # xAAL messages subscribers
81
+ #####################################################
82
+ def receive_msg(self):
83
+ """return new received message or None"""
84
+ result = None
85
+ data = self.network.get_data()
86
+ if data:
87
+ try:
88
+ msg = self.msg_factory.decode_msg(data,self.msg_filter)
89
+ except MessageParserError as e:
90
+ logger.warning(e)
91
+ msg = None
92
+ result = msg
93
+ return result
94
+
95
+ def process_subscribers(self):
96
+ """process incomming messages"""
97
+ msg = self.receive_msg()
98
+ if msg:
99
+ for func in self.subscribers:
100
+ func(msg)
101
+ self.process_attributes_change()
102
+
103
+ def handle_request(self, msg):
104
+ """
105
+ Filter msg for devices according default xAAL API then process the
106
+ request for each targets identied in the engine
107
+ """
108
+ if not msg.is_request():
109
+ return
110
+
111
+ targets = core.filter_msg_for_devices(msg, self.devices)
112
+ for target in targets:
113
+ if msg.action == 'is_alive':
114
+ self.send_alive(target)
115
+ else:
116
+ self.handle_action_request(msg, target)
117
+
118
+ def handle_action_request(self, msg, target):
119
+ """
120
+ Run method (xAAL exposed method) on device:
121
+ - None is returned if device method do not return anything
122
+ - result is returned if device method gives a response
123
+ - Errors are raised if an error occured:
124
+ * Internal error
125
+ * error returned on the xAAL bus
126
+ """
127
+ try:
128
+ result = run_action(msg, target)
129
+ if result != None:
130
+ self.send_reply(dev=target,targets=[msg.source],action=msg.action,body=result)
131
+ except CallbackError as e:
132
+ self.send_error(target, e.code, e.description)
133
+ except XAALError as e:
134
+ logger.error(e)
135
+
136
+ #####################################################
137
+ # timers
138
+ #####################################################
139
+ def process_timers(self):
140
+ """Process all timers to find out which ones should be run"""
141
+ expire_list = []
142
+
143
+ if len(self.timers)!=0 :
144
+ now = time.time()
145
+ # little hack to avoid to check timer to often.
146
+ # w/ this enable timer precision is bad, but far enougth
147
+ if (now - self.__last_timer) < 0.4: return
148
+
149
+ for t in self.timers:
150
+ if t.deadline < now:
151
+ try:
152
+ t.func()
153
+ except CallbackError as e:
154
+ logger.error(e.description)
155
+ if t.counter != -1:
156
+ t.counter-= 1
157
+ if t.counter == 0:
158
+ expire_list.append(t)
159
+ t.deadline = now + t.period
160
+ # delete expired timers
161
+ for t in expire_list:
162
+ self.remove_timer(t)
163
+
164
+ self.__last_timer = now
165
+
166
+ #####################################################
167
+ # Mainloops & run ..
168
+ #####################################################
169
+ def loop(self):
170
+ """
171
+ Process incomming xAAL msg
172
+ Process timers
173
+ Process attributes change for devices
174
+ Process is_alive for device
175
+ Send msgs from the Tx Buffer
176
+ """
177
+ # Process xAAL msg received, filter msg and process request
178
+ self.process_subscribers()
179
+ # Process timers
180
+ self.process_timers()
181
+ # Process attributes change for devices due to timers
182
+ self.process_attributes_change()
183
+ # Process Alives
184
+ self.process_alives()
185
+ # Process xAAL msgs to send
186
+ self.process_tx_msg()
187
+
188
+ def start(self):
189
+ """Start the core engine: send queue alive msg"""
190
+ if self.state in [EngineState.started,EngineState.running]:
191
+ return
192
+ self.network.connect()
193
+ for dev in self.devices:
194
+ self.send_alive(dev)
195
+ self.state = EngineState.started
196
+
197
+ def stop(self):
198
+ self.state = EngineState.halted
199
+
200
+ def shutdown(self):
201
+ self.stop()
202
+
203
+ def run(self):
204
+ self.start()
205
+ self.state = EngineState.running
206
+ while self.state == EngineState.running:
207
+ self.loop()
208
+
209
+ def is_running(self):
210
+ if self.state == EngineState.running:
211
+ return True
212
+ return False
213
+
214
+ def run_action(msg,device):
215
+ """
216
+ Extract an action & launch it
217
+ Return:
218
+ - action result
219
+ - None if no result
220
+
221
+ Note: If an exception raised, it's logged, and raise an XAALError.
222
+ """
223
+ method,params = core.search_action(msg,device)
224
+ result = None
225
+ try:
226
+ result = method(**params)
227
+ except Exception as e:
228
+ logger.error(e)
229
+ raise XAALError("Error in method:%s params:%s" % (msg.action,params))
230
+ return result
231
+
xaal/lib/exceptions.py ADDED
@@ -0,0 +1,20 @@
1
+
2
+ # devices.py
3
+ class DeviceError(Exception):pass
4
+
5
+ # core.py
6
+ class EngineError(Exception):pass
7
+ class XAALError(Exception):pass
8
+ class CallbackError(Exception):
9
+ def __init__(self, code, desc):
10
+ self.code = code
11
+ self.description = desc
12
+
13
+ # messages.py
14
+ class MessageParserError(Exception):pass
15
+ class MessageError(Exception):pass
16
+
17
+ # binding.py
18
+ class UUIDError(Exception):pass
19
+
20
+ __all__ = ["DeviceError","EngineError","XAALError","CallbackError","MessageParserError","MessageError","UUIDError"]
xaal/lib/helpers.py ADDED
@@ -0,0 +1,80 @@
1
+ """
2
+ This file contains some helpers functions. This functions aren't used in the lib itself
3
+ but can be usefull for xaal packages developpers
4
+ """
5
+
6
+ import logging
7
+ import logging.handlers
8
+ import os
9
+ import time
10
+ import coloredlogs
11
+ from decorator import decorator
12
+
13
+ from . import config
14
+
15
+ def singleton(class_):
16
+ instances = {}
17
+ def getinstance(*args, **kwargs):
18
+ if class_ not in instances:
19
+ instances[class_] = class_(*args, **kwargs)
20
+ return instances[class_]
21
+ return getinstance
22
+
23
+
24
+ @decorator
25
+ def timeit(method,*args,**kwargs):
26
+ logger = logging.getLogger(__name__)
27
+ ts = time.time()
28
+ result = method(*args, **kwargs)
29
+ te = time.time()
30
+ logger.debug('%r (%r, %r) %2.6f sec' % (method.__name__, args, kwargs, te-ts))
31
+ return result
32
+
33
+ def set_console_title(value):
34
+ # set xterm title
35
+ print("\x1B]0;xAAL => %s\x07" % value,end='\r')
36
+
37
+ def setup_console_logger(level=config.log_level):
38
+ fmt = '%(asctime)s %(name)-25s %(funcName)-18s %(levelname)-8s %(message)s'
39
+ #fmt = '[%(name)s] %(funcName)s %(levelname)s: %(message)s'
40
+ coloredlogs.install(level=level,fmt=fmt)
41
+
42
+ def setup_file_logger(name,level=config.log_level,filename = None):
43
+ filename = filename or os.path.join(config.log_path,'%s.log' % name)
44
+ formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')
45
+ handler = logging.handlers.RotatingFileHandler(filename, 'a', 10000, 1, 'utf8')
46
+ handler.setLevel(level)
47
+ handler.setFormatter(formatter)
48
+ # register the new handler
49
+ logger = logging.getLogger(name)
50
+ logger.root.addHandler(handler)
51
+ logger.root.setLevel('DEBUG')
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # TBD: We should merge this stuffs, and add support for default config file
55
+ # and commnand line parsing.
56
+ #
57
+ # Default arguments console_log and file_log are (and should) never be used.
58
+ # ---------------------------------------------------------------------------
59
+ def run_package(pkg_name,pkg_setup,console_log = True,file_log=False):
60
+ if console_log:
61
+ set_console_title(pkg_name)
62
+ setup_console_logger()
63
+ if file_log:
64
+ setup_file_logger(pkg_name)
65
+ logger = logging.getLogger(pkg_name)
66
+ logger.info('starting xaal package: %s'% pkg_name )
67
+
68
+ from .engine import Engine
69
+ eng = Engine()
70
+ result = pkg_setup(eng)
71
+
72
+ if result != True:
73
+ logger.critical("something goes wrong with package: %s" % pkg_name)
74
+ try:
75
+ eng.run()
76
+ except KeyboardInterrupt:
77
+ eng.shutdown()
78
+ logger.info("exit")
79
+
80
+ __all__ = ['singleton','timeit','set_console_title','setup_console_logger','setup_file_logger','run_package']
xaal/lib/messages.py ADDED
@@ -0,0 +1,336 @@
1
+ #
2
+ # Copyright 2014, Jérôme Colin, Jérôme Kerdreux, Philippe Tanguy,
3
+ # Telecom Bretagne.
4
+ #
5
+ # This file is part of xAAL.
6
+ #
7
+ # xAAL is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # xAAL is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with xAAL. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ from . import tools
22
+ from . import config
23
+ from .bindings import UUID
24
+ from .exceptions import MessageError, MessageParserError
25
+ from . import cbor
26
+
27
+ from enum import Enum
28
+ from tabulate import tabulate
29
+ import pprint
30
+ import datetime
31
+ import pysodium
32
+ import struct
33
+ import sys
34
+
35
+ import logging
36
+ logger = logging.getLogger(__name__)
37
+
38
+ ALIVE_ADDR = UUID("00000000-0000-0000-0000-000000000000")
39
+
40
+
41
+ class MessageFactory(object):
42
+ """Message Factory:
43
+ - Build xAAL message
44
+ - Apply security layer, Ciphering/De-Ciphering chacha20 poly1305
45
+ - Serialize/Deserialize data in CBOR"""
46
+
47
+ def __init__(self, cipher_key):
48
+ self.cipher_key = cipher_key # key encode / decode message built from passphrase
49
+
50
+ def encode_msg(self, msg):
51
+ """Apply security layer and return encode MSG in CBOR
52
+ :param msg: xAAL msg instance
53
+ :type msg: Message
54
+ :return: return an xAAL msg ciphered and serialized in CBOR
55
+ :rtype: CBOR
56
+ """
57
+
58
+ # Format security layer
59
+ result = []
60
+ result.append(msg.version)
61
+ result.append(msg.timestamp[0])
62
+ result.append(msg.timestamp[1])
63
+ # list of UUID in bytes format (no Tag here)
64
+ result.append(cbor.dumps([t.bytes for t in msg.targets]))
65
+
66
+ # Format payload & ciphering
67
+ buf = []
68
+ buf.append(msg.source.bytes)
69
+ buf.append(msg.dev_type)
70
+ buf.append(msg.msg_type.value)
71
+ buf.append(msg.action)
72
+ if msg.body:
73
+ buf.append(msg.body)
74
+ clear = cbor.dumps(buf)
75
+ # Additionnal Data == cbor serialization of the targets array
76
+ ad = result[3]
77
+ nonce = build_nonce(msg.timestamp)
78
+ payload = pysodium.crypto_aead_chacha20poly1305_ietf_encrypt(clear, ad, nonce, self.cipher_key)
79
+
80
+ # Final CBOR serialization
81
+ result.append(payload)
82
+ pkt = cbor.dumps(result)
83
+ return pkt
84
+
85
+ def decode_msg(self, data, filter_func=None):
86
+ """Decode incoming CBOR data and De-Ciphering
87
+ :param data: data received from the multicast bus
88
+ :type data: cbor
89
+ :filter_func: function to filter incoming messages
90
+ :type filter_func: function
91
+ :return: xAAL msg
92
+ :rtype: Message
93
+ """
94
+ # Decode cbor incoming data
95
+ try:
96
+ data_rx = cbor.loads(data)
97
+ except:
98
+ raise MessageParserError("Unable to parse CBOR data")
99
+
100
+ # Instanciate Message, parse the security layer
101
+ msg = Message()
102
+ try:
103
+ msg.version = data_rx[0]
104
+ msg_time = data_rx[1]
105
+ targets = cbor.loads(data_rx[3])
106
+ msg.targets = [UUID(bytes=t) for t in targets]
107
+ msg.timestamp = [data_rx[1], data_rx[2]]
108
+ except IndexError:
109
+ raise MessageParserError("Bad Message, wrong fields")
110
+
111
+ # filter some messages
112
+ if filter_func is not None:
113
+ if not filter_func(msg):
114
+ return None # filter out the message
115
+
116
+ # Replay attack, window fixed to CIPHER_WINDOW in seconds
117
+ now = build_timestamp()[0] # test done only on seconds ...
118
+ if msg_time < (now - config.cipher_window):
119
+ raise MessageParserError("Potential replay attack, message too old: %d sec" % round(now - msg_time) )
120
+
121
+ if msg_time > (now + config.cipher_window):
122
+ raise MessageParserError("Potential replay attack, message too young: %d sec" % round(now - msg_time))
123
+
124
+ # Payload De-Ciphering
125
+ try:
126
+ ciph = data_rx[4]
127
+ except IndexError:
128
+ raise MessageParserError("Bad Message, no payload found!")
129
+
130
+ # chacha20 deciphering
131
+ ad = data_rx[3]
132
+ nonce = build_nonce(msg.timestamp)
133
+ try:
134
+ clear = pysodium.crypto_aead_chacha20poly1305_ietf_decrypt(ciph, ad, nonce, self.cipher_key)
135
+ except :
136
+ raise MessageParserError("Unable to decrypt msg")
137
+
138
+ # Decode application layer (payload)
139
+ try:
140
+ payload = cbor.loads(clear)
141
+ except:
142
+ raise MessageParserError("Unable to parse CBOR data in payload after decrypt")
143
+ try:
144
+ msg.source = UUID(bytes=payload[0])
145
+ msg.dev_type = payload[1]
146
+ msg.msg_type = payload[2]
147
+ msg.action = payload[3]
148
+ except IndexError:
149
+ raise MessageParserError("Unable to parse payload headers")
150
+ if len(payload) == 5:
151
+ msg.body = payload[4]
152
+
153
+ # Sanity check incomming message
154
+ if not tools.is_valid_address(msg.source):
155
+ raise MessageParserError("Wrong message source [%s]" % msg.source)
156
+ if not tools.is_valid_dev_type(msg.dev_type):
157
+ raise MessageParserError("Wrong message dev_type [%s]" % msg.dev_type)
158
+ return msg
159
+
160
+ #####################################################
161
+ # MSG builder
162
+ #####################################################
163
+ def build_msg(self, dev=None, targets=[], msg_type=None, action=None, body=None):
164
+ """the build method takes in parameters :
165
+ -A device
166
+ -The list of targets of the message
167
+ -The type of the message
168
+ -The action of the message
169
+ -A body if it's necessary (None if not)
170
+ it will return a message encoded in CBOR and ciphered.
171
+ """
172
+ message = Message()
173
+ if dev:
174
+ message.source = dev.address
175
+ message.dev_type = dev.dev_type
176
+
177
+ message.targets = targets
178
+ message.timestamp = build_timestamp()
179
+
180
+ if msg_type:
181
+ message.msg_type = msg_type
182
+ if action:
183
+ message.action = action
184
+ if body is not None and body != {}:
185
+ message.body = body
186
+
187
+ data = self.encode_msg(message)
188
+ return data
189
+
190
+ def build_alive_for(self, dev, timeout=0):
191
+ """Build Alive message for a given device
192
+ timeout = 0 is the minimum value
193
+ """
194
+ body = {}
195
+ body['timeout'] = timeout
196
+ message = self.build_msg(dev=dev, targets=[], msg_type=MessageType.NOTIFY, action=MessageAction.ALIVE.value, body=body)
197
+ return message
198
+
199
+ def build_error_msg(self, dev, errcode, description=None):
200
+ """Build a Error message"""
201
+ message = Message()
202
+ body = {}
203
+ body['code'] = errcode
204
+ if description:
205
+ body['description'] = description
206
+ message = self.build_msg(dev, [], MessageType.NOTIFY, "error", body)
207
+ return message
208
+
209
+
210
+ class MessageType(Enum):
211
+ NOTIFY = 0
212
+ REQUEST = 1
213
+ REPLY = 2
214
+
215
+
216
+ class MessageAction(Enum):
217
+ ALIVE = "alive"
218
+ IS_ALIVE = "is_alive"
219
+ ATTRIBUTES_CHANGE = "attributes_change"
220
+ GET_ATTRIBUTES = "get_attributes"
221
+ GET_DESCRIPTION = "get_description"
222
+
223
+
224
+ class Message(object):
225
+ """Message object used for incomming & outgoint message"""
226
+
227
+ __slots__ = ['version', 'timestamp', 'source', 'dev_type', 'msg_type', 'action', 'body', '__targets']
228
+
229
+ def __init__(self):
230
+ self.version = config.STACK_VERSION # message API version
231
+ self.__targets = [] # target property
232
+ self.timestamp = None # message timestamp
233
+ self.source = None # message source
234
+ self.dev_type = None # message dev_type
235
+ self.msg_type = None # message type
236
+ self.action = None # message action
237
+ self.body = {} # message body
238
+
239
+ @property
240
+ def targets(self):
241
+ return self.__targets
242
+
243
+ @targets.setter
244
+ def targets(self, values):
245
+ if not isinstance(values, list):
246
+ raise MessageError("Expected a list for targetsList, got %s" % (type(values),))
247
+ for uid in values:
248
+ if not tools.is_valid_address(uid):
249
+ raise MessageError("Bad target addr: %s" % uid)
250
+ self.__targets = values
251
+
252
+ def targets_as_string(self):
253
+ return [str(k) for k in self.targets]
254
+
255
+ def dump(self):
256
+ r = []
257
+ r.append(["version", self.version])
258
+ r.append(["targets", str(self.targets)])
259
+ r.append(["timestamp", str(self.timestamp)])
260
+ r.append(["source", self.source])
261
+ r.append(["dev_type", self.dev_type])
262
+ r.append(["msg_type", MessageType(self.msg_type)])
263
+ r.append(["action", self.action])
264
+ if self.body:
265
+ tmp=""
266
+ for k,v in self.body.items():
267
+ k = k + ':'
268
+ v = pprint.pformat(v,width=55)
269
+ tmp = tmp+"- %-12s %s\n" % (k,v)
270
+ #tmp = tmp.strip()
271
+ r.append(["body", tmp])
272
+ print(tabulate(r, headers=["Fied", "Value"], tablefmt="psql"))
273
+
274
+ def __repr__(self):
275
+ return f"<xaal.Message {id(self):x} {self.source} {self.dev_type} {self.msg_type} {self.action}>"
276
+
277
+ def is_request(self):
278
+ if MessageType(self.msg_type) == MessageType.REQUEST:
279
+ return True
280
+ return False
281
+
282
+ def is_reply(self):
283
+ if MessageType(self.msg_type) == MessageType.REPLY:
284
+ return True
285
+ return False
286
+
287
+ def is_notify(self):
288
+ if MessageType(self.msg_type) == MessageType.NOTIFY:
289
+ return True
290
+ return False
291
+
292
+ def is_alive(self):
293
+ if self.is_notify() and self.action == MessageAction.ALIVE.value:
294
+ return True
295
+ return False
296
+
297
+ def is_request_isalive(self):
298
+ if self.is_request() and self.action == MessageAction.IS_ALIVE.value:
299
+ return True
300
+ return False
301
+
302
+ def is_attributes_change(self):
303
+ if self.is_notify() and self.action == MessageAction.ATTRIBUTES_CHANGE.value:
304
+ return True
305
+ return False
306
+
307
+ def is_get_attribute_reply(self):
308
+ if self.is_reply() and self.action == MessageAction.GET_ATTRIBUTES.value:
309
+ return True
310
+ return False
311
+
312
+ def is_get_description_reply(self):
313
+ if self.is_reply() and self.action == MessageAction.GET_DESCRIPTION.value:
314
+ return True
315
+ return False
316
+
317
+
318
+ def build_nonce(data):
319
+ """ Big-Endian, time in seconds and time in microseconds """
320
+ nonce = struct.pack('>QL', data[0], data[1])
321
+ return nonce
322
+
323
+
324
+ def build_timestamp():
325
+ """Return array [seconds since epoch, microseconds since last seconds] Time = UTC+0000"""
326
+ epoch = datetime.datetime.utcfromtimestamp(0)
327
+ timestamp = datetime.datetime.utcnow() - epoch
328
+ return _packtimestamp(timestamp.total_seconds(), timestamp.microseconds)
329
+
330
+
331
+ # for better performance, I choose to use this trick to fix the change in size for Py3.
332
+ # only test once.
333
+ if sys.version_info.major == 2:
334
+ _packtimestamp = lambda t1,t2: [long(t1),int(t2)]
335
+ else:
336
+ _packtimestamp = lambda t1,t2: [int(t1),int(t2)]