src/eric7/Cooperation/Connection.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
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()))

eric ide

mercurial