eric6/Cooperation/Connection.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7192
a22eee00b052
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
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()))

eric ide

mercurial