#include "../beaker.h" #include "beaker_globals.h" #include #include #include #include #include #include #include void set_handler(const char *path, RequestHandler handler) { if (handler_count < MAX_HANDLERS) { strncpy(handlers[handler_count].path, path, MAX_PATH_LEN - 1); handlers[handler_count].path[MAX_PATH_LEN - 1] = '\0'; handlers[handler_count].handler = handler; handler_count++; } else { fprintf(stderr, "[WARNING] set_handler: Maximum number of handlers reached. Cannot register handler for '%s'.\n", path); } } const char *get_mime_type(const char *file_path) { const char *ext = strrchr(file_path, '.'); if (!ext) { return "application/octet-stream"; } ext++; if (strcmp(ext, "html") == 0 || strcmp(ext, "htm") == 0) return "text/html"; if (strcmp(ext, "css") == 0) return "text/css"; if (strcmp(ext, "js") == 0) return "application/javascript"; if (strcmp(ext, "json") == 0) return "application/json"; if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0) return "image/jpeg"; if (strcmp(ext, "png") == 0) return "image/png"; if (strcmp(ext, "gif") == 0) return "image/gif"; if (strcmp(ext, "ico") == 0) return "image/x-icon"; if (strcmp(ext, "svg") == 0) return "image/svg+xml"; if (strcmp(ext, "pdf") == 0) return "application/pdf"; if (strcmp(ext, "txt") == 0) return "text/plain"; return "application/octet-stream"; } static int canonicalize_path(char *canonical, const char *path, size_t max_len) { char components[MAX_URL_PARAMS][MAX_KEY_LEN]; int component_count = 0; char *path_copy = strdup(path); if (!path_copy) { return -1; } char *token = strtok(path_copy, "/"); while (token) { if (strcmp(token, ".") == 0) { } else if (strcmp(token, "..") == 0) { if (component_count > 0) { component_count--; } else { fprintf(stderr, "[SECURITY] Path traversal attempt: %s\n", path); free(path_copy); return -1; } } else if (strlen(token) > 0) { if (component_count < MAX_URL_PARAMS) { strncpy(components[component_count], token, MAX_KEY_LEN - 1); components[component_count][MAX_KEY_LEN - 1] = '\0'; component_count++; } } token = strtok(NULL, "/"); } free(path_copy); canonical[0] = '\0'; for (int i = 0; i < component_count; i++) { if (strlen(canonical) + strlen(components[i]) + 2 > max_len) { fprintf(stderr, "[ERROR] Canonical path too long\n"); return -1; } strcat(canonical, "/"); strcat(canonical, components[i]); } if (canonical[0] == '\0') { strcpy(canonical, "/"); } return 0; } static bool is_safe_path_component(const char *component) { if (strstr(component, "..") || strstr(component, "//") || strchr(component, '\0') != component + strlen(component)) { return false; } for (size_t i = 0; component[i]; i++) { if (component[i] < 32 && component[i] != '\t') { return false; } } return true; } static int url_decode(char *dst, const char *src, size_t dst_size) { size_t i = 0, j = 0; while (src[i] && j < dst_size - 1) { if (src[i] == '%') { if (src[i+1] && src[i+2]) { char hex[3] = {src[i+1], src[i+2], '\0'}; char *endptr; long value = strtol(hex, &endptr, 16); if (*endptr != '\0') { fprintf(stderr, "[SECURITY] Invalid URL encoding: %%%s\n", hex); return -1; } if (value == 0) { fprintf(stderr, "[SECURITY] Null byte in URL encoding\n"); return -1; } dst[j++] = (char)value; i += 3; } else { fprintf(stderr, "[SECURITY] Incomplete URL encoding at end\n"); return -1; } } else if (src[i] == '+') { dst[j++] = ' '; i++; } else { dst[j++] = src[i++]; } } dst[j] = '\0'; return 0; } char *parse_request_url(const char *request_line, UrlParams *params) { char method[16]; char raw_url_full[MAX_PATH_LEN]; char http_version[16]; if (sscanf(request_line, "%15s %255s %15s", method, raw_url_full, http_version) != 3) { fprintf(stderr, "[ERROR] parse_request_url: Malformed request line\n"); return NULL; } params->count = 0; char *working_raw = strdup(raw_url_full); if (!working_raw) { perror("Failed to allocate memory for URL copy"); return NULL; } char *query_start = strchr(working_raw, '?'); if (query_start) { *query_start = '\0'; } char decoded_path[MAX_PATH_LEN]; if (url_decode(decoded_path, working_raw, sizeof(decoded_path)) != 0) { fprintf(stderr, "[SECURITY] Invalid URL encoding in path\n"); free(working_raw); return NULL; } char canonical_path[MAX_PATH_LEN]; if (canonicalize_path(canonical_path, decoded_path, sizeof(canonical_path)) != 0) { fprintf(stderr, "[SECURITY] Path canonicalization failed\n"); free(working_raw); return NULL; } char *path_check = strdup(canonical_path); if (path_check) { char *token = strtok(path_check, "/"); while (token) { if (!is_safe_path_component(token)) { fprintf(stderr, "[SECURITY] Unsafe path component: %s\n", token); free(path_check); free(working_raw); return NULL; } token = strtok(NULL, "/"); } free(path_check); } if (query_start) { char *query_string = query_start + 1; char *pair; char *saveptr; pair = strtok_r(query_string, "&", &saveptr); while (pair && params->count < MAX_URL_PARAMS) { char *equals = strchr(pair, '='); if (equals) { *equals = '\0'; char *raw_key = pair; char *raw_val = equals + 1; if (url_decode(params->params[params->count].key, raw_key, MAX_KEY_LEN) == 0 && url_decode(params->params[params->count].value, raw_val, MAX_VALUE_LEN) == 0) { params->count++; } } pair = strtok_r(NULL, "&", &saveptr); } } char *final_path = strdup(canonical_path); free(working_raw); return final_path; } bool serve_static_file(const char *request_path_relative_to_static) { char full_static_path[MAX_PATH_LEN]; if (strstr(request_path_relative_to_static, "..") != NULL) { fprintf(stderr, "[SECURITY] Attempted directory traversal: %s\n", request_path_relative_to_static); const char *forbidden_response = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n"; send(current_client_socket, forbidden_response, strlen(forbidden_response), 0); return true; } snprintf(full_static_path, sizeof(full_static_path), "%s%s", STATIC_DIR, request_path_relative_to_static); FILE *fp = fopen(full_static_path, "rb"); if (fp == NULL) { fprintf(stderr, "[ERROR] serve_static_file: File '%s' not found or could not be opened. %s\n", full_static_path, strerror(errno)); return false; } struct stat st; if (fstat(fileno(fp), &st) < 0) { perror("fstat error"); fprintf(stderr, "[ERROR] serve_static_file: fstat failed for '%s'.\n", full_static_path); fclose(fp); const char *server_error_response = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n"; send(current_client_socket, server_error_response, strlen(server_error_response), 0); return true; } long file_size = st.st_size; const char *mime_type = get_mime_type(full_static_path); char http_header[BUFFER_SIZE]; snprintf(http_header, sizeof(http_header), "HTTP/1.1 200 OK\r\n" "Content-Type: %s\r\n" "Content-Length: %ld\r\n" "Connection: close\r\n" "\r\n", mime_type, file_size); if (send(current_client_socket, http_header, strlen(http_header), 0) < 0) { perror("Error sending static file header"); fprintf(stderr, "[ERROR] serve_static_file: Failed to send header for '%s'.\n", full_static_path); fclose(fp); return true; } char file_buffer[BUFFER_SIZE]; size_t bytes_read; while ((bytes_read = fread(file_buffer, 1, sizeof(file_buffer), fp)) > 0) { if (send(current_client_socket, file_buffer, bytes_read, 0) < 0) { perror("Error sending static file content"); fprintf(stderr, "[ERROR] serve_static_file: Failed to send content for '%s'.\n", full_static_path); break; } } fclose(fp); return true; }