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