diff --git a/include/http/handler.h b/include/http/handler.h new file mode 100644 index 0000000..6c7c21b --- /dev/null +++ b/include/http/handler.h @@ -0,0 +1,20 @@ +#ifndef CWS_HANDLER_H +#define CWS_HANDLER_H + +#include "http/request.h" +#include "http/response.h" + +/* Configuration for static file serving */ +typedef struct cws_handler_config { + const char *root_dir; /*< "www" */ + const char *index_file; /*< "index.html" */ +} cws_handler_config_s; + +/* Static file handler */ +cws_response_s *cws_handler_static_file(cws_request_s *request, cws_handler_config_s *config); + +/* Error handlers */ +cws_response_s *cws_handler_not_found(cws_request_s *request); +cws_response_s *cws_handler_not_implemented(cws_request_s *request); + +#endif diff --git a/include/http/request.h b/include/http/request.h index 23aa73f..a77e7df 100644 --- a/include/http/request.h +++ b/include/http/request.h @@ -1,6 +1,7 @@ #ifndef CWS_REQUEST_H #define CWS_REQUEST_H +#include "http/types.h" #include #include #include @@ -9,28 +10,13 @@ #define CWS_HTTP_HEADER_MAX 512 #define CWS_HTTP_HEADER_CONTENT_MAX 1024 -typedef enum cws_http_method { - HTTP_GET, - HTTP_POST, - HTTP_PUT, - HTTP_DELETE, - HTTP_HEAD, - HTTP_UNKNOWN, -} cws_http_method_e; - -typedef enum cws_http_status { - HTTP_OK, - HTTP_NOT_FOUND, - HTTP_NOT_IMPLEMENTED, -} cws_http_status_e; - typedef struct cws_request { - int sockfd; cws_http_method_e method; - string_s *location; - string_s *location_path; + string_s *path; + string_s *query_string; string_s *http_version; hashmap_s *headers; + string_s *body; } cws_request_s; cws_request_s *cws_http_parse(string_s *request_str); diff --git a/include/http/response.h b/include/http/response.h index b2db6df..42df63c 100644 --- a/include/http/response.h +++ b/include/http/response.h @@ -1,8 +1,39 @@ #ifndef CWS_RESPONSE_H #define CWS_RESPONSE_H -#include "http/request.h" +#include "http/types.h" +#include +#include +#include +#include -void cws_http_send_response(cws_request_s *request, cws_http_status_e status); +typedef enum cws_response_body_type { + RESPONSE_BODY_NONE, + RESPONSE_BODY_STRING, + RESPONSE_BODY_FILE, +} cws_response_body_type_e; + +typedef struct cws_response { + cws_http_status_e status; + hashmap_s *headers; + + /* Body handling */ + cws_response_body_type_e body_type; + string_s *body_string; + FILE *body_file; + size_t content_length; +} cws_response_s; + +cws_response_s *cws_response_new(cws_http_status_e status); +void cws_response_free(cws_response_s *response); + +void cws_response_set_header(cws_response_s *response, const char *key, const char *value); +void cws_response_set_body_string(cws_response_s *response, const char *body); +void cws_response_set_body_file(cws_response_s *response, const char *filepath); + +cws_response_s *cws_response_html(cws_http_status_e status, const char *title, const char *body); +cws_response_s *cws_response_error(cws_http_status_e status, const char *message); + +int cws_response_send(int sockfd, cws_response_s *response); #endif diff --git a/include/http/types.h b/include/http/types.h new file mode 100644 index 0000000..989d73e --- /dev/null +++ b/include/http/types.h @@ -0,0 +1,27 @@ +#ifndef CWS_HTTP_TYPES_H +#define CWS_HTTP_TYPES_H + +#include + +/* HTTP Methods */ +typedef enum cws_http_method { + HTTP_GET, + HTTP_POST, + HTTP_PUT, + HTTP_DELETE, + HTTP_HEAD, + HTTP_UNKNOWN, +} cws_http_method_e; + +/* HTTP Status Codes */ +typedef enum cws_http_status { + HTTP_OK = 200, + HTTP_BAD_REQUEST = 400, + HTTP_NOT_FOUND = 404, + HTTP_INTERNAL_ERROR = 500, + HTTP_NOT_IMPLEMENTED = 501, +} cws_http_status_e; + +const char *cws_http_status_string(cws_http_status_e status); + +#endif diff --git a/src/core/worker.c b/src/core/worker.c index 9e92df7..9a16eec 100644 --- a/src/core/worker.c +++ b/src/core/worker.c @@ -7,6 +7,7 @@ #include "core/epoll.h" #include "core/socket.h" +#include "http/handler.h" #include "http/request.h" #include "http/response.h" #include "utils/error.h" @@ -26,38 +27,26 @@ static void worker_close_client(int epfd, int client_fd) { close(client_fd); } -/* Read client request data; 0 = incomplete, <0 = disconnect */ -static cws_return worker_read_data(int epfd, int client_fd, string_s *data) { - ssize_t total_bytes = cws_read_data(client_fd, data); +static cws_return worker_handle_client_data(int epfd, int client_fd) { + string_s *data = string_new("", 4096); + + /* Read data from socket */ + ssize_t total_bytes = cws_socket_read(client_fd, data); if (total_bytes == 0) { /* Partial request; wait for more data */ - /* - * TODO: do not return CWS_OK - * instead free data and continue - */ + string_free(data); + return CWS_OK; } if (total_bytes < 0) { - /* Client closed or read error */ worker_close_client(epfd, client_fd); + string_free(data); return CWS_CLIENT_DISCONNECTED_ERROR; } - return CWS_OK; -} - -static cws_return worker_handle_client_data(int epfd, int client_fd) { - string_s *data = string_new("", 4096); - - cws_return ret = worker_read_data(epfd, client_fd, data); - if (ret != CWS_OK) { - string_free(data); - return ret; - } - - /* Parse full HTTP request */ + /* Parse HTTP request */ cws_request_s *request = cws_http_parse(data); string_free(data); if (request == NULL) { @@ -65,11 +54,19 @@ static cws_return worker_handle_client_data(int epfd, int client_fd) { return CWS_HTTP_PARSE_ERROR; } - request->sockfd = client_fd; + /* Configure handler */ + cws_handler_config_s config = {.root_dir = "www", .index_file = "index.html"}; - /* TODO: do not send HTTP_OK */ - cws_http_send_response(request, HTTP_OK); + /* Handle request and generate response */ + cws_response_s *response = cws_handler_static_file(request, &config); + /* Send response */ + if (response) { + cws_response_send(client_fd, response); + cws_response_free(response); + } + + /* Cleanup */ cws_http_free(request); worker_close_client(epfd, client_fd); diff --git a/src/http/handler.c b/src/http/handler.c new file mode 100644 index 0000000..bef5669 --- /dev/null +++ b/src/http/handler.c @@ -0,0 +1,68 @@ +#include "http/handler.h" +#include "utils/debug.h" +#include +#include +#include + +/* Sanitize and resolve file path */ +static string_s *resolve_file_path(const char *url_path, cws_handler_config_s *config) { + string_s *full_path = string_new(config->root_dir, 256); + + if (strcmp(url_path, "/") == 0) { + string_append(full_path, "/"); + string_append(full_path, config->index_file); + return full_path; + } + + string_append(full_path, url_path); + + return full_path; +} + +static bool file_exists(const char *filepath) { + struct stat st; + return stat(filepath, &st) == 0 && S_ISREG(st.st_mode); +} + +cws_response_s *cws_handler_static_file(cws_request_s *request, cws_handler_config_s *config) { + if (!request || !config) { + return cws_response_error(HTTP_INTERNAL_ERROR, "Invalid request or configuration"); + } + + if (request->method != HTTP_GET) { + return cws_handler_not_implemented(request); + } + + string_s *filepath = resolve_file_path(string_cstr(request->path), config); + const char *path = string_cstr(filepath); + + CWS_LOG_DEBUG("Resolved path: %s", path); + + if (!file_exists(path)) { + string_free(filepath); + return cws_handler_not_found(request); + } + + cws_response_s *response = cws_response_new(HTTP_OK); + if (!response) { + string_free(filepath); + return cws_response_error(HTTP_INTERNAL_ERROR, "Failed to create response"); + } + + cws_response_set_body_file(response, path); + string_free(filepath); + + CWS_LOG_DEBUG("Serving file: %s (%zu bytes)", path, response->content_length); + + return response; +} + +cws_response_s *cws_handler_not_found(cws_request_s *request) { + (void)request; + return cws_response_error(HTTP_NOT_FOUND, "The requested resource was not found."); +} + +cws_response_s *cws_handler_not_implemented(cws_request_s *request) { + (void)request; + return cws_response_error(HTTP_NOT_IMPLEMENTED, "Method not implemented."); +} diff --git a/src/http/request.c b/src/http/request.c index 541e43c..e891119 100644 --- a/src/http/request.c +++ b/src/http/request.c @@ -1,5 +1,6 @@ #include "http/request.h" +#include #include #include #include @@ -9,15 +10,16 @@ #include "utils/hash.h" static cws_request_s *http_new() { - cws_request_s *request = malloc(sizeof *request); + cws_request_s *request = malloc(sizeof(*request)); if (!request) { return NULL; } - memset(request, 0, sizeof *request); + memset(request, 0, sizeof(*request)); request->http_version = string_new("", 16); - request->location = string_new("", 128); - request->location_path = string_new("", 128); + request->path = string_new("", 256); + request->query_string = NULL; + request->body = NULL; return request; } @@ -26,10 +28,18 @@ static cws_http_method_e http_parse_method(const char *method) { if (strcmp(method, "GET") == 0) { return HTTP_GET; } - if (strcmp(method, "POST") == 0) { return HTTP_POST; } + if (strcmp(method, "PUT") == 0) { + return HTTP_PUT; + } + if (strcmp(method, "DELETE") == 0) { + return HTTP_DELETE; + } + if (strcmp(method, "HEAD") == 0) { + return HTTP_HEAD; + } return HTTP_UNKNOWN; } @@ -58,7 +68,7 @@ static bool parse_location(cws_request_s *req, char **cursor) { s[len] = '\0'; CWS_LOG_DEBUG("location: %s", s); - string_append(req->location, s); + string_append(req->path, s); *cursor = s + len + 1; return true; @@ -112,7 +122,6 @@ static bool parse_headers(cws_request_s *req, char **cursor) { strncpy(hv, header_value, sizeof(hv) - 1); hv[sizeof(hv) - 1] = '\0'; - // CWS_LOG_DEBUG("%s:%s", hk, hv); hm_set(req->headers, hk, hv); /* Move to the next line */ @@ -125,7 +134,7 @@ static bool parse_headers(cws_request_s *req, char **cursor) { } cws_request_s *cws_http_parse(string_s *request_str) { - if (!request_str || !request_str->data) { + if (!request_str || !string_cstr(request_str)) { return NULL; } @@ -134,7 +143,7 @@ cws_request_s *cws_http_parse(string_s *request_str) { return NULL; } - char *str = strdup(request_str->data); + char *str = string_copy(request_str); if (!str) { cws_http_free(request); return NULL; @@ -142,31 +151,33 @@ cws_request_s *cws_http_parse(string_s *request_str) { char *orig = str; /* Parse HTTP method */ - parse_method(request, &str); - - /* Parse location */ - parse_location(request, &str); - - /* Adjust location path */ - /* @TODO: fix path traversal */ - string_append(request->location_path, "www"); - if (strcmp(request->location->data, "/") == 0) { - string_append(request->location_path, "/index.html"); - } else { - string_append(request->location_path, request->location->data); + if (!parse_method(request, &str)) { + free(orig); + cws_http_free(request); + return NULL; + } + + /* Parse location (URL path) */ + if (!parse_location(request, &str)) { + free(orig); + cws_http_free(request); + return NULL; } - CWS_LOG_DEBUG("location path: %s", request->location_path->data); /* Parse HTTP version */ - parse_version(request, &str); + if (!parse_version(request, &str)) { + free(orig); + cws_http_free(request); + return NULL; + } /* Parse headers */ - parse_headers(request, &str); + if (!parse_headers(request, &str)) { + free(orig); + cws_http_free(request); + return NULL; + } - /* TODO: Parse body */ - /* orig is at the beginning of the body */ - - /* Free the original string */ free(orig); return request; @@ -185,12 +196,16 @@ void cws_http_free(cws_request_s *request) { string_free(request->http_version); } - if (request->location) { - string_free(request->location); + if (request->path) { + string_free(request->path); } - if (request->location_path) { - string_free(request->location_path); + if (request->query_string) { + string_free(request->query_string); + } + + if (request->body) { + string_free(request->body); } free(request); diff --git a/src/http/response.c b/src/http/response.c index 14619ea..24b0e30 100644 --- a/src/http/response.c +++ b/src/http/response.c @@ -1,122 +1,174 @@ #include "http/response.h" - #include "http/mime.h" -#include "http/request.h" #include "utils/debug.h" +#include "utils/hash.h" #include +#include #include #include #include #include -/* 8 KB reading buffer length */ #define CHUNK_SIZE 8192 -#define HEADERS_LEN 512 +#define HEADERS_BUFFER_SIZE 2048 -static const char *http_status_string(cws_http_status_e status) { - switch (status) { - case HTTP_OK: { - return "200 OK"; - } - case HTTP_NOT_FOUND: { - return "404 Not Found"; - } - case HTTP_NOT_IMPLEMENTED: { - return "501 Not Implemented"; - } - } - - return "?"; +static hashmap_s *response_headers_new(void) { + return hm_new(my_str_hash_fn, my_str_equal_fn, my_str_free_fn, my_str_free_fn, sizeof(char) * 256, + sizeof(char) * 512); } -static void http_send_headers(int sockfd, const char *status_str, const char *content_type, size_t content_length) { - char headers[HEADERS_LEN]; - int len = snprintf(headers, sizeof headers, - "HTTP/1.1 %s\r\n" - "Content-Type: %s\r\n" - "Content-Length: %zu\r\n" - "Connection: close\r\n" - "\r\n", - status_str, content_type, content_length); - - if (len > 0) { - cws_send_data(sockfd, headers, len, 0); +cws_response_s *cws_response_new(cws_http_status_e status) { + cws_response_s *resp = malloc(sizeof(*resp)); + if (!resp) { + return NULL; } + + resp->status = status; + resp->headers = response_headers_new(); + resp->body_type = RESPONSE_BODY_NONE; + resp->body_string = NULL; + resp->body_file = NULL; + resp->content_length = 0; + + cws_response_set_header(resp, "Connection", "close"); + + return resp; } -static void http_send_file(cws_request_s *request) { - const char *path = request->location_path->data; - FILE *fp = fopen(path, "rb"); +void cws_response_free(cws_response_s *response) { + if (!response) { + return; + } + + if (response->headers) { + hm_free(response->headers); + } + + if (response->body_string) { + string_free(response->body_string); + } + + if (response->body_file) { + fclose(response->body_file); + } + + free(response); +} + +void cws_response_set_header(cws_response_s *response, const char *key, const char *value) { + if (!response || !key || !value) { + return; + } + + char k[256], v[512]; + strncpy(k, key, sizeof(k) - 1); + k[sizeof(k) - 1] = '\0'; + strncpy(v, value, sizeof(v) - 1); + v[sizeof(v) - 1] = '\0'; + + hm_set(response->headers, k, v); +} + +void cws_response_set_body_string(cws_response_s *response, const char *body) { + if (!response || !body) { + return; + } + + if (response->body_string) { + string_free(response->body_string); + } + + response->body_type = RESPONSE_BODY_STRING; + response->body_string = string_new(body, strlen(body) + 1); + response->content_length = strlen(body); +} + +void cws_response_set_body_file(cws_response_s *response, const char *filepath) { + if (!response || !filepath) { + return; + } + + FILE *fp = fopen(filepath, "rb"); if (!fp) { - CWS_LOG_ERROR("Cannot open file: %s", path); - cws_http_send_response(request, HTTP_NOT_FOUND); + CWS_LOG_ERROR("Cannot open file: %s", filepath); return; } - /* Get file size */ struct stat st; - if (stat(path, &st) != 0) { + if (stat(filepath, &st) != 0) { fclose(fp); - cws_http_send_response(request, HTTP_NOT_FOUND); return; } - size_t file_size = st.st_size; - char content_tye[CWS_HTTP_CONTENT_TYPE]; - mime_get_content_type(path, content_tye); + char content_type[64]; + cws_mime_get_ct(filepath, content_type); + cws_response_set_header(response, "Content-Type", content_type); - /* Send headers */ - http_send_headers(request->sockfd, http_status_string(HTTP_OK), content_tye, file_size); - - /* Send the file in chunks */ - char buffer[CHUNK_SIZE]; - size_t bytes_read = 0; - - while ((bytes_read = fread(buffer, 1, sizeof buffer, fp)) > 0) { - /* TODO: check return */ - cws_send_data(request->sockfd, buffer, bytes_read, 0); - } - - fclose(fp); - CWS_LOG_DEBUG("Served file: %s (%zu bytes)", path, file_size); + response->body_type = RESPONSE_BODY_FILE; + response->body_file = fp; + response->content_length = st.st_size; } -void http_send_simple_html(cws_request_s *request, cws_http_status_e status, const char *title, const char *desc) { - const char *fmt = "\n" - "%s\n" - "

%s

\n" - ""; - - int body_len = snprintf(NULL, 0, fmt, title, desc); - if (body_len < 0) - body_len = 0; - - http_send_headers(request->sockfd, http_status_string(status), "text/html", body_len); - - char *body = malloc(body_len + 1); - if (body) { - snprintf(body, body_len + 1, fmt, title, desc); - cws_send_data(request->sockfd, body, body_len, 0); - free(body); +cws_response_s *cws_response_html(cws_http_status_e status, const char *title, const char *body) { + cws_response_s *resp = cws_response_new(status); + if (!resp) { + return NULL; } + + char html[4096]; + snprintf(html, sizeof(html), + "\n" + "%s\n" + "

%s

%s

\n" + "", + title, title, body); + + cws_response_set_header(resp, "Content-Type", "text/html"); + cws_response_set_body_string(resp, html); + + return resp; } -void cws_http_send_response(cws_request_s *request, cws_http_status_e status) { - switch (status) { - case HTTP_OK: - http_send_file(request); - break; - - case HTTP_NOT_FOUND: - http_send_simple_html(request, HTTP_NOT_FOUND, "404 Not Found", "The requested resource was not found."); - break; - - case HTTP_NOT_IMPLEMENTED: - http_send_simple_html(request, HTTP_NOT_IMPLEMENTED, "501 Not Implemented", "Method not implemented."); - break; - - default: - http_send_simple_html(request, status, "Error", "An unexpected error occurred."); - break; - } +cws_response_s *cws_response_error(cws_http_status_e status, const char *message) { + const char *status_str = cws_http_status_string(status); + return cws_response_html(status, status_str, message); +} + +int cws_response_send(int sockfd, cws_response_s *response) { + if (!response) { + return -1; + } + + char headers[HEADERS_BUFFER_SIZE]; + int offset = snprintf(headers, sizeof(headers), "HTTP/1.1 %s\r\n", cws_http_status_string(response->status)); + + char content_length_str[32]; + snprintf(content_length_str, sizeof(content_length_str), "%zu", response->content_length); + cws_response_set_header(response, "Content-Length", content_length_str); + + offset += snprintf(headers + offset, sizeof(headers) - offset, + "Content-Length: %zu\r\n" + "Connection: close\r\n", + response->content_length); + + offset += snprintf(headers + offset, sizeof(headers) - offset, "\r\n"); + + if (cws_socket_send(sockfd, headers, offset, 0) < 0) { + return -1; + } + + if (response->body_type == RESPONSE_BODY_STRING && response->body_string) { + const char *body = string_cstr(response->body_string); + cws_socket_send(sockfd, body, response->content_length, 0); + } else if (response->body_type == RESPONSE_BODY_FILE && response->body_file) { + char buffer[CHUNK_SIZE]; + size_t bytes_read; + while ((bytes_read = fread(buffer, 1, sizeof(buffer), response->body_file)) > 0) { + if (cws_socket_send(sockfd, buffer, bytes_read, 0) < 0) { + return -1; + } + } + } + + return 0; } diff --git a/src/http/types.c b/src/http/types.c new file mode 100644 index 0000000..d0ea66c --- /dev/null +++ b/src/http/types.c @@ -0,0 +1,18 @@ +#include "http/types.h" + +const char *cws_http_status_string(cws_http_status_e status) { + switch (status) { + case HTTP_OK: + return "200 OK"; + case HTTP_BAD_REQUEST: + return "400 Bad Request"; + case HTTP_NOT_FOUND: + return "404 Not Found"; + case HTTP_INTERNAL_ERROR: + return "500 Internal Server Error"; + case HTTP_NOT_IMPLEMENTED: + return "501 Not Implemented"; + default: + return "500 Internal Server Error"; + } +}