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

3
.gitignore vendored
View File

@ -5,6 +5,7 @@ venv
/SyncPlay.egg-info /SyncPlay.egg-info
/build /build
/cert
/dist /dist
/syncplay v* /syncplay v*
/syncplay_v* /syncplay_v*
@ -13,4 +14,4 @@ dist.7z
.* .*
!.travis.yml !.travis.yml
!.appveyor.yml !.appveyor.yml
__pycache__ __pycache__

View File

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

View File

@ -12,11 +12,12 @@ import syncplay
APP = ['syncplayClient.py'] APP = ['syncplayClient.py']
DATA_FILES = [ DATA_FILES = [
('resources', glob('resources/*.png') + glob('resources/*.rtf') + glob('resources/*.lua')), ('resources', glob('resources/*.png') + glob('resources/*.rtf') + glob('resources/*.lua')),
('resources/lua/intf', glob('resources/lua/intf/*.lua'))
] ]
OPTIONS = { OPTIONS = {
'iconfile': 'resources/icon.icns', 'iconfile': 'resources/icon.icns',
'extra_scripts': 'syncplayServer.py', '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'}, 'excludes': {'PySide', 'PySide.QtCore', 'PySide.QtUiTools', 'PySide.QtGui'},
'qt_plugins': [ 'qt_plugins': [
'platforms/libqcocoa.dylib', 'platforms/libqcocoa.dylib',

View File

@ -658,6 +658,7 @@ guiIcons = [
'resources/mpv.png', 'resources/vlc.png', 'resources/house.png', 'resources/film_link.png', '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/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/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/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/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', '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;} {\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;} {\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;} {\*\expandedcolortbl;;}
\vieww13920\viewh8980\viewkind0
\deftab529 \deftab529
\pard\tx529\pardeftab529\pardirnatural\partightenfactor0 \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.\ 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\ \b Qt for Python\
\b0 \ \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 \ 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-\ 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-\ 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' version = '1.6.3'
revision = ' beta' revision = ' beta'
milestone = 'Yoitsu' milestone = 'Yoitsu'
release_number = '72' release_number = '73'
projectURL = 'https://syncplay.pl/' projectURL = 'https://syncplay.pl/'

View File

@ -2,6 +2,7 @@
import ast import ast
import collections import collections
import hashlib import hashlib
import os
import os.path import os.path
import random import random
import re import re
@ -16,6 +17,24 @@ from twisted.internet.protocol import ClientFactory
from twisted.internet import reactor, task, defer, threads from twisted.internet import reactor, task, defer, threads
from twisted.application.internet import ClientService 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 import utils, constants, version
from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \ from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \
PRIVACY_HIDDENFILENAME PRIVACY_HIDDENFILENAME
@ -108,6 +127,8 @@ class SyncplayClient(object):
self.fileSwitch = FileSwitchManager(self) self.fileSwitch = FileSwitchManager(self)
self.playlist = SyncplayPlaylist(self) self.playlist = SyncplayPlaylist(self)
self._serverSupportsTLS = True
if constants.LIST_RELATIVE_CONFIGS and 'loadedRelativePaths' in self._config and self._config['loadedRelativePaths']: if constants.LIST_RELATIVE_CONFIGS and 'loadedRelativePaths' in self._config and self._config['loadedRelativePaths']:
paths = "; ".join(self._config['loadedRelativePaths']) paths = "; ".join(self._config['loadedRelativePaths'])
self.ui.showMessage(getMessage("relative-config-notification").format(paths), noPlayer=True, noTimestamp=True) 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)) self.ui.showMessage(getMessage("current-offset-notification").format(self._userOffset))
def onDisconnect(self): def onDisconnect(self):
self.ui.setSSLMode(False)
if self._config['pauseOnLeave']: if self._config['pauseOnLeave']:
self.setPaused(True) self.setPaused(True)
self.lastPausedOnLeaveTime = time.time() self.lastPausedOnLeaveTime = time.time()
@ -704,9 +726,20 @@ class SyncplayClient(object):
host = host.strip('[]') host = host.strip('[]')
port = int(port) port = int(port)
self._endpoint = HostnameEndpoint(reactor, host, 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): def retry(retries):
self._lastGlobalUpdate = None self._lastGlobalUpdate = None
self.ui.setSSLMode(False)
if retries == 0: if retries == 0:
self.onDisconnect() self.onDisconnect()
if retries > constants.RECONNECT_RETRIES: if retries > constants.RECONNECT_RETRIES:
@ -719,7 +752,7 @@ class SyncplayClient(object):
self.reconnecting = True self.reconnecting = True
return(0.1 * (2 ** min(retries, 5))) 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) waitForConnection = self._reconnectingService.whenConnected(failAfterFailures=1)
self._reconnectingService.startService() self._reconnectingService.startService()
@ -1456,6 +1489,9 @@ class UiManager(object):
self.showOSDMessage(messageString, duration=constants.OSD_DURATION) self.showOSDMessage(messageString, duration=constants.OSD_DURATION)
self.__ui.showMessage(messageString) 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): def showMessage(self, message, noPlayer=False, noTimestamp=False, OSDType=constants.OSD_NOTIFICATION, mood=constants.MESSAGE_NEUTRAL):
if not noPlayer: if not noPlayer:
self.showOSDMessage(message, duration=constants.OSD_DURATION, OSDType=OSDType, mood=mood) self.showOSDMessage(message, duration=constants.OSD_DURATION, OSDType=OSDType, mood=mood)

View File

@ -310,6 +310,20 @@ de = {
"userguide-menu-label": "&Benutzerhandbuch öffnen", "userguide-menu-label": "&Benutzerhandbuch öffnen",
"update-menu-label": "auf &Aktualisierung prüfen", "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 dialog - TODO: Translate
"about-menu-label": "&About Syncplay", "about-menu-label": "&About Syncplay",
"about-dialog-title": "About Syncplay", "about-dialog-title": "About Syncplay",
@ -403,6 +417,8 @@ de = {
"reset-tooltip": "Alle Einstellungen auf Standardwerte zurücksetzen.", "reset-tooltip": "Alle Einstellungen auf Standardwerte zurücksetzen.",
"update-server-list-tooltip": "Mit syncplay.pl verbinden um die Liste öffentlicher Server zu aktualisieren.", "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.", "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.", "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", "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-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-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-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-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 {}.", "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", "run-label": "Run Syncplay",
"storeandrun-label": "Store configuration and 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-label": "Join room",
"joinroom-menu-label": "Join room {}", "joinroom-menu-label": "Join room {}",
@ -312,6 +312,19 @@ en = {
"userguide-menu-label": "Open user &guide", "userguide-menu-label": "Open user &guide",
"update-menu-label": "Check for &update", "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 dialog
"about-menu-label": "&About Syncplay", "about-menu-label": "&About Syncplay",
"about-dialog-title": "About Syncplay", "about-dialog-title": "About Syncplay",
@ -403,6 +416,8 @@ en = {
"reset-tooltip": "Reset all settings to the default configuration.", "reset-tooltip": "Reset all settings to the default configuration.",
"update-server-list-tooltip": "Connect to syncplay.pl to update list of public servers.", "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.", "joinroom-tooltip": "Leave current room and joins specified room.",
"seektime-msgbox-label": "Jump to specified time (in seconds / min:sec). Use +/- for relative seek.", "seektime-msgbox-label": "Jump to specified time (in seconds / min:sec). Use +/- for relative seek.",
"ready-tooltip": "Indicates whether you are ready to watch.", "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-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-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-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-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.", "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", "run-label": "Avvia Syncplay",
"storeandrun-label": "Salva la configurazione e 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-label": "Entra nella stanza",
"joinroom-menu-label": "Entra nella stanza {}", "joinroom-menu-label": "Entra nella stanza {}",
@ -312,6 +312,19 @@ it = {
"userguide-menu-label": "Apri guida &utente", "userguide-menu-label": "Apri guida &utente",
"update-menu-label": "Controlla la presenza di &aggiornamenti", "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 dialog
"about-menu-label": "&Informazioni su Syncplay", "about-menu-label": "&Informazioni su Syncplay",
"about-dialog-title": "Informazioni su Syncplay", "about-dialog-title": "Informazioni su Syncplay",
@ -403,6 +416,8 @@ it = {
"reset-tooltip": "Ripristina le impostazioni iniziali di Syncplay.", "reset-tooltip": "Ripristina le impostazioni iniziali di Syncplay.",
"update-server-list-tooltip": "Scarica la lista dei server pubblici da syncplay.pl.", "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.", "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.", "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.", "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-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-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-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-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.", "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": "Запустить", "run-label": "Запустить",
"storeandrun-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-label": "Зайти в комнату",
"joinroom-menu-label": "Зайти в комнату {}", "joinroom-menu-label": "Зайти в комнату {}",
@ -313,6 +313,20 @@ ru = {
"userguide-menu-label": "&Руководство пользователя", "userguide-menu-label": "&Руководство пользователя",
"update-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 dialog - TODO: Translate
"about-menu-label": "&About Syncplay", "about-menu-label": "&About Syncplay",
"about-dialog-title": "About Syncplay", "about-dialog-title": "About Syncplay",
@ -404,6 +418,8 @@ ru = {
"reset-tooltip": "Сбрасывает все настройки Syncplay в начальное состояние.", "reset-tooltip": "Сбрасывает все настройки Syncplay в начальное состояние.",
"update-server-list-tooltip": "Обновить список публичных серверов от syncplay.pl.", "update-server-list-tooltip": "Обновить список публичных серверов от syncplay.pl.",
"sslconnection-tooltip": "Securely connected to server. Click for certificate details.", # TODO: Translate
"joinroom-tooltip": "Покинуть комнату и зайти в другую, указанную комнату.", "joinroom-tooltip": "Покинуть комнату и зайти в другую, указанную комнату.",
"seektime-msgbox-label": "Перемотать к определенному моменту времени (указывать в секундах или мин:сек). Используйте +/-, чтобы перемотать вперед/назад относительно настоящего момента.", "seektime-msgbox-label": "Перемотать к определенному моменту времени (указывать в секундах или мин:сек). Используйте +/-, чтобы перемотать вперед/назад относительно настоящего момента.",
"ready-tooltip": "Показывает, готовы ли Вы к просмотру или нет.", "ready-tooltip": "Показывает, готовы ли Вы к просмотру или нет.",
@ -439,6 +455,7 @@ ru = {
"server-chat-maxchars-argument": "Maximum number of characters in a chat message (default is {})", # TODO: Translate "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-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-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-unescaped-placeholders" : "MOTD-сообщение содержит неэкранированные спец.символы. Все знаки $ должны быть продублированы ($$).",
"server-messed-up-motd-too-long" : "MOTD-сообщение слишком длинное: максимальная длина - {} символ(ов), текущая длина - {} символ(ов).", "server-messed-up-motd-too-long" : "MOTD-сообщение слишком длинное: максимальная длина - {} символ(ов), текущая длина - {} символ(ов).",

View File

@ -4,6 +4,8 @@ import time
from functools import wraps from functools import wraps
from twisted.protocols.basic import LineReceiver from twisted.protocols.basic import LineReceiver
from twisted.internet.interfaces import IHandshakeListener
from zope.interface.declarations import implementer
import syncplay 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 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]) self.handleError(message[1])
elif command == "Chat": elif command == "Chat":
self.handleChat(message[1]) self.handleChat(message[1])
elif command == "TLS":
self.handleTLS(message[1])
else: else:
self.dropWithError(getMessage("unknown-command-server-error").format(message[1])) # TODO: log, not drop self.dropWithError(getMessage("unknown-command-server-error").format(message[1])) # TODO: log, not drop
@ -59,6 +63,7 @@ class JSONCommandProtocol(LineReceiver):
raise NotImplementedError() raise NotImplementedError()
@implementer(IHandshakeListener)
class SyncClientProtocol(JSONCommandProtocol): class SyncClientProtocol(JSONCommandProtocol):
def __init__(self, client): def __init__(self, client):
self._client = client self._client = client
@ -72,9 +77,27 @@ class SyncClientProtocol(JSONCommandProtocol):
def connectionMade(self): def connectionMade(self):
self._client.initProtocol(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): 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() self._client.destroyProtocol()
def dropWithError(self, error): def dropWithError(self, error):
@ -296,11 +319,49 @@ class SyncClientProtocol(JSONCommandProtocol):
}) })
def handleError(self, error): 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): def sendError(self, message):
self.sendMessage({"Error": {"message": 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): class SyncServerProtocol(JSONCommandProtocol):
def __init__(self, factory): def __init__(self, factory):
@ -602,6 +663,18 @@ class SyncServerProtocol(JSONCommandProtocol):
def sendError(self, message): def sendError(self, message):
self.sendMessage({"Error": {"message": 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): class PingService(object):

View File

@ -10,6 +10,12 @@ from twisted.enterprise import adbapi
from twisted.internet import task, reactor from twisted.internet import task, reactor
from twisted.internet.protocol import Factory from twisted.internet.protocol import Factory
try:
from OpenSSL import crypto
from twisted.internet import ssl
except:
pass
import syncplay import syncplay
from syncplay import constants from syncplay import constants
from syncplay.messages import getMessage from syncplay.messages import getMessage
@ -20,7 +26,7 @@ from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomString
class SyncFactory(Factory): class SyncFactory(Factory):
def __init__(self, port='', password='', motdFilePath=None, isolateRooms=False, salt=None, def __init__(self, port='', password='', motdFilePath=None, isolateRooms=False, salt=None,
disableReady=False, disableChat=False, maxChatMessageLength=constants.MAX_CHAT_MESSAGE_LENGTH, 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 self.isolateRooms = isolateRooms
print(getMessage("welcome-server-notification").format(syncplay.version)) print(getMessage("welcome-server-notification").format(syncplay.version))
self.port = port self.port = port
@ -48,6 +54,9 @@ class SyncFactory(Factory):
self._statsRecorder.startRecorder(statsDelay) self._statsRecorder.startRecorder(statsDelay)
else: else:
self._statsDbHandle = None self._statsDbHandle = None
self.options = None
if tlsCertPath is not None:
self._allowTLSconnections(tlsCertPath)
def buildProtocol(self, addr): def buildProtocol(self, addr):
return SyncServerProtocol(self) return SyncServerProtocol(self)
@ -194,6 +203,30 @@ class SyncFactory(Factory):
else: else:
watcher.setPlaylistIndex(room.getName(), room.getPlaylistIndex()) 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): class StatsRecorder(object):
def __init__(self, dbHandle, roomManager): 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-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('--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('--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): def showErrorMessage(self, message, criticalerror=False):
print("ERROR:\t" + message) print("ERROR:\t" + message)
def setSSLMode(self, sslMode, sslInformation):
pass
def _extractSign(self, m): def _extractSign(self, m):
if m: if m:
if m == "-": if m == "-":

View File

@ -124,6 +124,7 @@ class AboutDialog(QtWidgets.QDialog):
self.setWindowTitle(getMessage("about-dialog-title")) self.setWindowTitle(getMessage("about-dialog-title"))
if isWindows(): if isWindows():
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self.setWindowIcon(QtGui.QPixmap(resourcespath + 'syncplay.png'))
nameLabel = QtWidgets.QLabel("<center><strong>Syncplay</strong></center>") nameLabel = QtWidgets.QLabel("<center><strong>Syncplay</strong></center>")
nameLabel.setFont(QtGui.QFont("Helvetica", 18)) nameLabel.setFont(QtGui.QFont("Helvetica", 18))
linkLabel = QtWidgets.QLabel("<center><a href=\"https://syncplay.pl\">syncplay.pl</a></center>") 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")) 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): class MainWindow(QtWidgets.QMainWindow):
insertPosition = None insertPosition = None
playlistState = [] playlistState = []
updatingPlaylist = False updatingPlaylist = False
playlistIndex = None playlistIndex = None
sslInformation = "N/A"
sslMode = False
def setPlaylistInsertPosition(self, newPosition): def setPlaylistInsertPosition(self, newPosition):
if not self.playlist.isEnabled(): if not self.playlist.isEnabled():
@ -431,6 +477,14 @@ class MainWindow(QtWidgets.QMainWindow):
self.chatInput.setMaxLength(constants.MAX_CHAT_MESSAGE_LENGTH) self.chatInput.setMaxLength(constants.MAX_CHAT_MESSAGE_LENGTH)
self.roomInput.setMaxLength(constants.MAX_ROOM_NAME_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): def showMessage(self, message, noTimestamp=False):
message = str(message) message = str(message)
username = None username = None
@ -783,6 +837,7 @@ class MainWindow(QtWidgets.QMainWindow):
if criticalerror: if criticalerror:
QtWidgets.QMessageBox.critical(self, "Syncplay", message) QtWidgets.QMessageBox.critical(self, "Syncplay", message)
message = message.replace("&", "&amp;").replace('"', "&quot;").replace("<", "&lt;").replace(">", "&gt;") 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 = message.replace("\n", "<br />")
message = "<span style=\"{}\">".format(constants.STYLE_ERRORNOTIFICATION) + message + "</span>" message = "<span style=\"{}\">".format(constants.STYLE_ERRORNOTIFICATION) + message + "</span>"
self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "<br />") 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.outputbox.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
window.outputlabel = QtWidgets.QLabel(getMessage("notifications-heading-label")) window.outputlabel = QtWidgets.QLabel(getMessage("notifications-heading-label"))
window.outputlabel.setMinimumHeight(27)
window.chatInput = QtWidgets.QLineEdit() window.chatInput = QtWidgets.QLineEdit()
window.chatInput.setMaxLength(constants.MAX_CHAT_MESSAGE_LENGTH) window.chatInput.setMaxLength(constants.MAX_CHAT_MESSAGE_LENGTH)
window.chatInput.returnPressed.connect(self.sendChatMessage) window.chatInput.returnPressed.connect(self.sendChatMessage)
@ -1240,21 +1296,37 @@ class MainWindow(QtWidgets.QMainWindow):
self.listTreeView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) self.listTreeView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.listTreeView.customContextMenuRequested.connect(self.openRoomMenu) self.listTreeView.customContextMenuRequested.connect(self.openRoomMenu)
window.listlabel = QtWidgets.QLabel(getMessage("userlist-heading-label")) 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 = QtWidgets.QFrame()
window.listFrame.setLineWidth(0) window.listFrame.setLineWidth(0)
window.listFrame.setMidLineWidth(0) window.listFrame.setMidLineWidth(0)
window.listFrame.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) window.listFrame.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
window.listLayout.setContentsMargins(0, 0, 0, 0) window.listLayout.setContentsMargins(0, 0, 0, 0)
window.userlistLayout = QtWidgets.QVBoxLayout() window.userlistLayout = QtWidgets.QGridLayout()
window.userlistFrame = QtWidgets.QFrame() window.userlistFrame = QtWidgets.QFrame()
window.userlistFrame.setLineWidth(0) window.userlistFrame.setLineWidth(0)
window.userlistFrame.setMidLineWidth(0) window.userlistFrame.setMidLineWidth(0)
window.userlistFrame.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) window.userlistFrame.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
window.userlistLayout.setContentsMargins(0, 0, 0, 0) window.userlistLayout.setContentsMargins(0, 0, 0, 0)
window.userlistFrame.setLayout(window.userlistLayout) window.userlistFrame.setLayout(window.userlistLayout)
window.userlistLayout.addWidget(window.listlabel) window.userlistLayout.addWidget(window.listlabel, 0, 0, Qt.AlignLeft)
window.userlistLayout.addWidget(window.listTreeView) 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 = QtWidgets.QSplitter(Qt.Vertical, self)
window.listSplit.addWidget(window.userlistFrame) window.listSplit.addWidget(window.userlistFrame)
@ -1513,6 +1585,12 @@ class MainWindow(QtWidgets.QMainWindow):
if not isMacOS(): if not isMacOS():
window.mainLayout.setMenuBar(window.menuBar) window.mainLayout.setMenuBar(window.menuBar)
@needsClient
def openSSLDetails(self):
sslDetailsBox = CertificateDialog(self.getSSLInformation())
sslDetailsBox.exec_()
self.sslButton.setDown(False)
def openAbout(self): def openAbout(self):
aboutMsgBox = AboutDialog() aboutMsgBox = AboutDialog()
aboutMsgBox.exec_() aboutMsgBox.exec_()

View File

@ -13,38 +13,56 @@ except AttributeError:
import warnings import warnings
warnings.warn("You must run Syncplay with Python 3.4 or newer!") 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 from syncplay.server import SyncFactory, ConfigurationGetter
class DualStackPort(tcp.Port): class ServerStatus: pass
def __init__(self, port, factory, backlog=50, interface='', reactor=None): def isListening6(f):
tcp.Port.__init__(self, port, factory, backlog, interface, reactor) 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__': if __name__ == '__main__':
argsGetter = ConfigurationGetter() argsGetter = ConfigurationGetter()
args = argsGetter.getConfiguration() args = argsGetter.getConfiguration()
dsp = DualStackPort(int(args.port), factory = SyncFactory(
SyncFactory( args.port,
args.port, args.password,
args.password, args.motd_file,
args.motd_file, args.isolate_rooms,
args.isolate_rooms, args.salt,
args.salt, args.disable_ready,
args.disable_ready, args.disable_chat,
args.disable_chat, args.max_chat_message_length,
args.max_chat_message_length, args.max_username_length,
args.max_username_length, args.stats_db_file,
args.stats_db_file), args.tls
interface='::') )
dsp.startListening() endpoint6 = TCP6ServerEndpoint(reactor, int(args.port))
reactor.run() 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()