284 lines
9.7 KiB
C
284 lines
9.7 KiB
C
/**
|
|
* @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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <netdb.h>
|
|
#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;
|
|
int opt_f = 0;
|
|
char *dir = NULL;
|
|
int opt_d = 0;
|
|
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;
|
|
opt_f = 1;
|
|
break;
|
|
case 'd':
|
|
dir = optarg;
|
|
opt_d = 1;
|
|
break;
|
|
case 'h':
|
|
print_usage();
|
|
break;
|
|
case '?':
|
|
print_usage();
|
|
break;
|
|
default:
|
|
abort();
|
|
}
|
|
}
|
|
|
|
/// Check for the case that both output file and directory are set
|
|
if(opt_d == 1 && opt_f == 1) {
|
|
printf("Either output file or directory should be specified. You cannot use both.\n");
|
|
print_usage();
|
|
}
|
|
|
|
/// Check if the required argument is there
|
|
if(argv[optind] == NULL) {
|
|
fprintf(stderr, "Mandatory argument 'url' missing.\n");
|
|
print_usage();
|
|
} else {
|
|
url = argv[optind];
|
|
}
|
|
|
|
char proto[8];
|
|
memcpy(proto, url, 7);
|
|
proto[7] = 0;
|
|
|
|
if(strcmp(proto, "http://") != 0){
|
|
printf("ERROR: The url doesn't start with http://\n");
|
|
print_usage();
|
|
}
|
|
|
|
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
|
|
if(ofile == NULL && dir != NULL)
|
|
ofile = "index.html";
|
|
} else {
|
|
if(ofile == NULL && dir != NULL) {
|
|
ofile = strchr(path, '/');
|
|
}
|
|
}
|
|
|
|
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) {
|
|
puts("Usage:");
|
|
puts("client [-p PORT] [ -o FILE | -d DIR ] URL\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
/**
|
|
* @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 {
|
|
FILE *out = NULL;
|
|
if(ofile != NULL && dir == NULL) {
|
|
out = fopen(ofile, "w");
|
|
|
|
if(out == NULL) {
|
|
(void) fprintf(stderr, "ERROR: Could not open ./%s for writing.", ofile);
|
|
exit(EXIT_FAILURE);
|
|
} else {
|
|
fwrite(body, 1, strlen(body), out);
|
|
fclose(out);
|
|
printf("File ./%s successfully written.\n", ofile);
|
|
}
|
|
} else if(ofile != NULL && dir != NULL) {
|
|
|
|
char path[sizeof(dir) + 100];
|
|
|
|
snprintf(path, sizeof path, "%s%s", dir, ofile);
|
|
|
|
out = fopen(path, "w"); //< Open for writing
|
|
|
|
if(out == NULL) {
|
|
(void) fprintf(stderr, "ERROR: Could not open %s%s for writing.\n", dir, ofile);
|
|
exit(EXIT_FAILURE);
|
|
} else {
|
|
fwrite(body, 1, strlen(body), out);
|
|
fclose(out);
|
|
printf("File %s%s successfully written.\n", dir, ofile);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
fprintf(stderr, "ERROR: Protocol error.\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|