Skip to content
Snippets Groups Projects
Select Git revision
  • 4a439eec89633a866431cf9a26b063528aac4d4c
  • main default
2 results

rsh_server.c

Blame
  • rsh_server.c 20.39 KiB
    
    #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 <errno.h>
    
    //INCLUDES for extra credit
    //#include <signal.h>
    //#include <pthread.h>
    //-------------------------
    
    #include "dshlib.h"
    #include "rshlib.h"
    
    
    /*
     * 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){
    
        (void)is_threaded;
        int svr_socket;
        int rc;
    
        svr_socket = boot_server(ifaces, port);
        if (svr_socket < 0){
            int err_code = svr_socket;  //server socket will carry error code
            return err_code;
        }
    
        rc = process_cli_requests(svr_socket);
    
        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;
        int ret;
        struct sockaddr_in addr;
    
        // TODO set up the socket - this is very similar to the demo code
    
        svr_socket = socket(AF_INET, SOCK_STREAM, 0);
        if (svr_socket == -1) {
            perror("socket");
            return ERR_RDSH_SERVER;
        }
    
        int enable = 1;
        if (setsockopt(svr_socket, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0) {
            perror("setsockopt");
            close(svr_socket);
            return ERR_RDSH_SERVER;
        }
    
        addr.sin_family = AF_INET;
        if (inet_pton(AF_INET, ifaces, &addr.sin_addr) <= 0) {
            perror("inet_pton");
            close(svr_socket);
            return ERR_RDSH_SERVER;
        }
        addr.sin_port = htons(port);
    
    
        ret = bind(svr_socket, (const struct sockaddr *) &addr, sizeof(struct sockaddr_in));
    
        if (ret == -1 ) { 
            perror("bind");
            close(svr_socket);
            return ERR_RDSH_SERVER;
        }
    
        /*
         * Prepare for accepting connections. The backlog size is set
         * to 20. So while one request is being processed other requests
         * can be waiting.
         */
        ret = listen(svr_socket, 20);
        if (ret == -1) {
            perror("listen");
            close(svr_socket);
            return ERR_RDSH_COMMUNICATION;
        }
        
        printf("SERVER BOOTED on %s:%d\n", ifaces, port);
        return svr_socket;
    }
    
    /*
     * 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;
        char buffer[RDSH_COMM_BUFF_SZ];
        ssize_t bytes_received;
        int rc = OK;
    
        while (1) {
            // Accept a client connection
            cli_socket = accept(svr_socket, NULL, NULL);
            if (cli_socket == -1) {
                perror("accept");
                rc = ERR_RDSH_COMMUNICATION;
                break;
            }
    
            // Receive data from the client
            bytes_received = recv(cli_socket, buffer, sizeof(buffer), 0);
            if (bytes_received < 0) {
                perror("recv");
                close(cli_socket);
                rc = ERR_RDSH_COMMUNICATION;
                break;
            } else if (bytes_received == 0) {
                // Client disconnected
                close(cli_socket);
                continue;
            }
    
            // Check for special commands
            if (strncmp(buffer, "stop-server", strlen("stop-server")) == 0) {
                printf("Client requested server to stop\n");
                rc = OK_EXIT;
                close(cli_socket);
                break;
            }
    
            // Handle client requests
            rc = exec_client_requests(cli_socket);
            if (rc < 0) {
                fprintf(stderr, "Error processing client requests: %d\n", rc);
                close(cli_socket);
                break;
            }
    
            // Close the client socket
            close(cli_socket);
            printf("Client socket closed properly\n");
        }
    
        // Clean up and stop the server
        stop_server(svr_socket);
        return rc;
    }
    
    /*
     * 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 cmd_buff[RDSH_COMM_BUFF_SZ];
        ssize_t recv_bytes;
        int rc = OK;
    
        while (1) {
            recv_bytes = recv(cli_socket, cmd_buff, RDSH_COMM_BUFF_SZ, 0);
            if (recv_bytes <= 0) {
                close(cli_socket);
                printf("Client disconnected\n");
                return OK;
            }
    
            cmd_buff[recv_bytes] = '\0';
    
            cmd_buff[strcspn(cmd_buff, "\n")] = '\0';
    
            if (strlen(cmd_buff) == 0) {
                send_message_string(cli_socket, CMD_WARN_NO_CMD);
                send_message_eof(cli_socket);
                rc = WARN_NO_CMDS;
                continue;
            }
    
            if (strstr(cmd_buff, "|") != NULL) {
                command_list_t cmd_list;
                if (parse_pipeline(cmd_buff, &cmd_list) != OK) {
                    send_message_string(cli_socket, CMD_ERR_PIPE_LIMIT);
                    send_message_eof(cli_socket);
                    rc = ERR_TOO_MANY_COMMANDS;
                    continue;
                }
    
                rc = rsh_execute_pipeline(cli_socket, &cmd_list);
    
                for (int i = 0; i < cmd_list.num; i++) {
                    free(cmd_list.commands[i]._cmd_buffer);
                }
            } else {
                cmd_buff_t cmd;
                if (build_cmd_buff(cmd_buff, &cmd) != OK) {
                    send_message_string(cli_socket, CMD_ERR_PIPE_LIMIT);
                    send_message_eof(cli_socket);
                    rc = ERR_MEMORY;
                    continue;
                }
    
                if (strcmp(cmd.argv[0], EXIT_CMD) == 0) {
                    free(cmd._cmd_buffer);
                    send_message_string(cli_socket, "Exiting...\n");
                    send_message_eof(cli_socket);
                    return OK;
                } else if (strcmp(cmd.argv[0], "cd") == 0) {
                    if (cmd.argc == 1) {
                        chdir(getenv("HOME"));
                    } else if (cmd.argc == 2) {
                        if (chdir(cmd.argv[1]) != 0) {
                            char error_msg[256];
                            snprintf(error_msg, sizeof(error_msg), "cd: %s\n", strerror(errno));
                            send_message_string(cli_socket, error_msg);
                        }
                    } else {
                        send_message_string(cli_socket, CMD_ERR_PIPE_LIMIT);
                    }
                    send_message_eof(cli_socket);
                    free(cmd._cmd_buffer);
                    continue;
                }
    
                pid_t pid = fork();
                if (pid < 0) {
                    perror("fork failed");
                    rc = ERR_MEMORY;
                } else if (pid == 0) {
                    execvp(cmd.argv[0], cmd.argv);
                    perror("execvp failed");
                    exit(EXIT_FAILURE);
                } else {
                    int status;
                    wait(&status);
    
                    if (WIFEXITED(status)) {
                        if (WEXITSTATUS(status) != 0) {
                            char error_msg[256];
                            snprintf(error_msg, sizeof(error_msg), "Command failed with exit code %d\n", WEXITSTATUS(status));
                            send_message_string(cli_socket, error_msg);
                        }
                    }
                }
                free(cmd._cmd_buffer);
            }
            send_message_eof(cli_socket);
        }
    
        return rc;
    }
    
    
    
    
    /*
     * 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) {
        if (send(cli_socket, &RDSH_EOF_CHAR, 1, 0) < 0) {
            perror("send");
            return ERR_RDSH_COMMUNICATION;
        }
        return OK;
    }
    
    
    
    /*
     * 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;
    
        bytes_sent = send(cli_socket, buff, strlen(buff), 0);
        if (bytes_sent < 0) {
            perror("send");
            return ERR_RDSH_COMMUNICATION;
        }
    
        return OK;
    }
    
    
    /*
     * 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 pipes[clist->num - 1][2];  // Array of pipes
        pid_t pids[clist->num];
        int pids_st[clist->num];       // Array to store process statuses
        int exit_code;
    
        // Create all necessary pipes
        for (int i = 0; i < clist->num - 1; i++) {
            if (pipe(pipes[i]) == -1) {
                perror("pipe");
                return ERR_RDSH_COMMUNICATION;
            }
        }
    
        // Fork and execute each command in the pipeline
        for (int i = 0; i < clist->num; i++) {
            pids[i] = fork();
            if (pids[i] == -1) {
                perror("fork");
                return ERR_RDSH_COMMUNICATION;
            }
    
            if (pids[i] == 0) {  // Child process
                // Redirect input for the first command
                if (i == 0) {
                    if (dup2(cli_sock, STDIN_FILENO) == -1) {
                        perror("dup2 stdin");
                        exit(EXIT_FAILURE);
                    }
                } else {
                    // Redirect stdin to the read end of the previous pipe
                    if (dup2(pipes[i - 1][0], STDIN_FILENO) == -1) {
                        perror("dup2 stdin");
                        exit(EXIT_FAILURE);
                    }
                }
    
                // Redirect output for the last command
                if (i == clist->num - 1) {
                    // Redirect stdout and stderr to the client socket
                    if (dup2(cli_sock, STDOUT_FILENO) == -1) {
                        perror("dup2 stdout");
                        exit(EXIT_FAILURE);
                    }
                    if (dup2(cli_sock, STDERR_FILENO) == -1) {
                        perror("dup2 stderr");
                        exit(EXIT_FAILURE);
                    }
                } else {
                    // Redirect stdout to the write end of the current pipe
                    if (dup2(pipes[i][1], STDOUT_FILENO) == -1) {
                        perror("dup2 stdout");
                        exit(EXIT_FAILURE);
                    }
                }
    
                // Close all pipe ends in the child process
                for (int j = 0; j < clist->num - 1; j++) {
                    close(pipes[j][0]);
                    close(pipes[j][1]);
                }
    
                // Execute the command
                execvp(clist->commands[i].argv[0], clist->commands[i].argv);
                perror("execvp");
                exit(EXIT_FAILURE);
            }
        }
    
        // Parent process: close all pipe ends
        for (int i = 0; i < clist->num - 1; i++) {
            close(pipes[i][0]);
            close(pipes[i][1]);
        }
    
        // Wait for all children
        for (int i = 0; i < clist->num; i++) {
            waitpid(pids[i], &pids_st[i], 0);
        }
    
        // Determine the exit code
        exit_code = WEXITSTATUS(pids_st[clist->num - 1]);
        for (int i = 0; i < clist->num; i++) {
            if (WEXITSTATUS(pids_st[i]) == EXIT_SC) {
                exit_code = EXIT_SC;
                break;
            }
        }
    
        return exit_code;
    }