client.c 9.75 KB
Newer Older
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/**
 * @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/
 *
 **/

20 21 22 23 24
#define _GNU_SOURCE ///< in order for optarg to be defined
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
25 26 27
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
28 29
#include "shared/http.h"

Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
30
static struct addrinfo* get_host_info(const char *hostname, char *port);
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
31 32
static int create_socket(struct addrinfo* config);
static void process_response(char reply[sizeof(int32_t)], char *ofile, char *dir);
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
33

34 35 36
int main(int argc, char *argv[]) {
    check_opts_number(argc);

Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
37
    char *port = "80";
38 39 40 41 42 43 44 45 46
    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':
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
47
                port = optarg;
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
                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];
    }

Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
83
    char *base_url = strstr(url, "http://") + 7; //< the base url starts after http://
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
84

Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
85 86
    const char *path = strstr(base_url, "/");
    if(path == NULL) path = "/"; //< Set the root path if there is none
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
87

Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
    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[sizeof(int32_t)]; //< 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
118 119

    exit(EXIT_SUCCESS); //< Exit with success
120 121 122 123 124 125 126 127 128 129
}

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
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
130
 * @param argc - Number of arguments obtained from the command line
131 132 133 134 135 136 137 138 139 140 141 142 143 144
 * @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);
    }
}
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
145 146 147 148

/**
 * @brief Function to check if the address is resolvable by the client
 * @details Checks if the address is resolvable by the client.
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
149
 *          If the address is resolvable, it returns the result
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
 *          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;
}
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
179 180

/**
181
 * @brief Function to create a socket using the supplied configuration
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
182 183 184 185 186 187 188
 * @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) {
189
    int sockfd = socket(config -> ai_family, config -> ai_socktype, config -> ai_protocol);
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213

    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
214
        strncpy(cresponse_code, strstr(reply, "HTTP/1.1 ") + sizeof(http_version) + 1, 3); //< Get the response code as string.
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
215 216 217 218 219 220 221 222

        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
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
        } 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];
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
243
                    if(dir[strlen(dir) - 1] == '/')
244
                        snprintf(path, sizeof path, "%sresult.html", dir);
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
245
                    else
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
                        snprintf(path, sizeof path, "%s/result.html", dir);

                    out = fopen(path, "w"); //< Open <path>/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);
                    }
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
265 266 267 268 269 270 271 272
                }
            }
        }
    } else {
        fprintf(stderr, "ERROR: Protocol error.\n");
        exit(EXIT_FAILURE);
    }
}