diff --git a/client.py b/client.py index 14a754c..302e63d 100644 --- a/client.py +++ b/client.py @@ -8,12 +8,11 @@ import argparse class VoiceClient: - # Audio parameters CHUNK = 1024 FORMAT = pyaudio.paInt16 CHANNELS = 1 RATE = 44100 - + def __init__(self, host, port): self.host = host self.port = port @@ -21,25 +20,19 @@ def __init__(self, host, port): self.name = '' self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.audio = pyaudio.PyAudio() - - # Set up audio streams + self.call_active = False + self.input_stream = self.audio.open( - format=self.FORMAT, - channels=self.CHANNELS, - rate=self.RATE, - input=True, + format=self.FORMAT, channels=self.CHANNELS, + rate=self.RATE, input=True, frames_per_buffer=self.CHUNK ) - self.output_stream = self.audio.open( - format=self.FORMAT, - channels=self.CHANNELS, - rate=self.RATE, - output=True, + format=self.FORMAT, channels=self.CHANNELS, + rate=self.RATE, output=True, frames_per_buffer=self.CHUNK ) - - # Connect to server + print(colored(f"Connecting to voice server at {host}:{port}...", "yellow")) try: self.sock.connect((host, port)) @@ -48,40 +41,30 @@ def __init__(self, host, port): self.cleanup() sys.exit(1) print(colored("Connected to voice server.", "green")) - + def start(self): - """Start the voice client""" - # Set up signal handler signal.signal(signal.SIGINT, self.handle_signal) - - # Get user's name while not self.name: self.name = input(colored("Enter your name: ", "blue")).strip() - - # Send name to server self.sock.send(f"NAME:{self.name}".encode()) - - # Start audio threads - receive_thread = threading.Thread(target=self.receive_audio) - send_thread = threading.Thread(target=self.send_audio) - - receive_thread.daemon = True - send_thread.daemon = True - + + receive_thread = threading.Thread(target=self.receive_audio, daemon=True) + send_thread = threading.Thread(target=self.send_audio, daemon=True) + text_thread = threading.Thread(target=self.user_input_loop, daemon=True) + receive_thread.start() send_thread.start() - + text_thread.start() + print(colored("Voice chat started! Press Ctrl+C to exit.", "yellow")) - - # Keep main thread alive (cross-platform) + try: while self.running: - time.sleep(0.1) # Sleep briefly to prevent high CPU usage + time.sleep(0.1) except KeyboardInterrupt: self.handle_signal(None, None) def send_audio(self): - """Capture and send audio to server""" while self.running: try: data = self.input_stream.read(self.CHUNK, exception_on_overflow=False) @@ -91,64 +74,92 @@ def send_audio(self): if self.running: print(colored(f"Error sending audio: {e}", "red")) break - + def receive_audio(self): - """Receive and play audio from server""" while self.running: try: data = self.sock.recv(self.CHUNK * 2) if not data: break - - # Check for control messages + + # Handle control / text messages if data.startswith(b"CONTROL:"): message = data[8:].decode() - print(colored(message, "cyan")) + + if message.startswith("INCOMING_CALL:"): + caller = message[len("INCOMING_CALL:"):] + print(colored(f"[!] Incoming call from {caller}. Type /accept or /reject", "cyan")) + + elif message == "CALL_ACCEPTED": + self.call_active = True + print(colored("[✓] Call accepted!", "green")) + + elif "CALL_ENDED" in message or "CALL_REJECTED" in message: + self.call_active = False + print(colored("[!] Call ended or rejected.", "red")) + + else: + print(colored(message, "cyan")) continue - - # Check for server full message - if data == b"SERVER_FULL": - print(colored("Server is full. Try again later.", "red")) - self.running = False - break - - # Play audio data - self.output_stream.write(data) - + + # If actual audio + if self.call_active or True: + self.output_stream.write(data) + except Exception as e: if self.running: print(colored(f"Error receiving audio: {e}", "red")) break - + + def user_input_loop(self): + """Handle slash commands from user (text)""" + while self.running: + cmd = input().strip() + if cmd: + if cmd == "/quit": + self.handle_signal(None, None) + else: + self.sock.send(cmd.encode()) + def handle_signal(self, signum, frame): - """Handle Ctrl+C""" print(colored("\nExiting voice chat...", "yellow")) self.running = False self.cleanup() sys.exit(0) - + def cleanup(self): - """Cleanup resources""" self.running = False if hasattr(self, 'input_stream'): - self.input_stream.stop_stream() - self.input_stream.close() + try: + self.input_stream.stop_stream() + self.input_stream.close() + except: + pass if hasattr(self, 'output_stream'): - self.output_stream.stop_stream() - self.output_stream.close() + try: + self.output_stream.stop_stream() + self.output_stream.close() + except: + pass if hasattr(self, 'audio'): - self.audio.terminate() + try: + self.audio.terminate() + except: + pass if hasattr(self, 'sock'): - self.sock.close() - + try: + self.sock.close() + except: + pass + def main(): parser = argparse.ArgumentParser(description='Voice Chat Client') parser.add_argument('host', help='server address') parser.add_argument('port', type=int, help='server port') args = parser.parse_args() - + client = VoiceClient(args.host, args.port) client.start() - + if __name__ == "__main__": main() \ No newline at end of file diff --git a/identity.py b/identity.py new file mode 100644 index 0000000..a12c74d --- /dev/null +++ b/identity.py @@ -0,0 +1,28 @@ +# identity.py + +import hashlib +from ecdsa import SigningKey, Ed25519 +import os + +KEY_PATH = "device_key.pem" + +def get_device_id_and_key(): + # Load or generate a new private key + if os.path.exists(KEY_PATH): + with open(KEY_PATH, 'rb') as f: + sk = SigningKey.from_pem(f.read()) + else: + sk = SigningKey.generate(curve=Ed25519) + with open(KEY_PATH, 'wb') as f: + f.write(sk.to_pem(format="pkcs8")) + + vk = sk.verifying_key + public_key_bytes = vk.to_string() + + # Create a device ID by hashing the public key + device_id = hashlib.sha256(public_key_bytes).hexdigest()[:20] + + # Export public key as PEM + public_key_pem = vk.to_pem().decode() + + return device_id, public_key_pem diff --git a/register_client.py b/register_client.py new file mode 100644 index 0000000..563693f --- /dev/null +++ b/register_client.py @@ -0,0 +1,31 @@ +# register_client.py + +import json +import socket +from identity import get_device_id_and_key + +def register(server_ip='127.0.0.1', port=9000, local_ip='127.0.0.1', voice_port=8081): + device_id, public_key = get_device_id_and_key() + + request = { + 'device_id': device_id, + 'public_key': public_key, + 'ip': local_ip, + 'port': voice_port + } + + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((server_ip, port)) + s.send(json.dumps(request).encode()) + response = s.recv(1024).decode() + s.close() + + print("Registry Server Response:") + print(response) + + except Exception as e: + print(f"Failed to connect to registry server: {e}") + +if __name__ == "__main__": + register() diff --git a/registry_server.py b/registry_server.py new file mode 100644 index 0000000..162a3fb --- /dev/null +++ b/registry_server.py @@ -0,0 +1,74 @@ +# registry_server.py + +import socket +import threading +import json +import os + +REGISTRY_FILE = 'db.json' +HOST = '0.0.0.0' +PORT = 9000 + +# Load or initialize the registry +if os.path.exists(REGISTRY_FILE): + with open(REGISTRY_FILE, 'r') as f: + registry = json.load(f) +else: + registry = {} + +def save_registry(): + with open(REGISTRY_FILE, 'w') as f: + json.dump(registry, f, indent=2) + +def handle_client(conn, addr): + try: + data = conn.recv(4096).decode() + request = json.loads(data) + + device_id = request['device_id'] + public_key = request['public_key'] + ip = request['ip'] + port = request['port'] + + # Check if device is already registered + existing = next((num for num, v in registry.items() if v['device_id'] == device_id), None) + + if existing: + number = existing + else: + # Assign next available 3-digit number + for i in range(100, 1000): + number = str(i) + if number not in registry: + registry[number] = { + 'device_id': device_id, + 'public_key': public_key, + 'ip': ip, + 'port': port + } + save_registry() + break + + response = { + 'status': 'ok', + 'number': number + } + conn.send(json.dumps(response).encode()) + + except Exception as e: + conn.send(json.dumps({'status': 'error', 'error': str(e)}).encode()) + finally: + conn.close() + +def main(): + print(f"Starting Registry Server on port {PORT}...") + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.bind((HOST, PORT)) + server.listen(5) + + while True: + conn, addr = server.accept() + threading.Thread(target=handle_client, args=(conn, addr)).start() + +if __name__ == "__main__": + main() diff --git a/server.py b/server.py index d391147..f87f5b4 100644 --- a/server.py +++ b/server.py @@ -1,137 +1,194 @@ import socket import threading -from typing import List, Dict +from typing import Dict import logging import signal import sys -# Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s') logger = logging.getLogger(__name__) MAX_CLIENTS = 10 -CHUNK_SIZE = 4096 # Larger chunk size for audio data +CHUNK_SIZE = 4096 class VoiceServer: def __init__(self, host='0.0.0.0', port=8081): self.host = host self.port = port - self.clients: Dict[socket.socket, str] = {} # Socket to client name mapping + self.clients: Dict[socket.socket, str] = {} self.clients_lock = threading.Lock() self.running = True - - # Initialize server socket + self.active_calls: Dict[socket.socket, socket.socket] = {} # socket -> partner or pending target + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - + def start(self): - """Start the voice server""" try: self.server_socket.bind((self.host, self.port)) self.server_socket.listen(MAX_CLIENTS) logger.info(f"Voice Server listening on port {self.port} (max {MAX_CLIENTS} clients)...") - - # Handle Ctrl+C gracefully signal.signal(signal.SIGINT, self.handle_shutdown) - + while self.running: try: client_socket, client_address = self.server_socket.accept() + if len(self.clients) >= MAX_CLIENTS: - logger.info(f"Max clients reached. Rejecting {client_address}") client_socket.send(b"SERVER_FULL") client_socket.close() continue - - # Start new client thread + client_thread = threading.Thread( target=self.handle_client, args=(client_socket, client_address) ) client_thread.daemon = True client_thread.start() - + except Exception as e: logger.error(f"Error accepting connection: {e}") - + except Exception as e: logger.error(f"Server error: {e}") finally: self.cleanup() - - def handle_client(self, client_socket: socket.socket, address): - """Handle individual client connection""" + + def handle_client(self, client_socket, address): try: - # First message should be the client's name name_data = client_socket.recv(1024).decode() if name_data.startswith("NAME:"): client_name = name_data[5:] with self.clients_lock: self.clients[client_socket] = client_name logger.info(f"New client connected: {client_name} from {address}") - - # Broadcast join notification self.broadcast_control_message(f"SERVER: {client_name} joined", client_socket) - + while self.running: - # Receive audio data - audio_data = client_socket.recv(CHUNK_SIZE) - if not audio_data: + data = client_socket.recv(CHUNK_SIZE) + if not data: break - - # Broadcast to other clients - self.broadcast_audio(audio_data, client_socket) - + + # Handle slash commands text + try: + text = data.decode().strip() + if text.startswith("/"): + self.handle_command(text, client_socket) + continue + except: + pass + + self.broadcast_audio(data, client_socket) + except Exception as e: logger.error(f"Error handling client {address}: {e}") finally: self.remove_client(client_socket) - - def broadcast_audio(self, audio_data: bytes, sender_socket: socket.socket): - """Broadcast audio data to all other clients""" + + def handle_command(self, cmd, client_socket): + user = self.clients.get(client_socket) + + if cmd.startswith("/call "): + target_name = cmd.split(" ", 1)[1].strip() + with self.clients_lock: + for sock, name in self.clients.items(): + if name == target_name: + sock.send(f"CONTROL:INCOMING_CALL:{user}".encode()) + client_socket.send(b"CONTROL:CALLING...") + # mark as pending: caller_socket -> target_socket + self.active_calls[client_socket] = sock + return + client_socket.send(b"CONTROL:User not found") + + elif cmd == "/accept": + # find a pending call where this socket is target + for caller_socket, target_socket in list(self.active_calls.items()): + if target_socket == client_socket: + # establish call both directions + self.active_calls[caller_socket] = client_socket + self.active_calls[client_socket] = caller_socket + caller_socket.send(b"CONTROL:CALL_ACCEPTED") + client_socket.send(b"CONTROL:CALL_ACCEPTED") + return + client_socket.send(b"CONTROL:No call to accept") + + elif cmd == "/reject": + # find pending caller + for caller_socket, target_socket in list(self.active_calls.items()): + if target_socket == client_socket: + self.active_calls.pop(caller_socket, None) + caller_socket.send(b"CONTROL:CALL_REJECTED") + client_socket.send(b"CONTROL:CALL_REJECTED") + return + client_socket.send(b"CONTROL:No call to reject") + + elif cmd == "/end": + if client_socket in self.active_calls: + partner = self.active_calls.pop(client_socket) + # Remove reverse link if exists + if partner in self.active_calls: + self.active_calls.pop(partner, None) + client_socket.send(b"CONTROL:CALL_ENDED") + partner.send(b"CONTROL:CALL_ENDED") + else: + client_socket.send(b"CONTROL:No active call") + + def broadcast_audio(self, audio_data, sender_socket): + # Private call routing + if sender_socket in self.active_calls: + partner = self.active_calls[sender_socket] + try: + partner.send(audio_data) + except: + pass + return + + # Group lobby with self.clients_lock: - for client_socket in self.clients: - if client_socket != sender_socket: + for sock in self.clients: + if sock != sender_socket: try: - client_socket.send(audio_data) + sock.send(audio_data) except: continue - + def broadcast_control_message(self, message: str, exclude_socket=None): - """Broadcast control messages to clients""" with self.clients_lock: - for client_socket in self.clients: - if client_socket != exclude_socket: + for sock in self.clients: + if sock != exclude_socket: try: - client_socket.send(f"CONTROL:{message}".encode()) + sock.send(f"CONTROL:{message}".encode()) except: continue - - def remove_client(self, client_socket: socket.socket): - """Remove a client and cleanup""" + + def remove_client(self, client_socket): with self.clients_lock: if client_socket in self.clients: client_name = self.clients[client_socket] del self.clients[client_socket] logger.info(f"Client disconnected: {client_name}") self.broadcast_control_message(f"SERVER: {client_name} left") + # End any active call involving this client + if client_socket in self.active_calls: + partner = self.active_calls.pop(client_socket) + if partner in self.active_calls: + self.active_calls.pop(partner, None) + partner.send(b"CONTROL:CALL_ENDED") client_socket.close() - + def handle_shutdown(self, signum, frame): - """Handle server shutdown""" logger.info("Shutting down server...") self.running = False self.cleanup() sys.exit(0) - + def cleanup(self): - """Cleanup server resources""" with self.clients_lock: for client_socket in self.clients: client_socket.close() self.clients.clear() self.server_socket.close() - + if __name__ == "__main__": server = VoiceServer() server.start() \ No newline at end of file diff --git a/tcp_server.py b/tcp_server.py deleted file mode 100644 index aaeece9..0000000 --- a/tcp_server.py +++ /dev/null @@ -1,126 +0,0 @@ -import socket -import threading -from typing import List -import socket -from threading import Lock -import dns.resolver -import dns.reversename - -MAX_CLIENTS = 10 - -class ClientHandler: - def __init__(self): - self.clients: List[socket.socket] = [] - self.clients_lock = Lock() - - def handle_client(self, client_socket: socket.socket): - """Handle individual client connections.""" - try: - while True: - # Receive data from client - data = client_socket.recv(1024) - if not data: - print("Client disconnected.") - break - - # Broadcast to all other clients - with self.clients_lock: - # Remove dead sockets - self.remove_dead_clients() - - # Send message to all other clients - for other_socket in self.clients: - if other_socket != client_socket: - try: - other_socket.send(data) - except: - continue - except Exception as e: - print(f"Error handling client: {e}") - finally: - # Remove client from list - with self.clients_lock: - if client_socket in self.clients: - self.clients.remove(client_socket) - client_socket.close() - - def remove_dead_clients(self): - """Remove disconnected clients from the list.""" - self.clients = [client for client in self.clients if self._is_socket_connected(client)] - - def _is_socket_connected(self, sock: socket.socket) -> bool: - """Check if a socket is still connected.""" - try: - sock.getpeername() - return True - except: - return False - - def add_client(self, client_socket: socket.socket) -> bool: - """Add a new client to the list if space available.""" - with self.clients_lock: - if len(self.clients) >= MAX_CLIENTS: - return False - self.clients.append(client_socket) - return True - -def lookup_addr(ip_addr: str) -> str: - """Perform reverse DNS lookup.""" - try: - addr = dns.reversename.from_address(ip_addr) - return str(dns.resolver.resolve(addr, "PTR")[0]) - except Exception: - return None - -def main(): - # Create server socket - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - # Bind and listen - server_address = ('0.0.0.0', 8080) - server_socket.bind(server_address) - server_socket.listen(10) - - print(f"Server listening on port 8080 (max {MAX_CLIENTS} clients)...") - - client_handler = ClientHandler() - - try: - while True: - try: - client_socket, client_address = server_socket.accept() - ip_addr = client_address[0] - - # Try to get hostname - hostname = lookup_addr(ip_addr) - if hostname: - print(f"New connection from {client_address} ({hostname})") - else: - print(f"New connection from {client_address}") - - # Check if we can add more clients - if not client_handler.add_client(client_socket): - print(f"Max clients reached. Rejecting {client_address}") - client_socket.send(b"Server full.\n") - client_socket.close() - continue - - # Start new thread for client - client_thread = threading.Thread( - target=client_handler.handle_client, - args=(client_socket,) - ) - client_thread.daemon = True - client_thread.start() - - except Exception as e: - print(f"Connection failed: {e}") - - except KeyboardInterrupt: - print("\nShutting down server...") - finally: - server_socket.close() - -if __name__ == "__main__": - main() \ No newline at end of file