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/__init__.py +1 -0
- xaal/lib/__init__.py +23 -0
- xaal/lib/__main__.py +2 -0
- xaal/lib/aioengine.py +388 -0
- xaal/lib/aiohelpers.py +38 -0
- xaal/lib/aionetwork.py +74 -0
- xaal/lib/bindings.py +98 -0
- xaal/lib/cbor.py +62 -0
- xaal/lib/config.py +53 -0
- xaal/lib/core.py +308 -0
- xaal/lib/devices.py +285 -0
- xaal/lib/engine.py +231 -0
- xaal/lib/exceptions.py +20 -0
- xaal/lib/helpers.py +80 -0
- xaal/lib/messages.py +336 -0
- xaal/lib/network.py +100 -0
- xaal/lib/test.py +140 -0
- xaal/lib/tools.py +130 -0
- xaal.lib-0.7.2.dist-info/METADATA +131 -0
- xaal.lib-0.7.2.dist-info/RECORD +22 -0
- xaal.lib-0.7.2.dist-info/WHEEL +5 -0
- xaal.lib-0.7.2.dist-info/top_level.txt +1 -0
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)]
|