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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
|
# ==============================================================================
# Copyright (C) 2019 - Philip Paquette
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <https://www.gnu.org/licenses/>.
# ==============================================================================
""" Implements the low-level messages sent over a stream """
from abc import ABCMeta, abstractmethod
from enum import Enum
import logging
from tornado import gen
# Constants
DAIDE_VERSION = 1
LOGGER = logging.getLogger(__name__)
class MessageType(Enum):
""" Enumeration of message types """
INITIAL = 0
REPRESENTATION = 1
DIPLOMACY = 2
FINAL = 3
ERROR = 4
class ErrorCode(Enum):
""" Enumeration of error codes for error messages """
IM_TIMER_POPPED = 0x01
IM_NOT_FIRST_MESSAGE = 0x02
IM_WRONG_ENDIAN = 0x03
IM_WRONG_MAGIC_NUMBER = 0x04
VERSION_INCOMPATIBILITY = 0x05
MORE_THAN_1_IM_SENT = 0x06
IM_SENT_BY_SERVER = 0x07
UNKNOWN_MESSAGE = 0x08
MESSAGE_SHORTER_THAN_EXPECTED = 0x09
DM_SENT_BEFORE_RM = 0x0A
RM_NOT_FIRST_MSG_BY_SERVER = 0x0B
MORE_THAN_1_RM_SENT = 0x0C
RM_SENT_BY_CLIENT = 0x0D
INVALID_TOKEN_DM = 0x0E
class DaideMessage(metaclass=ABCMeta):
""" Low-level DAIDE Message (Sent or Received) """
def __init__(self, message_type):
""" Constructor """
self.message_type = message_type
self.is_valid = True
self.error_code = None # type: ErrorCode
self.content = b''
@abstractmethod
@gen.coroutine
def build(self, stream, remaining_length):
""" Builds a message from a stream and its declared length """
raise NotImplementedError()
@staticmethod
@gen.coroutine
def from_stream(stream):
""" Builds a message from the stream
:param stream: An opened Tornado stream.
:type stream: tornado.iostream.BaseIOStream
"""
if stream.reading():
return None
data = yield stream.read_bytes(4) # Message type, Pad, Remaining Length (2x)
# Parsing data
message_type = data[0]
remaining_length = data[2] * 256 + data[3]
# Creating message
message_cls = {MessageType.INITIAL.value: InitialMessage,
MessageType.REPRESENTATION.value: RepresentationMessage,
MessageType.DIPLOMACY.value: DiplomacyMessage,
MessageType.FINAL.value: FinalMessage,
MessageType.ERROR.value: ErrorMessage}.get(message_type, None)
# Invalid message type
if message_cls is None:
raise ValueError('Unknown Message Type %d' % message_type)
# Otherwise, return message
message = message_cls()
yield message.build(stream, remaining_length)
return message
class InitialMessage(DaideMessage):
""" Initial message sent from a client """
def __init__(self):
""" Constructor """
super(InitialMessage, self).__init__(MessageType.INITIAL)
def __bytes__(self):
""" Converts message to byte array """
return bytes([MessageType.INITIAL.value, # Message Type
0, # Padding
0, 4, # Remaining length (2 bytes)
0, DAIDE_VERSION, # Daide version (2 bytes)
0xDA, 0x10]) # Magic Number (2 bytes)
@gen.coroutine
def build(self, stream, remaining_length):
""" Builds a message from a stream and its declared length """
# Checking length
if remaining_length != 4:
LOGGER.error('Expected 4 bytes remaining in initial message. Got %d. Aborting.', remaining_length)
self.is_valid = False
return
# Getting data and validating version
data = yield stream.read_bytes(remaining_length) # Version (x2) - Magic Number (x2)
version = data[0] * 256 + data[1]
magic_number = data[2] * 256 + data[3]
# Wrong version
if version != DAIDE_VERSION:
self.is_valid = False
self.error_code = ErrorCode.VERSION_INCOMPATIBILITY
LOGGER.error('Client sent version %d. Server version is %d', version, DAIDE_VERSION)
return
# Wrong Endian / Magic Number
if magic_number == 0x10DA:
self.is_valid = False
self.error_code = ErrorCode.IM_WRONG_ENDIAN
elif magic_number != 0xDA10:
self.is_valid = False
self.error_code = ErrorCode.IM_WRONG_MAGIC_NUMBER
class RepresentationMessage(DaideMessage):
""" Representation message sent from the server """
def __init__(self):
""" Constructor """
super(RepresentationMessage, self).__init__(MessageType.REPRESENTATION)
def __bytes__(self):
""" Converts message to byte array """
return bytes([MessageType.REPRESENTATION.value, # Message Type
0, # Padding
0, 0]) # Remaining length (2 bytes)
@gen.coroutine
def build(self, stream, remaining_length):
""" Builds a message from a stream and its declared length """
if remaining_length:
yield stream.read_bytes(remaining_length)
self.is_valid = False
self.error_code = ErrorCode.RM_SENT_BY_CLIENT
class DiplomacyMessage(DaideMessage):
""" Diplomacy message sent/received by/from the server """
def __init__(self):
""" Constructor """
super(DiplomacyMessage, self).__init__(MessageType.DIPLOMACY)
def __bytes__(self):
""" Converts message to byte array """
if not self.is_valid:
return bytes()
header = bytes([MessageType.DIPLOMACY.value, # Message Type
0, # Padding
len(self.content) // 256, len(self.content) % 256]) # Remaining length
return header + self.content
@gen.coroutine
def build(self, stream, remaining_length):
""" Builds a message from a stream and its declared length """
if remaining_length < 2 or remaining_length % 2 == 1:
self.is_valid = False
if remaining_length:
yield stream.read_bytes(remaining_length)
LOGGER.warning('Got a diplomacy message of length %d. Ignoring.', remaining_length)
# Getting data
self.content = yield stream.read_bytes(remaining_length)
class FinalMessage(DaideMessage):
""" Final message sent/received by/from the server """
def __init__(self):
""" Constructor """
super(FinalMessage, self).__init__(MessageType.FINAL)
def __bytes__(self):
""" Converts message to byte array """
return bytes([MessageType.FINAL.value, # Message Type
0, # Padding
0, 0]) # Remaining length (2 bytes)
@gen.coroutine
def build(self, stream, remaining_length):
""" Builds a message from a stream and its declared length """
if remaining_length:
yield stream.read_bytes(remaining_length)
class ErrorMessage(DaideMessage):
""" Error message sent/received by/from the server """
def __init__(self):
""" Constructor """
super(ErrorMessage, self).__init__(MessageType.ERROR)
def __bytes__(self):
""" Converts message to byte array """
error_code = 0 if self.error_code is None else self.error_code.value
return bytes([MessageType.ERROR.value, # Message Type
0, # Padding
0, 2, # Remaining length (2 bytes)
0, error_code]) # Error code (2 bytes)
@gen.coroutine
def build(self, stream, remaining_length):
""" Builds a message from a stream and its declared length """
if remaining_length != 2:
self.is_valid = False
yield stream.read_bytes(remaining_length)
return
# Parsing error
data = yield stream.read_bytes(remaining_length)
self.error_code = ErrorCode(data[1])
|