-
Notifications
You must be signed in to change notification settings - Fork 22
/
message.py
217 lines (193 loc) · 9.06 KB
/
message.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# Copyright (C) 2023 Salvatore Sanfilippo <[email protected]>
# All Rights Reserved
#
# This code is released under the BSD 2 clause license.
# See the LICENSE file for more information
import struct, time, urandom, machine
from micropython import const
# Message types
MessageTypeData = const(0)
MessageTypeAck = const(1)
MessageTypeHello = const(2)
MessageTypeBulkStart = const(3)
MessageTypeBulkData = const(4)
MessageTypeBulkEND = const(5)
MessageTypeBulkReply = const(6)
# Message flags
MessageFlagsNone = const(0) # No flags
MessageFlagsRelayed = const(1<<0) # Repeated message
MessageFlagsPleaseRelay = const(1<<1) # Please repeat this message
MessageFlagsFragment = const(1<<2) # One fragment of many
MessageFlagsMedia = const(1<<3) # Message contains some media
MessageFlagsEncr = const(1<<4) # Message is encrypted
# Virtual flags: not really in the packet header, but added
# in the message object representing the packet to provide
# further information.
MessageFlagsBadCRC = const(1<<8) # Message CRC is bad
# Media types
MessageMediaTypeImageFCI = const(0)
MessageMediaTypeSensorData = const(1)
# Sensor data media type readings
MessageSensorDataTemperature = const(0)
MessageSensorDataAirHumidity = const(1)
MessageSensorDataGroundHumidity = const(2)
MessageSensorDataBattery = const(3)
# The message object represents a FreakWAN message, and is also responsible
# of the decoding and encoding of the messages to be sent to the "wire".
class Message:
def __init__(self, nick="", text="", media_type=255, media_data=False, uid=False, ttl=15, mtype=MessageTypeData, sender=False, flags=0, rssi=0, ack_type=0, seen=0, key_name=None):
self.ctime = time.ticks_ms() # To evict old messages
# send_time is only useful for sending, to introduce a random delay.
self.send_time = self.ctime
# Number of times to transmit this message. Each time the message
# is transmitted, this value is reduced by one. When it reaches
# zero, the message is removed from the send queue.
self.num_tx = 1
self.acks = {} # Device IDs we received ACKs from
self.type = mtype
self.flags = flags
self.nick = nick
self.text = text
self.media_type = media_type
self.media_data = media_data
self.uid = uid if uid != False else self.gen_uid()
self.sender = sender if sender != False else self.get_this_sender()
self.ttl = ttl # Only DATA
self.ack_type = ack_type # Only ACK
self.seen = seen # Only HELLO
self.rssi = rssi
self.key_name = key_name
self.no_key = False # True if it was not possible to decrypt.
# If key_name is set, encoded messages will be encrypted, too.
# When messages are decoded, key_name is set to the key that
# decrypted the message, if any.
# Sometimes we want to supporess sending of packets that may
# already be inside the TX queue. Instead of scanning the queue
# to look for the message, we just set this flag to True.
self.send_canceled = False
# Generate a 32 bit unique message ID.
def gen_uid(self):
return urandom.getrandbits(32)
# Get the sender address for this device. We just take 6 bytes
# of the device unique ID.
def get_this_sender(self):
return machine.unique_id()[-6:]
# Return the sender as a printable hex string.
def sender_to_str(self):
if self.sender:
s = self.sender
return "%02x%02x%02x%02x%02x%02x" % (s[0],s[1],s[2],s[3],s[4],s[5])
else:
return "ffffffffffff"
# Turn the message into its binary representation.
def encode(self,keychain=None):
if self.no_key == True:
# Message that we were not able to decrypt. In this case
# we saved the packet, and we just need to encode the
# plaintext header and concatenate the saved packet from the
# IV field till the end.
return struct.pack("<BBLB",self.type,self.flags,self.uid,self.ttl)+self.packet[7:]
elif self.type == MessageTypeData:
# Encode with the encryption flag set, if we are going to
# encrypt the packet.
encr_flag = MessageFlagsEncr if self.key_name else MessageFlagsNone
encoded = struct.pack("<BBLB6sB",self.type,self.flags|encr_flag,self.uid,self.ttl,self.sender,len(self.nick))+self.nick
if self.flags & MessageFlagsMedia:
encoded += bytes([self.media_type])+self.media_data
else:
encoded += self.text
# Encrypt if needed and if a keychain was provided.
if self.key_name:
if keychain:
encoded = keychain.encrypt(encoded,self.key_name)
else:
printf("Warning: no keychain provided to Message.encode(). Message with key_name set will be unencrypted.")
return encoded
elif self.type == MessageTypeAck:
return struct.pack("<BBLB",self.type,self.flags,self.uid,self.ack_type)+self.sender
elif self.type == MessageTypeHello:
return struct.pack("<BB6sBB",self.type,self.flags,self.sender,self.seen,len(self.nick))+self.nick+self.text
else:
print("WARNING Message.encode() unknown msg type",self.type)
return None
# Fill the message with the data found in the binary representation
# provided in 'msg'.
def decode(self,msg,keychain=None):
try:
mtype,flags = struct.unpack("<BB",msg)
# If the message is encrypted, try to decrypt it.
if mtype == MessageTypeData and flags & MessageFlagsEncr:
if not keychain:
printf("Encrypted message received, no keychain given")
plain = None
else:
plain = keychain.decrypt(msg)
# Messages for which we don't have a valid key
# are returned in a "raw" form, useful only for relaying.
# We signal that the message is in this state by
# setting .no_key to True. We also decode what is in the
# unencrypted part of the header.
if not plain:
self.type,self.flags,self.uid,self.ttl = struct.unpack("<BBLB",msg)
self.no_key = True
self.packet = msg # Save the encrypted message.
return True
# If we have the key, the message is now decrypted.
# We can continue with the normal code path after
# populating key_name.
self.key_name = plain[0]
msg = plain[1]
# Decode according to message type.
if mtype == MessageTypeData:
self.type,self.flags,self.uid,self.ttl,self.sender,nick_len = struct.unpack("<BBLB6sB",msg)
self.nick = msg[14:14+nick_len].decode("utf-8")
msg = msg[14+nick_len:] # Discard header and nick
if self.flags & MessageFlagsMedia:
self.media_type = msg[0]
self.media_data = msg[1:]
else:
self.text = msg.decode("utf-8")
return True
elif mtype == MessageTypeAck:
self.type,self.flags,self.uid,self.ack_type,self.sender = struct.unpack("<BBLB6s",msg)
return True
elif mtype == MessageTypeHello:
self.type,self.flags,self.sender,self.seen,nick_len = struct.unpack("<BB6sBB",msg)
self.nick = msg[10:10+nick_len].decode("utf-8")
self.text = msg[10+nick_len:].decode("utf-8")
return True
else:
print("!!! Decoding message: wrong message type %d" % mtype)
return False
except Exception as e:
print("!!! Message decode error msg="+str(msg)+" err="+str(e))
return False
# Create a message object from the binary representation of a message.
def from_encoded(encoded,keychain):
m = Message()
if m.decode(encoded,keychain):
return m
else:
return False
# Turn the media data in the message into a string that can be parsed
# by other programs.
def sensor_data_to_str(self):
l = len(self.media_data)
off = 0
res = ""
while l > 0:
fieldtype = self.media_data[off]
off += 1
l -= 1
if fieldtype == MessageSensorDataTemperature or \
fieldtype == MessageSensorDataAirHumidity or \
fieldtype == MessageSensorDataGroundHumidity or \
fieldtype == MessageSensorDataBattery:
if l < 4: return "field data missing"
val = struct.unpack("f",self.media_data[off:])
off += 4
l -= 4
res += "%d:%.2f " % (fieldtype, val[0])
else:
return "field type error"
return res