|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2010 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a class representing a peer connection. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals |
|
11 try: |
|
12 str = unicode |
|
13 except NameError: |
|
14 pass |
|
15 |
|
16 from PyQt5.QtCore import pyqtSignal, QTimer, QTime, QByteArray |
|
17 from PyQt5.QtNetwork import QTcpSocket, QHostInfo |
|
18 |
|
19 from E5Gui import E5MessageBox |
|
20 from E5Gui.E5Application import e5App |
|
21 |
|
22 import Preferences |
|
23 |
|
24 MaxBufferSize = 1024 * 1024 |
|
25 TransferTimeout = 30 * 1000 |
|
26 PongTimeout = 60 * 1000 |
|
27 PingInterval = 5 * 1000 |
|
28 SeparatorToken = '|||' |
|
29 SeparatorToken_b = b'|||' |
|
30 |
|
31 |
|
32 class Connection(QTcpSocket): |
|
33 """ |
|
34 Class representing a peer connection. |
|
35 |
|
36 @signal readyForUse() emitted when the connection is ready for use |
|
37 @signal newMessage(user, message) emitted after a new message has |
|
38 arrived (string, string) |
|
39 @signal getParticipants() emitted after a get participants message has |
|
40 arrived |
|
41 @signal participants(participants) emitted after the list of participants |
|
42 has arrived (list of strings of "host:port") |
|
43 @signal editorCommand(hash, fn, message) emitted after an editor command |
|
44 has arrived (string, string, string) |
|
45 @signal rejected(message) emitted after a connection has been rejected |
|
46 (string) |
|
47 """ |
|
48 WaitingForGreeting = 0 |
|
49 ReadingGreeting = 1 |
|
50 ReadyForUse = 2 |
|
51 |
|
52 PlainText = 0 |
|
53 Ping = 1 |
|
54 Pong = 2 |
|
55 Greeting = 3 |
|
56 GetParticipants = 4 |
|
57 Participants = 5 |
|
58 Editor = 6 |
|
59 Undefined = 99 |
|
60 |
|
61 ProtocolMessage = "MESSAGE" |
|
62 ProtocolPing = "PING" |
|
63 ProtocolPong = "PONG" |
|
64 ProtocolGreeting = "GREETING" |
|
65 ProtocolGetParticipants = "GET_PARTICIPANTS" |
|
66 ProtocolParticipants = "PARTICIPANTS" |
|
67 ProtocolEditor = "EDITOR" |
|
68 |
|
69 readyForUse = pyqtSignal() |
|
70 newMessage = pyqtSignal(str, str) |
|
71 getParticipants = pyqtSignal() |
|
72 participants = pyqtSignal(list) |
|
73 editorCommand = pyqtSignal(str, str, str) |
|
74 rejected = pyqtSignal(str) |
|
75 |
|
76 def __init__(self, parent=None): |
|
77 """ |
|
78 Constructor |
|
79 |
|
80 @param parent referenec to the parent object (QObject) |
|
81 """ |
|
82 super(Connection, self).__init__(parent) |
|
83 |
|
84 self.__greetingMessage = self.tr("undefined") |
|
85 self.__username = self.tr("unknown") |
|
86 self.__serverPort = 0 |
|
87 self.__state = Connection.WaitingForGreeting |
|
88 self.__currentDataType = Connection.Undefined |
|
89 self.__numBytesForCurrentDataType = -1 |
|
90 self.__transferTimerId = 0 |
|
91 self.__isGreetingMessageSent = False |
|
92 self.__pingTimer = QTimer(self) |
|
93 self.__pingTimer.setInterval(PingInterval) |
|
94 self.__pongTime = QTime() |
|
95 self.__buffer = QByteArray() |
|
96 self.__client = None |
|
97 |
|
98 self.readyRead.connect(self.__processReadyRead) |
|
99 self.disconnected.connect(self.__disconnected) |
|
100 self.__pingTimer.timeout.connect(self.__sendPing) |
|
101 self.connected.connect(self.__sendGreetingMessage) |
|
102 |
|
103 def name(self): |
|
104 """ |
|
105 Public method to get the connection name. |
|
106 |
|
107 @return connection name (string) |
|
108 """ |
|
109 return self.__username |
|
110 |
|
111 def serverPort(self): |
|
112 """ |
|
113 Public method to get the server port. |
|
114 |
|
115 @return server port (integer) |
|
116 """ |
|
117 return self.__serverPort |
|
118 |
|
119 def setClient(self, client): |
|
120 """ |
|
121 Public method to set the reference to the cooperation client. |
|
122 |
|
123 @param client reference to the cooperation client (CooperationClient) |
|
124 """ |
|
125 self.__client = client |
|
126 |
|
127 def setGreetingMessage(self, message, serverPort): |
|
128 """ |
|
129 Public method to set the greeting message. |
|
130 |
|
131 @param message greeting message (string) |
|
132 @param serverPort port number to include in the message (integer) |
|
133 """ |
|
134 self.__greetingMessage = "{0}:{1}".format(message, serverPort) |
|
135 |
|
136 def sendMessage(self, message): |
|
137 """ |
|
138 Public method to send a message. |
|
139 |
|
140 @param message message to be sent (string) |
|
141 @return flag indicating a successful send (boolean) |
|
142 """ |
|
143 if message == "": |
|
144 return False |
|
145 |
|
146 msg = QByteArray(message.encode("utf-8")) |
|
147 data = QByteArray("{0}{1}{2}{1}".format( |
|
148 Connection.ProtocolMessage, SeparatorToken, msg.size()) |
|
149 .encode("utf-8")) + msg |
|
150 return self.write(data) == data.size() |
|
151 |
|
152 def timerEvent(self, evt): |
|
153 """ |
|
154 Protected method to handle timer events. |
|
155 |
|
156 @param evt reference to the timer event (QTimerEvent) |
|
157 """ |
|
158 if evt.timerId() == self.__transferTimerId: |
|
159 self.abort() |
|
160 self.killTimer(self.__transferTimerId) |
|
161 self.__transferTimerId = 0 |
|
162 |
|
163 def __processReadyRead(self): |
|
164 """ |
|
165 Private slot to handle the readyRead signal. |
|
166 """ |
|
167 if self.__state == Connection.WaitingForGreeting: |
|
168 if not self.__readProtocolHeader(): |
|
169 return |
|
170 if self.__currentDataType != Connection.Greeting: |
|
171 self.abort() |
|
172 return |
|
173 self.__state = Connection.ReadingGreeting |
|
174 |
|
175 if self.__state == Connection.ReadingGreeting: |
|
176 if not self.__hasEnoughData(): |
|
177 return |
|
178 |
|
179 self.__buffer = QByteArray( |
|
180 self.read(self.__numBytesForCurrentDataType)) |
|
181 if self.__buffer.size() != self.__numBytesForCurrentDataType: |
|
182 self.abort() |
|
183 return |
|
184 |
|
185 try: |
|
186 user, serverPort = \ |
|
187 str(self.__buffer, encoding="utf-8").split(":") |
|
188 except ValueError: |
|
189 self.abort() |
|
190 return |
|
191 self.__serverPort = int(serverPort) |
|
192 |
|
193 hostInfo = QHostInfo.fromName(self.peerAddress().toString()) |
|
194 self.__username = "{0}@{1}@{2}".format( |
|
195 user, |
|
196 hostInfo.hostName(), |
|
197 self.peerPort() |
|
198 ) |
|
199 self.__currentDataType = Connection.Undefined |
|
200 self.__numBytesForCurrentDataType = 0 |
|
201 self.__buffer.clear() |
|
202 |
|
203 if not self.isValid(): |
|
204 self.abort() |
|
205 return |
|
206 |
|
207 bannedName = "{0}@{1}".format( |
|
208 user, |
|
209 hostInfo.hostName(), |
|
210 ) |
|
211 Preferences.syncPreferences() |
|
212 if bannedName in Preferences.getCooperation("BannedUsers"): |
|
213 self.rejected.emit(self.tr( |
|
214 "* Connection attempted by banned user '{0}'.") |
|
215 .format(bannedName)) |
|
216 self.abort() |
|
217 return |
|
218 |
|
219 if self.__serverPort != self.peerPort() and \ |
|
220 not Preferences.getCooperation("AutoAcceptConnections"): |
|
221 # don't ask for reverse connections or |
|
222 # if we shall accept automatically |
|
223 res = E5MessageBox.yesNo( |
|
224 None, |
|
225 self.tr("New Connection"), |
|
226 self.tr("""<p>Accept connection from """ |
|
227 """<strong>{0}@{1}</strong>?</p>""").format( |
|
228 user, hostInfo.hostName()), |
|
229 yesDefault=True) |
|
230 if not res: |
|
231 self.abort() |
|
232 return |
|
233 |
|
234 if self.__client is not None: |
|
235 chatWidget = self.__client.chatWidget() |
|
236 if chatWidget is not None and not chatWidget.isVisible(): |
|
237 e5App().getObject( |
|
238 "UserInterface").activateCooperationViewer() |
|
239 |
|
240 if not self.__isGreetingMessageSent: |
|
241 self.__sendGreetingMessage() |
|
242 |
|
243 self.__pingTimer.start() |
|
244 self.__pongTime.start() |
|
245 self.__state = Connection.ReadyForUse |
|
246 self.readyForUse.emit() |
|
247 |
|
248 while self.bytesAvailable(): |
|
249 if self.__currentDataType == Connection.Undefined: |
|
250 if not self.__readProtocolHeader(): |
|
251 return |
|
252 |
|
253 if not self.__hasEnoughData(): |
|
254 return |
|
255 |
|
256 self.__processData() |
|
257 |
|
258 def __sendPing(self): |
|
259 """ |
|
260 Private slot to send a ping message. |
|
261 """ |
|
262 if self.__pongTime.elapsed() > PongTimeout: |
|
263 self.abort() |
|
264 return |
|
265 |
|
266 self.write(QByteArray("{0}{1}1{1}p".format( |
|
267 Connection.ProtocolPing, SeparatorToken).encode("utf-8"))) |
|
268 |
|
269 def __sendGreetingMessage(self): |
|
270 """ |
|
271 Private slot to send a greeting message. |
|
272 """ |
|
273 greeting = QByteArray(self.__greetingMessage.encode("utf-8")) |
|
274 data = QByteArray("{0}{1}{2}{1}".format( |
|
275 Connection.ProtocolGreeting, SeparatorToken, greeting.size()) |
|
276 .encode("utf-8")) + greeting |
|
277 if self.write(data) == data.size(): |
|
278 self.__isGreetingMessageSent = True |
|
279 |
|
280 def __readDataIntoBuffer(self, maxSize=MaxBufferSize): |
|
281 """ |
|
282 Private method to read some data into the buffer. |
|
283 |
|
284 @param maxSize maximum size of data to read (integer) |
|
285 @return size of data read (integer) |
|
286 """ |
|
287 if maxSize > MaxBufferSize: |
|
288 return 0 |
|
289 |
|
290 numBytesBeforeRead = self.__buffer.size() |
|
291 if numBytesBeforeRead == MaxBufferSize: |
|
292 self.abort() |
|
293 return 0 |
|
294 |
|
295 while self.bytesAvailable() and self.__buffer.size() < maxSize: |
|
296 self.__buffer.append(self.read(1)) |
|
297 if self.__buffer.endsWith(SeparatorToken_b): |
|
298 break |
|
299 |
|
300 return self.__buffer.size() - numBytesBeforeRead |
|
301 |
|
302 def __dataLengthForCurrentDataType(self): |
|
303 """ |
|
304 Private method to get the data length for the current data type. |
|
305 |
|
306 @return data length (integer) |
|
307 """ |
|
308 if self.bytesAvailable() <= 0 or \ |
|
309 self.__readDataIntoBuffer() <= 0 or \ |
|
310 not self.__buffer.endsWith(SeparatorToken_b): |
|
311 return 0 |
|
312 |
|
313 self.__buffer.chop(len(SeparatorToken_b)) |
|
314 number = self.__buffer.toInt()[0] |
|
315 self.__buffer.clear() |
|
316 return number |
|
317 |
|
318 def __readProtocolHeader(self): |
|
319 """ |
|
320 Private method to read the protocol header. |
|
321 |
|
322 @return flag indicating a successful read (boolean) |
|
323 """ |
|
324 if self.__transferTimerId: |
|
325 self.killTimer(self.__transferTimerId) |
|
326 self.__transferTimerId = 0 |
|
327 |
|
328 if self.__readDataIntoBuffer() <= 0: |
|
329 self.__transferTimerId = self.startTimer(TransferTimeout) |
|
330 return False |
|
331 |
|
332 self.__buffer.chop(len(SeparatorToken)) |
|
333 protocolHeader = str(self.__buffer, encoding="utf-8") |
|
334 if protocolHeader == Connection.ProtocolPing: |
|
335 self.__currentDataType = Connection.Ping |
|
336 elif protocolHeader == Connection.ProtocolPong: |
|
337 self.__currentDataType = Connection.Pong |
|
338 elif protocolHeader == Connection.ProtocolMessage: |
|
339 self.__currentDataType = Connection.PlainText |
|
340 elif protocolHeader == Connection.ProtocolGreeting: |
|
341 self.__currentDataType = Connection.Greeting |
|
342 elif protocolHeader == Connection.ProtocolGetParticipants: |
|
343 self.__currentDataType = Connection.GetParticipants |
|
344 elif protocolHeader == Connection.ProtocolParticipants: |
|
345 self.__currentDataType = Connection.Participants |
|
346 elif protocolHeader == Connection.ProtocolEditor: |
|
347 self.__currentDataType = Connection.Editor |
|
348 else: |
|
349 self.__currentDataType = Connection.Undefined |
|
350 self.abort() |
|
351 return False |
|
352 |
|
353 self.__buffer.clear() |
|
354 self.__numBytesForCurrentDataType = \ |
|
355 self.__dataLengthForCurrentDataType() |
|
356 return True |
|
357 |
|
358 def __hasEnoughData(self): |
|
359 """ |
|
360 Private method to check, if enough data is available. |
|
361 |
|
362 @return flag indicating availability of enough data (boolean) |
|
363 """ |
|
364 if self.__transferTimerId: |
|
365 self.killTimer(self.__transferTimerId) |
|
366 self.__transferTimerId = 0 |
|
367 |
|
368 if self.__numBytesForCurrentDataType <= 0: |
|
369 self.__numBytesForCurrentDataType = \ |
|
370 self.__dataLengthForCurrentDataType() |
|
371 |
|
372 if self.bytesAvailable() < self.__numBytesForCurrentDataType or \ |
|
373 self.__numBytesForCurrentDataType <= 0: |
|
374 self.__transferTimerId = self.startTimer(TransferTimeout) |
|
375 return False |
|
376 |
|
377 return True |
|
378 |
|
379 def __processData(self): |
|
380 """ |
|
381 Private method to process the received data. |
|
382 """ |
|
383 self.__buffer = QByteArray( |
|
384 self.read(self.__numBytesForCurrentDataType)) |
|
385 if self.__buffer.size() != self.__numBytesForCurrentDataType: |
|
386 self.abort() |
|
387 return |
|
388 |
|
389 if self.__currentDataType == Connection.PlainText: |
|
390 self.newMessage.emit( |
|
391 self.__username, str(self.__buffer, encoding="utf-8")) |
|
392 elif self.__currentDataType == Connection.Ping: |
|
393 self.write(QByteArray("{0}{1}1{1}p".format( |
|
394 Connection.ProtocolPong, SeparatorToken).encode("utf-8"))) |
|
395 elif self.__currentDataType == Connection.Pong: |
|
396 self.__pongTime.restart() |
|
397 elif self.__currentDataType == Connection.GetParticipants: |
|
398 self.getParticipants.emit() |
|
399 elif self.__currentDataType == Connection.Participants: |
|
400 msg = str(self.__buffer, encoding="utf-8") |
|
401 if msg == "<empty>": |
|
402 participantsList = [] |
|
403 else: |
|
404 participantsList = msg.split(SeparatorToken) |
|
405 self.participants.emit(participantsList[:]) |
|
406 elif self.__currentDataType == Connection.Editor: |
|
407 hashStr, fn, msg = \ |
|
408 str(self.__buffer, encoding="utf-8").split(SeparatorToken) |
|
409 self.editorCommand.emit(hashStr, fn, msg) |
|
410 |
|
411 self.__currentDataType = Connection.Undefined |
|
412 self.__numBytesForCurrentDataType = 0 |
|
413 self.__buffer.clear() |
|
414 |
|
415 def sendGetParticipants(self): |
|
416 """ |
|
417 Public method to request a list of participants. |
|
418 """ |
|
419 self.write(QByteArray( |
|
420 "{0}{1}1{1}l".format( |
|
421 Connection.ProtocolGetParticipants, SeparatorToken |
|
422 ).encode("utf-8") |
|
423 )) |
|
424 |
|
425 def sendParticipants(self, participants): |
|
426 """ |
|
427 Public method to send the list of participants. |
|
428 |
|
429 @param participants list of participants (list of strings of |
|
430 "host:port") |
|
431 """ |
|
432 if participants: |
|
433 message = SeparatorToken.join(participants) |
|
434 else: |
|
435 message = "<empty>" |
|
436 msg = QByteArray(message.encode("utf-8")) |
|
437 data = QByteArray("{0}{1}{2}{1}".format( |
|
438 Connection.ProtocolParticipants, SeparatorToken, msg.size()) |
|
439 .encode("utf-8")) + msg |
|
440 self.write(data) |
|
441 |
|
442 def sendEditorCommand(self, projectHash, filename, message): |
|
443 """ |
|
444 Public method to send an editor command. |
|
445 |
|
446 @param projectHash hash of the project (string) |
|
447 @param filename project relative universal file name of |
|
448 the sending editor (string) |
|
449 @param message editor command to be sent (string) |
|
450 """ |
|
451 msg = QByteArray("{0}{1}{2}{1}{3}".format( |
|
452 projectHash, SeparatorToken, filename, message).encode("utf-8")) |
|
453 data = QByteArray("{0}{1}{2}{1}".format( |
|
454 Connection.ProtocolEditor, SeparatorToken, msg.size()) |
|
455 .encode("utf-8")) + msg |
|
456 self.write(data) |
|
457 |
|
458 def __disconnected(self): |
|
459 """ |
|
460 Private slot to handle the connection being dropped. |
|
461 """ |
|
462 self.__pingTimer.stop() |
|
463 if self.__state == Connection.WaitingForGreeting: |
|
464 self.rejected.emit(self.tr( |
|
465 "* Connection to {0}:{1} refused.").format( |
|
466 self.peerName(), self.peerPort())) |