Merge pull request #223 from albertosottile/master

Opportunistic TLS support
This commit is contained in:
Etoh 2019-02-18 22:17:21 +00:00 committed by GitHub
commit 14af13c526
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 493 additions and 61 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ venv
/SyncPlay.egg-info
/build
/cert
/dist
/syncplay v*
/syncplay_v*

View File

@ -23,7 +23,7 @@ before_install:
- python3 -c "from py2app.recipes import pyside2"
install:
- pip3 install twisted appnope requests
- pip3 install twisted[tls] appnope requests certifi
before_deploy:
- pip3 install dmgbuild

View File

@ -12,11 +12,12 @@ import syncplay
APP = ['syncplayClient.py']
DATA_FILES = [
('resources', glob('resources/*.png') + glob('resources/*.rtf') + glob('resources/*.lua')),
('resources/lua/intf', glob('resources/lua/intf/*.lua'))
]
OPTIONS = {
'iconfile': 'resources/icon.icns',
'extra_scripts': 'syncplayServer.py',
'includes': {'PySide2.QtCore', 'PySide2.QtUiTools', 'PySide2.QtGui', 'PySide2.QtWidgets', 'certifi'},
'includes': {'PySide2.QtCore', 'PySide2.QtUiTools', 'PySide2.QtGui', 'PySide2.QtWidgets', 'certifi', 'cffi'},
'excludes': {'PySide', 'PySide.QtCore', 'PySide.QtUiTools', 'PySide.QtGui'},
'qt_plugins': [
'platforms/libqcocoa.dylib',

View File

@ -658,6 +658,7 @@ guiIcons = [
'resources/mpv.png', 'resources/vlc.png', 'resources/house.png', 'resources/film_link.png',
'resources/eye.png', 'resources/comments.png', 'resources/cog_delete.png', 'resources/chevrons_right.png',
'resources/user_key.png', 'resources/lock.png', 'resources/key_go.png', 'resources/page_white_key.png',
'resources/lock_green.png', 'resources/lock_green_dialog.png',
'resources/tick.png', 'resources/lock_open.png', 'resources/empty_checkbox.png', 'resources/tick_checkbox.png',
'resources/world_explore.png', 'resources/application_get.png', 'resources/cog.png', 'resources/arrow_switch.png',
'resources/film_go.png', 'resources/world_go.png', 'resources/arrow_refresh.png', 'resources/bullet_right_grey.png',

BIN
resources/lock_green.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

View File

@ -1,7 +1,8 @@
{\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf830
{\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf600
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\vieww13920\viewh8980\viewkind0
\deftab529
\pard\tx529\pardeftab529\pardirnatural\partightenfactor0
@ -24,25 +25,6 @@ The above copyright notice and this permission notice shall be included in all\
copies or substantial portions of the Software.\
\
\b PySide\
\b0 \
Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).\
Contact: PySide team <contact@pyside.org>\
\
This library is free software; you can redistribute it and/or\
modify it under the terms of the GNU Lesser General Public\
License as published by the Free Software Foundation; either\
version 2.1 of the License, or (at your option) any later version.\
This library is distributed in the hope that it will be useful,\
but WITHOUT ANY WARRANTY; without even the implied warranty of\
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\
Lesser General Public License for more details.\
\
You should have received a copy of the GNU Lesser General Public License\
along with this program. If not, see <http://www.gnu.org/licenses/>\
\
\b Qt for Python\
\b0 \
@ -318,4 +300,143 @@ http://www.apache.org/licenses/LICENSE-2.0\
Unless required by applicable law or agreed to in writing, software distributed under the \
License is distributed on an \'93AS IS\'94 BASIS, WITHOUT WARRANTIES OR CONDI-\
TIONS OF ANY KIND, either express or implied. See the License for the specific lang-\
uage governing permissions and limitations under the License.}
uage governing permissions and limitations under the License.\
\
\b python-certifi
\b0 \
\
This Source Code Form is subject to the terms of the Mozilla Public License,\
v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain\
one at http://mozilla.org/MPL/2.0/.\
\
\b cffi
\b0 \
\
\pard\pardeftab720\partightenfactor0
\cf0 This package has been mostly done by Armin Rigo with help from\
Maciej Fija\uc0\u322 kowski. The idea is heavily based (although not directly\
copied) from LuaJIT ffi by Mike Pall.\
\
Other contributors:\
\
Google Inc.\
\pard\tx529\pardeftab529\pardirnatural\partightenfactor0
\cf0 \
The MIT License\
\
Permission is hereby granted, free of charge, to any person \
obtaining a copy of this software and associated documentation \
files (the "Software"), to deal in the Software without \
restriction, including without limitation the rights to use, \
copy, modify, merge, publish, distribute, sublicense, and/or \
sell copies of the Software, and to permit persons to whom the \
Software is furnished to do so, subject to the following conditions:\
\
The above copyright notice and this permission notice shall be included \
in all copies or substantial portions of the Software.\
\
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS \
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, \
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL \
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER \
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING \
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER \
DEALINGS IN THE SOFTWARE.\
\
\b service-identity
\b0 \
\
Copyright (c) 2014 Hynek Schlawack\
\
Permission is hereby granted, free of charge, to any person obtaining a copy of\
this software and associated documentation files (the "Software"), to deal in\
the Software without restriction, including without limitation the rights to\
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\
of the Software, and to permit persons to whom the Software is furnished to do\
so, subject to the following conditions:\
\
The above copyright notice and this permission notice shall be included in all\
copies or substantial portions of the Software.\
\
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\
SOFTWARE.\
\
\b pyopenssl
\b0 \
\
Licensed under the Apache License, Version 2.0 (the \'93License\'94); you may not use this file\
except in compliance with the License. You may obtain a copy of the License at\
\
http://www.apache.org/licenses/LICENSE-2.0\
\
Unless required by applicable law or agreed to in writing, software distributed under the \
License is distributed on an \'93AS IS\'94 BASIS, WITHOUT WARRANTIES OR CONDI-\
TIONS OF ANY KIND, either express or implied. See the License for the specific lang-\
uage governing permissions and limitations under the License.\
\
\b cryptography
\b0 \
\
Authors listed here: https://github.com/pyca/cryptography/blob/master/AUTHORS.rst\
\
Licensed under the Apache License, Version 2.0 (the \'93License\'94); you may not use this file\
except in compliance with the License. You may obtain a copy of the License at\
\
http://www.apache.org/licenses/LICENSE-2.0\
\
Unless required by applicable law or agreed to in writing, software distributed under the \
License is distributed on an \'93AS IS\'94 BASIS, WITHOUT WARRANTIES OR CONDI-\
TIONS OF ANY KIND, either express or implied. See the License for the specific lang-\
uage governing permissions and limitations under the License.\
\
\b Icons\
\
\b0 Syncplay uses the following icons and images:\
\
- Silk icon set 1.3\
_________________________________________\
Mark James\
http://www.famfamfam.com/lab/icons/silk/\
_________________________________________\
\
This work is licensed under a\
Creative Commons Attribution 2.5 License.\
[ http://creativecommons.org/licenses/by/2.5/ ]\
\
This means you may use it for any purpose,\
and make any changes you like.\
All I ask is that you include a link back\
to this page in your credits.\
\
Are you using this icon set? Send me an email\
(including a link or picture if available) to\
mjames@gmail.com\
\
Any other questions about this icon set please\
contact mjames@gmail.com\
\
- Silk Companion 1\
\
\pard\pardeftab720\partightenfactor0
\cf0 Copyright Damien Guard - CC-BY 3.0\
https://damieng.com/creative/icons/silk-companion-1-icons\
\
- Padlock free icon\
CC-BY 3.0\
Icon made by Maxim Basinski from https://www.flaticon.com/free-icon/padlock_291248\
\
\pard\tx529\pardeftab529\pardirnatural\partightenfactor0
\cf0 \
}

View File

@ -1,5 +1,5 @@
version = '1.6.3'
revision = ' beta'
milestone = 'Yoitsu'
release_number = '72'
release_number = '73'
projectURL = 'https://syncplay.pl/'

View File

@ -2,6 +2,7 @@
import ast
import collections
import hashlib
import os
import os.path
import random
import re
@ -16,6 +17,24 @@ from twisted.internet.protocol import ClientFactory
from twisted.internet import reactor, task, defer, threads
from twisted.application.internet import ClientService
try:
import certifi
from twisted.internet.ssl import Certificate, optionsForClientTLS
certPath = certifi.where()
if os.path.exists(certPath):
os.environ['SSL_CERT_FILE'] = certPath
elif 'zip' in certPath:
import tempfile
import zipfile
zipPath, memberPath = certPath.split('.zip/')
zipPath += '.zip'
archive = zipfile.ZipFile(zipPath, 'r')
tmpDir = tempfile.gettempdir()
extractedPath = archive.extract(memberPath, tmpDir)
os.environ['SSL_CERT_FILE'] = extractedPath
except:
pass
from syncplay import utils, constants, version
from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \
PRIVACY_HIDDENFILENAME
@ -108,6 +127,8 @@ class SyncplayClient(object):
self.fileSwitch = FileSwitchManager(self)
self.playlist = SyncplayPlaylist(self)
self._serverSupportsTLS = True
if constants.LIST_RELATIVE_CONFIGS and 'loadedRelativePaths' in self._config and self._config['loadedRelativePaths']:
paths = "; ".join(self._config['loadedRelativePaths'])
self.ui.showMessage(getMessage("relative-config-notification").format(paths), noPlayer=True, noTimestamp=True)
@ -388,6 +409,7 @@ class SyncplayClient(object):
self.ui.showMessage(getMessage("current-offset-notification").format(self._userOffset))
def onDisconnect(self):
self.ui.setSSLMode(False)
if self._config['pauseOnLeave']:
self.setPaused(True)
self.lastPausedOnLeaveTime = time.time()
@ -704,9 +726,20 @@ class SyncplayClient(object):
host = host.strip('[]')
port = int(port)
self._endpoint = HostnameEndpoint(reactor, host, port)
try:
caCertFP = open(os.environ['SSL_CERT_FILE'])
caCertTwisted = Certificate.loadPEM(caCertFP.read())
caCertFP.close()
self.protocolFactory.options = optionsForClientTLS(hostname=host)
self._clientSupportsTLS = True
except Exception as e:
self.ui.showDebugMessage(str(e))
self.protocolFactory.options = None
self._clientSupportsTLS = False
def retry(retries):
self._lastGlobalUpdate = None
self.ui.setSSLMode(False)
if retries == 0:
self.onDisconnect()
if retries > constants.RECONNECT_RETRIES:
@ -719,7 +752,7 @@ class SyncplayClient(object):
self.reconnecting = True
return(0.1 * (2 ** min(retries, 5)))
self._reconnectingService = ClientService(self._endpoint, self.protocolFactory , retryPolicy=retry)
self._reconnectingService = ClientService(self._endpoint, self.protocolFactory, retryPolicy=retry)
waitForConnection = self._reconnectingService.whenConnected(failAfterFailures=1)
self._reconnectingService.startService()
@ -1456,6 +1489,9 @@ class UiManager(object):
self.showOSDMessage(messageString, duration=constants.OSD_DURATION)
self.__ui.showMessage(messageString)
def setSSLMode(self, sslMode, sslInformation=""):
self.__ui.setSSLMode(sslMode, sslInformation)
def showMessage(self, message, noPlayer=False, noTimestamp=False, OSDType=constants.OSD_NOTIFICATION, mood=constants.MESSAGE_NEUTRAL):
if not noPlayer:
self.showOSDMessage(message, duration=constants.OSD_DURATION, OSDType=OSDType, mood=mood)

View File

@ -310,6 +310,20 @@ de = {
"userguide-menu-label": "&Benutzerhandbuch öffnen",
"update-menu-label": "auf &Aktualisierung prüfen",
# startTLS messages - TODO: Translate
"startTLS-initiated": "Attempting secure connection",
"startTLS-secure-connection-ok": "Secure connection established ({})",
"startTLS-server-certificate-invalid": 'Secure connection failed. The server uses an invalid security certificate. This communication could be intercepted by a third party. For further details and troubleshooting see <a href="https://syncplay.pl/trouble">here</a>.',
"startTLS-not-supported-client": "This client does not support TLS",
"startTLS-not-supported-server": "This server does not support TLS",
# TLS certificate dialog - TODO: Translate
"tls-information-title": "Certificate Details",
"tls-dialog-status-label": "<strong>Syncplay is using an encrypted connection to {}.</strong>",
"tls-dialog-desc-label": "Encryption with a digital certificate keeps information private as it is sent to or from the<br/>server {}.",
"tls-dialog-connection-label": "Information encrypted using Transport Layer Security (TLS), version {} with the cipher<br/>suite: {}.",
"tls-dialog-certificate-label": "Certificate issued by {} valid until {}.",
# About dialog - TODO: Translate
"about-menu-label": "&About Syncplay",
"about-dialog-title": "About Syncplay",
@ -403,6 +417,8 @@ de = {
"reset-tooltip": "Alle Einstellungen auf Standardwerte zurücksetzen.",
"update-server-list-tooltip": "Mit syncplay.pl verbinden um die Liste öffentlicher Server zu aktualisieren.",
"sslconnection-tooltip": "Securely connected to server. Click for certificate details.", # TODO: Translate
"joinroom-tooltip": "Den aktuellen Raum verlassen und stattdessen den angegebenen betreten.",
"seektime-msgbox-label": "Springe zur angegebenen Zeit (in Sekunden oder min:sek). Verwende +/- zum relativen Springen.",
"ready-tooltip": "Zeigt an, ob du bereit zum anschauen bist",
@ -438,6 +454,7 @@ de = {
"server-chat-maxchars-argument": "Maximum number of characters in a chat message (default is {})", # TODO: Translate
"server-maxusernamelength-argument": "Maximum number of characters in a username (default is {})", # TODO: Translate
"server-stats-db-file-argument": "Enable server stats using the SQLite db file provided", # TODO: Translate
"server-startTLS-argument": "Enable TLS connections using the certificate files in the path provided", # TODO: Translate
"server-messed-up-motd-unescaped-placeholders": "Die Nachricht des Tages hat unmaskierte Platzhalter. Alle $-Zeichen sollten verdoppelt werden ($$).",
"server-messed-up-motd-too-long": "Die Nachricht des Tages ist zu lang - Maximal {} Zeichen, aktuell {}.",

View File

@ -267,7 +267,7 @@ en = {
"run-label": "Run Syncplay",
"storeandrun-label": "Store configuration and run Syncplay",
"contact-label": "Feel free to e-mail <a href=\"mailto:dev@syncplay.pl\"><nobr>dev@syncplay.pl</nobr></a>, chat via the <a href=\"https://webchat.freenode.net/?channels=#syncplay\"><nobr>#Syncplay IRC channel</nobr></a> on irc.freenode.net, <a href=\"https://github.com/Uriziel/syncplay/issues\"><nobr>raise an issue</nobr></a> via GitHub, <a href=\"https://www.facebook.com/SyncplaySoftware\"><nobr>like us on Facebook</nobr></a>, <a href=\"https://twitter.com/Syncplay/\"><nobr>follow us on Twitter</nobr></a>, or visit <a href=\"https://syncplay.pl/\"><nobr>https://syncplay.pl/</nobr></a>. NOTE: Chat messages are not encrypted so do not use Syncplay to send sensitive information.",
"contact-label": "Feel free to e-mail <a href=\"mailto:dev@syncplay.pl\"><nobr>dev@syncplay.pl</nobr></a>, chat via the <a href=\"https://webchat.freenode.net/?channels=#syncplay\"><nobr>#Syncplay IRC channel</nobr></a> on irc.freenode.net, <a href=\"https://github.com/Uriziel/syncplay/issues\"><nobr>raise an issue</nobr></a> via GitHub, <a href=\"https://www.facebook.com/SyncplaySoftware\"><nobr>like us on Facebook</nobr></a>, <a href=\"https://twitter.com/Syncplay/\"><nobr>follow us on Twitter</nobr></a>, or visit <a href=\"https://syncplay.pl/\"><nobr>https://syncplay.pl/</nobr></a>. Do not use Syncplay to send sensitive information.",
"joinroom-label": "Join room",
"joinroom-menu-label": "Join room {}",
@ -312,6 +312,19 @@ en = {
"userguide-menu-label": "Open user &guide",
"update-menu-label": "Check for &update",
"startTLS-initiated": "Attempting secure connection",
"startTLS-secure-connection-ok": "Secure connection established ({})",
"startTLS-server-certificate-invalid": 'Secure connection failed. The server uses an invalid security certificate. This communication could be intercepted by a third party. For further details and troubleshooting see <a href="https://syncplay.pl/trouble">here</a>.',
"startTLS-not-supported-client": "This client does not support TLS",
"startTLS-not-supported-server": "This server does not support TLS",
# TLS certificate dialog
"tls-information-title": "Certificate Details",
"tls-dialog-status-label": "<strong>Syncplay is using an encrypted connection to {}.</strong>",
"tls-dialog-desc-label": "Encryption with a digital certificate keeps information private as it is sent to or from the<br/>server {}.",
"tls-dialog-connection-label": "Information encrypted using Transport Layer Security (TLS), version {} with the cipher<br/>suite: {}.",
"tls-dialog-certificate-label": "Certificate issued by {} valid until {}.",
# About dialog
"about-menu-label": "&About Syncplay",
"about-dialog-title": "About Syncplay",
@ -403,6 +416,8 @@ en = {
"reset-tooltip": "Reset all settings to the default configuration.",
"update-server-list-tooltip": "Connect to syncplay.pl to update list of public servers.",
"sslconnection-tooltip": "Securely connected to server. Click for certificate details.",
"joinroom-tooltip": "Leave current room and joins specified room.",
"seektime-msgbox-label": "Jump to specified time (in seconds / min:sec). Use +/- for relative seek.",
"ready-tooltip": "Indicates whether you are ready to watch.",
@ -439,6 +454,7 @@ en = {
"server-chat-maxchars-argument": "Maximum number of characters in a chat message (default is {})", # Default number of characters
"server-maxusernamelength-argument": "Maximum number of characters in a username (default is {})",
"server-stats-db-file-argument": "Enable server stats using the SQLite db file provided",
"server-startTLS-argument": "Enable TLS connections using the certificate files in the path provided",
"server-messed-up-motd-unescaped-placeholders": "Message of the Day has unescaped placeholders. All $ signs should be doubled ($$).",
"server-messed-up-motd-too-long": "Message of the Day is too long - maximum of {} chars, {} given.",

View File

@ -267,7 +267,7 @@ it = {
"run-label": "Avvia Syncplay",
"storeandrun-label": "Salva la configurazione e avvia Syncplay",
"contact-label": "Sentiti libero di inviare un'e-mail a <a href=\"mailto:dev@syncplay.pl\"><nobr>dev@syncplay.pl</nobr></a>, chattare tramite il <a href=\"https://webchat.freenode.net/?channels=#syncplay\"><nobr>canale IRC #Syncplay</nobr></a> su irc.freenode.net, <a href=\"https://github.com/Uriziel/syncplay/issues\"><nobr>segnalare un problema</nobr></a> su GitHub, <a href=\"https://www.facebook.com/SyncplaySoftware\"><nobr>lasciare un like sulla nostra pagina Facebook</nobr></a>, <a href=\"https://twitter.com/Syncplay/\"><nobr>seguirci su Twitter</nobr></a>, o visitare <a href=\"https://syncplay.pl/\"><nobr>https://syncplay.pl/</nobr></a>. NOTA: i messaggi di chat non sono cifrati, quindi non usare Syncplay per inviare dati sensibili.",
"contact-label": "Sentiti libero di inviare un'e-mail a <a href=\"mailto:dev@syncplay.pl\"><nobr>dev@syncplay.pl</nobr></a>, chattare tramite il <a href=\"https://webchat.freenode.net/?channels=#syncplay\"><nobr>canale IRC #Syncplay</nobr></a> su irc.freenode.net, <a href=\"https://github.com/Uriziel/syncplay/issues\"><nobr>segnalare un problema</nobr></a> su GitHub, <a href=\"https://www.facebook.com/SyncplaySoftware\"><nobr>lasciare un like sulla nostra pagina Facebook</nobr></a>, <a href=\"https://twitter.com/Syncplay/\"><nobr>seguirci su Twitter</nobr></a>, o visitare <a href=\"https://syncplay.pl/\"><nobr>https://syncplay.pl/</nobr></a>. Non usare Syncplay per inviare dati sensibili.", # TODO: Check translation
"joinroom-label": "Entra nella stanza",
"joinroom-menu-label": "Entra nella stanza {}",
@ -312,6 +312,19 @@ it = {
"userguide-menu-label": "Apri guida &utente",
"update-menu-label": "Controlla la presenza di &aggiornamenti",
"startTLS-initiated": "Tentativo di connessione sicura in corso",
"startTLS-secure-connection-ok": "Connessione sicura stabilita ({})",
"startTLS-server-certificate-invalid": 'Connessione sicura non riuscita. Il certificato di sicurezza di questo server non è valido. La comunicazione potrebbe essere intercettata da una terza parte. Per ulteriori dettagli e informazioni sulla risoluzione del problema, clicca <a href="https://syncplay.pl/trouble">qui</a>.',
"startTLS-not-supported-client": "Questo client non supporta TLS",
"startTLS-not-supported-server": "Questo server non supporta TLS",
# TLS certificate dialog
"tls-information-title": "Informazioni sul certificato",
"tls-dialog-status-label": "<strong>Syncplay è connesso a {} tramite una connessione codificata.</strong>",
"tls-dialog-desc-label": "La codifica con un certificato digitale mantiene private le informazioni quando vengono<br/>inviate dal/al server {}.",
"tls-dialog-connection-label": "Informazioni codificate usando Transport Layer Security (TLS), versione {} usando gli<br/>algoritmi di cifratura: {}.",
"tls-dialog-certificate-label": "Certificato rilasciato da {} valido fino al {}.",
# About dialog
"about-menu-label": "&Informazioni su Syncplay",
"about-dialog-title": "Informazioni su Syncplay",
@ -403,6 +416,8 @@ it = {
"reset-tooltip": "Ripristina le impostazioni iniziali di Syncplay.",
"update-server-list-tooltip": "Scarica la lista dei server pubblici da syncplay.pl.",
"sslconnection-tooltip": "Connessione sicura al server. Clicca per informazioni sul certificato.",
"joinroom-tooltip": "Lascia la stanza attuale e entra in quella specificata.",
"seektime-msgbox-label": "Salta all'istante di tempo specificato (in secondi / min:sec). Usa +/- per una ricerca relativa.",
"ready-tooltip": "Indica quando sei pronto a guardare.",
@ -439,6 +454,7 @@ it = {
"server-chat-maxchars-argument": "Numero massimo di caratteri in un messaggio di chat (default è {})", # Default number of characters
"server-maxusernamelength-argument": "Numero massimo di caratteri in un nome utente (default è {})",
"server-stats-db-file-argument": "Abilita la raccolta dei dati statistici nel file SQLite indicato",
"server-startTLS-argument": "Abilita il protocollo TLS usando i certificati contenuti nel percorso indicato",
"server-messed-up-motd-unescaped-placeholders": "Il messaggio del giorno ha dei caratteri non 'escaped'. Tutti i simboli $ devono essere doppi ($$).",
"server-messed-up-motd-too-long": "Il messaggio del giorno è troppo lungo - numero massimo di caratteri è {}, {} trovati.",

View File

@ -269,7 +269,7 @@ ru = {
"run-label": "Запустить",
"storeandrun-label": "Сохранить и запустить",
"contact-label": "Есть идея, нашли ошибку или хотите оставить отзыв? Пишите на <a href=\"mailto:dev@syncplay.pl\">dev@syncplay.pl</a>, в <a href=\"https://webchat.freenode.net/?channels=#syncplay\">IRC канал #Syncplay</a> на irc.freenode.net или <a href=\"https://github.com/Uriziel/syncplay/issues\">задавайте вопросы через GitHub</a>. Кроме того, заходите на <a href=\"https://syncplay.pl/\">www.syncplay.pl</a> за инорфмацией, помощью и обновлениями! NOTE: Chat messages are not encrypted so do not use Syncplay to send sensitive information.", # TODO: Translate last sentence
"contact-label": "Есть идея, нашли ошибку или хотите оставить отзыв? Пишите на <a href=\"mailto:dev@syncplay.pl\">dev@syncplay.pl</a>, в <a href=\"https://webchat.freenode.net/?channels=#syncplay\">IRC канал #Syncplay</a> на irc.freenode.net или <a href=\"https://github.com/Uriziel/syncplay/issues\">задавайте вопросы через GitHub</a>. Кроме того, заходите на <a href=\"https://syncplay.pl/\">www.syncplay.pl</a> за инорфмацией, помощью и обновлениями! Do not use Syncplay to send sensitive information.", # TODO: Translate last sentence
"joinroom-label": "Зайти в комнату",
"joinroom-menu-label": "Зайти в комнату {}",
@ -313,6 +313,20 @@ ru = {
"userguide-menu-label": "&Руководство пользователя",
"update-menu-label": "Проверить &обновления",
# startTLS messages - TODO: Translate
"startTLS-initiated": "Attempting secure connection",
"startTLS-secure-connection-ok": "Secure connection established ({})",
"startTLS-server-certificate-invalid": 'Secure connection failed. The server uses an invalid security certificate. This communication could be intercepted by a third party. For further details and troubleshooting see <a href="https://syncplay.pl/trouble">here</a>.',
"startTLS-not-supported-client": "This client does not support TLS",
"startTLS-not-supported-server": "This server does not support TLS",
# TLS certificate dialog - TODO: Translate
"tls-information-title": "Certificate Details",
"tls-dialog-status-label": "<strong>Syncplay is using an encrypted connection to {}.</strong>",
"tls-dialog-desc-label": "Encryption with a digital certificate keeps information private as it is sent to or from the<br/>server {}.",
"tls-dialog-connection-label": "Information encrypted using Transport Layer Security (TLS), version {} with the cipher<br/>suite: {}.",
"tls-dialog-certificate-label": "Certificate issued by {} valid until {}.",
# About dialog - TODO: Translate
"about-menu-label": "&About Syncplay",
"about-dialog-title": "About Syncplay",
@ -404,6 +418,8 @@ ru = {
"reset-tooltip": "Сбрасывает все настройки Syncplay в начальное состояние.",
"update-server-list-tooltip": "Обновить список публичных серверов от syncplay.pl.",
"sslconnection-tooltip": "Securely connected to server. Click for certificate details.", # TODO: Translate
"joinroom-tooltip": "Покинуть комнату и зайти в другую, указанную комнату.",
"seektime-msgbox-label": "Перемотать к определенному моменту времени (указывать в секундах или мин:сек). Используйте +/-, чтобы перемотать вперед/назад относительно настоящего момента.",
"ready-tooltip": "Показывает, готовы ли Вы к просмотру или нет.",
@ -439,6 +455,7 @@ ru = {
"server-chat-maxchars-argument": "Maximum number of characters in a chat message (default is {})", # TODO: Translate
"server-maxusernamelength-argument": "Maximum number of characters in a username (default is {})", # TODO: Translate
"server-stats-db-file-argument": "Enable server stats using the SQLite db file provided", # TODO: Translate
"server-startTLS-argument": "Enable TLS connections using the certificate files in the path provided", # TODO: Translate
"server-messed-up-motd-unescaped-placeholders" : "MOTD-сообщение содержит неэкранированные спец.символы. Все знаки $ должны быть продублированы ($$).",
"server-messed-up-motd-too-long" : "MOTD-сообщение слишком длинное: максимальная длина - {} символ(ов), текущая длина - {} символ(ов).",

View File

@ -4,6 +4,8 @@ import time
from functools import wraps
from twisted.protocols.basic import LineReceiver
from twisted.internet.interfaces import IHandshakeListener
from zope.interface.declarations import implementer
import syncplay
from syncplay.constants import PING_MOVING_AVERAGE_WEIGHT, CONTROLLED_ROOMS_MIN_VERSION, USER_READY_MIN_VERSION, SHARED_PLAYLIST_MIN_VERSION, CHAT_MIN_VERSION
@ -27,6 +29,8 @@ class JSONCommandProtocol(LineReceiver):
self.handleError(message[1])
elif command == "Chat":
self.handleChat(message[1])
elif command == "TLS":
self.handleTLS(message[1])
else:
self.dropWithError(getMessage("unknown-command-server-error").format(message[1])) # TODO: log, not drop
@ -59,6 +63,7 @@ class JSONCommandProtocol(LineReceiver):
raise NotImplementedError()
@implementer(IHandshakeListener)
class SyncClientProtocol(JSONCommandProtocol):
def __init__(self, client):
self._client = client
@ -72,9 +77,27 @@ class SyncClientProtocol(JSONCommandProtocol):
def connectionMade(self):
self._client.initProtocol(self)
self.sendHello()
if self._client._clientSupportsTLS:
if self._client._serverSupportsTLS:
self.sendTLS({"startTLS": "send"})
self._client.ui.showMessage(getMessage("startTLS-initiated"))
else:
self._client.ui.showErrorMessage(getMessage("startTLS-not-supported-server"))
self.sendHello()
else:
self._client.ui.showMessage(getMessage("startTLS-not-supported-client"))
self.sendHello()
def connectionLost(self, reason):
try:
if "Invalid DNS-ID" in str(reason.value):
self._client._serverSupportsTLS = False
elif "tlsv1 alert protocol version" in str(reason.value):
self._client._clientSupportsTLS = False
elif "certificate verify failed" in str(reason.value):
self.dropWithError(getMessage("startTLS-server-certificate-invalid"))
except:
pass
self._client.destroyProtocol()
def dropWithError(self, error):
@ -296,11 +319,49 @@ class SyncClientProtocol(JSONCommandProtocol):
})
def handleError(self, error):
self.dropWithError(error["message"])
if "startTLS" in error["message"] and not self.logged:
self._client._serverSupportsTLS = False
else:
self.dropWithError(error["message"])
def sendError(self, message):
self.sendMessage({"Error": {"message": message}})
def sendTLS(self, message):
self.sendMessage({"TLS": message})
def handleTLS(self, message):
answer = message["startTLS"] if "startTLS" in message else None
if "true" in answer and not self.logged and self._client.protocolFactory.options is not None:
self.transport.startTLS(self._client.protocolFactory.options)
elif "false" in answer:
self._client.ui.showErrorMessage(getMessage("startTLS-not-supported-server"))
self.sendHello()
def handshakeCompleted(self):
from datetime import datetime
self._serverCertificateTLS = self.transport.getPeerCertificate()
self._subjectTLS = self._serverCertificateTLS.get_subject().CN
self._issuerTLS = self._serverCertificateTLS.get_issuer().CN
self._expiredTLS =self._serverCertificateTLS.has_expired()
self._expireDateTLS = datetime.strptime(self._serverCertificateTLS.get_notAfter().decode('ascii'), '%Y%m%d%H%M%SZ')
self._encryptedConnectionTLS = self.transport.protocol._tlsConnection
self._connVersionNumberTLS = self._encryptedConnectionTLS.get_protocol_version()
self._connVersionStringTLS = self._encryptedConnectionTLS.get_protocol_version_name()
self._cipherNameTLS = self._encryptedConnectionTLS.get_cipher_name()
if self._connVersionNumberTLS == 771:
self._connVersionNumberTLS = '1.2'
elif self._connVersionNumberTLS == 772:
self._connVersionNumberTLS = '1.3'
self._client.ui.showMessage(getMessage("startTLS-secure-connection-ok").format(self._connVersionStringTLS))
self._client.ui.setSSLMode( True,
{'subject': self._subjectTLS, 'issuer': self._issuerTLS, 'expires': self._expireDateTLS,
'protocolString': self._connVersionStringTLS, 'protocolVersion': self._connVersionNumberTLS,
'cipher': self._cipherNameTLS})
class SyncServerProtocol(JSONCommandProtocol):
def __init__(self, factory):
@ -602,6 +663,18 @@ class SyncServerProtocol(JSONCommandProtocol):
def sendError(self, message):
self.sendMessage({"Error": {"message": message}})
def sendTLS(self, message):
self.sendMessage({"TLS": message})
def handleTLS(self, message):
inquiry = message["startTLS"] if "startTLS" in message else None
if "send" in inquiry:
if not self.isLogged() and self._factory.options is not None:
self.sendTLS({"startTLS": "true"})
self.transport.startTLS(self._factory.options)
else:
self.sendTLS({"startTLS": "false"})
class PingService(object):

View File

@ -10,6 +10,12 @@ from twisted.enterprise import adbapi
from twisted.internet import task, reactor
from twisted.internet.protocol import Factory
try:
from OpenSSL import crypto
from twisted.internet import ssl
except:
pass
import syncplay
from syncplay import constants
from syncplay.messages import getMessage
@ -20,7 +26,7 @@ from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomString
class SyncFactory(Factory):
def __init__(self, port='', password='', motdFilePath=None, isolateRooms=False, salt=None,
disableReady=False, disableChat=False, maxChatMessageLength=constants.MAX_CHAT_MESSAGE_LENGTH,
maxUsernameLength=constants.MAX_USERNAME_LENGTH, statsDbFile=None):
maxUsernameLength=constants.MAX_USERNAME_LENGTH, statsDbFile=None, tlsCertPath=None):
self.isolateRooms = isolateRooms
print(getMessage("welcome-server-notification").format(syncplay.version))
self.port = port
@ -48,6 +54,9 @@ class SyncFactory(Factory):
self._statsRecorder.startRecorder(statsDelay)
else:
self._statsDbHandle = None
self.options = None
if tlsCertPath is not None:
self._allowTLSconnections(tlsCertPath)
def buildProtocol(self, addr):
return SyncServerProtocol(self)
@ -194,6 +203,30 @@ class SyncFactory(Factory):
else:
watcher.setPlaylistIndex(room.getName(), room.getPlaylistIndex())
def _allowTLSconnections(self, path):
try:
privKey = open(path+'/privkey.pem', 'rt').read()
certif = open(path+'/cert.pem', 'rt').read()
chain = open(path+'/chain.pem', 'rt').read()
privKeyPySSL = crypto.load_privatekey(crypto.FILETYPE_PEM, privKey)
certifPySSL = crypto.load_certificate(crypto.FILETYPE_PEM, certif)
chainPySSL = [crypto.load_certificate(crypto.FILETYPE_PEM, chain)]
cipherListString = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:"\
"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:"\
"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384"
accCiphers = ssl.AcceptableCiphers.fromOpenSSLCipherString(cipherListString)
contextFactory = ssl.CertificateOptions(privateKey=privKeyPySSL, certificate=certifPySSL,
extraCertChain=chainPySSL, acceptableCiphers=accCiphers,
raiseMinimumTo=ssl.TLSVersion.TLSv1_2)
self.options = contextFactory
except Exception as e:
self.options = None
print(e)
print("TLS support is not enabled.")
class StatsRecorder(object):
def __init__(self, dbHandle, roomManager):
@ -624,3 +657,4 @@ class ConfigurationGetter(object):
self._argparser.add_argument('--max-chat-message-length', metavar='maxChatMessageLength', type=int, nargs='?', help=getMessage("server-chat-maxchars-argument").format(constants.MAX_CHAT_MESSAGE_LENGTH))
self._argparser.add_argument('--max-username-length', metavar='maxUsernameLength', type=int, nargs='?', help=getMessage("server-maxusernamelength-argument").format(constants.MAX_USERNAME_LENGTH))
self._argparser.add_argument('--stats-db-file', metavar='file', type=str, nargs='?', help=getMessage("server-stats-db-file-argument"))
self._argparser.add_argument('--tls', metavar='path', type=str, nargs='?', help=getMessage("server-startTLS-argument"))

View File

@ -108,6 +108,9 @@ class ConsoleUI(threading.Thread):
def showErrorMessage(self, message, criticalerror=False):
print("ERROR:\t" + message)
def setSSLMode(self, sslMode, sslInformation):
pass
def _extractSign(self, m):
if m:
if m == "-":

View File

@ -124,6 +124,7 @@ class AboutDialog(QtWidgets.QDialog):
self.setWindowTitle(getMessage("about-dialog-title"))
if isWindows():
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self.setWindowIcon(QtGui.QPixmap(resourcespath + 'syncplay.png'))
nameLabel = QtWidgets.QLabel("<center><strong>Syncplay</strong></center>")
nameLabel.setFont(QtGui.QFont("Helvetica", 18))
linkLabel = QtWidgets.QLabel("<center><a href=\"https://syncplay.pl\">syncplay.pl</a></center>")
@ -171,11 +172,56 @@ class AboutDialog(QtWidgets.QDialog):
QtGui.QDesktopServices.openUrl(QUrl("file://" + resourcespath + "third-party-notices.rtf"))
class CertificateDialog(QtWidgets.QDialog):
def __init__(self, tlsData, parent=None):
super(CertificateDialog, self).__init__(parent)
if isMacOS():
self.setWindowTitle("")
self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint | Qt.CustomizeWindowHint)
else:
self.setWindowTitle(getMessage("tls-information-title"))
if isWindows():
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self.setWindowIcon(QtGui.QPixmap(resourcespath + 'syncplay.png'))
statusLabel = QtWidgets.QLabel(getMessage("tls-dialog-status-label").format(tlsData["subject"]))
descLabel = QtWidgets.QLabel(getMessage("tls-dialog-desc-label").format(tlsData["subject"]))
connDataLabel = QtWidgets.QLabel(getMessage("tls-dialog-connection-label").format(tlsData["protocolVersion"], tlsData["cipher"]))
certDataLabel = QtWidgets.QLabel(getMessage("tls-dialog-certificate-label").format(tlsData["issuer"], tlsData["expires"]))
if isMacOS():
statusLabel.setFont(QtGui.QFont("Helvetica", 12))
descLabel.setFont(QtGui.QFont("Helvetica", 12))
connDataLabel.setFont(QtGui.QFont("Helvetica", 12))
certDataLabel.setFont(QtGui.QFont("Helvetica", 12))
lockIconPixmap = QtGui.QPixmap(resourcespath + "lock_green_dialog.png")
lockIconLabel = QtWidgets.QLabel()
lockIconLabel.setPixmap(lockIconPixmap.scaled(64, 64, Qt.KeepAspectRatio))
certLayout = QtWidgets.QGridLayout()
certLayout.addWidget(lockIconLabel, 1, 0, 3, 1, Qt.AlignLeft | Qt.AlignTop)
certLayout.addWidget(statusLabel, 0, 1, 1, 3)
certLayout.addWidget(descLabel, 1, 1, 1, 3)
certLayout.addWidget(connDataLabel, 2, 1, 1, 3)
certLayout.addWidget(certDataLabel, 3, 1, 1, 3)
closeButton = QtWidgets.QPushButton("Close")
closeButton.setFixedWidth(100)
closeButton.setAutoDefault(False)
closeButton.clicked.connect(self.closeDialog)
certLayout.addWidget(closeButton, 4, 3, 1, 1)
certLayout.setVerticalSpacing(10)
certLayout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
self.setSizeGripEnabled(False)
self.setLayout(certLayout)
def closeDialog(self):
self.close()
class MainWindow(QtWidgets.QMainWindow):
insertPosition = None
playlistState = []
updatingPlaylist = False
playlistIndex = None
sslInformation = "N/A"
sslMode = False
def setPlaylistInsertPosition(self, newPosition):
if not self.playlist.isEnabled():
@ -431,6 +477,14 @@ class MainWindow(QtWidgets.QMainWindow):
self.chatInput.setMaxLength(constants.MAX_CHAT_MESSAGE_LENGTH)
self.roomInput.setMaxLength(constants.MAX_ROOM_NAME_LENGTH)
def setSSLMode(self, sslMode, sslInformation):
self.sslMode = sslMode
self.sslInformation = sslInformation
self.sslButton.setVisible(sslMode)
def getSSLInformation(self):
return self.sslInformation
def showMessage(self, message, noTimestamp=False):
message = str(message)
username = None
@ -783,6 +837,7 @@ class MainWindow(QtWidgets.QMainWindow):
if criticalerror:
QtWidgets.QMessageBox.critical(self, "Syncplay", message)
message = message.replace("&", "&amp;").replace('"', "&quot;").replace("<", "&lt;").replace(">", "&gt;")
message = message.replace("&lt;a href=&quot;https://syncplay.pl/trouble&quot;&gt;", '<a href="https://syncplay.pl/trouble">').replace("&lt;/a&gt;", "</a>")
message = message.replace("\n", "<br />")
message = "<span style=\"{}\">".format(constants.STYLE_ERRORNOTIFICATION) + message + "</span>"
self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "<br />")
@ -1204,6 +1259,7 @@ class MainWindow(QtWidgets.QMainWindow):
window.outputbox.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
window.outputlabel = QtWidgets.QLabel(getMessage("notifications-heading-label"))
window.outputlabel.setMinimumHeight(27)
window.chatInput = QtWidgets.QLineEdit()
window.chatInput.setMaxLength(constants.MAX_CHAT_MESSAGE_LENGTH)
window.chatInput.returnPressed.connect(self.sendChatMessage)
@ -1240,21 +1296,37 @@ class MainWindow(QtWidgets.QMainWindow):
self.listTreeView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.listTreeView.customContextMenuRequested.connect(self.openRoomMenu)
window.listlabel = QtWidgets.QLabel(getMessage("userlist-heading-label"))
window.listlabel.setMinimumHeight(27)
if isMacOS:
window.sslButton = QtWidgets.QPushButton(QtGui.QPixmap(resourcespath + 'lock_green.png').scaled(14, 14),"")
window.sslButton.setVisible(False)
window.sslButton.setFixedHeight(21)
window.sslButton.setFixedWidth(21)
window.sslButton.setMinimumSize(21, 21)
window.sslButton.setStyleSheet("QPushButton:!hover{border: 1px solid gray;} QPushButton:hover{border:2px solid black;}")
else:
window.sslButton = QtWidgets.QPushButton(QtGui.QPixmap(resourcespath + 'lock_green.png'),"")
window.sslButton.setVisible(False)
window.sslButton.setFixedHeight(27)
window.sslButton.setFixedWidth(27)
window.sslButton.pressed.connect(self.openSSLDetails)
window.sslButton.setToolTip(getMessage("sslconnection-tooltip"))
window.listFrame = QtWidgets.QFrame()
window.listFrame.setLineWidth(0)
window.listFrame.setMidLineWidth(0)
window.listFrame.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
window.listLayout.setContentsMargins(0, 0, 0, 0)
window.userlistLayout = QtWidgets.QVBoxLayout()
window.userlistLayout = QtWidgets.QGridLayout()
window.userlistFrame = QtWidgets.QFrame()
window.userlistFrame.setLineWidth(0)
window.userlistFrame.setMidLineWidth(0)
window.userlistFrame.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
window.userlistLayout.setContentsMargins(0, 0, 0, 0)
window.userlistFrame.setLayout(window.userlistLayout)
window.userlistLayout.addWidget(window.listlabel)
window.userlistLayout.addWidget(window.listTreeView)
window.userlistLayout.addWidget(window.listlabel, 0, 0, Qt.AlignLeft)
window.userlistLayout.addWidget(window.sslButton, 0, 2, Qt.AlignRight)
window.userlistLayout.addWidget(window.listTreeView, 1, 0, 1, 3)
window.listSplit = QtWidgets.QSplitter(Qt.Vertical, self)
window.listSplit.addWidget(window.userlistFrame)
@ -1513,6 +1585,12 @@ class MainWindow(QtWidgets.QMainWindow):
if not isMacOS():
window.mainLayout.setMenuBar(window.menuBar)
@needsClient
def openSSLDetails(self):
sslDetailsBox = CertificateDialog(self.getSSLInformation())
sslDetailsBox.exec_()
self.sslButton.setDown(False)
def openAbout(self):
aboutMsgBox = AboutDialog()
aboutMsgBox.exec_()

View File

@ -13,38 +13,56 @@ except AttributeError:
import warnings
warnings.warn("You must run Syncplay with Python 3.4 or newer!")
from twisted.internet import reactor, tcp
from twisted.internet import reactor
from twisted.internet.endpoints import TCP4ServerEndpoint, TCP6ServerEndpoint
from twisted.internet.error import CannotListenError
from syncplay.server import SyncFactory, ConfigurationGetter
class DualStackPort(tcp.Port):
class ServerStatus: pass
def __init__(self, port, factory, backlog=50, interface='', reactor=None):
tcp.Port.__init__(self, port, factory, backlog, interface, reactor)
def isListening6(f):
ServerStatus.listening6 = True
def isListening4(f):
ServerStatus.listening4 = True
def failed6(f):
ServerStatus.listening6 = False
print(f.value)
print("IPv6 listening failed.")
def failed4(f):
ServerStatus.listening4 = False
if f.type is CannotListenError and ServerStatus.listening6:
pass
else:
print(f.value)
print("IPv4 listening failed.")
def createInternetSocket(self):
s = tcp.Port.createInternetSocket(self)
try:
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
except:
pass
return s
if __name__ == '__main__':
argsGetter = ConfigurationGetter()
args = argsGetter.getConfiguration()
dsp = DualStackPort(int(args.port),
SyncFactory(
args.port,
args.password,
args.motd_file,
args.isolate_rooms,
args.salt,
args.disable_ready,
args.disable_chat,
args.max_chat_message_length,
args.max_username_length,
args.stats_db_file),
interface='::')
dsp.startListening()
reactor.run()
factory = SyncFactory(
args.port,
args.password,
args.motd_file,
args.isolate_rooms,
args.salt,
args.disable_ready,
args.disable_chat,
args.max_chat_message_length,
args.max_username_length,
args.stats_db_file,
args.tls
)
endpoint6 = TCP6ServerEndpoint(reactor, int(args.port))
endpoint6.listen(factory).addCallbacks(isListening6, failed6)
endpoint4 = TCP4ServerEndpoint(reactor, int(args.port))
endpoint4.listen(factory).addCallbacks(isListening4, failed4)
if ServerStatus.listening6 or ServerStatus.listening4:
reactor.run()
else:
print("Unable to listen using either IPv4 and IPv6 protocols. Quitting the server now.")
sys.exit()