/** * @file client.c * @author Ivaylo Ivanov 11777707 * @date 03.11.2018 * * @brief Client program module. * * The client module takes a URL as input, connects to the corresponding server on the corresponding * port(`80` by default) and requests the file specified in the URL. * The content of that file is written to `stdout`, to a file or to a directory. * * SYNOPSIS * client [-p PORT] [ -o FILE | -d DIR ] URL * * EXAMPLE * client http://ivayloivanov.eu/en/ * **/ #define _GNU_SOURCE ///< in order for optarg to be defined #include #include #include #include #include #include #include #include "shared/http.h" static struct addrinfo* get_host_info(const char *hostname, char *port); static int create_socket(struct addrinfo* config); static void process_response(char reply[sizeof(int32_t)], char *ofile, char *dir); int main(int argc, char *argv[]) { check_opts_number(argc); char *port = "80"; char *ofile = NULL; char *dir = NULL; char *url = NULL; int c; /// Process the options while((c = getopt(argc, argv, "p:o:d:h")) != -1) { switch(c) { case 'p': port = optarg; break; case 'o': ofile = optarg; break; case 'd': dir = optarg; break; case 'h': printf("Command usage:\n"); print_usage(); exit(EXIT_SUCCESS); case '?': print_usage(); exit(EXIT_FAILURE); default: abort(); } } /// Check for the case that both output file and directory are set if(ofile != NULL && dir != NULL) { printf("Either output file or directory is specified. You cannot use both.\n"); print_usage(); exit(EXIT_FAILURE); } /// Check if the required argument is there if(argv[optind] == NULL) { fprintf(stderr, "Mandatory argument 'url' missing.\n"); print_usage(); exit(EXIT_FAILURE); } else { url = argv[optind]; } char *base_url = strstr(url, "http://") + 7; //< the base url starts after http:// char *path = malloc(strlen(base_url) + 1); strcpy(path, base_url); //< Copying so that strtok() doesn't change it path = strstr(path, "/"); if(path == NULL) path = "/"; //< Set the root path if there is none const char *hostname = strtok(base_url, ";/?:@=&"); //< Get the FQDN of the remote struct addrinfo* config = get_host_info(hostname, port); //< Check if the address is resolvable char header[strlen(base_url) + 100]; //< Create a variable to store the header snprintf(header, sizeof header, "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", path, hostname); //< Build the header int sockfd = create_socket(config); if (connect(sockfd, config -> ai_addr, config -> ai_addrlen) == -1) { //< Connect to the host (void) fprintf(stderr, "ERROR: Failure connecting to the server.\n"); exit(EXIT_FAILURE); } if((send(sockfd, header, strlen(header), 0)) < 0) { //< Send the GET request (void) fprintf(stderr, "ERROR: Failure sending the request to the server.\n"); exit(EXIT_FAILURE); } char reply[__INT16_MAX__]; //< Set the variable to hold the server response. Set to a big length if((recv(sockfd, reply, strlen(reply) - 1, 0)) < 0) { //< Receive response from the remote (-1 for the trailing terminating char) (void) fprintf(stderr, "ERROR: Did not receive response from the remote.\n"); exit(EXIT_FAILURE); } process_response(reply, ofile, dir); freeaddrinfo(config); //< Free the addrinfo struct exit(EXIT_SUCCESS); //< Exit with success } static void print_usage(void) { printf("client [-p PORT] [ -o FILE | -d DIR ] URL\n"); } /** * @brief Function to check the number of arguments * @details Checks the number of arguments supplied to the command line. * If the arguments are less than the required or more than possible, it exits * @param argc - Number of arguments obtained from the command line * @return none * **/ static void check_opts_number(int argc) { if(argc <= 1) { fprintf(stderr, "At least one argument expected.\n"); print_usage(); exit(EXIT_FAILURE); } else if(argc > 6) { fprintf(stderr, "Too many arguments supplied.\n"); print_usage(); exit(EXIT_FAILURE); } } /** * @brief Function to check if the address is resolvable by the client * @details Checks if the address is resolvable by the client. * If the address is resolvable, it returns the result * If the address is not resolvable, it exits with an error * @param * hostname - The address to be resolved * port - The port to try * @return res - The result of getaddrinfo() * **/ static struct addrinfo* get_host_info(const char *hostname, char *port) { struct addrinfo hints; //< Configuration for the client hints.ai_family = AF_INET; //< Use IPv4 hints.ai_socktype = SOCK_STREAM; //< Stream socket hints.ai_flags = 0; //< Do not use any flags hints.ai_protocol = IPPROTO_TCP; //< Use only TCP struct addrinfo* res = NULL; int err = getaddrinfo(hostname, port, &hints, &res); if(err != 0) { (void) fprintf(stderr, "ERROR: %s\n", gai_strerror(err)); exit(EXIT_FAILURE); //< Exit if there is an error } if(res == NULL) { (void) fprintf(stderr, "ERROR: Could not resolve address %s\n", hostname); exit(EXIT_FAILURE); //< Exit if the address is not resolved } return res; } /** * @brief Function to create a socket using the supplied configuration * @details Creates a socket using the supplied configuration * If there was no error, it returns the socket * If there was an error, it exits and displays an error message * @param config - The the configuration to be used for the socket creation * @return sockfd - The socket obtained from the socket creation **/ static int create_socket(struct addrinfo* config) { int sockfd = socket(config -> ai_family, config -> ai_socktype, config -> ai_protocol); if(sockfd < 0) { (void) fprintf(stderr, "ERROR: Socket creation failed.\n"); exit(EXIT_FAILURE); } return sockfd; } /** * @brief Function to process the response of the remote host * @details Processes the reply according to its' content and the ofile and dir parameters * The function checks the response code does the following: * - if the reponse code is invalid, it exits with an error * - if the response code is different from 200, it prints the whole response and exits with status 3 * - if the response code is 200, it prints it to the console or saves it as a file * If there was an error, it exits and displays an error message * @param config - The the configuration to be used for the socket creation * @return null **/ static void process_response(char reply[sizeof(int32_t)], char *ofile, char *dir) { char *http_version = "HTTP/1.1"; if(strncmp(reply, http_version, strlen(http_version)) == 0) { //< Check if the remote gives a correct response header char cresponse_code[3]; //< Array for the response code. Response codes are always 3 digits long strncpy(cresponse_code, strstr(reply, "HTTP/1.1 ") + sizeof(http_version) + 1, 3); //< Get the response code as string. int response_code = strtol(cresponse_code, NULL, 10); //< Get the response code as integer if(response_code == 0) { fprintf(stderr, "ERROR: Protocol error.\n"); exit(EXIT_FAILURE); } else if (response_code != 200) { printf("%s\n", strtok(reply, "\r\n")); //< Get the first line } else { char *body = strstr(reply, "\r\n\r\n") + 4; if(ofile == NULL && dir == NULL) printf("%s", body); //< Print the body of the response else { if(ofile != NULL) { FILE *out = NULL; out = fopen(ofile, "w"); if(out == NULL) { (void) fprintf(stderr, "ERROR: Could not open ./%s for writing.", ofile); exit(EXIT_FAILURE); } else { fprintf(out, "%s", body); fclose(out); printf("File ./%s successfully written.\n", ofile); } } else if(dir != NULL) { FILE *out = NULL; char path[sizeof(dir) + 100]; if(dir[strlen(dir) - 1] == '/') snprintf(path, sizeof path, "%sresult.html", dir); else snprintf(path, sizeof path, "%s/result.html", dir); out = fopen(path, "w"); //< Open /result.html for writing if(out == NULL) { if(dir[strlen(dir) - 1] == '/') (void) fprintf(stderr, "ERROR: Could not open %sresult.html for writing.\n", dir); else (void) fprintf(stderr, "ERROR: Could not open %s/result.html for writing.\n", dir); exit(EXIT_FAILURE); } else { fprintf(out, "%s", body); fclose(out); if(dir[strlen(dir) - 1] == '/') printf("File %sresult.html successfully written.\n", dir); else printf("File %s/result.html successfully written.\n", dir); } } } } } else { fprintf(stderr, "ERROR: Protocol error.\n"); exit(EXIT_FAILURE); } }