This commit is contained in:
parent
f183873104
commit
30df8c4c77
78
protocol_prototype/DryBox/UI/client_state.py
Normal file
78
protocol_prototype/DryBox/UI/client_state.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# client_state.py
|
||||||
|
from queue import Queue
|
||||||
|
from session import NoiseXKSession
|
||||||
|
|
||||||
|
class ClientState:
|
||||||
|
def __init__(self, client_id):
|
||||||
|
self.client_id = client_id
|
||||||
|
self.command_queue = Queue()
|
||||||
|
self.initiator = None
|
||||||
|
self.keypair = None
|
||||||
|
self.peer_pubkey = None
|
||||||
|
self.session = None
|
||||||
|
self.handshake_in_progress = False
|
||||||
|
self.handshake_start_time = None
|
||||||
|
self.call_active = False
|
||||||
|
|
||||||
|
def process_command(self, client):
|
||||||
|
"""Process commands from the queue."""
|
||||||
|
if not self.command_queue.empty():
|
||||||
|
print(f"Client {self.client_id} processing command queue, size: {self.command_queue.qsize()}")
|
||||||
|
command = self.command_queue.get()
|
||||||
|
if command == "handshake":
|
||||||
|
try:
|
||||||
|
print(f"Client {self.client_id} starting handshake, initiator: {self.initiator}")
|
||||||
|
self.session = NoiseXKSession(self.keypair, self.peer_pubkey)
|
||||||
|
self.session.handshake(client.sock, self.initiator)
|
||||||
|
print(f"Client {self.client_id} handshake complete")
|
||||||
|
client.send("HANDSHAKE_DONE")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Client {self.client_id} handshake failed: {e}")
|
||||||
|
client.state_changed.emit("CALL_END", "", self.client_id)
|
||||||
|
finally:
|
||||||
|
self.handshake_in_progress = False
|
||||||
|
self.handshake_start_time = None
|
||||||
|
|
||||||
|
def start_handshake(self, initiator, keypair, peer_pubkey):
|
||||||
|
"""Queue handshake command."""
|
||||||
|
self.initiator = initiator
|
||||||
|
self.keypair = keypair
|
||||||
|
self.peer_pubkey = peer_pubkey
|
||||||
|
print(f"Client {self.client_id} queuing handshake, initiator: {initiator}")
|
||||||
|
self.handshake_in_progress = True
|
||||||
|
self.handshake_start_time = time.time()
|
||||||
|
self.command_queue.put("handshake")
|
||||||
|
|
||||||
|
def handle_data(self, client, data):
|
||||||
|
"""Handle received data (control or audio)."""
|
||||||
|
try:
|
||||||
|
decoded_data = data.decode('utf-8').strip()
|
||||||
|
print(f"Client {self.client_id} received raw: {decoded_data}")
|
||||||
|
if decoded_data in ["RINGING", "CALL_END", "CALL_DROPPED", "IN_CALL", "HANDSHAKE", "HANDSHAKE_DONE"]:
|
||||||
|
client.state_changed.emit(decoded_data, decoded_data, self.client_id)
|
||||||
|
if decoded_data == "HANDSHAKE":
|
||||||
|
self.handshake_in_progress = True
|
||||||
|
elif decoded_data == "HANDSHAKE_DONE":
|
||||||
|
self.call_active = True
|
||||||
|
else:
|
||||||
|
print(f"Client {self.client_id} ignored unexpected text message: {decoded_data}")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
if self.call_active and self.session:
|
||||||
|
try:
|
||||||
|
print(f"Client {self.client_id} received audio packet, length={len(data)}")
|
||||||
|
decrypted_data = self.session.decrypt(data)
|
||||||
|
print(f"Client {self.client_id} decrypted audio packet, length={len(decrypted_data)}")
|
||||||
|
client.data_received.emit(decrypted_data, self.client_id)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Client {self.client_id} failed to process audio packet: {e}")
|
||||||
|
else:
|
||||||
|
print(f"Client {self.client_id} ignored non-text message: {data.hex()}")
|
||||||
|
|
||||||
|
def check_handshake_timeout(self, client):
|
||||||
|
"""Check for handshake timeout."""
|
||||||
|
if self.handshake_in_progress and self.handshake_start_time:
|
||||||
|
if time.time() - self.handshake_start_time > 30:
|
||||||
|
print(f"Client {self.client_id} handshake timeout after 30s")
|
||||||
|
client.state_changed.emit("CALL_END", "", self.client_id)
|
||||||
|
self.handshake_in_progress = False
|
||||||
|
self.handshake_start_time = None
|
@ -1,50 +1,23 @@
|
|||||||
|
# main.py
|
||||||
import sys
|
import sys
|
||||||
import secrets
|
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
QPushButton, QLabel, QFrame, QSizePolicy, QStyle
|
QPushButton, QLabel, QFrame, QSizePolicy, QStyle
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt, QTimer, QSize
|
from PyQt5.QtCore import Qt, QSize
|
||||||
from PyQt5.QtGui import QFont
|
from PyQt5.QtGui import QFont
|
||||||
from phone_client import PhoneClient
|
from phone_manager import PhoneManager
|
||||||
from waveform_widget import WaveformWidget
|
from waveform_widget import WaveformWidget
|
||||||
from phone_state import PhoneState
|
|
||||||
from session import NoiseXKSession
|
|
||||||
|
|
||||||
class PhoneUI(QMainWindow):
|
class PhoneUI(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle("Enhanced Dual Phone Interface")
|
self.setWindowTitle("Enhanced Dual Phone Interface")
|
||||||
self.setGeometry(100, 100, 900, 750)
|
self.setGeometry(100, 100, 900, 750)
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("...")
|
||||||
QMainWindow { background-color: #333333; }
|
|
||||||
QLabel { color: #E0E0E0; font-size: 14px; }
|
self.manager = PhoneManager()
|
||||||
QPushButton {
|
self.manager.initialize_phones()
|
||||||
background-color: #0078D4; color: white; border: none;
|
|
||||||
padding: 10px 15px; border-radius: 5px; font-size: 14px;
|
|
||||||
min-height: 30px;
|
|
||||||
}
|
|
||||||
QPushButton:hover { background-color: #005A9E; }
|
|
||||||
QPushButton:pressed { background-color: #003C6B; }
|
|
||||||
QPushButton#settingsButton { background-color: #555555; }
|
|
||||||
QPushButton#settingsButton:hover { background-color: #777777; }
|
|
||||||
QFrame#phoneDisplay {
|
|
||||||
background-color: #1E1E1E; border: 2px solid #0078D4;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
QLabel#phoneTitleLabel {
|
|
||||||
font-size: 18px; font-weight: bold; padding-bottom: 5px;
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
QLabel#mainTitleLabel {
|
|
||||||
font-size: 24px; font-weight: bold; color: #00A2E8;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
QWidget#phoneWidget {
|
|
||||||
border: 1px solid #4A4A4A; border-radius: 8px;
|
|
||||||
padding: 10px; background-color: #3A3A3A;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Main widget and layout
|
# Main widget and layout
|
||||||
main_widget = QWidget()
|
main_widget = QWidget()
|
||||||
@ -67,39 +40,18 @@ class PhoneUI(QMainWindow):
|
|||||||
phone_controls_layout.setAlignment(Qt.AlignCenter)
|
phone_controls_layout.setAlignment(Qt.AlignCenter)
|
||||||
main_layout.addLayout(phone_controls_layout)
|
main_layout.addLayout(phone_controls_layout)
|
||||||
|
|
||||||
# Initialize phones
|
# Setup UI for phones
|
||||||
self.phones = []
|
for phone in self.manager.phones:
|
||||||
self.handshake_done_count = 0
|
phone_container_widget, phone_button, waveform_widget, phone_status_label = self._create_phone_ui(
|
||||||
for i in range(2):
|
f"Phone {phone['id']+1}", lambda checked, pid=phone['id']: self.manager.phone_action(pid, self)
|
||||||
client = PhoneClient("localhost", 12345, i)
|
|
||||||
client.data_received.connect(lambda data, cid=i: self.update_waveform(cid, data))
|
|
||||||
client.state_changed.connect(lambda state, num, cid=i: self.set_phone_state(cid, self.map_state(state), num))
|
|
||||||
client.start()
|
|
||||||
|
|
||||||
# Generate keypair for each phone
|
|
||||||
keypair = NoiseXKSession.generate_keypair()
|
|
||||||
|
|
||||||
phone_container_widget, phone_display_frame, phone_button, waveform_widget, phone_status_label = self._create_phone_ui(
|
|
||||||
f"Phone {i+1}", lambda checked, phone_id=i: self.phone_action(phone_id)
|
|
||||||
)
|
)
|
||||||
self.phones.append({
|
phone['button'] = phone_button
|
||||||
'id': i,
|
phone['waveform'] = waveform_widget
|
||||||
'client': client,
|
phone['status_label'] = phone_status_label
|
||||||
'state': PhoneState.IDLE,
|
|
||||||
'button': phone_button,
|
|
||||||
'waveform': waveform_widget,
|
|
||||||
'number': "123-4567" if i == 0 else "987-6543",
|
|
||||||
'audio_timer': None,
|
|
||||||
'status_label': phone_status_label,
|
|
||||||
'keypair': keypair,
|
|
||||||
'public_key': keypair.public,
|
|
||||||
'is_initiator': False
|
|
||||||
})
|
|
||||||
phone_controls_layout.addWidget(phone_container_widget)
|
phone_controls_layout.addWidget(phone_container_widget)
|
||||||
|
phone['client'].data_received.connect(lambda data, cid=phone['id']: self.manager.update_waveform(cid, data))
|
||||||
# Share public key between phones
|
phone['client'].state_changed.connect(lambda state, num, cid=phone['id']: self.set_phone_state(cid, state, num))
|
||||||
self.phones[0]['peer_public_key'] = self.phones[1]['public_key']
|
phone['client'].start()
|
||||||
self.phones[1]['peer_public_key'] = self.phones[0]['public_key']
|
|
||||||
|
|
||||||
# Spacer
|
# Spacer
|
||||||
main_layout.addStretch(1)
|
main_layout.addStretch(1)
|
||||||
@ -117,11 +69,12 @@ class PhoneUI(QMainWindow):
|
|||||||
settings_layout.addStretch()
|
settings_layout.addStretch()
|
||||||
main_layout.addLayout(settings_layout)
|
main_layout.addLayout(settings_layout)
|
||||||
|
|
||||||
# Initialize button states
|
# Initialize UI
|
||||||
for phone in self.phones:
|
for phone in self.manager.phones:
|
||||||
self._update_phone_button_ui(phone['button'], phone['status_label'], phone['state'])
|
self.update_phone_ui(phone['id'])
|
||||||
|
|
||||||
def _create_phone_ui(self, title, action_slot):
|
def _create_phone_ui(self, title, action_slot):
|
||||||
|
# Same as existing _create_phone_ui
|
||||||
phone_container_widget = QWidget()
|
phone_container_widget = QWidget()
|
||||||
phone_container_widget.setObjectName("phoneWidget")
|
phone_container_widget.setObjectName("phoneWidget")
|
||||||
phone_layout = QVBoxLayout()
|
phone_layout = QVBoxLayout()
|
||||||
@ -162,7 +115,15 @@ class PhoneUI(QMainWindow):
|
|||||||
|
|
||||||
return phone_container_widget, phone_display_frame, phone_button, waveform_widget, phone_status_label
|
return phone_container_widget, phone_display_frame, phone_button, waveform_widget, phone_status_label
|
||||||
|
|
||||||
def _update_phone_button_ui(self, button, status_label, state, phone_number=""):
|
def update_phone_ui(self, phone_id):
|
||||||
|
"""Update phone button and status label based on state."""
|
||||||
|
phone = self.manager.phones[phone_id]
|
||||||
|
other_phone = self.manager.phones[1 - phone_id]
|
||||||
|
state = phone['state']
|
||||||
|
phone_number = other_phone['number'] if state != PhoneState.IDLE else ""
|
||||||
|
button = phone['button']
|
||||||
|
status_label = phone['status_label']
|
||||||
|
|
||||||
if state == PhoneState.IDLE:
|
if state == PhoneState.IDLE:
|
||||||
button.setText("Call")
|
button.setText("Call")
|
||||||
button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
|
button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
|
||||||
@ -184,123 +145,31 @@ class PhoneUI(QMainWindow):
|
|||||||
status_label.setText(f"Incoming Call from {phone_number}")
|
status_label.setText(f"Incoming Call from {phone_number}")
|
||||||
button.setStyleSheet("background-color: #107C10;")
|
button.setStyleSheet("background-color: #107C10;")
|
||||||
|
|
||||||
def phone_action(self, phone_id):
|
def set_phone_state(self, client_id, state_str, number):
|
||||||
phone = self.phones[phone_id]
|
"""Handle state changes from client."""
|
||||||
other_phone = self.phones[1 - phone_id]
|
state = self.manager.map_state(state_str)
|
||||||
print(f"Phone {phone_id + 1} Action, current state: {phone['state']}, is_initiator: {phone['is_initiator']}")
|
phone = self.manager.phones[client_id]
|
||||||
|
other_phone = self.manager.phones[1 - phone_id]
|
||||||
if phone['state'] == PhoneState.IDLE:
|
|
||||||
# Initiate a call
|
|
||||||
phone['state'] = PhoneState.CALLING
|
|
||||||
other_phone['state'] = PhoneState.RINGING
|
|
||||||
# Set init/resp
|
|
||||||
phone['is_initiator'] = True
|
|
||||||
other_phone['is_initiator'] = False
|
|
||||||
self._update_phone_button_ui(phone['button'], phone['status_label'], phone['state'], other_phone['number'])
|
|
||||||
self._update_phone_button_ui(other_phone['button'], other_phone['status_label'], other_phone['state'], phone['number'])
|
|
||||||
phone['client'].send("RINGING")
|
|
||||||
|
|
||||||
elif phone['state'] == PhoneState.RINGING:
|
|
||||||
# Answer the call
|
|
||||||
phone['state'] = PhoneState.IN_CALL
|
|
||||||
other_phone['state'] = PhoneState.IN_CALL
|
|
||||||
self._update_phone_button_ui(phone['button'], phone['status_label'], phone['state'], other_phone['number'])
|
|
||||||
self._update_phone_button_ui(other_phone['button'], other_phone['status_label'], other_phone['state'], phone['number'])
|
|
||||||
phone['client'].send("IN_CALL")
|
|
||||||
|
|
||||||
elif phone['state'] == PhoneState.IN_CALL or phone['state'] == PhoneState.CALLING:
|
|
||||||
# Hang up or cancel
|
|
||||||
if not phone['client'].handshake_in_progress and phone['state'] != PhoneState.CALLING:
|
|
||||||
phone['state'] = PhoneState.IDLE
|
|
||||||
other_phone['state'] = PhoneState.IDLE
|
|
||||||
self._update_phone_button_ui(phone['button'], phone['status_label'], phone['state'], "")
|
|
||||||
self._update_phone_button_ui(other_phone['button'], other_phone['status_label'], other_phone['state'], "")
|
|
||||||
phone['client'].send("CALL_END")
|
|
||||||
# Stop audio timers for both phones
|
|
||||||
for p in [phone, other_phone]:
|
|
||||||
if p['audio_timer']:
|
|
||||||
p['audio_timer'].stop()
|
|
||||||
else:
|
|
||||||
print(f"Phone {phone_id + 1} cannot hang up during handshake or call setup")
|
|
||||||
|
|
||||||
def start_audio(self, client_id):
|
|
||||||
"""Start audio timer after both clients send HANDSHAKE_DONE."""
|
|
||||||
self.handshake_done_count += 1
|
|
||||||
print(f"HANDSHAKE_DONE received for client {client_id}, count: {self.handshake_done_count}")
|
|
||||||
if self.handshake_done_count == 2:
|
|
||||||
for phone in self.phones:
|
|
||||||
if phone['state'] == PhoneState.IN_CALL:
|
|
||||||
if not phone['audio_timer'] or not phone['audio_timer'].isActive():
|
|
||||||
phone['audio_timer'] = QTimer(self)
|
|
||||||
phone['audio_timer'].timeout.connect(lambda pid=phone['id']: self.send_audio(pid))
|
|
||||||
phone['audio_timer'].start(100) # 100ms for smoother updates
|
|
||||||
self.handshake_done_count = 0
|
|
||||||
|
|
||||||
def send_audio(self, phone_id):
|
|
||||||
phone = self.phones[phone_id]
|
|
||||||
if phone['state'] == PhoneState.IN_CALL and phone['client'].session and phone['client'].sock:
|
|
||||||
# Generate mock 16-byte audio data
|
|
||||||
mock_audio = secrets.token_bytes(16)
|
|
||||||
try:
|
|
||||||
# Encrypt with Noise session, send over socket
|
|
||||||
phone['client'].session.send(phone['client'].sock, mock_audio)
|
|
||||||
print(f"Client {phone_id} sent encrypted audio packet, length=32")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Client {phone_id} failed to send audio: {e}")
|
|
||||||
|
|
||||||
def update_waveform(self, client_id, data):
|
|
||||||
print(f"Updating waveform for client_id {client_id}, data_length={len(data)}")
|
|
||||||
waveform = self.phones[client_id]['waveform']
|
|
||||||
waveform.set_data(data)
|
|
||||||
|
|
||||||
def map_state(self, state_str):
|
|
||||||
if state_str == "RINGING":
|
|
||||||
return PhoneState.RINGING
|
|
||||||
elif state_str in ["CALL_END", "CALL_DROPPED"]:
|
|
||||||
return PhoneState.IDLE
|
|
||||||
elif state_str == "IN_CALL":
|
|
||||||
return PhoneState.IN_CALL
|
|
||||||
elif state_str == "HANDSHAKE":
|
|
||||||
return PhoneState.IN_CALL # Stay in IN_CALL, trigger handshake
|
|
||||||
elif state_str == "HANDSHAKE_DONE":
|
|
||||||
return PhoneState.IN_CALL # Stay in IN_CALL, start audio
|
|
||||||
return PhoneState.IDLE
|
|
||||||
|
|
||||||
def set_phone_state(self, client_id, state, number=""):
|
|
||||||
phone = self.phones[client_id]
|
|
||||||
other_phone = self.phones[1 - client_id]
|
|
||||||
print(f"Setting state for Phone {client_id + 1}: {state}, number: {number}, is_initiator: {phone['is_initiator']}")
|
print(f"Setting state for Phone {client_id + 1}: {state}, number: {number}, is_initiator: {phone['is_initiator']}")
|
||||||
phone['state'] = state
|
phone['state'] = state
|
||||||
if state == PhoneState.RINGING:
|
if state == PhoneState.IN_CALL:
|
||||||
self._update_phone_button_ui(phone['button'], phone['status_label'], state, other_phone['number'])
|
|
||||||
elif state == PhoneState.IN_CALL:
|
|
||||||
print(f"Phone {client_id + 1} confirmed in IN_CALL state")
|
print(f"Phone {client_id + 1} confirmed in IN_CALL state")
|
||||||
self._update_phone_button_ui(phone['button'], phone['status_label'], state, other_phone['number'])
|
|
||||||
if number == "IN_CALL" and phone['is_initiator']:
|
if number == "IN_CALL" and phone['is_initiator']:
|
||||||
# Initiator starts handshake after receiving IN_CALL
|
|
||||||
print(f"Phone {client_id + 1} (initiator) starting handshake")
|
print(f"Phone {client_id + 1} (initiator) starting handshake")
|
||||||
phone['client'].send("HANDSHAKE")
|
phone['client'].send("HANDSHAKE")
|
||||||
phone['client'].start_handshake(initiator=True, keypair=phone['keypair'], peer_pubkey=other_phone['public_key'])
|
phone['client'].start_handshake(initiator=True, keypair=phone['keypair'], peer_pubkey=other_phone['public_key'])
|
||||||
elif number == "HANDSHAKE" and not phone['is_initiator']:
|
elif number == "HANDSHAKE" and not phone['is_initiator']:
|
||||||
# Responder starts handshake after receiving HANDSHAKE
|
|
||||||
print(f"Phone {client_id + 1} (responder) starting handshake")
|
print(f"Phone {client_id + 1} (responder) starting handshake")
|
||||||
phone['client'].start_handshake(initiator=False, keypair=phone['keypair'], peer_pubkey=other_phone['public_key'])
|
phone['client'].start_handshake(initiator=False, keypair=phone['keypair'], peer_pubkey=other_phone['public_key'])
|
||||||
elif number == "HANDSHAKE_DONE":
|
elif number == "HANDSHAKE_DONE":
|
||||||
# Start audio after HANDSHAKE_DONE
|
self.manager.start_audio(client_id)
|
||||||
self.start_audio(client_id)
|
self.update_phone_ui(client_id)
|
||||||
else:
|
|
||||||
# Handle disconnect gracefully
|
|
||||||
self._update_phone_button_ui(phone['button'], phone['status_label'], state, "")
|
|
||||||
if state == PhoneState.IDLE and number == "CALL_END":
|
|
||||||
print(f"Phone {client_id + 1} resetting due to disconnect")
|
|
||||||
if state == PhoneState.IDLE and phone['audio_timer']:
|
|
||||||
phone['audio_timer'].stop()
|
|
||||||
|
|
||||||
def settings_action(self):
|
def settings_action(self):
|
||||||
print("Settings clicked")
|
print("Settings clicked")
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
for phone in self.phones:
|
for phone in self.manager.phones:
|
||||||
phone['client'].stop()
|
phone['client'].stop()
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
@ -1,45 +1,37 @@
|
|||||||
|
# phone_client.py
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
import select
|
import select
|
||||||
from PyQt5.QtCore import QThread, pyqtSignal
|
from PyQt5.QtCore import QThread, pyqtSignal
|
||||||
from queue import Queue
|
from client_state import ClientState
|
||||||
from session import NoiseXKSession
|
|
||||||
|
|
||||||
class PhoneClient(QThread):
|
class PhoneClient(QThread):
|
||||||
data_received = pyqtSignal(bytes, int) # Include client_id
|
data_received = pyqtSignal(bytes, int)
|
||||||
state_changed = pyqtSignal(str, str, int) # Include client_id
|
state_changed = pyqtSignal(str, str, int)
|
||||||
|
|
||||||
def __init__(self, host, port, client_id):
|
def __init__(self, client_id):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.host = host
|
self.host = "localhost"
|
||||||
self.port = port
|
self.port = 12345
|
||||||
self.client_id = client_id
|
self.client_id = client_id
|
||||||
self.sock = None
|
self.sock = None
|
||||||
self.running = True
|
self.running = True
|
||||||
self.command_queue = Queue()
|
self.state = ClientState(client_id)
|
||||||
self.initiator = None
|
|
||||||
self.keypair = None
|
|
||||||
self.peer_pubkey = None
|
|
||||||
self.session = None
|
|
||||||
self.handshake_in_progress = False
|
|
||||||
self.handshake_start_time = None
|
|
||||||
self.call_active = False # Track active call after HANDSHAKE_DONE
|
|
||||||
|
|
||||||
def connect_socket(self):
|
def connect_socket(self):
|
||||||
"""Attempt to connect to the server with retries."""
|
|
||||||
retries = 3
|
retries = 3
|
||||||
for attempt in range(retries):
|
for attempt in range(retries):
|
||||||
try:
|
try:
|
||||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||||
self.sock.settimeout(120) # 120s for socket operations
|
self.sock.settimeout(120)
|
||||||
self.sock.connect((self.host, self.port))
|
self.sock.connect((self.host, self.port))
|
||||||
print(f"Client {self.client_id} connected to {self.host}:{self.port}")
|
print(f"Client {self.client_id} connected to {self.host}:{self.port}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Client {self.client_id} connection attempt {attempt + 1} failed: {e}")
|
print(f"Client {self.client_id} connection attempt {attempt + 1} failed: {e}")
|
||||||
if attempt < retries - 1:
|
if attempt < retries - 1:
|
||||||
time.sleep(1) # Wait before retrying
|
time.sleep(1)
|
||||||
self.sock = None
|
self.sock = None
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -52,94 +44,27 @@ class PhoneClient(QThread):
|
|||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
while self.running:
|
while self.running:
|
||||||
# print(f"Client {self.client_id} run loop iteration")
|
self.state.process_command(self)
|
||||||
# Check command queue first
|
self.state.check_handshake_timeout(self)
|
||||||
if not self.command_queue.empty():
|
if not self.state.handshake_in_progress:
|
||||||
print(f"Client {self.client_id} processing command queue, size: {self.command_queue.qsize()}")
|
readable, _, _ = select.select([self.sock], [], [], 0.01)
|
||||||
command = self.command_queue.get()
|
if readable:
|
||||||
if command == "handshake":
|
|
||||||
try:
|
try:
|
||||||
print(f"Client {self.client_id} starting handshake, initiator: {self.initiator}")
|
print(f"Client {self.client_id} attempting sock.recv")
|
||||||
self.session = NoiseXKSession(self.keypair, self.peer_pubkey)
|
data = self.sock.recv(1024)
|
||||||
self.session.handshake(self.sock, self.initiator)
|
if not data:
|
||||||
print(f"Client {self.client_id} handshake complete")
|
print(f"Client {self.client_id} disconnected")
|
||||||
self.send("HANDSHAKE_DONE")
|
self.state_changed.emit("CALL_END", "", self.client_id)
|
||||||
except socket.timeout:
|
break
|
||||||
print(f"Client {self.client_id} handshake timed out")
|
self.state.handle_data(self, data)
|
||||||
|
except socket.error as e:
|
||||||
|
print(f"Client {self.client_id} socket error: {e}")
|
||||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
self.state_changed.emit("CALL_END", "", self.client_id)
|
||||||
break
|
break
|
||||||
except Exception as e:
|
|
||||||
print(f"Client {self.client_id} handshake failed: {e}")
|
|
||||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
self.handshake_in_progress = False
|
|
||||||
self.handshake_start_time = None
|
|
||||||
else:
|
else:
|
||||||
# Check for handshake timeout
|
self.msleep(20)
|
||||||
if self.handshake_in_progress and self.handshake_start_time:
|
print(f"Client {self.client_id} yielding during handshake")
|
||||||
if time.time() - self.handshake_start_time > 30: # 30s handshake timeout
|
self.msleep(1)
|
||||||
print(f"Client {self.client_id} handshake timeout after 30s")
|
|
||||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
|
||||||
self.handshake_in_progress = False
|
|
||||||
self.handshake_start_time = None
|
|
||||||
break
|
|
||||||
# Only read socket if not in handshake
|
|
||||||
if not self.handshake_in_progress:
|
|
||||||
# Use select to check if data is available
|
|
||||||
readable, _, _ = select.select([self.sock], [], [], 0.01) # 10ms timeout
|
|
||||||
if readable:
|
|
||||||
try:
|
|
||||||
print(f"Client {self.client_id} attempting sock.recv")
|
|
||||||
data = self.sock.recv(1024)
|
|
||||||
if not data:
|
|
||||||
print(f"Client {self.client_id} disconnected")
|
|
||||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
|
||||||
break
|
|
||||||
# Handle control messages (UTF-8)
|
|
||||||
try:
|
|
||||||
decoded_data = data.decode('utf-8').strip()
|
|
||||||
print(f"Client {self.client_id} received raw: {decoded_data}")
|
|
||||||
if decoded_data in ["RINGING", "CALL_END", "CALL_DROPPED", "IN_CALL", "HANDSHAKE", "HANDSHAKE_DONE"]:
|
|
||||||
self.state_changed.emit(decoded_data, decoded_data, self.client_id)
|
|
||||||
if decoded_data == "HANDSHAKE":
|
|
||||||
self.handshake_in_progress = True # Block further reads
|
|
||||||
elif decoded_data == "HANDSHAKE_DONE":
|
|
||||||
self.call_active = True # Enable audio processing
|
|
||||||
else:
|
|
||||||
print(f"Client {self.client_id} ignored unexpected text message: {decoded_data}")
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
# Handle binary data (audio packets)
|
|
||||||
if self.call_active and self.session:
|
|
||||||
try:
|
|
||||||
print(f"Client {self.client_id} received audio packet, length={len(data)}")
|
|
||||||
decrypted_data = self.session.decrypt(data)
|
|
||||||
print(f"Client {self.client_id} decrypted audio packet, length={len(decrypted_data)}")
|
|
||||||
self.data_received.emit(decrypted_data, self.client_id)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Client {self.client_id} failed to process audio packet: {e}")
|
|
||||||
else:
|
|
||||||
print(f"Client {self.client_id} ignored non-text message: {data.hex()}")
|
|
||||||
except socket.timeout:
|
|
||||||
print(f"Client {self.client_id} timed out waiting for data")
|
|
||||||
continue
|
|
||||||
except socket.error as e:
|
|
||||||
print(f"Client {self.client_id} socket error: {e}")
|
|
||||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Client {self.client_id} error: {e}")
|
|
||||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# print(f"Client {self.client_id} no data available, skipping recv")
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# Yield during handshake
|
|
||||||
self.msleep(20) # 20ms sleep to yield CPU
|
|
||||||
print(f"Client {self.client_id} yielding during handshake")
|
|
||||||
# Short sleep to yield Qt event loop
|
|
||||||
self.msleep(1) # 1ms sleep
|
|
||||||
finally:
|
finally:
|
||||||
if self.sock:
|
if self.sock:
|
||||||
self.sock.close()
|
self.sock.close()
|
||||||
@ -153,7 +78,6 @@ class PhoneClient(QThread):
|
|||||||
self.sock.send(data)
|
self.sock.send(data)
|
||||||
print(f"Client {self.client_id} sent: {message}, length={len(data)}")
|
print(f"Client {self.client_id} sent: {message}, length={len(data)}")
|
||||||
else:
|
else:
|
||||||
# Send binary data (audio)
|
|
||||||
self.sock.send(message)
|
self.sock.send(message)
|
||||||
print(f"Client {self.client_id} sent binary data, length={len(message)}")
|
print(f"Client {self.client_id} sent binary data, length={len(message)}")
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
@ -165,13 +89,6 @@ class PhoneClient(QThread):
|
|||||||
if self.sock:
|
if self.sock:
|
||||||
self.sock.close()
|
self.sock.close()
|
||||||
self.sock = None
|
self.sock = None
|
||||||
|
|
||||||
def start_handshake(self, initiator, keypair, peer_pubkey):
|
def start_handshake(self, initiator, keypair, peer_pubkey):
|
||||||
"""Queue the handshake command with necessary parameters."""
|
self.state.start_handshake(initiator, keypair, peer_pubkey)
|
||||||
self.initiator = initiator
|
|
||||||
self.keypair = keypair
|
|
||||||
self.peer_pubkey = peer_pubkey
|
|
||||||
print(f"Client {self.client_id} queuing handshake, initiator: {initiator}")
|
|
||||||
self.handshake_in_progress = True # Block recv before handshake starts
|
|
||||||
self.handshake_start_time = time.time()
|
|
||||||
self.command_queue.put("handshake")
|
|
89
protocol_prototype/DryBox/UI/phone_manager.py
Normal file
89
protocol_prototype/DryBox/UI/phone_manager.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# phone_manager.py
|
||||||
|
import secrets
|
||||||
|
from PyQt5.QtCore import QTimer
|
||||||
|
from phone_client import PhoneClient
|
||||||
|
from session import NoiseXKSession
|
||||||
|
from phone_state import PhoneState
|
||||||
|
|
||||||
|
class PhoneManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.phones = []
|
||||||
|
self.handshake_done_count = 0
|
||||||
|
|
||||||
|
def initialize_phones(self):
|
||||||
|
"""Initialize phone clients and their keypairs."""
|
||||||
|
for i in range(2):
|
||||||
|
client = PhoneClient(i)
|
||||||
|
keypair = NoiseXKSession.generate_keypair()
|
||||||
|
phone = {
|
||||||
|
'id': i,
|
||||||
|
'client': client,
|
||||||
|
'state': PhoneState.IDLE,
|
||||||
|
'number': "123-4567" if i == 0 else "987-6543",
|
||||||
|
'audio_timer': None,
|
||||||
|
'keypair': keypair,
|
||||||
|
'public_key': keypair.public,
|
||||||
|
'is_initiator': False
|
||||||
|
}
|
||||||
|
self.phones.append(phone)
|
||||||
|
|
||||||
|
# Share public keys
|
||||||
|
self.phones[0]['peer_public_key'] = self.phones[1]['public_key']
|
||||||
|
self.phones[1]['peer_public_key'] = self.phones[0]['public_key']
|
||||||
|
|
||||||
|
def phone_action(self, phone_id, ui_manager):
|
||||||
|
"""Handle phone actions (call, answer, disconnect)."""
|
||||||
|
phone = self.phones[phone_id]
|
||||||
|
other_phone = self.phones[1 - phone_id]
|
||||||
|
print(f"Phone {phone_id + 1} Action, current state: {phone['state']}, is_initiator: {phone['is_initiator']}")
|
||||||
|
|
||||||
|
if phone['state'] == PhoneState.IDLE:
|
||||||
|
phone['state'] = PhoneState.CALLING
|
||||||
|
other_phone['state'] = PhoneState.RINGING
|
||||||
|
phone['is_initiator'] = True
|
||||||
|
other_phone['is_initiator'] = False
|
||||||
|
phone['client'].send("RINGING")
|
||||||
|
elif phone['state'] == PhoneState.RINGING:
|
||||||
|
phone['state'] = other_phone['state'] = PhoneState.IN_CALL
|
||||||
|
phone['client'].send("IN_CALL")
|
||||||
|
elif phone['state'] in [PhoneState.IN_CALL, PhoneState.CALLING]:
|
||||||
|
if not phone['client'].handshake_in_progress and phone['state'] != PhoneState.CALLING:
|
||||||
|
phone['state'] = other_phone['state'] = PhoneState.IDLE
|
||||||
|
phone['client'].send("CALL_END")
|
||||||
|
for p in [phone, other_phone]:
|
||||||
|
if p['audio_timer']:
|
||||||
|
p['audio_timer'].stop()
|
||||||
|
else:
|
||||||
|
print(f"Phone {phone_id + 1} cannot hang up during handshake or call setup")
|
||||||
|
|
||||||
|
# Update UI
|
||||||
|
ui_manager.update_phone_ui(phone_id)
|
||||||
|
ui_manager.update_phone_ui(1 - phone_id)
|
||||||
|
|
||||||
|
def send_audio(self, phone_id):
|
||||||
|
"""Send mock audio data."""
|
||||||
|
phone = self.phones[phone_id]
|
||||||
|
if phone['state'] == PhoneState.IN_CALL and phone['client'].session and phone['client'].sock:
|
||||||
|
mock_audio = secrets.token_bytes(16)
|
||||||
|
try:
|
||||||
|
phone['client'].session.send(phone['client'].sock, mock_audio)
|
||||||
|
print(f"Client {phone_id} sent encrypted audio packet, length=32")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Client {phone_id} failed to send audio: {e}")
|
||||||
|
|
||||||
|
def start_audio(self, client_id):
|
||||||
|
"""Start audio timer after HANDSHAKE_DONE."""
|
||||||
|
self.handshake_done_count += 1
|
||||||
|
print(f"HANDSHAKE_DONE received for client {client_id}, count: {self.handshake_done_count}")
|
||||||
|
if self.handshake_done_count == 2:
|
||||||
|
for phone in self.phones:
|
||||||
|
if phone['state'] == PhoneState.IN_CALL:
|
||||||
|
if not phone['audio_timer'] or not phone['audio_timer'].isActive():
|
||||||
|
phone['audio_timer'] = QTimer()
|
||||||
|
phone['audio_timer'].timeout.connect(lambda pid=phone['id']: self.send_audio(pid))
|
||||||
|
phone['audio_timer'].start(100)
|
||||||
|
self.handshake_done_count = 0
|
||||||
|
|
||||||
|
def update_waveform(self, client_id, data):
|
||||||
|
"""Update waveform with received audio data."""
|
||||||
|
print(f"Updating waveform for client_id {client_id}, data_length={len(data)}")
|
13
protocol_prototype/DryBox/devnote.txt
Normal file
13
protocol_prototype/DryBox/devnote.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
simulator/
|
||||||
|
├── gsm_simulator.py # gsm_simulator
|
||||||
|
├── launch_gsm_simulator.sh # use to start docker and simulator, run in terminal
|
||||||
|
|
||||||
|
2 clients nect to gsm_simulator and simulate a call using noise protocol
|
||||||
|
UI/
|
||||||
|
├── main.py # UI setup and event handling
|
||||||
|
├── phone_manager.py # Phone state, client init, audio logic
|
||||||
|
├── phone_client.py # Socket communication and threading
|
||||||
|
├── client_state.py # Client state and command processing
|
||||||
|
├── session.py # Noise XK crypto session
|
||||||
|
├── waveform_widget.py # Waveform UI component
|
||||||
|
├── phone_state.py # State constants
|
Loading…
Reference in New Issue
Block a user