From 9c8be518179a88b7dfd1839e0aa10e8e25a578bb Mon Sep 17 00:00:00 2001 From: Ziheng Chen <zc328@dragons.drexel.edu> Date: Mon, 3 Mar 2025 04:02:33 +0000 Subject: [PATCH] Upload New File --- w8/rsh_server.c | 724 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 724 insertions(+) create mode 100644 w8/rsh_server.c diff --git a/w8/rsh_server.c b/w8/rsh_server.c new file mode 100644 index 0000000..c59bef0 --- /dev/null +++ b/w8/rsh_server.c @@ -0,0 +1,724 @@ + +#include <sys/socket.h> +#include <sys/wait.h> +#include <arpa/inet.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/un.h> +#include <fcntl.h> +#include <signal.h> +#include <pthread.h> + +//INCLUDES for extra credit +//#include <signal.h> +//#include <pthread.h> +//------------------------- + +#include "dshlib.h" +#include "rshlib.h" + +typedef struct { + int cli_socket; +} client_thread_args_t; + +// Function to handle client in a separate thread +void *client_handler(void *args) { + client_thread_args_t *client_args = (client_thread_args_t *)args; + exec_client_requests(client_args->cli_socket); + close(client_args->cli_socket); + free(client_args); + pthread_exit(NULL); +} + +/* + * start_server(ifaces, port, is_threaded) + * ifaces: a string in ip address format, indicating the interface + * where the server will bind. In almost all cases it will + * be the default "0.0.0.0" which binds to all interfaces. + * note the constant RDSH_DEF_SVR_INTFACE in rshlib.h + * + * port: The port the server will use. Note the constant + * RDSH_DEF_PORT which is 1234 in rshlib.h. If you are using + * tux you may need to change this to your own default, or even + * better use the command line override -s implemented in dsh_cli.c + * For example ./dsh -s 0.0.0.0:5678 where 5678 is the new port + * + * is_threded: Used for extra credit to indicate the server should implement + * per thread connections for clients + * + * This function basically runs the server by: + * 1. Booting up the server + * 2. Processing client requests until the client requests the + * server to stop by running the `stop-server` command + * 3. Stopping the server. + * + * This function is fully implemented for you and should not require + * any changes for basic functionality. + * + * IF YOU IMPLEMENT THE MULTI-THREADED SERVER FOR EXTRA CREDIT YOU NEED + * TO DO SOMETHING WITH THE is_threaded ARGUMENT HOWEVER. + */ +int start_server(char *ifaces, int port, int is_threaded){ + int svr_socket; + int rc; + //(void)is_threaded; // Suppress unused parameter warning + // + //TODO: If you are implementing the extra credit, please add logic + // to keep track of is_threaded to handle this feature + // + + + + svr_socket = boot_server(ifaces, port); + if (svr_socket < 0){ + int err_code = svr_socket; //server socket will carry error code + return err_code; + } + + + printf("Server started in %s mode.\n", is_threaded ? "Multi-Threaded" : "Single-Threaded"); + + rc = process_cli_requests(svr_socket, is_threaded); + + + stop_server(svr_socket); + + + return rc; +} + +/* + * stop_server(svr_socket) + * svr_socket: The socket that was created in the boot_server() + * function. + * + * This function simply returns the value of close() when closing + * the socket. + */ +int stop_server(int svr_socket){ + return close(svr_socket); +} + +/* + * boot_server(ifaces, port) + * ifaces & port: see start_server for description. They are passed + * as is to this function. + * + * This function "boots" the rsh server. It is responsible for all + * socket operations prior to accepting client connections. Specifically: + * + * 1. Create the server socket using the socket() function. + * 2. Calling bind to "bind" the server to the interface and port + * 3. Calling listen to get the server ready to listen for connections. + * + * after creating the socket and prior to calling bind you might want to + * include the following code: + * + * int enable=1; + * setsockopt(svr_socket, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + * + * when doing development you often run into issues where you hold onto + * the port and then need to wait for linux to detect this issue and free + * the port up. The code above tells linux to force allowing this process + * to use the specified port making your life a lot easier. + * + * Returns: + * + * server_socket: Sockets are just file descriptors, if this function is + * successful, it returns the server socket descriptor, + * which is just an integer. + * + * ERR_RDSH_COMMUNICATION: This error code is returned if the socket(), + * bind(), or listen() call fails. + * + */ +int boot_server(char *ifaces, int port) { + int svr_socket; + struct sockaddr_in server_addr; + int enable = 1; + + // Step 1: Create a socket + svr_socket = socket(AF_INET, SOCK_STREAM, 0); + if (svr_socket < 0) { + perror("Error creating server socket"); + fprintf(stderr, CMD_ERR_RDSH_COMM); + return ERR_RDSH_COMMUNICATION; + } + + // Step 2: Set socket options to allow address reuse + if (setsockopt(svr_socket, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0) { + perror("Error setting socket options"); + fprintf(stderr, CMD_ERR_RDSH_COMM); + close(svr_socket); + return ERR_RDSH_COMMUNICATION; + } + + // Step 3: Prepare server address structure + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(port); // Convert port to network byte order + + // Convert and assign the interface IP + if (inet_pton(AF_INET, ifaces, &server_addr.sin_addr) <= 0) { + perror("Invalid server IP address"); + fprintf(stderr, CMD_ERR_RDSH_COMM); + close(svr_socket); + return ERR_RDSH_COMMUNICATION; + } + + // Step 4: Bind the socket + if (bind(svr_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { + perror("Error binding server socket"); + fprintf(stderr, CMD_ERR_RDSH_COMM); + close(svr_socket); + return ERR_RDSH_COMMUNICATION; + } + + // Step 5: Put the socket in listening mode + if (listen(svr_socket, SOMAXCONN) < 0) { + perror("Error setting server to listen mode"); + fprintf(stderr, CMD_ERR_RDSH_COMM); + close(svr_socket); + return ERR_RDSH_COMMUNICATION; + } + + printf("Server listening on %s:%d\n", ifaces, port); + return svr_socket; + + //return WARN_RDSH_NOT_IMPL; +} + +/* + * process_cli_requests(svr_socket) + * svr_socket: The server socket that was obtained from boot_server() + * + * This function handles managing client connections. It does this using + * the following logic + * + * 1. Starts a while(1) loop: + * + * a. Calls accept() to wait for a client connection. Recall that + * the accept() function returns another socket specifically + * bound to a client connection. + * b. Calls exec_client_requests() to handle executing commands + * sent by the client. It will use the socket returned from + * accept(). + * c. Loops back to the top (step 2) to accept connecting another + * client. + * + * note that the exec_client_requests() return code should be + * negative if the client requested the server to stop by sending + * the `stop-server` command. If this is the case step 2b breaks + * out of the while(1) loop. + * + * 2. After we exit the loop, we need to cleanup. Dont forget to + * free the buffer you allocated in step #1. Then call stop_server() + * to close the server socket. + * + * Returns: + * + * OK_EXIT: When the client sends the `stop-server` command this function + * should return OK_EXIT. + * + * ERR_RDSH_COMMUNICATION: This error code terminates the loop and is + * returned from this function in the case of the accept() + * function failing. + * + * OTHERS: See exec_client_requests() for return codes. Note that positive + * values will keep the loop running to accept additional client + * connections, and negative values terminate the server. + * + */ + +/* +int process_cli_requests(int svr_socket) { + int cli_socket; + int rc; + + while (1) { + // Step 1: Accept an incoming client connection + cli_socket = accept(svr_socket, NULL, NULL); + if (cli_socket < 0) { + perror("Error accepting client connection"); + fprintf(stderr, CMD_ERR_RDSH_COMM); + return ERR_RDSH_COMMUNICATION; + } + + printf("Client connected\n"); + + // Step 2: Handle client requests + rc = exec_client_requests(cli_socket); + + // Step 3: Close client socket after handling requests + close(cli_socket); + printf(RCMD_MSG_CLIENT_EXITED); + + // Step 4: If client requested server to stop, break loop + if (rc == OK_EXIT) { + printf(RCMD_MSG_SVR_STOP_REQ); + break; + } + } + + return OK_EXIT; + + + //return WARN_RDSH_NOT_IMPL; +} + +*/ + +int process_cli_requests(int svr_socket, int is_threaded) { + int cli_socket; + int rc; + + while (1) { + // Step 1: Accept a client connection + cli_socket = accept(svr_socket, NULL, NULL); + if (cli_socket < 0) { + perror("Error accepting client connection"); + fprintf(stderr, CMD_ERR_RDSH_COMM); + return ERR_RDSH_COMMUNICATION; + } + + //printf("Client connected\n"); + printf("Client connected (socket %d)\n", cli_socket); + fflush(stdout); + + // Multi-threaded mode: Create a new thread for each client + if (is_threaded) { + pthread_t client_thread; + client_thread_args_t *args = malloc(sizeof(client_thread_args_t)); + if (!args) { + perror("Memory allocation failed for client thread"); + close(cli_socket); + continue; + } + + args->cli_socket = cli_socket; + + if (pthread_create(&client_thread, NULL, client_handler, (void *)args) != 0) { + perror("Failed to create client thread"); + free(args); + close(cli_socket); + continue; + } + + // Detach thread so it cleans up after itself + pthread_detach(client_thread); + } else { + // Single-threaded mode: Handle client in main thread + rc = exec_client_requests(cli_socket); + close(cli_socket); + printf(RCMD_MSG_CLIENT_EXITED); + + if (rc == OK_EXIT) { + printf(RCMD_MSG_SVR_STOP_REQ); + break; + } + } + } + + return OK_EXIT; +} + + +/* + * exec_client_requests(cli_socket) + * cli_socket: The server-side socket that is connected to the client + * + * This function handles accepting remote client commands. The function will + * loop and continue to accept and execute client commands. There are 2 ways + * that this ongoing loop accepting client commands ends: + * + * 1. When the client executes the `exit` command, this function returns + * to process_cli_requests() so that we can accept another client + * connection. + * 2. When the client executes the `stop-server` command this function + * returns to process_cli_requests() with a return code of OK_EXIT + * indicating that the server should stop. + * + * Note that this function largely follows the implementation of the + * exec_local_cmd_loop() function that you implemented in the last + * shell program deliverable. The main difference is that the command will + * arrive over the recv() socket call rather than reading a string from the + * keyboard. + * + * This function also must send the EOF character after a command is + * successfully executed to let the client know that the output from the + * command it sent is finished. Use the send_message_eof() to accomplish + * this. + * + * Of final note, this function must allocate a buffer for storage to + * store the data received by the client. For example: + * io_buff = malloc(RDSH_COMM_BUFF_SZ); + * And since it is allocating storage, it must also properly clean it up + * prior to exiting. + * + * Returns: + * + * OK: The client sent the `exit` command. Get ready to connect + * another client. + * OK_EXIT: The client sent `stop-server` command to terminate the server + * + * ERR_RDSH_COMMUNICATION: A catch all for any socket() related send + * or receive errors. + */ +int exec_client_requests(int cli_socket) { + char *io_buff; + ssize_t recv_size; + int rc; + + // Step 1: Allocate buffer for receiving client data + io_buff = (char *)malloc(RDSH_COMM_BUFF_SZ); + if (!io_buff) { + fprintf(stderr, "Error: Memory allocation failed\n"); + return ERR_RDSH_COMMUNICATION; + } + + while (1) { + // Send the prompt before reading input + send_message_string(cli_socket, SH_PROMPT); + // Step 2: Receive command from client + recv_size = recv(cli_socket, io_buff, RDSH_COMM_BUFF_SZ - 1, 0); + if (recv_size < 0) { + perror("Error receiving command from client"); + fprintf(stderr, CMD_ERR_RDSH_COMM); + free(io_buff); + return ERR_RDSH_COMMUNICATION; + } + + // Step 3: Check for client disconnection + if (recv_size == 0) { + printf("Client disconnected\n"); + free(io_buff); + return OK; + } + + // Ensure null termination of received data + io_buff[recv_size] = '\0'; + + printf(RCMD_MSG_SVR_EXEC_REQ, io_buff); + + // Step 4: Check for built-in commands + if (strcmp(io_buff, "exit") == 0) { + send_message_string(cli_socket, RCMD_MSG_CLIENT_EXITED "\n"); + + free(io_buff); + return OK; + } + + if (strcmp(io_buff, "stop-server") == 0) { + send_message_string(cli_socket, "dsh4> stop-server\n" RCMD_MSG_SVR_STOP_REQ "\n"); + fflush(stdout); + printf("stopping ..."); + usleep(200000); + free(io_buff); + printf("stopping ..."); + return OK_EXIT; + } + + // Step 5: Parse and execute the command pipeline + command_list_t clist; + rc = build_cmd_list(io_buff, &clist); + if (rc != OK) { + send_message_string(cli_socket, CMD_ERR_RDSH_EXEC); + } else { + rc = rsh_execute_pipeline(cli_socket, &clist); + free_cmd_list(&clist); + } + + // Step 6: Send EOF character to mark end of response + send_message_eof(cli_socket); + } + + // Cleanup before exiting + free(io_buff); + return OK; + + //return WARN_RDSH_NOT_IMPL; +} + +/* + * send_message_eof(cli_socket) + * cli_socket: The server-side socket that is connected to the client + + * Sends the EOF character to the client to indicate that the server is + * finished executing the command that it sent. + * + * Returns: + * + * OK: The EOF character was sent successfully. + * + * ERR_RDSH_COMMUNICATION: The send() socket call returned an error or if + * we were unable to send the EOF character. + */ +int send_message_eof(int cli_socket){ + + int bytes_sent; + + // Send EOF character to client + bytes_sent = send(cli_socket, &RDSH_EOF_CHAR, 1, 0); + if (bytes_sent < 0) { + perror("Error sending EOF to client"); + fprintf(stderr, CMD_ERR_RDSH_COMM); + return ERR_RDSH_COMMUNICATION; + } + + return OK; + + //return WARN_RDSH_NOT_IMPL; +} + +/* + * send_message_string(cli_socket, char *buff) + * cli_socket: The server-side socket that is connected to the client + * buff: A C string (aka null terminated) of a message we want + * to send to the client. + * + * Sends a message to the client. Note this command executes both a send() + * to send the message and a send_message_eof() to send the EOF character to + * the client to indicate command execution terminated. + * + * Returns: + * + * OK: The message in buff followed by the EOF character was + * sent successfully. + * + * ERR_RDSH_COMMUNICATION: The send() socket call returned an error or if + * we were unable to send the message followed by the EOF character. + */ +int send_message_string(int cli_socket, char *buff){ + + int bytes_sent, msg_length; + + // Validate input + if (!buff) { + fprintf(stderr, "Error: Null buffer passed to send_message_string()\n"); + return ERR_RDSH_COMMUNICATION; + } + + msg_length = strlen(buff); // Get message length + + // Step 1: Send the message + bytes_sent = send(cli_socket, buff, msg_length, 0); + if (bytes_sent < 0) { + perror("Error sending message to client"); + fprintf(stderr, CMD_ERR_RDSH_COMM); + return ERR_RDSH_COMMUNICATION; + } + + // Check for partial send + if (bytes_sent != msg_length) { + fprintf(stderr, CMD_ERR_RDSH_SEND, bytes_sent, msg_length); + return ERR_RDSH_COMMUNICATION; + } + + if (msg_length == 0) { + return send_message_eof(cli_socket); // Ensure EOF is always sent + } + + // Step 2: Send EOF character + return send_message_eof(cli_socket); + + + + //return WARN_RDSH_NOT_IMPL; +} + + +/* + * rsh_execute_pipeline(int cli_sock, command_list_t *clist) + * cli_sock: The server-side socket that is connected to the client + * clist: The command_list_t structure that we implemented in + * the last shell. + * + * This function executes the command pipeline. It should basically be a + * replica of the execute_pipeline() function from the last deliverable. + * The only thing different is that you will be using the cli_sock as the + * main file descriptor on the first executable in the pipeline for STDIN, + * and the cli_sock for the file descriptor for STDOUT, and STDERR for the + * last executable in the pipeline. See picture below: + * + * + *┌───────────┐ ┌───────────┐ + *│ cli_sock │ │ cli_sock │ + *└─────┬─────┘ └────▲──▲───┘ + * │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ + * │ │ Process 1 │ │ Process 2 │ │ Process N │ │ │ + * │ │ │ │ │ │ │ │ │ + * └───▶stdin stdout├─┬──▶│stdin stdout├─┬──▶│stdin stdout├──┘ │ + * │ │ │ │ │ │ │ │ │ + * │ stderr├─┘ │ stderr├─┘ │ stderr├─────┘ + * └──────────────┘ └──────────────┘ └──────────────┘ + * WEXITSTATUS() + * of this last + * process to get + * the return code + * for this function + * + * Returns: + * + * EXIT_CODE: This function returns the exit code of the last command + * executed in the pipeline. If only one command is executed + * that value is returned. Remember, use the WEXITSTATUS() + * macro that we discussed during our fork/exec lecture to + * get this value. + */ +int rsh_execute_pipeline(int cli_sock, command_list_t *clist) { + + int num_cmds = clist->num; + int prev_pipe = -1; + int fd[2]; + pid_t pids[num_cmds]; + + for (int i = 0; i < num_cmds; i++) { + if (i < num_cmds - 1 && pipe(fd) < 0) { + perror("pipe"); + fprintf(stderr, CMD_ERR_RDSH_EXEC); + return ERR_RDSH_CMD_EXEC; + } + + pids[i] = fork(); + if (pids[i] < 0) { + perror("fork"); + fprintf(stderr, CMD_ERR_RDSH_EXEC); + return ERR_RDSH_CMD_EXEC; + } + + if (pids[i] == 0) { // Child Process + if (i == 0) { + // First command: Redirect stdin from the client socket + dup2(cli_sock, STDIN_FILENO); + } else { + // Redirect stdin from the previous pipe + dup2(prev_pipe, STDIN_FILENO); + close(prev_pipe); + } + + if (i < num_cmds - 1) { + // Not the last command: Redirect stdout to the pipe + dup2(fd[1], STDOUT_FILENO); + close(fd[1]); + } else { + // Last command: Redirect stdout and stderr to client socket + dup2(cli_sock, STDOUT_FILENO); + dup2(cli_sock, STDERR_FILENO); + } + + // Close all open file descriptors + if (i > 0) { + close(prev_pipe); + } + if (i < num_cmds - 1) { + close(fd[0]); + close(fd[1]); // Ensure both pipe ends are closed + } + + + execvp(clist->commands[i].argv[0], clist->commands[i].argv); + perror("execvp failed"); + fprintf(stderr, CMD_ERR_RDSH_EXEC); + exit(ERR_RDSH_CMD_EXEC); + } else { // Parent Process + if (i > 0) close(prev_pipe); + if (i < num_cmds - 1) { + prev_pipe = fd[0]; + close(fd[1]); + } + } + } + + // Wait for all children to finish + int status; + for (int i = 0; i < num_cmds; i++) { + waitpid(pids[i], &status, 0); + } + + // Send EOF to indicate command completion + send_message_eof(cli_sock); + + return WEXITSTATUS(status); + + + //return WARN_RDSH_NOT_IMPL; +} + +/************** OPTIONAL STUFF ***************/ +/**** + **** NOTE THAT THE FUNCTIONS BELOW ALIGN TO HOW WE CRAFTED THE SOLUTION + **** TO SEE IF A COMMAND WAS BUILT IN OR NOT. YOU CAN USE A DIFFERENT + **** STRATEGY IF YOU WANT. IF YOU CHOOSE TO DO SO PLEASE REMOVE THESE + **** FUNCTIONS AND THE PROTOTYPES FROM rshlib.h + **** + */ + +/* + * rsh_match_command(const char *input) + * cli_socket: The string command for a built-in command, e.g., dragon, + * cd, exit-server + * + * This optional function accepts a command string as input and returns + * one of the enumerated values from the BuiltInCmds enum as output. For + * example: + * + * Input Output + * exit BI_CMD_EXIT + * dragon BI_CMD_DRAGON + * + * This function is entirely optional to implement if you want to handle + * processing built-in commands differently in your implementation. + * + * Returns: + * + * BI_CMD_*: If the command is built-in returns one of the enumeration + * options, for example "cd" returns BI_CMD_CD + * + * BI_NOT_BI: If the command is not "built-in" the BI_NOT_BI value is + * returned. + */ +Built_In_Cmds rsh_match_command(const char *input) +{ + (void)input; // Suppress unused parameter warning + return BI_NOT_IMPLEMENTED; +} + +/* + * rsh_built_in_cmd(cmd_buff_t *cmd) + * cmd: The cmd_buff_t of the command, remember, this is the + * parsed version fo the command + * + * This optional function accepts a parsed cmd and then checks to see if + * the cmd is built in or not. It calls rsh_match_command to see if the + * cmd is built in or not. Note that rsh_match_command returns BI_NOT_BI + * if the command is not built in. If the command is built in this function + * uses a switch statement to handle execution if appropriate. + * + * Again, using this function is entirely optional if you are using a different + * strategy to handle built-in commands. + * + * Returns: + * + * BI_NOT_BI: Indicates that the cmd provided as input is not built + * in so it should be sent to your fork/exec logic + * BI_EXECUTED: Indicates that this function handled the direct execution + * of the command and there is nothing else to do, consider + * it executed. For example the cmd of "cd" gets the value of + * BI_CMD_CD from rsh_match_command(). It then makes the libc + * call to chdir(cmd->argv[1]); and finally returns BI_EXECUTED + * BI_CMD_* Indicates that a built-in command was matched and the caller + * is responsible for executing it. For example if this function + * returns BI_CMD_STOP_SVR the caller of this function is + * responsible for stopping the server. If BI_CMD_EXIT is returned + * the caller is responsible for closing the client connection. + * + * AGAIN - THIS IS TOTALLY OPTIONAL IF YOU HAVE OR WANT TO HANDLE BUILT-IN + * COMMANDS DIFFERENTLY. + */ +Built_In_Cmds rsh_built_in_cmd(cmd_buff_t *cmd) +{ + (void)cmd; // Suppress unused parameter warning + return BI_NOT_IMPLEMENTED; +} -- GitLab