Fix desktop WebSocket certificate verification
This commit is contained in:
@@ -1,22 +1,27 @@
|
||||
import asyncio
|
||||
import json
|
||||
import ssl
|
||||
import time
|
||||
|
||||
import certifi
|
||||
import websockets
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
|
||||
|
||||
class SyncClientThread(QThread):
|
||||
# Signals from WebSocket -> PyQt UI
|
||||
connected = pyqtSignal()
|
||||
disconnected = pyqtSignal()
|
||||
connection_error = pyqtSignal(str)
|
||||
room_joined = pyqtSignal(dict)
|
||||
room_rejoined = pyqtSignal(dict)
|
||||
room_error = pyqtSignal(str)
|
||||
file_check_needed = pyqtSignal(dict) # msg
|
||||
file_check_needed = pyqtSignal(dict) # msg
|
||||
users_updated = pyqtSignal(list)
|
||||
chat_message = pyqtSignal(str, str, int) # author, text, timestamp
|
||||
chat_message = pyqtSignal(str, str, int) # author, text, timestamp
|
||||
system_message = pyqtSignal(str)
|
||||
sync_event = pyqtSignal(dict)
|
||||
latency_updated = pyqtSignal(int) # latency in ms
|
||||
latency_updated = pyqtSignal(int) # latency in ms
|
||||
|
||||
def __init__(self, url="ws://localhost:3000/ws"):
|
||||
super().__init__()
|
||||
@@ -26,13 +31,31 @@ class SyncClientThread(QThread):
|
||||
self.running = False
|
||||
self._ping_sent_at = 0
|
||||
|
||||
@staticmethod
|
||||
def _create_ssl_context():
|
||||
"""Build a verified TLS context from the CA bundle shipped with the app."""
|
||||
return ssl.create_default_context(cafile=certifi.where())
|
||||
|
||||
@staticmethod
|
||||
def _certificate_error(exc):
|
||||
"""Return a nested certificate verification error, if present."""
|
||||
while exc is not None:
|
||||
if isinstance(exc, ssl.SSLCertVerificationError):
|
||||
return exc
|
||||
exc = exc.__cause__ or exc.__context__
|
||||
return None
|
||||
|
||||
def run(self):
|
||||
"""Runs strictly within the newly created QThread"""
|
||||
self.running = True
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self.loop.run_until_complete(self._connect_and_listen())
|
||||
self.loop.close()
|
||||
try:
|
||||
self.loop.run_until_complete(self._connect_and_listen())
|
||||
finally:
|
||||
self.ws = None
|
||||
self.running = False
|
||||
self.loop.close()
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
@@ -61,12 +84,17 @@ class SyncClientThread(QThread):
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _connect_and_listen(self):
|
||||
ssl_context = (
|
||||
self._create_ssl_context() if self.url.startswith("wss://") else None
|
||||
)
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
async with websockets.connect(self.url) as ws:
|
||||
connect_options = {"ssl": ssl_context} if ssl_context else {}
|
||||
async with websockets.connect(self.url, **connect_options) as ws:
|
||||
self.ws = ws
|
||||
self.connected.emit()
|
||||
|
||||
|
||||
ping_task = asyncio.create_task(self._ping_loop(ws))
|
||||
try:
|
||||
async for message in ws:
|
||||
@@ -77,38 +105,60 @@ class SyncClientThread(QThread):
|
||||
pass
|
||||
finally:
|
||||
ping_task.cancel()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
certificate_error = self._certificate_error(e)
|
||||
if certificate_error:
|
||||
verification_reason = getattr(
|
||||
certificate_error, "verify_message", str(certificate_error)
|
||||
)
|
||||
message = (
|
||||
"Secure connection failed because the server certificate could not be "
|
||||
f"verified ({verification_reason}). "
|
||||
"Check that your computer's date and time are correct, then update or "
|
||||
"restart VideoSync."
|
||||
)
|
||||
print(f"WebSocket TLS Error: {certificate_error}")
|
||||
self.connection_error.emit(message)
|
||||
self.running = False
|
||||
break
|
||||
print(f"WebSocket Error: {e}")
|
||||
|
||||
self.disconnected.emit()
|
||||
|
||||
finally:
|
||||
self.ws = None
|
||||
self.disconnected.emit()
|
||||
|
||||
if self.running:
|
||||
# Reconnect backoff
|
||||
await asyncio.sleep(2)
|
||||
|
||||
def _handle_message(self, msg: dict):
|
||||
t = msg.get("type")
|
||||
|
||||
|
||||
if t == "room_created":
|
||||
self.room_joined.emit(msg)
|
||||
|
||||
|
||||
elif t == "room_joined":
|
||||
self.room_joined.emit(msg)
|
||||
|
||||
|
||||
elif t == "room_rejoined":
|
||||
self.room_rejoined.emit(msg)
|
||||
|
||||
|
||||
elif t == "error":
|
||||
self.room_error.emit(msg.get("message", "Unknown error"))
|
||||
|
||||
|
||||
elif t == "room_file_check":
|
||||
self.file_check_needed.emit(msg)
|
||||
|
||||
|
||||
elif t in ["user_joined", "user_left"]:
|
||||
self.users_updated.emit(msg.get("users", []))
|
||||
|
||||
|
||||
elif t == "chat":
|
||||
self.chat_message.emit(msg.get("username", "Unknown"), msg.get("message", ""), msg.get("timestamp", 0))
|
||||
|
||||
self.chat_message.emit(
|
||||
msg.get("username", "Unknown"),
|
||||
msg.get("message", ""),
|
||||
msg.get("timestamp", 0),
|
||||
)
|
||||
|
||||
elif t == "sync":
|
||||
self.sync_event.emit(msg)
|
||||
|
||||
Reference in New Issue
Block a user