client.c 9.71 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
    char *ofile = NULL;
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
39
    int opt_f = 0;
40
    char *dir = NULL;
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
41
    int opt_d = 0;
42 43 44 45 46 47 48
    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
49
                port = optarg;
50 51 52
                break;
            case 'o':
                ofile = optarg;
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
53
                opt_f = 1;
54 55 56
                break;
            case 'd':
                dir = optarg;
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
57
                opt_d = 1;
58 59 60
                break;
            case 'h':
                print_usage();
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
61
                break;
62 63
            case '?':
                print_usage();
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
64
                break;
65 66 67 68 69 70
            default:
                abort();
        }
    }

    /// Check for the case that both output file and directory are set
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
71 72
    if(opt_d == 1 && opt_f == 1) {
        printf("Either output file or directory should be specified. You cannot use both.\n");
73 74 75 76 77 78 79 80 81 82 83
        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];
    }

Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
84 85 86 87 88 89 90 91 92
    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();
    }

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

95 96 97
    char *path = malloc(strlen(base_url) + 1);
    strcpy(path, base_url); //< Copying so that strtok() doesn't change it
    path = strstr(path, "/");
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
98 99 100 101 102 103 104 105 106 107

    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, '/');
        }
    }
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
108

Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
    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);
    }

Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
127
    char reply[__INT16_MAX__]; //< Set the variable to hold the server response. Set to a big length
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
128 129 130 131 132 133 134 135 136

    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
137 138

    exit(EXIT_SUCCESS); //< Exit with success
139 140 141
}

static void print_usage(void) {
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
142 143 144
    puts("Usage:");
    puts("client [-p PORT] [ -o FILE | -d DIR ] URL\n");
    exit(EXIT_FAILURE);
145 146 147 148 149 150
}

/**
 * @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
151
 * @param argc - Number of arguments obtained from the command line
152 153 154 155 156 157 158 159 160 161 162 163 164 165
 * @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
166 167 168 169

/**
 * @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
170
 *          If the address is resolvable, it returns the result
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
 *          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
200 201

/**
202
 * @brief Function to create a socket using the supplied configuration
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
203 204 205 206 207 208 209
 * @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) {
210
    int sockfd = socket(config -> ai_family, config -> ai_socktype, config -> ai_protocol);
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234

    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
235
        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
236 237 238 239 240 241 242 243

        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
244 245 246 247
        } 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 {
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
248 249
                FILE *out = NULL;
                if(ofile != NULL && dir == NULL) {
250 251 252 253 254 255
                    out = fopen(ofile, "w");

                    if(out == NULL) {
                        (void) fprintf(stderr, "ERROR: Could not open ./%s for writing.", ofile);
                        exit(EXIT_FAILURE);
                    } else {
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
256
                        fwrite(body, 1, strlen(body), out);
257 258 259
                        fclose(out);
                        printf("File ./%s successfully written.\n", ofile);
                    }
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
260
                } else if(ofile != NULL && dir != NULL) {
261 262 263

                    char path[sizeof(dir) + 100];

Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
264 265 266
                    snprintf(path, sizeof path, "%s%s", dir, ofile);

                    out = fopen(path, "w"); //< Open for writing
267 268

                    if(out == NULL) {
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
269
                        (void) fprintf(stderr, "ERROR: Could not open %s%s for writing.\n", dir, ofile);
270 271
                        exit(EXIT_FAILURE);
                    } else {
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
272
                        fwrite(body, 1, strlen(body), out);
273
                        fclose(out);
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
274
                        printf("File %s%s successfully written.\n", dir, ofile);
275
                    }
Ivaylo Ivanov's avatar
Ivaylo Ivanov committed
276 277 278 279 280 281 282 283
                }
            }
        }
    } else {
        fprintf(stderr, "ERROR: Protocol error.\n");
        exit(EXIT_FAILURE);
    }
}