Select Git revision
rsh_server.c
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;
}