From 68a05cb0214321e36f65c3fb3b4861a0307be886 Mon Sep 17 00:00:00 2001 From: Alberto Sottile Date: Thu, 8 Nov 2018 14:51:51 +0100 Subject: [PATCH 01/12] IPv6: use hostname endpoint in client --- syncplay/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/syncplay/client.py b/syncplay/client.py index 3bbb97b..13908c7 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -11,6 +11,7 @@ import time from copy import deepcopy from functools import wraps +from twisted.internet.endpoints import HostnameEndpoint from twisted.internet.protocol import ClientFactory from twisted.internet import reactor, task, defer, threads @@ -726,7 +727,8 @@ class SyncplayClient(object): self._playerClass = None self.protocolFactory = SyncClientFactory(self) port = int(port) - reactor.connectTCP(host, port, self.protocolFactory) + self._endpoint = HostnameEndpoint(reactor, host, port) + self._endpoint.connect(self.protocolFactory) reactor.run() def stop(self, promptForAction=False): From 21d4604578294002f4531b8bcea7da3fff5bb145 Mon Sep 17 00:00:00 2001 From: Alberto Sottile Date: Thu, 8 Nov 2018 15:13:29 +0100 Subject: [PATCH 02/12] IPv6: accept IPv6 address and port in hostname --- syncplay/ui/ConfigurationGetter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/syncplay/ui/ConfigurationGetter.py b/syncplay/ui/ConfigurationGetter.py index cc67d11..388837d 100755 --- a/syncplay/ui/ConfigurationGetter.py +++ b/syncplay/ui/ConfigurationGetter.py @@ -313,7 +313,9 @@ class ConfigurationGetter(object): port = constants.DEFAULT_PORT if not self._config["port"] else self._config["port"] if host: if ':' in host: - host, port = host.split(':', 1) + host, port = host.rsplit(':', 1) + if '[' in host: + host = host.strip('[]') try: port = int(port) except ValueError: From 83d12eca9f61fa1962848251e82fafdf3fc5c515 Mon Sep 17 00:00:00 2001 From: Alberto Sottile Date: Thu, 8 Nov 2018 16:22:49 +0100 Subject: [PATCH 03/12] IPv6: server accepts IPv4 and IPv6 connections --- syncplayServer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/syncplayServer.py b/syncplayServer.py index 28cfd1b..62431fc 100755 --- a/syncplayServer.py +++ b/syncplayServer.py @@ -12,6 +12,7 @@ except AttributeError: import warnings warnings.warn("You must run Syncplay with Python 3.4 or newer!") +from twisted.internet.endpoints import TCP6ServerEndpoint from twisted.internet import reactor from syncplay.server import SyncFactory, ConfigurationGetter @@ -19,8 +20,8 @@ from syncplay.server import SyncFactory, ConfigurationGetter if __name__ == '__main__': argsGetter = ConfigurationGetter() args = argsGetter.getConfiguration() - reactor.listenTCP( - int(args.port), + endpoint = TCP6ServerEndpoint(reactor, int(args.port)) + endpoint.listen( SyncFactory( args.port, args.password, From a78c6465561267742d646214b747946cf1edef79 Mon Sep 17 00:00:00 2001 From: Alberto Sottile Date: Fri, 9 Nov 2018 19:38:43 +0100 Subject: [PATCH 04/12] IPv6: GUI and client fixes --- syncplay/client.py | 6 +++--- syncplay/ui/ConfigurationGetter.py | 34 ++++++++++++++++++++++-------- syncplay/ui/GuiConfiguration.py | 6 +++--- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index 13908c7..c4bc20a 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -11,7 +11,6 @@ import time from copy import deepcopy from functools import wraps -from twisted.internet.endpoints import HostnameEndpoint from twisted.internet.protocol import ClientFactory from twisted.internet import reactor, task, defer, threads @@ -726,9 +725,10 @@ class SyncplayClient(object): reactor.callLater(0.1, self._playerClass.run, self, self._config['playerPath'], self._config['file'], self._config['playerArgs'], ) self._playerClass = None self.protocolFactory = SyncClientFactory(self) + if '[' in host: + host = host.strip('[]') port = int(port) - self._endpoint = HostnameEndpoint(reactor, host, port) - self._endpoint.connect(self.protocolFactory) + reactor.connectTCP(host, port, self.protocolFactory) reactor.run() def stop(self, promptForAction=False): diff --git a/syncplay/ui/ConfigurationGetter.py b/syncplay/ui/ConfigurationGetter.py index 388837d..2f9a2c6 100755 --- a/syncplay/ui/ConfigurationGetter.py +++ b/syncplay/ui/ConfigurationGetter.py @@ -313,16 +313,32 @@ class ConfigurationGetter(object): port = constants.DEFAULT_PORT if not self._config["port"] else self._config["port"] if host: if ':' in host: - host, port = host.rsplit(':', 1) - if '[' in host: - host = host.strip('[]') - try: - port = int(port) - except ValueError: + if host.count(':') == 1: + #IPv4 address or hostname, with port + host, port = host.rsplit(':', 1) try: - port = port.encode('ascii', 'ignore') - except: - port = "" + port = int(port) + except ValueError: + try: + port = port.encode('ascii', 'ignore') + except: + port = "" + else: + #IPv6 address + if ']' in host: + #IPv6 address in brackets + endBracket = host.index(']') + try: + #port explicitely indicated + port = int(host[endBracket+2:]) + except ValueError: + #no port after the bracket + pass + host = host[:endBracket+1] + else: + #IPv6 address with no port and no brackets + #add brackets to correctly store IPv6 addresses in configs + host = '[' + host + ']' return host, port def _checkForPortableFile(self): diff --git a/syncplay/ui/GuiConfiguration.py b/syncplay/ui/GuiConfiguration.py index 5ad81ae..d645321 100755 --- a/syncplay/ui/GuiConfiguration.py +++ b/syncplay/ui/GuiConfiguration.py @@ -556,7 +556,7 @@ class ConfigDialog(QtWidgets.QDialog): self.error = error if config['host'] is None: host = "" - elif ":" in config['host']: + elif ":" in config['host'] and '[' not in config['host']: host = config['host'] else: host = config['host'] + ":" + str(config['port']) @@ -580,7 +580,7 @@ class ConfigDialog(QtWidgets.QDialog): i += 1 self.hostCombobox.setEditable(True) self.hostCombobox.setEditText(host) - self.hostCombobox.setFixedWidth(165) + self.hostCombobox.setFixedWidth(250) self.hostLabel = QLabel(getMessage("host-label"), self) self.findServerButton = QtWidgets.QPushButton(QtGui.QIcon(resourcespath + 'arrow_refresh.png'), getMessage("update-server-list-label")) self.findServerButton.clicked.connect(self.updateServerList) @@ -634,7 +634,7 @@ class ConfigDialog(QtWidgets.QDialog): self.executablepathCombobox.setEditable(True) self.executablepathCombobox.currentIndexChanged.connect(self.updateExecutableIcon) self.executablepathCombobox.setEditText(self._tryToFillPlayerPath(config['playerPath'], playerpaths)) - self.executablepathCombobox.setFixedWidth(250) + self.executablepathCombobox.setFixedWidth(330) self.executablepathCombobox.editTextChanged.connect(self.updateExecutableIcon) self.executablepathLabel = QLabel(getMessage("executable-path-label"), self) From 218105d4ec38e311a4d187543ac47428eeb85c3e Mon Sep 17 00:00:00 2001 From: Alberto Sottile Date: Fri, 25 Jan 2019 10:13:18 +0100 Subject: [PATCH 05/12] IPv6: use HostnameEndpoint in client for both IPv4 and IPv6 This change was originally introduced and then deleted. Now is reintroduced after tests on both protocols. HostnameEndpoint is needed in some configurations to resolve IPv6-only hostnames, so it is required. The connection now works with both protocols on clients and servers. NOTE: now a wrong server/address/port DOES NOT trigger a Connection with Server Failed error immediately, the error stays in Deferred. This must be fixed before putting this code in production. --- syncplay/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/syncplay/client.py b/syncplay/client.py index c4bc20a..318efc4 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -11,6 +11,7 @@ import time from copy import deepcopy from functools import wraps +from twisted.internet.endpoints import HostnameEndpoint from twisted.internet.protocol import ClientFactory from twisted.internet import reactor, task, defer, threads @@ -728,7 +729,8 @@ class SyncplayClient(object): if '[' in host: host = host.strip('[]') port = int(port) - reactor.connectTCP(host, port, self.protocolFactory) + self._endpoint = HostnameEndpoint(reactor, host, port) + self._endpoint.connect(self.protocolFactory) reactor.run() def stop(self, promptForAction=False): From c7396d882d9816b196e50e8363af1c015342d2bc Mon Sep 17 00:00:00 2001 From: Alberto Sottile Date: Fri, 25 Jan 2019 15:51:53 +0100 Subject: [PATCH 06/12] IPv6: revert to listenTCP and override createInternetSocket to have a dual stack server --- syncplayServer.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/syncplayServer.py b/syncplayServer.py index 62431fc..b8ca65a 100755 --- a/syncplayServer.py +++ b/syncplayServer.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 #coding:utf8 +import socket import sys # libpath @@ -12,16 +13,24 @@ except AttributeError: import warnings warnings.warn("You must run Syncplay with Python 3.4 or newer!") -from twisted.internet.endpoints import TCP6ServerEndpoint -from twisted.internet import reactor +from twisted.internet import reactor, tcp from syncplay.server import SyncFactory, ConfigurationGetter +class DualStackPort(tcp.Port): + + def __init__(self, port, factory, backlog=50, interface='', reactor=None): + tcp.Port.__init__(self, port, factory, backlog, interface, reactor) + + def createInternetSocket(self): + s = tcp.Port.createInternetSocket(self) + s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + return s + if __name__ == '__main__': argsGetter = ConfigurationGetter() args = argsGetter.getConfiguration() - endpoint = TCP6ServerEndpoint(reactor, int(args.port)) - endpoint.listen( + dsp = DualStackPort(int(args.port), SyncFactory( args.port, args.password, @@ -32,5 +41,7 @@ if __name__ == '__main__': args.disable_chat, args.max_chat_message_length, args.max_username_length, - args.stats_db_file)) + args.stats_db_file), + interface='::') + dsp.startListening() reactor.run() From 04ab64582375b0b63d0a47b25900d0a48213a4ac Mon Sep 17 00:00:00 2001 From: Etoh Date: Sat, 26 Jan 2019 15:37:50 +0000 Subject: [PATCH 07/12] Reimplement connection error handling --- syncplay/client.py | 65 +++++++++++++++++++++------------------------- syncplayServer.py | 5 +++- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index 318efc4..f7955be 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -14,6 +14,7 @@ from functools import wraps from twisted.internet.endpoints import HostnameEndpoint from twisted.internet.protocol import ClientFactory from twisted.internet import reactor, task, defer, threads +from twisted.application.internet import ClientService from syncplay import utils, constants, version from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \ @@ -28,43 +29,11 @@ class SyncClientFactory(ClientFactory): self._client = client self.retry = retry self._timesTried = 0 - self.reconnecting = False def buildProtocol(self, addr): self._timesTried = 0 return SyncClientProtocol(self._client) - def startedConnecting(self, connector): - destination = connector.getDestination() - message = getMessage("connection-attempt-notification").format(destination.host, destination.port) - self._client.ui.showMessage(message) - - def clientConnectionLost(self, connector, reason): - if self._timesTried == 0: - self._client.onDisconnect() - if self._timesTried < self.retry: - self._timesTried += 1 - self._client.ui.showMessage(getMessage("reconnection-attempt-notification")) - self.reconnecting = True - reactor.callLater(0.1 * (2 ** min(self._timesTried, 5)), connector.connect) - else: - message = getMessage("disconnection-notification") - self._client.ui.showErrorMessage(message) - - def clientConnectionFailed(self, connector, reason): - if not self.reconnecting: - reactor.callLater(0.1, self._client.ui.showErrorMessage, getMessage("connection-failed-notification"), True) - reactor.callLater(0.1, self._client.stop, True) - else: - self.clientConnectionLost(connector, reason) - - def resetRetrying(self): - self._timesTried = 0 - - def stopRetrying(self): - self._timesTried = self.retry - - class SyncplayClient(object): def __init__(self, playerClass, ui, config): constants.SHOW_OSD = config['showOSD'] @@ -730,15 +699,41 @@ class SyncplayClient(object): host = host.strip('[]') port = int(port) self._endpoint = HostnameEndpoint(reactor, host, port) - self._endpoint.connect(self.protocolFactory) + + def retry(retries): + self._lastGlobalUpdate = None + if retries == 0: + self.onDisconnect() + if retries > constants.RECONNECT_RETRIES: + reactor.callLater(0.1, self.ui.showErrorMessage, getMessage("connection-failed-notification"), + True) + reactor.callLater(0.1, self.stop, True) + return None + + self.ui.showMessage(getMessage("reconnection-attempt-notification")) + self.reconnecting = True + return(0.1 * (2 ** min(retries, 5))) + + self._reconnectingService = ClientService(self._endpoint, self.protocolFactory , retryPolicy=retry) + waitForConnection = self._reconnectingService.whenConnected(failAfterFailures=1) + self._reconnectingService.startService() + + def connectedNow(f): + return + + def failed(f): + reactor.callLater(0.1, self.ui.showErrorMessage, getMessage("connection-failed-notification"), True) + reactor.callLater(0.1, self.stop, True) + + waitForConnection.addCallbacks(connectedNow, failed) + message = getMessage("connection-attempt-notification").format(host, port) + self.ui.showMessage(message) reactor.run() def stop(self, promptForAction=False): if not self._running: return self._running = False - if self.protocolFactory: - self.protocolFactory.stopRetrying() self.destroyProtocol() if self._player: self._player.drop() diff --git a/syncplayServer.py b/syncplayServer.py index b8ca65a..eaa9f34 100755 --- a/syncplayServer.py +++ b/syncplayServer.py @@ -24,7 +24,10 @@ class DualStackPort(tcp.Port): def createInternetSocket(self): s = tcp.Port.createInternetSocket(self) - s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + try: + s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + except: + pass return s if __name__ == '__main__': From cf06ac5fb0a508a674b9ba5bcae0afc0adac4b8a Mon Sep 17 00:00:00 2001 From: Etoh Date: Sat, 26 Jan 2019 15:50:52 +0000 Subject: [PATCH 08/12] Include win32pipe (to allow HostnameEndpoint to be imported) --- buildPy2exe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildPy2exe.py b/buildPy2exe.py index 31d628d..bd71b6b 100644 --- a/buildPy2exe.py +++ b/buildPy2exe.py @@ -783,8 +783,8 @@ info = dict( 'py2exe': { 'dist_dir': OUT_DIR, 'packages': 'PySide2', - 'includes': 'twisted, sys, encodings, datetime, os, time, math, liburl, ast, unicodedata, _ssl', - 'excludes': 'venv, doctest, pdb, unittest, win32clipboard, win32file, win32pdh, win32security, win32trace, win32ui, winxpgui, win32pipe, win32process, Tkinter', + 'includes': 'twisted, sys, encodings, datetime, os, time, math, liburl, ast, unicodedata, _ssl, win32pipe', + 'excludes': 'venv, doctest, pdb, unittest, win32clipboard, win32file, win32pdh, win32security, win32trace, win32ui, winxpgui, win32process, Tkinter', 'dll_excludes': 'msvcr71.dll, MSVCP90.dll, POWRPROF.dll', 'optimize': 2, 'compressed': 1 From a5496abea5d52059543ec06e2a1e3f80a4b84ae0 Mon Sep 17 00:00:00 2001 From: Etoh Date: Sat, 26 Jan 2019 16:08:52 +0000 Subject: [PATCH 09/12] Include win32file (to allow HostnameEndpoint to be imported) --- buildPy2exe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildPy2exe.py b/buildPy2exe.py index bd71b6b..bbe17fe 100644 --- a/buildPy2exe.py +++ b/buildPy2exe.py @@ -783,8 +783,8 @@ info = dict( 'py2exe': { 'dist_dir': OUT_DIR, 'packages': 'PySide2', - 'includes': 'twisted, sys, encodings, datetime, os, time, math, liburl, ast, unicodedata, _ssl, win32pipe', - 'excludes': 'venv, doctest, pdb, unittest, win32clipboard, win32file, win32pdh, win32security, win32trace, win32ui, winxpgui, win32process, Tkinter', + 'includes': 'twisted, sys, encodings, datetime, os, time, math, liburl, ast, unicodedata, _ssl, win32pipe, win32file', + 'excludes': 'venv, doctest, pdb, unittest, win32clipboard, win32pdh, win32security, win32trace, win32ui, winxpgui, win32process, Tkinter', 'dll_excludes': 'msvcr71.dll, MSVCP90.dll, POWRPROF.dll', 'optimize': 2, 'compressed': 1 From 2fa37400cf4a3ee815fd09d97765e69851423540 Mon Sep 17 00:00:00 2001 From: Alberto Sottile Date: Sun, 27 Jan 2019 11:16:32 +0100 Subject: [PATCH 10/12] Upver to 1.6.3 beta / build 72 --- syncplay/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/syncplay/__init__.py b/syncplay/__init__.py index cda75ca..7eefcf6 100755 --- a/syncplay/__init__.py +++ b/syncplay/__init__.py @@ -1,5 +1,5 @@ -version = '1.6.2' -revision = '' +version = '1.6.3' +revision = ' beta' milestone = 'Yoitsu' -release_number = '71' +release_number = '72' projectURL = 'https://syncplay.pl/' From be6c84f34de5bf0c8d86040cf7f4378a4057e032 Mon Sep 17 00:00:00 2001 From: Alberto Sottile Date: Sun, 27 Jan 2019 15:52:03 +0100 Subject: [PATCH 11/12] Show the destination IP of the connection in the UI --- syncplay/client.py | 4 +++- syncplay/messages_de.py | 1 + syncplay/messages_en.py | 1 + syncplay/messages_it.py | 1 + syncplay/messages_ru.py | 1 + 5 files changed, 7 insertions(+), 1 deletion(-) diff --git a/syncplay/client.py b/syncplay/client.py index f7955be..1385b0f 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -719,13 +719,15 @@ class SyncplayClient(object): self._reconnectingService.startService() def connectedNow(f): + hostIP = connectionHandle.result.transport.addr[0] + self.ui.showMessage(getMessage("handshake-successful-notification").format(host, hostIP)) return def failed(f): reactor.callLater(0.1, self.ui.showErrorMessage, getMessage("connection-failed-notification"), True) reactor.callLater(0.1, self.stop, True) - waitForConnection.addCallbacks(connectedNow, failed) + connectionHandle = waitForConnection.addCallbacks(connectedNow, failed) message = getMessage("connection-attempt-notification").format(host, port) self.ui.showMessage(message) reactor.run() diff --git a/syncplay/messages_de.py b/syncplay/messages_de.py index 978685b..e370015 100755 --- a/syncplay/messages_de.py +++ b/syncplay/messages_de.py @@ -16,6 +16,7 @@ de = { "connection-failed-notification": "Verbindung zum Server fehlgeschlagen", "connected-successful-notification": "Erfolgreich mit Server verbunden", "retrying-notification": "%s, versuche erneut in %d Sekunden...", # Seconds + "handshake-successful-notification": "Connection established with {} ({})", # TODO: Translate "rewind-notification": "Zurückgespult wegen Zeitdifferenz mit {}", # User "fastforward-notification": "Vorgespult wegen Zeitdifferenz mit {}", # User diff --git a/syncplay/messages_en.py b/syncplay/messages_en.py index acf5c37..2cbf725 100755 --- a/syncplay/messages_en.py +++ b/syncplay/messages_en.py @@ -16,6 +16,7 @@ en = { "connection-failed-notification": "Connection with server failed", "connected-successful-notification": "Successfully connected to server", "retrying-notification": "%s, Retrying in %d seconds...", # Seconds + "handshake-successful-notification": "Connection established with {} ({})", "rewind-notification": "Rewinded due to time difference with {}", # User "fastforward-notification": "Fast-forwarded due to time difference with {}", # User diff --git a/syncplay/messages_it.py b/syncplay/messages_it.py index 2ecff68..cdc20a0 100755 --- a/syncplay/messages_it.py +++ b/syncplay/messages_it.py @@ -16,6 +16,7 @@ it = { "connection-failed-notification": "Connessione col server fallita", "connected-successful-notification": "Connessione al server effettuata con successo", "retrying-notification": "%s, Nuovo tentativo in %d secondi...", # Seconds + "handshake-successful-notification": "Connessione stabilita con {} ({})", "rewind-notification": "Riavvolgo a causa della differenza temporale con {}", # User "fastforward-notification": "Avanzamento rapido a causa della differenza temporale con {}", # User diff --git a/syncplay/messages_ru.py b/syncplay/messages_ru.py index 18eeebe..b836abf 100755 --- a/syncplay/messages_ru.py +++ b/syncplay/messages_ru.py @@ -16,6 +16,7 @@ ru = { "connection-failed-notification": "Не удалось подключиться к серверу", "connected-successful-notification": "Соединение с сервером установлено", "retrying-notification": "%s, следующая попытка через %d секунд(ы)...", # Seconds + "handshake-successful-notification": "Connection established with {} ({})", # TODO: Translate "rewind-notification": "Перемотано из-за разницы во времени с {}", # User "fastforward-notification": "Ускорено из-за разницы во времени с {}", # User From 43486e9be8b75c260ca3b563cfe54cc14dff8d1c Mon Sep 17 00:00:00 2001 From: Alberto Sottile Date: Sun, 27 Jan 2019 15:54:39 +0100 Subject: [PATCH 12/12] Stop connection retrying from client when bad packets are detected --- syncplay/client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/syncplay/client.py b/syncplay/client.py index 1385b0f..0758e2e 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -34,6 +34,11 @@ class SyncClientFactory(ClientFactory): self._timesTried = 0 return SyncClientProtocol(self._client) + def stopRetrying(self): + self._client._reconnectingService.stopService() + self._client.ui.showErrorMessage(getMessage("disconnection-notification")) + + class SyncplayClient(object): def __init__(self, playerClass, ui, config): constants.SHOW_OSD = config['showOSD']