diff --git a/httpd/.gitignore b/.gitignore similarity index 70% rename from httpd/.gitignore rename to .gitignore index a20dfd8..85e1c00 100644 --- a/httpd/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ *.log *.core httpd +__pycache__ +env/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0682316 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +CC = gcc +CFLAGS = -std=c99 -Werror -Wall -Wextra -Wvla -pedantic +LDFLAGS = +LDLIBS = + +CFLAGS_DBG = -g +ASAN_DBG_FLAGS = -fsanitize=address + +UTILS_SRCS = src/utils/string/string.c \ + src/utils/time/fmt_time.c \ + src/utils/parsing/words.c \ + src/utils/parsing/blanks.c \ + src/utils/files/files.c +MODULES_SRCS = src/config/config.c \ + src/server/server.c \ + src/http/http.c \ + src/http/headers.c \ + src/logger/logs.c \ + src/logger/errors.c \ + src/daemon/daemon.c +SRCS = $(UTILS_SRCS) \ + $(MODULES_SRCS) \ + src/main.c + +UTILS_OBJS = ${UTILS_SRCS:.c=.o} +MODULES_OBJS = ${MODULES_SRCS:.c=.o} +OBJS = ${SRCS:.c=.o} + +TARGET=httpd + +$(TARGET): $(OBJS) + $(CC) -o $@ $(OBJS) $(LDFLAGS) $(LDLIBS) + +check: $(TARGET) + cp $(TARGET) tests/$(TARGET) + cd tests + python3 -m venv env + env/bin/python -m pip install requests + env/bin/python -m pip install pytest + env/bin/python -m pip install pytest-timeout + - env/bin/pytest + +debug: CFLAGS += $(CFLAGS_DBG) +debug: $(OBJS) + $(CC) -o $(TARGET) $(OBJS) $(LDFLAGS) $(LDLIBS) + + +asan: CFLAGS += $(CFLAGS_DBG) +asan: LDFLAGS += $(ASAN_DBG_FLAGS) +asan: $(OBJS) + $(CC) -o $(TARGET) $(OBJS) $(LDFLAGS) $(LDLIBS) + +clean: + - pkill -9 $(TARGET) + $(RM) tests/$(TARGET) tests/out.log + $(RM) $(TARGET) + $(RM) $(OBJS) diff --git a/httpd/Makefile b/httpd/Makefile deleted file mode 100644 index 204e10d..0000000 --- a/httpd/Makefile +++ /dev/null @@ -1,37 +0,0 @@ -CC = gcc -CFLAGS = -std=c99 -Werror -Wall -Wextra -Wvla -pedantic -LDFLAGS = -LDLIBS = - -CFLAGS_DBG = -g -ASAN_DBG_FLAGS = -fsanitize=address - -UTILS_SRCS = src/utils/string/string.c -CONFIG_SRCS = src/config/config.c -SRCS = $(UTILS_SRCS) $(CONFIG_SRCS) src/main.c - -UTILS_OBJS = ${UTILS_SRCS:.c=.o} -CONFIG_OBJS = ${CONFIG_SRCS:.c=.o} -OBJS = ${SRCS:.c=.o} - -TARGET=httpd - -$(TARGET): $(OBJS) - $(CC) -o $@ $(OBJS) $(LDFLAGS) $(LDLIBS) - -check: - dash tests/run.sh - -debug: CFLAGS += $(CFLAGS_DBG) -debug: $(OBJS) - $(CC) -o $(TARGET) $(OBJS) $(LDFLAGS) $(LDLIBS) - - -asan: CFLAGS += $(CFLAGS_DBG) -asan: LDFLAGS += $(ASAN_DBG_FLAGS) -asan: $(OBJS) - $(CC) -o $(TARGET) $(OBJS) $(LDFLAGS) $(LDLIBS) - -clean: - $(RM) $(TARGET) - $(RM) $(OBJS) diff --git a/httpd/src/main.c b/httpd/src/main.c deleted file mode 100644 index d86a4b8..0000000 --- a/httpd/src/main.c +++ /dev/null @@ -1,10 +0,0 @@ -#include -#include - -#include "config/config.h" - -int main(int argc, char **argv) -{ - parse_configuration(argc, argv); - return 0; -} diff --git a/httpd/src/utils/string/string.c b/httpd/src/utils/string/string.c deleted file mode 100644 index 342d7b2..0000000 --- a/httpd/src/utils/string/string.c +++ /dev/null @@ -1,70 +0,0 @@ -#include "string.h" - -#include -#include - -struct string *string_create(const char *str, size_t size) -{ - struct string *res = malloc(sizeof(struct string)); - if (res == NULL) - return NULL; - - res->data = malloc(size * sizeof(char)); - if (res->data == NULL) - { - free(res); - return NULL; - } - - memcpy(res->data, str, size); - res->size = size; - return res; -} - -int string_compare_n_str(const struct string *str1, const char *str2, size_t n) -{ - size_t i = 0; - int res = 0; - char reached_str2_end = 0; - while (i < n) - { - if (i < str1->size) - res += str1->data[i]; - - if (!reached_str2_end) - { - if (str2[i] == '\0') - reached_str2_end = 1; - else - res -= str2[i]; - } - - i++; - } - - return res; -} - -void string_concat_str(struct string *str, const char *to_concat, size_t size) -{ - size_t new_size = str->size + size; - size_t str_size = str->size; - - if (new_size == 0) - return; - - str->data = realloc(str->data, new_size); - if (str->data == NULL) - return; // Handle ? - - for (size_t i = 0; i < size; i++) - { - str->data[str_size + i] = to_concat[i]; - } -} - -void string_destroy(struct string *str) -{ - free(str->data); - free(str); -} diff --git a/httpd/src/config/config.c b/src/config/config.c similarity index 73% rename from httpd/src/config/config.c rename to src/config/config.c index c1969b5..c8edde6 100644 --- a/httpd/src/config/config.c +++ b/src/config/config.c @@ -5,8 +5,8 @@ #include #include -#include "../utils/string/string.h" -#include "bits/getopt_ext.h" +// #include "../utils/string/string.h" +// #include "bits/getopt_ext.h" #define ARG_VALID 0 #define ARG_INVALID 1 @@ -50,6 +50,10 @@ static void print_help(char *program_name) // limit imposed by school static int parse_daemon_arg(struct config *cfg) { + // Multiple contradictory parameters + if (cfg->daemon != NO_OPTION) + return ARG_INVALID; + if (strcmp(optarg, "start") == 0) cfg->daemon = START; else if (strcmp(optarg, "stop") == 0) @@ -97,7 +101,7 @@ static int handle_opt(char **argv, char opt, struct config *cfg) // Server name case 's': - cfg->servers->server_name = string_create(optarg, strlen(optarg)); + cfg->servers->server_name = optarg; break; // Port @@ -117,7 +121,7 @@ static int handle_opt(char **argv, char opt, struct config *cfg) // Default file case 'D': - cfg->servers->ip = optarg; + cfg->servers->default_file = optarg; break; // PID file @@ -167,28 +171,48 @@ static void print_arg_error(int err, char **argv, struct option options[], printf("%s: Invalid value for '--%s'\n", argv[0], options[optindex].name); break; + + default: + printf( + "%s: An unknown error happened while trying to parse arguments\n", + argv[0]); + break; } } +// static void apply_default_values(struct config *cfg) +// { +// // Default file +// if (cfg->servers->default_file == NULL) +// { +// char *default_df = DEFAULT_DF; +// cfg->servers->default_file = +// malloc((strlen(default_df) + 1) * sizeof(char)); +// // TODO handle error +// strcpy(cfg->servers->default_file, default_df); +// } +// } + // == Main functions struct config *parse_configuration(int argc, char *argv[]) { - struct option options[] = { // Global - { "daemon", required_argument, NULL, 'd' }, - { "help", no_argument, NULL, 'h' }, - // Vhosts - { "server_name", required_argument, NULL, 's' }, - { "port", required_argument, NULL, 'p' }, - { "ip", required_argument, NULL, 'i' }, - { "root_dir", required_argument, NULL, 'r' }, - { "defaut_file", required_argument, NULL, 'D' }, - // Logging - { "pid_file", required_argument, NULL, 'P' }, - { "log_file", required_argument, NULL, 'L' }, - { "log", required_argument, NULL, 'l' }, - // End - { NULL, 0, NULL, 0 } + struct option options[] = { + // Global + { "daemon", required_argument, NULL, 'd' }, + { "help", no_argument, NULL, 'h' }, + // Vhosts + { "server_name", required_argument, NULL, 's' }, + { "port", required_argument, NULL, 'p' }, + { "ip", required_argument, NULL, 'i' }, + { "root_dir", required_argument, NULL, 'r' }, + { "default_file", required_argument, NULL, 'D' }, + // Logging + { "pid_file", required_argument, NULL, 'P' }, + { "log_file", required_argument, NULL, 'L' }, + { "log", required_argument, NULL, 'l' }, + // End + { NULL, 0, NULL, 0 } }; struct config *config = calloc(1, sizeof(struct config)); @@ -216,10 +240,12 @@ struct config *parse_configuration(int argc, char *argv[]) } } + // apply_default_values(config); + // Check config validity if (check_config(config) != 0) { - printf("%s: Missing mandatory flags, cannot continue.", argv[0]); + printf("%s: Missing mandatory flags, cannot continue.\n", argv[0]); config_destroy(config); return NULL; } diff --git a/httpd/src/config/config.h b/src/config/config.h similarity index 91% rename from httpd/src/config/config.h rename to src/config/config.h index aa4c318..72be5a8 100644 --- a/httpd/src/config/config.h +++ b/src/config/config.h @@ -2,9 +2,13 @@ #define CONFIG_H #define _XOPEN_SOURCE 500 +#define HTTP_VERSION "HTTP/1.1" #include +// Default values +#define DEFAULT_DF "index.html" + /* ** @brief Enum daemon ** NO_OPTION if the '--daemon' option is not given @@ -32,6 +36,7 @@ struct config char *pid_file; char *log_file; bool log; + char *protocol_version; struct server_config *servers; enum daemon daemon; @@ -48,7 +53,7 @@ struct config */ struct server_config { - struct string *server_name; + char *server_name; char *port; char *ip; char *root_dir; diff --git a/src/daemon/daemon.c b/src/daemon/daemon.c new file mode 100644 index 0000000..a918966 --- /dev/null +++ b/src/daemon/daemon.c @@ -0,0 +1,73 @@ +#include "daemon.h" + +#include +#include +#include +#include + +#include "../server/server.h" +#include "../utils/files/files.h" + +static struct config *config; + +// === Functions + +void daemon_init(struct config *cfg) +{ + config = cfg; +} + +int get_pid(void) +{ + FILE *stream = fopen(config->pid_file, "r"); + if (stream == NULL) + return -2; + + char buf[10]; + size_t nread = fread(buf, sizeof(char), 10, stream); + if (nread > 8) // PID is max 8 chars + return -3; + buf[nread] = '\0'; + + int res = atoi(buf); + + fclose(stream); + + return res; +} + +void stop_daemon(void) +{ + int pid = get_pid(); + if (pid > 0) + kill(pid, SIGINT); +} + +int start_daemon(void) +{ + pid_t pid = fork(); + if (!pid) // Daemon + { + start_server(config); + } + else // Parent + { + // Write pid + int err = write_pid(config->pid_file, pid); + if (err != 0) + { + kill(pid, SIGINT); + return 1; + } + } + return 0; +} + +int restart_daemon(void) +{ + // Attempt to kill process + stop_daemon(); + + // Start again + return start_daemon(); +} diff --git a/src/daemon/daemon.h b/src/daemon/daemon.h new file mode 100644 index 0000000..f1dcae8 --- /dev/null +++ b/src/daemon/daemon.h @@ -0,0 +1,30 @@ +#ifndef DAEMON_H +#define DAEMON_H + +#include "../config/config.h" + +/* @brief + * + * @return + */ +int get_pid(void); + +/* @brief + */ +void daemon_init(struct config *cfg); + +/* @brief + */ +void stop_daemon(void); + +/* @brief + */ +int start_daemon(void); + +/* @brief + * + * @return + */ +int restart_daemon(void); + +#endif // ! DAEMON_H diff --git a/src/http/headers.c b/src/http/headers.c new file mode 100644 index 0000000..ce7806c --- /dev/null +++ b/src/http/headers.c @@ -0,0 +1,162 @@ +#include "headers.h" + +#include +#include +#include + +#include "../utils/parsing/words.h" +#include "../utils/string/string.h" + +void destroy_headers(struct http_header *headers) +{ + while (headers != NULL) + { + struct http_header *next = headers->next; + string_destroy(headers->field); + string_destroy(headers->value); + free(headers); + headers = next; + } +} + +ssize_t read_field(struct string *str, size_t offset, struct string **res) +{ + ssize_t nread = read_word_delim(str, offset, res, ":\n"); + if (nread <= 0) + return ERR_HTTP_INVALID_INPUT; + + if (str->size <= offset + nread || str->data[offset + nread] != ':') + return ERR_HTTP_INVALID_INPUT; + + string_to_lowercase(*res); + + return nread; +} + +ssize_t read_value(struct string *str, size_t offset, struct string **res) +{ + ssize_t nread = read_word_delim(str, offset, res, "\n"); + if (nread <= 0) + return ERR_HTTP_INVALID_INPUT; + + if (str->size <= offset + nread || str->data[offset + nread] != '\n') + return ERR_HTTP_INVALID_INPUT; + + // Trim trailing \r + if ((*res)->size > 0 && (*res)->data[(*res)->size - 1] == '\r') + { + (*res)->size--; + } + + return nread; +} + +ssize_t parse_headers(struct http_request *res, struct string *req, + size_t offset) +{ + size_t i = offset; + struct http_header *header = NULL; + + // Yes I know I do one useless allocation but I really don't care at this + // point + while (i < req->size && req->data[i] != '\n' + && req->data[i] != '\r') // ! Blank line + { + if (header == NULL) + { + // Init list + header = calloc(1, sizeof(struct http_header)); + res->headers = header; + } + else + { + // Append + header->next = calloc(1, sizeof(struct http_header)); + header = header->next; + } + + // Check allocation + if (header == NULL) + return ERR_HTTP_OUT_OF_MEMORY; + + // Read field + ssize_t nread = read_field(req, i, &header->field); + if (nread <= 0) + return nread; // Contains error code when negative + + i += nread + 1; + + // Read value + nread = read_value(req, i, &header->value); + if (nread <= 0) + return nread; // Contains error code when negative + + i += nread + 1; + } + + if (i < req->size && req->data[i] == '\r') + i++; + if (i < req->size && req->data[i] == '\n') + i++; + + return i; +} + +struct http_header *get_header(struct http_header *headers, const char *field) +{ + while (headers != NULL) + { + if (string_compare_n_str(headers->field, field, strlen(field)) == 0) + return headers; + + headers = headers->next; + } + + return NULL; +} + +struct http_header *create_header(const char *field, const char *value) +{ + struct http_header *res = calloc(1, sizeof(struct http_header)); + if (res == NULL) + return NULL; + + // Field + ssize_t field_size = strlen(field); + if (field_size < 0) + { + free(res); + return NULL; + } + res->field = string_create(field, field_size); + + // Value + ssize_t value_size = strlen(value); + if (value_size < 0) + { + string_destroy(res->field); + free(res); + return NULL; + } + res->value = string_create(value, value_size); + + return res; +} +void append_header(struct http_header **list, struct http_header *element) +{ + if (list == NULL) + return; + + if (*list == NULL) + *list = element; + else + { + struct http_header *cur_elt = *list; + while (cur_elt->next != NULL) + { + cur_elt = cur_elt->next; + } + + cur_elt->next = element; + } +} diff --git a/src/http/headers.h b/src/http/headers.h new file mode 100644 index 0000000..460cd3e --- /dev/null +++ b/src/http/headers.h @@ -0,0 +1,82 @@ +#ifndef HEADERS_H +#define HEADERS_H + +#include +#include + +#include "http.h" + +// === Functions + +/* + * @brief + * + * @param headers + */ +void destroy_headers(struct http_header *headers); + +/* + * @brief + * + * @param str + * @param offset + * @param res + * + * @return + */ +ssize_t read_field(struct string *str, size_t offset, struct string **res); + +/* + * @brief + * + * @param str + * @param offset + * @param res + * + * @return + */ +ssize_t read_value(struct string *str, size_t offset, struct string **res); + +/* + * @brief + * + * @param res + * @param req + * @param offset + * + * @return + */ +ssize_t parse_headers(struct http_request *res, struct string *req, + size_t offset); + +/* + * @brief + * + * @param headers + * @param field + * + * @return + */ +struct http_header *get_header(struct http_header *headers, const char *field); + +/* + * @brief + * + * @param field + * @param value + * + * @return + */ +struct http_header *create_header(const char *field, const char *value); + +/* + * @brief + * + * @param field + * @param value + * + * @return + */ +void append_header(struct http_header **list, struct http_header *element); + +#endif // ! HEADERS_H diff --git a/src/http/http.c b/src/http/http.c new file mode 100644 index 0000000..4fb2280 --- /dev/null +++ b/src/http/http.c @@ -0,0 +1,511 @@ +#include "http.h" + +#include +#include +// #include +#include +#include +#include +#include +#include + +#include "../config/config.h" +#include "../logger/logs.h" +#include "../utils/files/files.h" +#include "../utils/parsing/words.h" +#include "../utils/time/fmt_time.h" +#include "headers.h" + +// === Static variables + +static struct config *config; + +// === Static functions + +// Parses the status line of req, stores the result in res and returns the +// number of read characters or a negative number on error +// WARNING res must be pre allocated +// (See the header for error codes) +static ssize_t parse_reqline(struct http_request *res, struct string *req) +{ + ssize_t i = 0; + ssize_t skipped; + + if (res == NULL || req == NULL) + return ERR_HTTP_INTERNAL_ERROR; + + // Method + if (string_compare_n_str(req, "GET", strlen("GET")) == 0) + { + res->method = GET; + i += strlen("GET"); + } + else if (string_compare_n_str(req, "HEAD", strlen("HEAD")) == 0) + { + res->method = HEAD; + i += strlen("HEAD"); + } + else + return ERR_HTTP_NOT_IMPLEMENTED; + + // Skip space + if (req->data[i++] != ' ') + return ERR_HTTP_INVALID_INPUT; + + // Target (path) + skipped = read_word(req, i, &res->target); + if (skipped <= 0) + return ERR_HTTP_INVALID_INPUT; + i += skipped; + + // Skip space + if (req->data[i++] != ' ') + return ERR_HTTP_INVALID_INPUT; + + // Protocol + skipped = read_word(req, i, &res->protocol); + if (skipped <= 0) + return ERR_HTTP_INVALID_INPUT; + i += skipped; + + // CRLF (EOL) oh qu'il est casse couilles celui-là + ssize_t req_size = req->size; // aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah + if (i < req_size && req->data[i] == '\r') + i++; + if (i >= req_size || req->data[i] != '\n') + return ERR_HTTP_INVALID_INPUT; + i++; + // Donc 2h de debug pour ça là ? Plutot envie de me tirer une balle si vous + // voulez mon avis + + return i; +} + +// Split target into path and queries +static void split_target(struct http_request *req) +{ + size_t i = 0; + while (i < req->target->size) + { + if (req->target->data[i] == '?') + { + req->queries = + string_create(req->target->data + i, req->target->size - i); + req->target->size = i; + return; + } + i++; + } +} + +// Finds a valid path based on the client input and returns the corresponding +// FILES return value (see files.h) +static int find_target(struct http_request *req) +{ + // Check filename + if (!check_filename(req->target)) + return ERR_FILES_FORBIDDEN; + + char *target = string_to_charptr(req->target); + int err = is_directory(target); + free(target); + + if (err == FILES_DIR) // Is a directory + { + // Append default file if directory + if (req->target->data[req->target->size - 1] != '/') + string_concat_str(req->target, "/", 1); + + string_concat_str(req->target, config->servers->default_file, + strlen(config->servers->default_file)); + // Recheck + target = string_to_charptr(req->target); + err = is_directory(target); + free(target); + return err; + } + + return err; +} + +// WARNING allocates result on the heap +static char *get_http_method(enum http_method method) +{ + char *res = malloc(8 * sizeof(char)); + switch (method) + { + case GET: + strcpy(res, "HEAD"); + return res; + case HEAD: + strcpy(res, "HEAD"); + return res; + default: + free(res); + return NULL; + } +} + +static struct string *generate_status_message(int status_code) +{ + char *message; + switch (status_code) + { + case 200: + message = "OK"; + break; + case 400: + message = "Bad Request"; + break; + case 403: + message = "Forbidden"; + break; + case 404: + message = "Not Found"; + break; + case 405: + message = "Method Not Allowed"; + break; + case 505: + message = "HTTP Version Not Supported"; + break; + default: + message = "WTF"; + } + + return string_create(message, strlen(message)); +} + +static void check_req(struct http_request *req, struct http_response *resp) +{ + // Method + if (req->method == INVALID_METHOD) + resp->status_code = 405; + + // Protocol + // Oui il y a plus beau, mais on a le temps ou on l'a pas, moi je l'ai pas, + // alors si t'es pas content t'as qu'à le modifier toi même vu que tu as + // visiblement le code source. <3 + if (string_compare_strictly_n_str(req->protocol, "HTTP/", strlen("HTTP/")) + != 0) + resp->status_code = 400; + else if (string_compare_strictly_n_str(req->protocol, "HTTP/1.1", + strlen("HTTP/1.1")) + != 0) + resp->status_code = 505; + + // Host + if (resp->status_code != 400 && resp->status_code != 505) + { + int host_count = 0; + struct http_header *cur = req->headers; + while (cur != NULL) + { + if (cur->field->size == 4 + && string_compare_n_str(cur->field, "host", 4) == 0) + { + host_count++; + if (cur->value == NULL || cur->value->size == 0) + { + resp->status_code = 400; + break; + } + } + cur = cur->next; + } + + if (host_count != 1) + resp->status_code = 400; + } + + // printf("%s %d\n", req->protocol->data, resp->status_code); +} + +// === Functions + +void http_init(struct config *cfg) +{ + config = cfg; +} + +// TODO handle logs +void handle_request(int client_fd, char *client_ip) +{ + char buffer[BUFFER_SIZE]; // Declared in server.h + struct string *str = string_create(NULL, 0); + if (str == NULL) + return; + + // Store request + ssize_t nread = 0; + // This line here is so hard... + // When you realise that you have to remake all your project to be able to + // parse requests of BUFFER_SIZE size... + // Rendez les bijoux de la couronne (svp) + while ((nread = recv(client_fd, buffer, BUFFER_SIZE, 0)) == BUFFER_SIZE) + { + string_concat_str(str, buffer, nread); + } + if (nread > 0) + string_concat_str(str, buffer, nread); + + // Parse request + struct http_request *req = parse_request(str); + if (req == NULL) + { + free(str); + return; + } + char *method = get_http_method(req->method); + char *target = string_to_charptr(req->target); + print_log_request(method, target, client_ip); + free(method); + free(target); + + // Generate response + struct http_response *resp = generate_response(req); + if (resp == NULL) + { + free(str); + free(req); + return; + } + + // Format response to string + struct string *res = format_response(resp); + if (res == NULL) + { + free(str); + free(req); + return; + } + + // Send response + ssize_t nsent; + size_t total_sent = 0; + while (total_sent < res->size) + { + nsent = + send(client_fd, res->data + total_sent, res->size - total_sent, 0); + if (nsent <= 0) + break; + total_sent += nsent; + } + + // Send file + struct http_header *cl_header = get_header(resp->headers, "Content-Length"); + if (cl_header != NULL) + { + char *target = string_to_charptr(req->target); + char *cl_str = string_to_charptr(cl_header->value); + int fd = open(target, O_RDONLY); + if (fd > 0) + sendfile(client_fd, fd, 0, atoi(cl_str)); + + free(target); + free(cl_str); + } + + // Log response + method = get_http_method(req->method); + target = string_to_charptr(req->target); + print_log_response(resp->status_code, method, target, client_ip); + free(method); + free(target); + + // Free + string_destroy(str); + string_destroy(res); + destroy_request(req); + destroy_response(resp); +} + +// TODO handle logs and free adequately +struct http_request *parse_request(struct string *req) +{ + struct http_request *res = calloc(1, sizeof(struct http_request)); + if (res == NULL) + return NULL; + + // Status line + size_t i = 0; + ssize_t nread = parse_reqline(res, req); + if (nread <= 0) + { + if (nread == ERR_HTTP_NOT_IMPLEMENTED) + res->status_code = 501; + else + res->status_code = 400; + + if (res->target == NULL) + res->target = string_create("", 0); + if (res->protocol == NULL) + res->protocol = string_create("HTTP/1.1", 8); + + return res; + } + + // Split path and query + split_target(res); + + i += nread; + + // Headers + nread = parse_headers(res, req, i); + if (nread <= 0) + { + res->status_code = 400; + return res; + } + + return res; +} + +// TODO handle logs and free adequately +struct http_response *generate_response(struct http_request *req) +{ + // Malloc + struct http_response *res = calloc(1, sizeof(struct http_response)); + if (res == NULL) + return NULL; + + // Protocol + // char *protocol = HTTP_VERSION; + char *protocol = "HTTP/1.1"; + res->protocol = string_create(protocol, strlen(protocol)); + + // Target + if (req->status_code == 0) + { + str_concat_string(config->servers->root_dir, + strlen(config->servers->root_dir), req->target); + } + + // Status code + if (req->status_code == 0) + { + switch (find_target(req)) + { + case FILES_REG: + res->status_code = 200; + break; + case ERR_FILES_NOT_FOUND: + res->status_code = 404; + break; + default: + res->status_code = 403; + break; + } + } + else + res->status_code = req->status_code; + + // Check protocol and method + if (req->status_code == 0) + check_req(req, res); + + // Headers + char *time = get_time(); + append_header(&res->headers, create_header("Date", time)); + free(time); // Yes, the one that completely disapeared this year + if (res->status_code == 200) + { + char buf[21] = { 0 }; // (20 ~= log10(2^64)) + 1 (null byte) + char *target = string_to_charptr(req->target); + ssize_t cl = get_file_content_size(target); + free(target); + if (cl >= 0) + { + sprintf(buf, "%lu", cl); + append_header(&res->headers, create_header("Content-Length", buf)); + } + else + { + res->status_code = 403; + } + } + else + { + append_header(&res->headers, create_header("Content-Length", "0")); + } + append_header(&res->headers, create_header("Connection", "close")); + + // Status msg + res->status_msg = generate_status_message(res->status_code); + + return res; +} + +struct string *format_response(struct http_response *resp) +{ + // Protocol + struct string *res = + string_create(resp->protocol->data, resp->protocol->size); + + string_concat_str(res, " ", 1); + + // Status code + char buf[4] = { 0 }; + sprintf(buf, "%d", resp->status_code); + string_concat_str(res, buf, strlen(buf)); + + string_concat_str(res, " ", 1); + + // Status message + char *status_msg = string_to_charptr(resp->status_msg); + string_concat_str(res, status_msg, strlen(status_msg)); + free(status_msg); + + string_concat_str(res, "\r\n", 2); + + // Headers + struct http_header *cur_header = resp->headers; + while (cur_header != NULL) + { + string_concat_str(res, cur_header->field->data, + cur_header->field->size); + + string_concat_str(res, ": ", 2); + + string_concat_str(res, cur_header->value->data, + cur_header->value->size); + + string_concat_str(res, "\r\n", 2); + + cur_header = cur_header->next; + } + + string_concat_str(res, "\r\n", 2); + + return res; +} + +void destroy_request(struct http_request *req) +{ + if (req != NULL) + { + if (req->target != NULL) + string_destroy(req->target); + if (req->protocol != NULL) + string_destroy(req->protocol); + + destroy_headers(req->headers); + + free(req); + } +} + +void destroy_response(struct http_response *resp) +{ + if (resp != NULL) + { + if (resp->status_msg != NULL) + string_destroy(resp->status_msg); + if (resp->protocol != NULL) + string_destroy(resp->protocol); + + destroy_headers(resp->headers); + + free(resp); + } +} diff --git a/src/http/http.h b/src/http/http.h new file mode 100644 index 0000000..8c3565c --- /dev/null +++ b/src/http/http.h @@ -0,0 +1,118 @@ +#ifndef HTTP_H +#define HTTP_H + +// === Definitions + +// #define _POSIX_C_SOURCE 200809L + +// Error codes +#define ERR_HTTP_INVALID_INPUT -1 +#define ERR_HTTP_NOT_IMPLEMENTED -2 +#define ERR_HTTP_OUT_OF_MEMORY -4 +#define ERR_HTTP_INTERNAL_ERROR -5 + +// === Includes + +#include "../config/config.h" +#include "../utils/string/string.h" + +// === Enums + +enum http_method +{ + INVALID_METHOD, + GET, + // POST, + // PUT, + // DELETE, + // PATCH, + HEAD, + // OPTIONS, + // CONNECT, + // TRACE +}; + +// === Structures + +struct http_header +{ + struct string *field; + struct string *value; + struct http_header *next; +}; + +struct http_request +{ + enum http_method method; + struct string *target; + struct string *queries; + struct string *protocol; + struct http_header *headers; // Headers linked list + int status_code; // Set by program +}; + +struct http_response +{ + struct string *protocol; + int status_code; + struct string *status_msg; + struct http_header *headers; // Headers linked list +}; + +// === Functions + +/* @brief Initializes the HTTP module with the given configuration + * + * @warn Do no use any other function of this module before calling that one + * + * @param cfg + */ +void http_init(struct config *cfg); + +/* @brief Reads, parses the request and responds adequately all-in-one + * + * @param client_fd + */ +void handle_request(int client_fd, char *client_ip); + +/* @brief Parses the HTTP request and splits it into a request structure + * + * @param req + * + * @return A pointer to the structure containing the request infos on success, + * NULL otherwise + */ +struct http_request *parse_request(struct string *req); + +/* @brief Generates a response to the given request + * + * @param req + * + * @return A pointer to the generated response struct on success, + * NULL otherwise + */ +struct http_response *generate_response(struct http_request *req); + +/* @brief Formats the given response structure into a valid HTTP response + * string + * + * @param resp + * + * @return A pointer to the string containing the response on success, + * NULL otherwise + */ +struct string *format_response(struct http_response *resp); + +/* @brief Free all allocated memory inside req and req itself + * + * @param req + */ +void destroy_request(struct http_request *req); + +/* @brief Free all allocated memory inside resp and resp itself + * + * @param resp + */ +void destroy_response(struct http_response *resp); + +#endif // ! HTTP_H diff --git a/src/logger/errors.c b/src/logger/errors.c new file mode 100644 index 0000000..4bbb369 --- /dev/null +++ b/src/logger/errors.c @@ -0,0 +1,61 @@ +#define _POSIX_C_SOURCE 200809L + +#include "errors.h" + +#include +#include +#include +#include +#include + +#include "../utils/time/fmt_time.h" +#include "logs.h" + +// === Static variable + +static struct logs_config config; + +// === Functions + +void errlog_init(bool enabled, int logfile_fd, struct server_config *serv_cfg) +{ + config.enabled = enabled; + if (logfile_fd == STDOUT_FILENO) + config.logfile_fd = STDERR_FILENO; + config.logfile_fd = logfile_fd; + config.server_cfg = serv_cfg; +} + +void print_err(void) +{ + print_log_err("%s", get_err()); +} + +void print_log_err(char *format, ...) +{ + if (!config.enabled) + return; + + // Log prefix (time and server name) + char *time = get_time(); + dprintf(config.logfile_fd, "%s [%s] ERROR ", time, + config.server_cfg->server_name); + free(time); + + // Print actual log + va_list args; + va_start(args, format); + vdprintf(config.logfile_fd, format, args); + va_end(args); + + // New line + dprintf(config.logfile_fd, "\n"); + + // Print to stderr + fprintf(stderr, "Error: %s", get_err()); +} + +char *get_err(void) +{ + return strerror(errno); +} diff --git a/src/logger/errors.h b/src/logger/errors.h new file mode 100644 index 0000000..09b8be2 --- /dev/null +++ b/src/logger/errors.h @@ -0,0 +1,27 @@ +#ifndef ERRORS_H +#define ERRORS_H + +#include +#include + +#include "../config/config.h" + +/* @brief Initialize the error logging submodule + * @warning Do not use 'as is', use log_init() instead + */ +void errlog_init(bool enabled, int logfile_fd, struct server_config *serv_cfg); + +/* @brief Retrieves the last error with errno and prints the corresponding + * error message in the logs and stderr + */ +void print_err(void); + +/* @brief Prints error logs, just like print_log() but for errors + */ +void print_log_err(char *format, ...); + +/* @brief Returns the string corresponding to the last error that happened + */ +char *get_err(void); + +#endif // ! ERRORS_H diff --git a/src/logger/logs.c b/src/logger/logs.c new file mode 100644 index 0000000..f82f706 --- /dev/null +++ b/src/logger/logs.c @@ -0,0 +1,79 @@ +#define _POSIX_C_SOURCE 200809L + +#include "logs.h" + +#include +#include +#include +#include + +#include "../utils/time/fmt_time.h" +#include "errors.h" + +// === Static variables + +static struct logs_config config; + +// === Functions + +int log_init(struct config *global_config) +{ + int return_value = 0; + config.enabled = global_config->log; + config.server_cfg = global_config->servers; + if (global_config->log_file != NULL) + { + config.logfile_fd = open(global_config->log_file, O_WRONLY); + if (config.logfile_fd <= 0) + { + config.enabled = false; + return_value = 1; + } + } + else + { + config.logfile_fd = STDOUT_FILENO; + } + + errlog_init(config.enabled, config.logfile_fd, config.server_cfg); + return return_value; +} + +void print_log(char *format, ...) +{ + if (!config.enabled) + return; + + // Log prefix (time and server name) + char *time = get_time(); + dprintf(config.logfile_fd, "%s [%s] ", time, + config.server_cfg->server_name); + free(time); + + // Print actual log + va_list args; + va_start(args, format); + vdprintf(config.logfile_fd, format, args); + va_end(args); + + // New line + dprintf(config.logfile_fd, "\n"); +} + +void print_log_request(char *request_type, char *target, char *client_ip) +{ + print_log("received %s, on '%s' from %s", request_type, target, client_ip); +} + +void print_log_response(int status_code, char *request_type, char *target, + char *client_ip) +{ + print_log("responding with %d to %s for %s on '%s'", status_code, client_ip, + request_type, target); +} + +void log_terminate(void) +{ + if (config.logfile_fd != 0 && config.logfile_fd != STDOUT_FILENO) + close(config.logfile_fd); +} diff --git a/src/logger/logs.h b/src/logger/logs.h new file mode 100644 index 0000000..f18a6d4 --- /dev/null +++ b/src/logger/logs.h @@ -0,0 +1,54 @@ +#ifndef LOGS_H +#define LOGS_H + +#include + +#include "../config/config.h" + +struct logs_config +{ + bool enabled; + int logfile_fd; + struct server_config *server_cfg; +}; + +/* @brief Initializes the logging module + * + * @param config + * + * @return 0 on success, an error code otherwise + */ +int log_init(struct config *config); + +/* @brief Prints logs (or not) conformly to the config given by the user. + * Works like printf (because it uses it under the hood) with a + * formatted string and variadic arguments. + * + * @param format + * @param ... + */ +void print_log(char *format, ...); + +/* @brief Prints request logs with the adequate format in the logfile + * + * @param request_type + * @param target + * @param client_ip + */ +void print_log_request(char *request_type, char *target, char *client_ip); + +/* @brief Prints response logs with the adequate format in the logfile + * + * @param status_code + * @param request_type + * @param target + * @param client_ip + */ +void print_log_response(int status_code, char *request_type, char *target, + char *client_ip); + +/* @brief Gracefully exits the logs module + */ +void log_terminate(void); + +#endif // ! LOGS_H diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..6f2c5b2 --- /dev/null +++ b/src/main.c @@ -0,0 +1,48 @@ +#include + +#include "config/config.h" +#include "daemon/daemon.h" +#include "http/http.h" +#include "logger/logs.h" +#include "server/server.h" + +#define ERR_ARG 2 + +int main(int argc, char **argv) +{ + // Parse config + struct config *config = parse_configuration(argc, argv); + if (config == NULL) + return ERR_ARG; + + // Initialize modules + log_init(config); // Ignore ret val + http_init(config); + daemon_init(config); + + // Start server + switch (config->daemon) + { + case NO_OPTION: + start_server(config); + break; + + case START: + start_daemon(); + break; + + case RESTART: + restart_daemon(); + break; + + case STOP: + stop_daemon(); + break; + + default: + return 2; + } + + config_destroy(config); + return 0; +} diff --git a/src/server/server.c b/src/server/server.c new file mode 100644 index 0000000..56c0c53 --- /dev/null +++ b/src/server/server.c @@ -0,0 +1,184 @@ +// === Definitions + +#define _POSIX_C_SOURCE 200112L + +#define BUFFER_SIZE 1024 + +// === Includes +#include "server.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../http/http.h" +#include "../logger/errors.h" +#include "../logger/logs.h" +// #include "../logger/logs.h" + +// === Static variables + +static int server_socket = 0; +static struct config *config; + +// === Static functions + +// Creates and bind the server communication socket +static int get_socket(const char *hostname, const char *port) +{ + // set lower-level protocols infos + struct addrinfo hints = { 0 }; + hints.ai_family = AF_INET; // IPv4 + hints.ai_socktype = SOCK_STREAM; // Socket type (necessary for TCP) + hints.ai_protocol = IPPROTO_TCP; // TCP + + // Retrieve node informations + struct addrinfo *client_addr = NULL; // Client addresses list + int err = getaddrinfo(hostname, port, &hints, &client_addr); + if (err != 0) + { + print_err(); + return -1; + } + + // Try each address + int sock_fd; + bool socket_bound = false; + struct addrinfo *cur_addr = client_addr; + while (cur_addr != NULL && !socket_bound) + { + // Create socket + sock_fd = socket(cur_addr->ai_family, cur_addr->ai_socktype, + cur_addr->ai_protocol); + if (sock_fd == -1) + { + cur_addr = cur_addr->ai_next; + continue; + } + + // ... Has something to do with not changing port + int sock_opt = -1; + setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &sock_opt, + sizeof(sock_opt)); + + // Assign name to socket (bind) + err = bind(sock_fd, cur_addr->ai_addr, cur_addr->ai_addrlen); + if (err != -1) + socket_bound = true; + else + close(sock_fd); + + cur_addr = cur_addr->ai_next; + } + + freeaddrinfo(client_addr); + return sock_fd; +} + +// Reads and sends back received data to client +// For testing purposes +// static void send_back(int client_fd) +// { +// ssize_t nread = 0; +// char buffer[BUFFER_SIZE]; + +// while ((nread = recv(client_fd, buffer, BUFFER_SIZE, 0)) > 0) +// { +// ssize_t sent; +// while (nread > 0) +// { +// sent = send(client_fd, buffer, nread, 0); +// if (sent == -1) // Send failed +// break; + +// nread -= sent; +// } +// } +// } + +// Retrieves client ipv4 address and stores it in res +// WARNING: res must be of size INET_ADDRSTRLEN +static void get_ip(struct sockaddr *client_addr, char *res) +{ + // NOTE: exceptionally authorized cast + struct in_addr ipAddr = ((struct sockaddr_in *)client_addr)->sin_addr; + inet_ntop(AF_INET, &ipAddr, res, INET_ADDRSTRLEN); +} + +static void signal_handler(int signal) +{ + print_log("EVENT Signal received: %d", signal); + switch (signal) + { + case SIGINT: { + print_log("STOPPING Stopping server..."); + stop_server(); + config_destroy(config); + exit(0); + } + default: + return; + } +} + +// === Functions + +void start_server(struct config *cfg) +{ + config = cfg; + const char *host = config->servers->ip; + const char *port = config->servers->port; + + server_socket = get_socket(host, port); + if (server_socket == -1) + // TODO log that + return; + + int err = listen(server_socket, SOMAXCONN); + if (err == -1) + return; + + // Signal handling + struct sigaction siga; + siga.sa_flags = 0; + siga.sa_handler = signal_handler; + // initialize mask + if (sigemptyset(&siga.sa_mask) < 0) + // TODO log that + return; + if (sigaction(SIGINT, &siga, NULL) == -1 + || sigaction(SIGPIPE, &siga, NULL) == -1) + return; + + // Main loop + while (1) + { + struct sockaddr client_addr; + socklen_t client_addr_len = sizeof(struct sockaddr); + int client_fd = accept(server_socket, &client_addr, &client_addr_len); + if (client_fd == -1) + continue; + + // TODO handle signals to stop + + // Get ip + char client_ip[INET_ADDRSTRLEN]; + get_ip(&client_addr, client_ip); + + handle_request(client_fd, client_ip); + // send_back(client_fd); + close(client_fd); + } + + stop_server(); +} + +void stop_server(void) +{ + close(server_socket); +} diff --git a/src/server/server.h b/src/server/server.h new file mode 100644 index 0000000..0c441ac --- /dev/null +++ b/src/server/server.h @@ -0,0 +1,19 @@ +#ifndef SERVER_H +#define SERVER_H + +#include "../config/config.h" + +/* @brief Starts the HTTP server + * + * @warn Make sure to initialize modules before calling + * + * @param hostname + * @param port + */ +void start_server(struct config *cfg); + +/* @brief Stops the currently running HTTP server + */ +void stop_server(void); + +#endif // ! SERVER_H diff --git a/src/utils/files/files.c b/src/utils/files/files.c new file mode 100644 index 0000000..0f08c25 --- /dev/null +++ b/src/utils/files/files.c @@ -0,0 +1,105 @@ +#define _POSIX_C_SOURCE 200112L + +#include "files.h" + +#include +#include +#include + +// #include "../string/string.h" + +// int file_exists(const char *path) +// {} + +int is_directory(const char *path) +{ + struct stat path_stat; + if (lstat(path, &path_stat) == 0) + { + if (S_ISDIR(path_stat.st_mode)) // Directory + return FILES_DIR; + else if (S_ISREG(path_stat.st_mode)) // Regular file + return FILES_REG; + else + return FILES_OTHER; + } + else + { + if (errno == ENOENT) // File not found + return ERR_FILES_NOT_FOUND; + else // Other errors + return ERR_FILES_FORBIDDEN; + } +} + +// TODO handle logging +// struct string *get_file_content(const char *path) +// { +// // Open file +// FILE *stream = fopen(path, "r"); +// if (stream == NULL) +// return NULL; + +// // Alloc result +// char buf[BUFFER_SIZE]; +// struct string *res = string_create(NULL, 0); +// if (res == NULL) +// { +// return NULL; +// } + +// int nread; +// while ((fgets(buf, BUFFER_SIZE, stream))) +// } + +// TODO not implemented +bool check_filename(struct string *path) +{ + if (path == NULL || path->size <= 0) + return false; + + return true; +} + +ssize_t get_file_content_size(const char *path) +{ + FILE *stream = fopen(path, "r"); + if (stream == NULL) + return -2; + + fseek(stream, 0, SEEK_END); + ssize_t res = ftell(stream); + fclose(stream); + + return res; +} + +int write_to_file(const char *path, struct string *buf) +{ + FILE *stream = fopen(path, "w"); + if (stream == NULL) + return 1; + + fwrite(buf->data, sizeof(char), buf->size, stream); + + fclose(stream); + + return 0; +} + +int write_pid(const char *filepath, int pid) +{ + FILE *stream = fopen(filepath, "w"); + if (stream == NULL) + return 1; + + if (fprintf(stream, "%d", pid) <= 0) + { + fclose(stream); + return 1; + } + + fclose(stream); + + return 0; +} diff --git a/src/utils/files/files.h b/src/utils/files/files.h new file mode 100644 index 0000000..0c2cdcc --- /dev/null +++ b/src/utils/files/files.h @@ -0,0 +1,109 @@ +#ifndef FILES_H +#define FILES_H + +// === Definitions + +#define BUFFER_SIZE 1024 + +// Return codes +#define FILES_REG 0 +#define FILES_DIR 1 +#define FILES_OTHER 8 + +// Errors +#define ERR_FILES_NOT_FOUND -1 +#define ERR_FILES_FORBIDDEN -2 + +// === Includes + +#include +#include +#include + +#include "../string/string.h" + +// === Functions + +/* + * @brief + * + * @param path + * + * @return + */ +// bool file_exists(const char *path); + +/* + * @brief + * + * @param path + * + * @return Returns the corresponding return value / error code (see header + * definitions) + */ +int is_directory(const char *path); + +/* + * @brief + * + * @param path + * + * @return + */ +// char *get_file(const char *path); + +/* + * @brief + * + * @param path + * + * @return + */ +struct string *get_file_content(const char *path); + +/* + * @brief + * + * @param path + * + * @return + */ +bool check_filename(struct string *path); + +/* + * @brief + * + * @param filename + * + * @return + */ +// bool sanitize_filename(struct string *filename); + +/* + * @brief + * + * @param path + * + * @return + */ +ssize_t get_file_content_size(const char *path); + +/* + * @brief + * + * @param path + * + * @return 0 on success, the corresponding error code otherwise + */ +int write_to_file(const char *path, struct string *buf); + +/* + * @brief + * + * @param path + * + * @return 0 on success, the corresponding error code otherwise + */ +int write_pid(const char *filepath, int pid); + +#endif // ! FILES_H diff --git a/src/utils/parsing/blanks.c b/src/utils/parsing/blanks.c new file mode 100644 index 0000000..ac7c20f --- /dev/null +++ b/src/utils/parsing/blanks.c @@ -0,0 +1,46 @@ +#include "blanks.h" + +bool is_space(char c) +{ + switch (c) + { + case ' ': + case '\t': + return true; + + default: + return false; + } +} + +bool is_blank(char c) +{ + switch (c) + { + case '\f': + case '\n': + case '\r': + case '\t': + case '\v': + case ' ': + return true; + + default: + return false; + } +} + +ssize_t skip_blanks(struct string *str, size_t offset) +{ + size_t i = offset; + while (i < str->size) + { + if (!is_space(str->data[i])) + return i; + + i++; + } + + // Reached EOL + return i; +} diff --git a/src/utils/parsing/blanks.h b/src/utils/parsing/blanks.h new file mode 100644 index 0000000..417dc0a --- /dev/null +++ b/src/utils/parsing/blanks.h @@ -0,0 +1,39 @@ +#ifndef BLANKS_H +#define BLANKS_H + +// === Definitions + +#define BLANKS_LIST "\f\n\r\t\v " + +// === Includes + +#include +#include +#include + +#include "../string/string.h" + +// === Functions +/* + * NOTE: + * As ispace(3) and isblank(3) functions from stdlib can be confusing and not + * always best suited for what I want to do, I decided to reimplement them. + * So be warned, they won't have the same behavior as their libc counterparts. + */ + +/* + * Doc: TODO + */ +bool is_space(char c); + +/* + * Doc: TODO + */ +bool is_blank(char c); + +/* + * Doc: TODO + */ +ssize_t skip_blanks(struct string *str, size_t offset); + +#endif // ! BLANKS_H diff --git a/src/utils/parsing/words.c b/src/utils/parsing/words.c new file mode 100644 index 0000000..cea5a79 --- /dev/null +++ b/src/utils/parsing/words.c @@ -0,0 +1,107 @@ +#include "words.h" + +#include "blanks.h" + +bool str_contains(const char *str, char c) +{ + int i = 0; + while (str[i] != '\0') + { + if (str[i] == c) + return true; + + i++; + } + + return false; +} + +ssize_t read_word(struct string *str, size_t offset, struct string **res) +{ + size_t i = offset; + while (i < str->size) + { + if (is_blank(str->data[i])) + break; + + i++; + } + + *res = string_create(str->data + offset, i - offset); + if (res == NULL) + return -1; + + return i - offset; +} + +ssize_t read_word_delim(struct string *str, size_t offset, struct string **res, + const char *delims) +{ + size_t i = offset; + bool found = false; + + while (i < str->size && !found) + { + if (str_contains(delims, str->data[i])) + { + found = true; + break; + } + + i++; + } + + *res = string_create(str->data + offset, i - offset); + if (res == NULL) + return -1; + + return i - offset; +} + +ssize_t read_word_restrict(struct string *str, size_t offset, + struct string **res, const char *restr) +{ + size_t i = offset; + bool found = false; + + while (i < str->size && !found) + { + if (!str_contains(restr, str->data[i])) + { + found = true; + break; + } + + i++; + } + + *res = string_create(str->data + offset, i - offset); + if (res == NULL) + return -1; + + return i - offset; +} + +ssize_t read_word_predicate(struct string *str, size_t offset, + struct string **res, bool (*predicate)(char c)) +{ + size_t i = offset; + bool found = false; + + while (i < str->size && !found) + { + if (predicate(str->data[i])) + { + found = true; + break; + } + + i++; + } + + *res = string_create(str->data + offset, i - offset); + if (res == NULL) + return -1; + + return i - offset; +} diff --git a/src/utils/parsing/words.h b/src/utils/parsing/words.h new file mode 100644 index 0000000..d259bff --- /dev/null +++ b/src/utils/parsing/words.h @@ -0,0 +1,42 @@ +#ifndef WORDS_H +#define WORDS_H + +// === Includes + +#include +#include +#include + +#include "../string/string.h" + +// === Functions + +/* + * Doc: TODO + */ +bool str_contains(const char *str, char c); + +/* + * Doc: TODO + */ +ssize_t read_word(struct string *str, size_t offset, struct string **res); + +/* + * Doc: TODO + */ +ssize_t read_word_delim(struct string *str, size_t offset, struct string **res, + const char *delims); + +/* + * Doc: TODO + */ +ssize_t read_word_restrict(struct string *str, size_t offset, + struct string **res, const char *restr); + +/* + * Doc: TODO + */ +ssize_t read_word_predicate(struct string *str, size_t offset, + struct string **res, bool (*predicate)(char c)); + +#endif // ! WORDS_H diff --git a/src/utils/string/string.c b/src/utils/string/string.c new file mode 100644 index 0000000..5a28b5a --- /dev/null +++ b/src/utils/string/string.c @@ -0,0 +1,175 @@ +#include "string.h" + +#include +#include +#include + +struct string *string_create(const char *str, size_t size) +{ + struct string *res = calloc(1, sizeof(struct string)); + if (res == NULL) + return NULL; + + if (size > 0) + { + res->data = malloc(size * sizeof(char)); + if (res->data == NULL) + { + free(res); + return NULL; + } + + memcpy(res->data, str, size); + } + + res->size = size; + return res; +} + +int string_compare_n_str(const struct string *str1, const char *str2, size_t n) +{ + size_t i = 0; + int res = 0; + char reached_str2_end = 0; + while (i < n) + { + if (i < str1->size) + res += str1->data[i]; + + if (!reached_str2_end) + { + if (str2[i] == '\0') + reached_str2_end = 1; + else + res -= str2[i]; + } + + i++; + } + + return res; +} + +void string_concat_str(struct string *str, const char *to_concat, size_t size) +{ + size_t new_size = str->size + size; + size_t str_size = str->size; + + if (new_size == 0) + return; + + str->size = new_size; + if (str_size == 0) + { + str->data = malloc(new_size); + if (str->data == NULL) + return; // Handle ? + } + else + { + str->data = realloc(str->data, new_size * sizeof(char)); + if (str->data == NULL) + return; // Handle ? + } + + for (size_t i = 0; i < size; i++) + { + str->data[str_size + i] = to_concat[i]; + } +} + +void str_concat_string(const char *str, size_t size, struct string *to_concat) +{ + size_t new_size = to_concat->size + size; + size_t tmp_size = to_concat->size; + + if (new_size == 0) + return; + + to_concat->size = new_size; + if (tmp_size == 0) + { + to_concat->data = malloc(new_size); + if (to_concat->data == NULL) + return; // Handle ? + } + else + { + // Temporary buffer + char *tmp = malloc(tmp_size * sizeof(char)); + if (tmp == NULL) + return; // Handle ? + + // (Duplicate) + memcpy(tmp, to_concat->data, tmp_size); + + // Reallocate string + char *new_data = realloc(to_concat->data, new_size * sizeof(char)); + if (to_concat->data == NULL) + { + to_concat->size = tmp_size; // Restore (original ptr still valid) + return; // Handle ? + } + to_concat->data = new_data; + + memcpy(to_concat->data, str, size); + memcpy(to_concat->data + size, tmp, tmp_size); + free(tmp); + } +} + +void string_to_lowercase(struct string *str) +{ + for (size_t i = 0; i < str->size; i++) + { + if (isupper(str->data[i])) + str->data[i] += 'a' - 'A'; + } +} + +void string_destroy(struct string *str) +{ + if (str != NULL) + { + if (str->data != NULL) + { + free(str->data); + } + free(str); + } +} + +char *string_to_charptr(struct string *str) +{ + if (str == NULL || str->data == NULL) + return NULL; + + char *res = calloc(str->size + 1, sizeof(char)); + if (res == NULL) + return NULL; + + memcpy(res, str->data, str->size); + + res[str->size] = '\0'; + + return res; +} + +// WARNING takes n as valid, will not stop on '\0' +int string_compare_strictly_n_str(const struct string *str1, const char *str2, + size_t n) +{ + if (str1->size < n) + return -1; + + size_t i = 0; + int res = 0; + while (i < n) + { + res += str1->data[i]; + res -= str2[i]; + i++; + } + + return res; +} diff --git a/httpd/src/utils/string/string.h b/src/utils/string/string.h similarity index 52% rename from httpd/src/utils/string/string.h rename to src/utils/string/string.h index afe6d9d..a69f47c 100644 --- a/httpd/src/utils/string/string.h +++ b/src/utils/string/string.h @@ -42,6 +42,48 @@ int string_compare_n_str(const struct string *str1, const char *str2, size_t n); */ void string_concat_str(struct string *str, const char *to_concat, size_t size); +/* + ** @brief Similar to string_concat_str but with str at the beginning of the + * result string + ** + ** @param str + ** @param to_concat + ** @param size + */ +void str_concat_string(const char *str, size_t size, struct string *to_concat); + +/* + ** @brief Concat a char * with its size in a struct string + ** + ** @param str + */ +void string_to_lowercase(struct string *str); + +/* + ** @brief Free all string content + ** + ** @param str + */ void string_destroy(struct string *str); +/* + ** @brief Converts the string to the native C implementation + ** + ** @param str + ** + ** @return a pointer to an allocated memory zone containing the string + */ +char *string_to_charptr(struct string *str); + +/* + ** @brief TODO + ** + ** @param str1 + ** @param str2 + ** @param n + ** + ** @return + */ +int string_compare_strictly_n_str(const struct string *str1, const char *str2, + size_t n); #endif /* ! STRING_H */ diff --git a/src/utils/time/fmt_time.c b/src/utils/time/fmt_time.c new file mode 100644 index 0000000..cc37959 --- /dev/null +++ b/src/utils/time/fmt_time.c @@ -0,0 +1,15 @@ +#include "fmt_time.h" + +#include +#include + +char *get_time(void) +{ + char *buf = malloc(64 * sizeof(char)); // Oui, 64 + time_t local_ts = time(NULL); + struct tm *gmt_time = gmtime(&local_ts); + + // return asctime(gmt_time); + strftime(buf, 64, "%a, %d %b %Y %H:%M:%S %Z", gmt_time); + return buf; +} diff --git a/src/utils/time/fmt_time.h b/src/utils/time/fmt_time.h new file mode 100644 index 0000000..81d5bd8 --- /dev/null +++ b/src/utils/time/fmt_time.h @@ -0,0 +1,11 @@ +#ifndef FMT_TIME_H +#define FMT_TIME_H + +/* @brief Calculates the GMT time based on machine's local time + * and returns it as a string + * + * @return A NULL-terminated string containing the fromatted GMT time + */ +char *get_time(); + +#endif // ! FMT_TIME_H diff --git a/httpd/config.txt b/tests/config.txt similarity index 75% rename from httpd/config.txt rename to tests/config.txt index c35b687..77d4974 100644 --- a/httpd/config.txt +++ b/tests/config.txt @@ -1,9 +1,11 @@ [global] log = true pid_file = /tmp/HTTPd.pid +daemon = start [[vhosts]] server_name = my_server ip = 127.0.0.1 port = 6996 root_dir = . +default_file='/dev/null' diff --git a/httpd/config_reader.sh b/tests/config_reader.sh similarity index 100% rename from httpd/config_reader.sh rename to tests/config_reader.sh diff --git a/tests/test_root_dir/index.html b/tests/test_root_dir/index.html new file mode 100644 index 0000000..f5bf13f --- /dev/null +++ b/tests/test_root_dir/index.html @@ -0,0 +1 @@ +

YEAAH

diff --git a/tests/test_suite.py b/tests/test_suite.py new file mode 100644 index 0000000..504f05a --- /dev/null +++ b/tests/test_suite.py @@ -0,0 +1,178 @@ +import subprocess as sp +import http +import requests +import socket +import pytest +import time + +host = "127.0.0.1" +port = "6994" + +executable = "./httpd" + +def spawn_httpd(stdout_filename, args=[]): + with open(stdout_filename,"w") as f: + httpd_proc = sp.Popen([executable,"--pid_file","/tmp/HTTPd.pid","--ip",host,"--port", port, "--root_dir","test_root_dir/","--server_name","httpd"] if args == [] else [executable] + args, stdout=f,stderr=sp.PIPE,bufsize=0) + time.sleep(0.2) + + return httpd_proc + +def kill_httpd(proc): + #proc.send_signal(sp.SIGINT) + proc.kill() + +# @pytest.mark.timeout(2) +def test_bad_config(): + proc = spawn_httpd("out.log", ["hello","world"]) + proc.wait(1) + try: + assert proc.returncode == 2 + finally: + kill_httpd(proc) + +# @pytest.mark.timeout(2) +def test_get_index(): + proc = spawn_httpd("out.log") + req = requests.get(f"http://{host}:{port}/index.html") + assert req.status_code == 200 + with open("./test_root_dir/index.html","r") as f: + try: + assert f.read() == req.text + finally: + kill_httpd(proc) + +# @pytest.mark.timeout(2) +def test_get_default(): + proc = spawn_httpd("out.log") + req = requests.get(f"http://{host}:{port}/") + assert req.status_code == 200 + with open("./test_root_dir/index.html","r") as f: + try: + assert f.read() == req.text + finally: + kill_httpd(proc) + +# @pytest.mark.timeout(2) +def test_no_file(): + proc = spawn_httpd("out.log") + req = requests.get(f"http://{host}:{port}/notindex.html") + assert req.status_code == 404 + +# @pytest.mark.timeout(2) +def test_bad_request(): + proc = spawn_httpd("out.log") + sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + sock.connect((host,int(port))) + + request = f"GET /index.html FTP/1.1\r\nHOST: {host}:{port}\r\nConnection: close\r\n\r\n" + + sock.sendall(request.encode()) + + resp = sock.recv(1024) + resp_decoded = resp.decode() + + try: + assert "400 Bad Request" in resp_decoded + finally: + kill_httpd(proc) + +# @pytest.mark.timeout(2) +def test_invalid_method(): + proc = spawn_httpd("out.log") + sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + sock.connect((host,int(port))) + + request = f"PUT /index.html HTTP/1.1\r\nHOST: {host}:{port}\r\nConnection: close\r\n\r\n" + + sock.sendall(request.encode()) + + response = http.client.HTTPResponse(sock) + response.begin() + + try: + assert response.status == 405 + finally: + kill_httpd(proc) + + +# @pytest.mark.timeout(2) +def test_invalid_version(): + proc = spawn_httpd("out.log") + sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + sock.connect((host,int(port))) + + request = f"GET /index.html HTTP/1.2\r\nHOST: {host}:{port}\r\nConnection: close\r\n\r\n" + + sock.sendall(request.encode()) + + response = http.client.HTTPResponse(sock) + response.begin() + + try: + assert response.status == 505 + finally: + kill_httpd(proc) + +# @pytest.mark.timeout(2) +def test_bad_request(): + proc = spawn_httpd("out.log") + sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + sock.connect((host,int(port))) + + request = f"GET /index.html FTP/1.1\r\nHOST: {host}:{port}\r\nConnection: close\r\n\r\n" + + sock.sendall(request.encode()) + + response = http.client.HTTPResponse(sock) + response.begin() + + try: + assert response.status == 400 + finally: + kill_httpd(proc) + +@pytest.mark.timeout(2) +def test_head_index(): + proc = spawn_httpd("out.log") + try: + req = requests.head(f"http://{host}:{port}/index.html") + assert req.status_code == 200 + assert req.text == "" + finally: + kill_httpd(proc) + +@pytest.mark.timeout(2) +def test_missing_host(): + proc = spawn_httpd("out.log") + sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + sock.connect((host,int(port))) + + request = f"GET /index.html HTTP/1.1\r\nConnection: close\r\n\r\n" + + sock.sendall(request.encode()) + + response = http.client.HTTPResponse(sock) + response.begin() + + try: + assert response.status == 400 + finally: + kill_httpd(proc) + +@pytest.mark.timeout(2) +def test_directory_traversal(): + proc = spawn_httpd("out.log") + sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + sock.connect((host,int(port))) + + request = f"GET /../test_suite.py HTTP/1.1\r\nHOST: {host}:{port}\r\nConnection: close\r\n\r\n" + + sock.sendall(request.encode()) + + response = http.client.HTTPResponse(sock) + response.begin() + + try: + assert response.status in [400, 403, 404] + finally: + kill_httpd(proc) diff --git a/tests/tests_mieux.sh b/tests/tests_mieux.sh new file mode 100755 index 0000000..6851190 --- /dev/null +++ b/tests/tests_mieux.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# Simple test script for HTTP/1.1 Host header compliance +# Usage: ./test_host_compliance.sh [IP] [PORT] + +IP=${1:-"127.0.0.1"} +PORT=${2:-"6996"} + +echo "Targeting server at $IP:$PORT" + +test_req() { + NAME="$1" + PAYLOAD="$2" + EXPECTED="$3" + + echo -n "Test: $NAME ... " + # Send payload, wait max 1s for response + RESP=$(printf "$PAYLOAD" | nc -w 1 $IP $PORT 2>/dev/null | head -n 1) + + if echo "$RESP" | grep -q "$EXPECTED"; then + echo "PASS" + else + echo "FAIL (Expected '$EXPECTED', got '$RESP')" + fi +} + +# 1. Valid Request +test_req "Valid Request" "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n" "200 OK" + +# 2. Missing Host Header +test_req "Missing Host" "GET / HTTP/1.1\r\n\r\n" "400 Bad Request" + +# 3. Empty Host Header +test_req "Empty Host" "GET / HTTP/1.1\r\nHost:\r\n\r\n" "400 Bad Request" + +# 4. Multiple Host Headers +test_req "Multiple Hosts" "GET / HTTP/1.1\r\nHost: a\r\nHost: b\r\n\r\n" "400 Bad Request" + +# 5. Bad Protocol Version (Should be 505 now with the fix) +test_req "Bad Protocol (HTTP/1.0)" "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n" "505" \ No newline at end of file