diff --git a/README.md b/README.md index 9fc7d4b..c5304f5 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ And then open the `docs/html/index.html`. ## Roadmap - Implement Keep-Alive +- Custom web pages (404) - Support for virtual hosts - HTTPS support with TLS diff --git a/include/http/http.h b/include/http/http.h index 38262a2..a783b35 100644 --- a/include/http/http.h +++ b/include/http/http.h @@ -1,6 +1,8 @@ #ifndef CWS_HTTP_H #define CWS_HTTP_H +#include + #include "utils/config.h" #include "utils/hashmap.h" @@ -16,6 +18,12 @@ typedef enum cws_http_method_t { CWS_HTTP_HEAD, } cws_http_method; +typedef enum cws_http_status_t { + CWS_HTTP_OK, + CWS_HTTP_NOT_FOUND, + CWS_HTTP_NOT_IMPLEMENTED, +} cws_http_status; + /** * @brief HTTP request struct * @@ -40,12 +48,21 @@ typedef struct cws_http_t { */ cws_http *cws_http_parse(char *request_str, int sockfd, cws_config *config); -void cws_http_parse_method(cws_http *request, const char *method); +int cws_http_parse_method(cws_http *request, const char *method); void cws_http_get_content_type(cws_http *request, char *content_type); +char *cws_http_status_string(cws_http_status status); -void cws_http_send_response(cws_http *request); -void cws_http_send_not_found(cws_http *request); -void cws_http_send_not_implemented(cws_http *request); +/** + * @brief Build the http response + * + * @return Returns the size of the response + */ +size_t cws_http_response_builder(char **response, char *http_version, cws_http_status status, char *content_type, char *connection, char *body, + size_t body_len_bytes); + +void cws_http_send_response(cws_http *request, cws_http_status status); +void cws_http_send_resource(cws_http *request); +void cws_http_send_error_page(cws_http *request, cws_http_status status, char *title, char *description); void cws_http_free(cws_http *request); diff --git a/include/server/server.h b/include/server/server.h index bc4fd3d..9a4d63a 100644 --- a/include/server/server.h +++ b/include/server/server.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "utils/config.h" @@ -19,7 +20,7 @@ #define CWS_SERVER_EPOLL_TIMEOUT -1 /* Main server loop */ -extern volatile bool cws_server_run; +extern volatile sig_atomic_t cws_server_run; /** * @brief Runs the server diff --git a/include/utils/colors.h b/include/utils/colors.h index 496abc7..5373b3d 100644 --- a/include/utils/colors.h +++ b/include/utils/colors.h @@ -22,7 +22,7 @@ #endif #ifdef EVELOPER -#define CWS_LOG_DEBUG(msg, ...) fprintf(stdout, _DEBUG " " msg "\n", ##__VA_ARGS__) +#define CWS_LOG_DEBUG(msg, ...) fprintf(stdout, _DEBUG " [%s:%d] " msg "\n", __FILE__, __LINE__, ##__VA_ARGS__) #else #define CWS_LOG_DEBUG(msg, ...) #endif diff --git a/src/http/http.c b/src/http/http.c index 3d85a7e..435d579 100644 --- a/src/http/http.c +++ b/src/http/http.c @@ -9,33 +9,52 @@ #include "utils/utils.h" cws_http *cws_http_parse(char *request_str, int sockfd, cws_config *config) { - cws_http *request = malloc(sizeof(cws_http)); + if (!request_str || !config) { + return NULL; + } + + cws_http *request = calloc(1, sizeof(cws_http)); if (request == NULL) { return NULL; } - memset(request, 0, sizeof(cws_http)); - - /* Insert socket file descriptor */ request->sockfd = sockfd; + char *request_str_cpy = strdup(request_str); + if (!request_str_cpy) { + free(request); + return NULL; + } + + char *saveptr = NULL; + char *pch = NULL; + /* Parse HTTP method */ - char *pch = strtok(request_str, " "); + pch = strtok_r(request_str_cpy, " ", &saveptr); if (pch == NULL) { cws_http_free(request); + free(request_str_cpy); return NULL; } - CWS_LOG_DEBUG("[http] method: %s", pch); - cws_http_parse_method(request, pch); + CWS_LOG_DEBUG("method: %s", pch); + int ret = cws_http_parse_method(request, pch); + if (ret < 0) { + /* Not implemented */ + cws_http_free(request); + free(request_str_cpy); + + cws_http_send_error_page(request, CWS_HTTP_NOT_IMPLEMENTED, "501 Not Implemented", "501 Not Implemented"); + } /* Parse location */ - pch = strtok(NULL, " "); + pch = strtok_r(NULL, " ", &saveptr); if (pch == NULL) { cws_http_free(request); + free(request_str_cpy); return NULL; } - CWS_LOG_DEBUG("[http] location: %s", pch); + CWS_LOG_DEBUG("location: %s", pch); strncpy(request->location, pch, CWS_HTTP_LOCATION_LEN); /* Adjust location path */ @@ -44,16 +63,17 @@ cws_http *cws_http_parse(char *request_str, int sockfd, cws_config *config) { } else { snprintf(request->location_path, CWS_HTTP_LOCATION_PATH_LEN, "%s%s", config->www, request->location); } - CWS_LOG_DEBUG("[http] location path: %s", request->location_path); + CWS_LOG_DEBUG("location path: %s", request->location_path); /* Parse HTTP version */ - pch = strtok(NULL, " \r\n"); + pch = strtok_r(NULL, " \r\n", &saveptr); if (pch == NULL) { cws_http_free(request); + free(request_str_cpy); return NULL; } - CWS_LOG_DEBUG("[http] version: %s", pch); + CWS_LOG_DEBUG("version: %s", pch); strncpy(request->http_version, pch, CWS_HTTP_VERSION_LEN); /* Parse headers until a \r\n */ @@ -61,7 +81,7 @@ cws_http *cws_http_parse(char *request_str, int sockfd, cws_config *config) { char *header_colon; while (pch) { /* Get header line */ - pch = strtok(NULL, "\r\n"); + pch = strtok_r(NULL, "\r\n", &saveptr); if (pch == NULL) { break; } @@ -83,29 +103,90 @@ cws_http *cws_http_parse(char *request_str, int sockfd, cws_config *config) { cws_hm_set(request->headers, hkey_dup, hvalue_dup); } - /* Parse body */ + /* TODO: Parse body */ return request; } -void cws_http_parse_method(cws_http *request, const char *method) { +int cws_http_parse_method(cws_http *request, const char *method) { if (strcmp(method, "GET") == 0) { request->method = CWS_HTTP_GET; - return; + return 0; } + if (strcmp(method, "POST") == 0) { request->method = CWS_HTTP_POST; - return; + return 0; } - cws_http_send_not_implemented(request); + return -1; } -void cws_http_send_response(cws_http *request) { +char *cws_http_status_string(cws_http_status status) { + switch (status) { + case CWS_HTTP_OK: { + return "200 OK"; + break; + } + case CWS_HTTP_NOT_FOUND: { + return "404 Not Found"; + break; + } + case CWS_HTTP_NOT_IMPLEMENTED: { + return "501 Not Implemented"; + break; + } + } + + return "?"; +} + +size_t cws_http_response_builder(char **response, char *http_version, cws_http_status status, char *content_type, char *connection, char *body, + size_t body_len_bytes) { + char *status_code = cws_http_status_string(status); + + size_t header_len = snprintf(NULL, 0, + "%s %s\r\n" + "Content-Type: %s\r\n" + "Content-Length: %ld\r\n" + "Connection: %s\r\n" + "\r\n", + http_version, status_code, content_type, body_len_bytes, connection); + + size_t total_len = header_len + body_len_bytes; + + *response = (char *)malloc(total_len); + if (*response == NULL) { + return 0; + } + + snprintf(*response, header_len + 1, "%s %s\r\nContent-Type: %s\r\nContent-Length: %ld\r\nConnection: %s\r\n\r\n", http_version, status_code, content_type, + body_len_bytes, connection); + + memcpy(*response + header_len, body, body_len_bytes); + + return total_len; +} + +void cws_http_send_response(cws_http *request, cws_http_status status) { + switch (status) { + case CWS_HTTP_OK: + break; + case CWS_HTTP_NOT_FOUND: { + cws_http_send_error_page(request, CWS_HTTP_NOT_FOUND, "404 Not Found", "Resource not found, 404."); + break; + } + case CWS_HTTP_NOT_IMPLEMENTED: { + cws_http_send_error_page(request, CWS_HTTP_NOT_IMPLEMENTED, "501 Not Implemented", "Method not implemented, 501."); + break; + } + } +} + +void cws_http_send_resource(cws_http *request) { FILE *file = fopen(request->location_path, "rb"); if (file == NULL) { - /* 404 */ - cws_http_send_not_found(request); + cws_http_send_response(request, CWS_HTTP_NOT_FOUND); return; } @@ -115,7 +196,7 @@ void cws_http_send_response(cws_http *request) { /* Retrieve file size */ fseek(file, 0, SEEK_END); - const size_t content_length = ftell(file); /* Returns the read bytes (we're at the end, so the file size) */ + const size_t content_length = ftell(file); rewind(file); /* Retrieve file data */ @@ -127,28 +208,33 @@ void cws_http_send_response(cws_http *request) { } /* Read 1 byte until content_length from file and put in file_data */ - fread(file_data, 1, content_length, file); + size_t read_bytes = fread(file_data, 1, content_length, file); fclose(file); - char response_header[2048]; - snprintf(response_header, sizeof response_header, - "%s 200 OK\r\n" - "Content-Type: %s\r\n" - "Content-Length: %zu\r\n" - "Connection: close\r\n" - "\r\n", - request->http_version, content_type, content_length); + if (read_bytes != content_length) { + free(file_data); + CWS_LOG_ERROR("Partial read from file."); + return; + } - send(request->sockfd, response_header, strlen(response_header), 0); - send(request->sockfd, file_data, content_length, 0); + char *response = NULL; + size_t response_len = cws_http_response_builder(&response, "HTTP/1.1", CWS_HTTP_OK, content_type, "close", file_data, content_length); + CWS_LOG_DEBUG("%s", response); + size_t bytes_sent = 0; + do { + size_t sent = send(request->sockfd, response + bytes_sent, response_len, 0); + bytes_sent += sent; + } while (bytes_sent < response_len); + + free(response); free(file_data); } void cws_http_get_content_type(cws_http *request, char *content_type) { char *ptr = strrchr(request->location_path, '.'); if (ptr == NULL) { - cws_http_send_not_found(request); + cws_http_send_error_page(request, CWS_HTTP_NOT_FOUND, "404 Not Found", "404 Not Found."); return; } ptr += 1; @@ -167,46 +253,26 @@ void cws_http_get_content_type(cws_http *request, char *content_type) { snprintf(content_type, 1024, "%s/%s", ct, ptr); } -void cws_http_send_not_implemented(cws_http *request) { - const char response[1024] = - "HTTP/1.1 501 Not Implemented\r\n" - "Content-Type: text/html\r\n" - "Content-Length: 216\r\n" - "\r\n" - "\n" - "\n" - " 501 Not Implemented\n" - "\n" - "\n" - "

501 Not Implemented

\n" - "\n" - ""; - const size_t response_len = strlen(response); - send(request->sockfd, response, response_len, 0); -} - -void cws_http_send_not_found(cws_http *request) { - const char html_body[] = - "\n" - "\n" - " 404 Not Found\n" - "\n" - "\n" - "

404 Not Found.

\n" - "\n" - ""; - int html_body_len = strlen(html_body); - - char response[1024]; - int response_len = snprintf(response, sizeof(response), - "HTTP/1.1 404 Not Found\r\n" - "Content-Type: text/html\r\n" - "Content-Length: %d\r\n" - "\r\n" - "%s", - html_body_len, html_body); - +void cws_http_send_error_page(cws_http *request, cws_http_status status, char *title, char *description) { + char body[512]; + memset(body, 0, sizeof(body)); + + snprintf(body, sizeof(body), + "\n" + "\n" + " %s\n" + "\n" + "\n" + "

%s

\n" + "" + "", + title, description); + size_t body_len = strlen(body) * sizeof(char); + + char *response = NULL; + size_t response_len = cws_http_response_builder(&response, "HTTP/1.1", status, "text/html", "close", body, body_len); send(request->sockfd, response, response_len, 0); + free(response); } void cws_http_free(cws_http *request) { diff --git a/src/main.c b/src/main.c index 7409dd5..d66d2ec 100644 --- a/src/main.c +++ b/src/main.c @@ -7,31 +7,29 @@ #include "utils/colors.h" #include "utils/config.h" -void cws_signal_handler(int signo) { cws_server_run = false; } +void cws_signal_handler(int signo) { cws_server_run = 0; } -int main(int argc, char **argv) { - int ret; - - struct sigaction act = {.sa_handler = cws_signal_handler}; - ret = sigaction(SIGINT, &act, NULL); - if (ret) { +int main(void) { + struct sigaction act = {.sa_handler = cws_signal_handler, .sa_flags = 0, .sa_mask = {{0}}}; + if (sigaction(SIGINT, &act, NULL)) { CWS_LOG_ERROR("sigaction(): %s", strerror(errno)); - return 1; + return EXIT_FAILURE; } cws_config *config = cws_config_init(); - if (config == NULL) { + if (!config) { CWS_LOG_ERROR("Unable to read config file"); - return 1; + return EXIT_FAILURE; } CWS_LOG_INFO("Running cws on http://%s:%s...", config->hostname, config->port); - ret = cws_server_start(config); + int ret = cws_server_start(config); if (ret < 0) { CWS_LOG_ERROR("Unable to start web server"); } + CWS_LOG_INFO("Shutting down cws..."); cws_config_free(config); - return 0; + return EXIT_SUCCESS; } diff --git a/src/server/server.c b/src/server/server.c index 7390f3f..eace65f 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -17,7 +17,7 @@ #include "utils/colors.h" #include "utils/utils.h" -volatile bool cws_server_run = 1; +volatile sig_atomic_t cws_server_run = 1; int cws_server_start(cws_config *config) { struct addrinfo hints; @@ -28,32 +28,32 @@ int cws_server_start(cws_config *config) { int status = getaddrinfo(config->hostname, config->port, &hints, &res); if (status != 0) { CWS_LOG_ERROR("getaddrinfo() error: %s", gai_strerror(status)); - exit(EXIT_FAILURE); + return -1; } int sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd < 0) { CWS_LOG_ERROR("socket(): %s", strerror(errno)); - exit(EXIT_FAILURE); + return -1; } const int opt = 1; status = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt); if (status != 0) { CWS_LOG_ERROR("setsockopt(): %s", strerror(errno)); - exit(EXIT_FAILURE); + return -1; } status = bind(sockfd, res->ai_addr, res->ai_addrlen); if (status != 0) { CWS_LOG_ERROR("bind(): %s", strerror(errno)); - exit(EXIT_FAILURE); + return -1; } status = listen(sockfd, CWS_SERVER_BACKLOG); if (status != 0) { CWS_LOG_ERROR("listen(): %s", strerror(status)); - exit(EXIT_FAILURE); + return -1; } cws_server_loop(sockfd, config); @@ -125,6 +125,7 @@ void cws_server_loop(int sockfd, cws_config *config) { cws_server_close_client(epfd, client_fd, clients); continue; } + if (bytes_read < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) { /* Error during read, handle it (close client) */ @@ -134,18 +135,15 @@ void cws_server_loop(int sockfd, cws_config *config) { continue; } - data[bytes_read] = '\0'; - /* Parse HTTP request */ cws_http *request = cws_http_parse(data, client_fd, config); if (request == NULL) { cws_server_close_client(epfd, client_fd, clients); - cws_http_free(request); continue; } - cws_http_send_response(request); + cws_http_send_resource(request); CWS_LOG_INFO("Client (%s) disconnected", ip); cws_server_close_client(epfd, client_fd, clients); cws_http_free(request); @@ -188,7 +186,7 @@ void cws_fd_set_nonblocking(int sockfd) { const int status = fcntl(sockfd, F_SETFL, O_NONBLOCK); if (status == -1) { - CWS_LOG_ERROR("fcntl(): %s", gai_strerror(status)); + CWS_LOG_ERROR("fcntl(): %s", strerror(errno)); exit(EXIT_FAILURE); } } diff --git a/src/utils/config.c b/src/utils/config.c index beb6ee1..26d5bfd 100644 --- a/src/utils/config.c +++ b/src/utils/config.c @@ -8,14 +8,12 @@ static const cyaml_config_t cyaml_config = { .log_level = CYAML_LOG_WARNING, }; -static const cyaml_schema_field_t top_mapping_schema[] = { - CYAML_FIELD_STRING_PTR("hostname", CYAML_FLAG_POINTER, cws_config, hostname, 0, CYAML_UNLIMITED), - CYAML_FIELD_STRING_PTR("port", CYAML_FLAG_POINTER, cws_config, port, 0, CYAML_UNLIMITED), - CYAML_FIELD_STRING_PTR("www", CYAML_FLAG_POINTER, cws_config, www, 0, CYAML_UNLIMITED), - CYAML_FIELD_STRING_PTR("cert", CYAML_FLAG_POINTER, cws_config, cert, 0, CYAML_UNLIMITED), - CYAML_FIELD_STRING_PTR("key", CYAML_FLAG_POINTER, cws_config, key, 0, CYAML_UNLIMITED), - CYAML_FIELD_END -}; +static const cyaml_schema_field_t top_mapping_schema[] = {CYAML_FIELD_STRING_PTR("hostname", CYAML_FLAG_POINTER, cws_config, hostname, 0, CYAML_UNLIMITED), + CYAML_FIELD_STRING_PTR("port", CYAML_FLAG_POINTER, cws_config, port, 0, CYAML_UNLIMITED), + CYAML_FIELD_STRING_PTR("www", CYAML_FLAG_POINTER, cws_config, www, 0, CYAML_UNLIMITED), + CYAML_FIELD_STRING_PTR("cert", CYAML_FLAG_POINTER, cws_config, cert, 0, CYAML_UNLIMITED), + CYAML_FIELD_STRING_PTR("key", CYAML_FLAG_POINTER, cws_config, key, 0, CYAML_UNLIMITED), + CYAML_FIELD_END}; static const cyaml_schema_value_t top_schema = { CYAML_VALUE_MAPPING(CYAML_FLAG_POINTER, cws_config, top_mapping_schema),