import os import socket import select CHATPORT = 8181 BUFFER_SIZE = 1024 MAX_CONNECT_QUEUE = 10 def format_file_info(file_path): """Helper function to format file information.""" file_size = os.path.getsize(file_path) if file_size < 1000: size_str = f"{file_size} B" elif file_size < 1000000: size_str = f"{file_size / 1000:.2f} KB" else: size_str = f"{file_size / 1000000:.2f} MB" file_type = "Directory" if os.path.isdir(file_path) else "File" return f"{os.path.basename(file_path)} - {file_type} - {size_str}" def main(): # set the flag variable to true for infinite loop quitFlag = True # create a socket object server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # set the socket options, SO_REUSEADDR allows to reuse the same port # SO_KEEPALIVE enables the socket to send keepalive messages server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # bind the socket to the address and port server_socket.bind(('', CHATPORT)) # listen for incoming connections server_socket.listen(MAX_CONNECT_QUEUE) # set the server socket to non-blocking server_socket.setblocking(False) # create a list to store the sockets, server and client socket_list = [server_socket] # since a socket is associated with a username and a chatroom, # and each one can only be associated one way at once, use dictionaries to # associate with each other. usernames = {} chat_rooms = {} # keep track of the states of each client connected, and if they are # technically in an 'unsafe' state like in change username or chatroom mode. client_states = {} # Add a dictionary to store user-specific paths user_paths = {} # let the user know the server is listening on port CHATPORT. print(f"Chatserver listening on port: {CHATPORT}") # infinite loop for the program while quitFlag: # use the select function to monitor the sockets read_sockets, _, _ = select.select(socket_list, [], []) # loop through the sockets for sock in read_sockets: # if the socket is the server socket, accept the connection if sock == server_socket: client_socket, client_address = server_socket.accept() client_socket.setblocking(False) socket_list.append(client_socket) print("\nAccepted connection from new client.") client_socket.send(b"\nWelcome to the chat server!\n") client_socket.send(b"\nUse '%!' to exit, or '%?' for help.\n") client_socket.send(b"\nType in a username: ") client_states[client_socket] = "WAITING_FOR_USERNAME" user_paths[client_socket] = None # Initialize path for new client # otherwise the socket is a client socket. else: # try to receive the message from the client try: message = sock.recv(BUFFER_SIZE).decode('utf-8') if message: state = client_states.get(sock, "NORMAL") if state == "WAITING_FOR_USERNAME": usernames[sock] = message.strip() chat_rooms[sock] = "default" sock.send(b"\nUsername accepted. You are now in the 'default' chatroom.") client_states[sock] = "NORMAL" user_paths[sock] = None # Initialize path for the user elif state == "WAITING_FOR_NEW_USERNAME": usernames[sock] = message.strip() sock.send(b"\nUsername changed successfully.") print(f"\nClient switched username to {usernames[sock]}.") client_states[sock] = "NORMAL" elif state == "WAITING_FOR_NEW_CHATROOM": old_chatroom = chat_rooms[sock] chat_rooms[sock] = message.strip() sock.send(f"\nSwitched to chatroom '{chat_rooms[sock]}.'".encode('utf-8')) print(f"\nClient switched to chatroom '{chat_rooms[sock]}.'") if old_chatroom != "default" and old_chatroom not in chat_rooms.values(): print(f"\nChatroom '{old_chatroom}' is empty and removed.") client_states[sock] = "NORMAL" elif state == "NORMAL": if message.startswith("@") and "-ls" in message: # Parse the message format: @RECEIVING_USERNAME@SENDING_USERNAME -ls parts = message.split() if len(parts) >= 2 and "-ls" in parts[1]: usernames_part = parts[0] if usernames_part.count("@") == 2: receiving_username, sending_username = usernames_part[1:].split("@") # Find the receiving user's socket receiving_sock = next( (s for s, u in usernames.items() if u == receiving_username), None ) sending_sock = next( (s for s, u in usernames.items() if u == sending_username), None ) if receiving_sock and sending_sock: user_path = user_paths.get(receiving_sock) if user_path and os.path.isdir(user_path): # List files in the receiving user's directory files = os.listdir(user_path) if files: file_list = "\n".join( [format_file_info(os.path.join(user_path, f)) for f in files] ) sending_sock.send( f"Files in {receiving_username}'s directory:\n{file_list}".encode('utf-8') ) else: sending_sock.send( f"No files in {receiving_username}'s directory.".encode('utf-8') ) else: sending_sock.send( f"Error: {receiving_username} has no valid directory set.".encode('utf-8') ) else: sock.send(b"Error: One or both users not found.") else: sock.send(b"Error: Invalid message format.") elif message.startswith("@") and "-f" in message: # Parse the message format: @RECEIVING_USERNAME@SENDING_USERNAME -f FILENAME parts = message.split() if len(parts) >= 3 and "-f" in parts[1]: usernames_part = parts[0] if usernames_part.count("@") == 2: receiving_username, sending_username = usernames_part[1:].split("@") # Find the receiving user's socket receiving_sock = next( (s for s, u in usernames.items() if u == receiving_username), None ) sending_sock = next( (s for s, u in usernames.items() if u == sending_username), None ) if receiving_sock and sending_sock: if len(parts) == 3: # File request: @RECEIVING_USERNAME@SENDING_USERNAME -f FILENAME filename = parts[2] receiving_sock.send( f"FILE_REQUEST {sending_username} {filename}".encode('utf-8') ) elif len(parts) > 5 and parts[-1] == "ENDFILE": # File transfer: @RECEIVING_USERNAME@SENDING_USERNAME -f FILENAME FILETYPE FILELENGTH FILEBYTES ENDFILE filename = parts[2] filetype = parts[3] filelength = int(parts[4]) filebytes = " ".join(parts[5:-1]).encode('utf-8') receiving_sock.send( f"FILE_TRANSFER {sending_username} {filename} {filetype} {filelength}".encode('utf-8') ) receiving_sock.send(filebytes) receiving_sock.send(b"ENDFILE") else: sock.send(b"Error: One or both users not found.") else: sock.send(b"Error: Invalid message format.") elif message.strip() == "%!": sock.close() socket_list.remove(sock) del usernames[sock] del chat_rooms[sock] del client_states[sock] print("\nClient has closed connection.") elif message.strip() == "%?": print("\nClient requested help.") sock.send(b"Help: (%?)\n") sock.send(b"Send messages like normal. Use enter to send a message.\n") sock.send(b"Available Commands:\n") sock.send(b"\t%? : Show this help prompt.\n") sock.send(b"\t%! : Exit from the Chat Server.\n") sock.send(b"\t%* : Switch to another chatroom.\n") sock.send(b"\t%$ : Change Username.\n") elif message.strip() == "%*": sock.send(b"\nEnter chatroom name: ") client_states[sock] = "WAITING_FOR_NEW_CHATROOM" elif message.strip() == "%$": sock.send(b"\nEnter new username: ") client_states[sock] = "WAITING_FOR_NEW_USERNAME" else: username = usernames[sock] chat_room = chat_rooms[sock] send_message = f"{username} >> {message}".encode('utf-8') for client_sock in socket_list: if client_sock != server_socket and chat_rooms.get(client_sock) == chat_room: client_sock.send(send_message) else: sock.close() socket_list.remove(sock) if sock in usernames: del usernames[sock] if sock in chat_rooms: old_chatroom = chat_rooms[sock] del chat_rooms[sock] if old_chatroom != "default" and old_chatroom not in chat_rooms.values(): print(f"\nChatroom '{old_chatroom}' is empty and has been removed.") if sock in client_states: del client_states[sock] print("\nClient has closed connection.") except Exception as err: print(f"Error: {err}") sock.close() socket_list.remove(sock) if sock in usernames: del usernames[sock] if sock in chat_rooms: old_chatroom = chat_rooms[sock] del chat_rooms[sock] if old_chatroom != "default" and old_chatroom not in chat_rooms.values(): print(f"\nChatroom '{old_chatroom}' is empty and has been removed.") if sock in client_states: del client_states[sock] print("\nClient has closed connection.") server_socket.close() if __name__ == "__main__": main()