diff options
| author | stab <stab@stab.ing> | 2026-03-31 04:57:15 +0300 |
|---|---|---|
| committer | frosty <gabriel@bwaaa.monster> | 2026-03-31 05:10:22 +0300 |
| commit | f38fe3c42ec01efe37820b0c00dd79a66c80c0de (patch) | |
| tree | 2af9e878661738d51efee43bbc9998bead3bf078 /src | |
| parent | c3ed9017385342944badec46de263560c6ab07c8 (diff) | |
| download | omnisearch-f38fe3c42ec01efe37820b0c00dd79a66c80c0de.tar.gz | |
Added rate limiting and settings fixes.
Diffstat (limited to 'src')
| -rw-r--r-- | src/Config.c | 10 | ||||
| -rw-r--r-- | src/Config.h | 4 | ||||
| -rw-r--r-- | src/Limiter/RateLimit.c | 193 | ||||
| -rw-r--r-- | src/Limiter/RateLimit.h | 20 | ||||
| -rw-r--r-- | src/Main.c | 6 | ||||
| -rw-r--r-- | src/Routes/Images.c | 56 | ||||
| -rw-r--r-- | src/Routes/Search.c | 58 |
7 files changed, 346 insertions, 1 deletions
diff --git a/src/Config.c b/src/Config.c index 0c3fc1c..c4bd1f1 100644 --- a/src/Config.c +++ b/src/Config.c @@ -100,6 +100,16 @@ int load_config(const char *filename, Config *config) { strncpy(config->engines, value, sizeof(config->engines) - 1); config->engines[sizeof(config->engines) - 1] = '\0'; } + } else if (strcmp(section, "rate_limit") == 0) { + if (strcmp(key, "search_requests") == 0) { + config->rate_limit_search_requests = atoi(value); + } else if (strcmp(key, "search_interval") == 0) { + config->rate_limit_search_interval = atoi(value); + } else if (strcmp(key, "images_requests") == 0) { + config->rate_limit_images_requests = atoi(value); + } else if (strcmp(key, "images_interval") == 0) { + config->rate_limit_images_interval = atoi(value); + } } } } diff --git a/src/Config.h b/src/Config.h index ce316f6..8e68eae 100644 --- a/src/Config.h +++ b/src/Config.h @@ -45,6 +45,10 @@ typedef struct { int cache_ttl_infobox; int cache_ttl_image; char engines[512]; + int rate_limit_search_requests; + int rate_limit_search_interval; + int rate_limit_images_requests; + int rate_limit_images_interval; } Config; int load_config(const char *filename, Config *config); diff --git a/src/Limiter/RateLimit.c b/src/Limiter/RateLimit.c new file mode 100644 index 0000000..3c6bbff --- /dev/null +++ b/src/Limiter/RateLimit.c @@ -0,0 +1,193 @@ +#include "RateLimit.h" +#include <arpa/inet.h> +#include <beaker.h> +#include <ctype.h> +#include <netinet/in.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <time.h> +#include <unistd.h> + +typedef struct RateLimitEntry { + char client_key[64]; + char scope[32]; + time_t window_start; + time_t last_seen; + int count; + struct RateLimitEntry *next; +} RateLimitEntry; + +extern __thread int current_client_socket; +extern __thread char current_request_buffer[]; + +static pthread_mutex_t rate_limit_mutex = PTHREAD_MUTEX_INITIALIZER; +static RateLimitEntry *rate_limit_entries = NULL; + +static int is_blank_char(char c) { + return c == ' ' || c == '\t' || c == '\r' || c == '\n'; +} + +static void trim_copy(char *dest, size_t dest_size, const char *src, + size_t src_len) { + while (src_len > 0 && is_blank_char(*src)) { + src++; + src_len--; + } + + while (src_len > 0 && is_blank_char(src[src_len - 1])) { + src_len--; + } + + if (dest_size == 0) + return; + + if (src_len >= dest_size) + src_len = dest_size - 1; + + memcpy(dest, src, src_len); + dest[src_len] = '\0'; +} + +static void get_client_key(char *client_key, size_t client_key_size) { + const char *header = strstr(current_request_buffer, "X-Forwarded-For:"); + if (!header) + return; + + header += strlen("X-Forwarded-For:"); + const char *line_end = strpbrk(header, "\r\n"); + size_t line_len = line_end ? (size_t)(line_end - header) : strlen(header); + const char *comma = memchr(header, ',', line_len); + size_t value_len = comma ? (size_t)(comma - header) : line_len; + + trim_copy(client_key, client_key_size, header, value_len); +} + +static void get_client_key_from_socket(char *client_key, + size_t client_key_size) { + struct sockaddr_storage addr; + socklen_t addr_len = sizeof(addr); + + if (getpeername(current_client_socket, (struct sockaddr *)&addr, &addr_len) != + 0) { + return; + } + + if (addr.ss_family == AF_INET) { + struct sockaddr_in *ipv4 = (struct sockaddr_in *)&addr; + inet_ntop(AF_INET, &ipv4->sin_addr, client_key, client_key_size); + } else if (addr.ss_family == AF_INET6) { + struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)&addr; + inet_ntop(AF_INET6, &ipv6->sin6_addr, client_key, client_key_size); + } else if (addr.ss_family == AF_UNIX) { + snprintf(client_key, client_key_size, "unix:%d", current_client_socket); + } +} + +void rate_limit_get_client_key(char *client_key, size_t client_key_size) { + if (!client_key || client_key_size == 0) + return; + + client_key[0] = '\0'; + get_client_key(client_key, client_key_size); + + if (client_key[0] == '\0') { + get_client_key_from_socket(client_key, client_key_size); + } + + if (client_key[0] == '\0') { + snprintf(client_key, client_key_size, "nun"); + } +} + +static void prune_stale_entries(time_t now) { + RateLimitEntry **cursor = &rate_limit_entries; + + while (*cursor) { + RateLimitEntry *entry = *cursor; + if (now - entry->last_seen > 9999) { + *cursor = entry->next; + free(entry); + continue; + } + cursor = &entry->next; + } +} + +static RateLimitEntry *find_entry(const char *client_key, const char *scope) { + for (RateLimitEntry *entry = rate_limit_entries; entry; entry = entry->next) { + if (strcmp(entry->client_key, client_key) == 0 && + strcmp(entry->scope, scope) == 0) { + return entry; + } + } + return NULL; +} + +static RateLimitEntry *create_entry(const char *client_key, const char *scope, + time_t now) { + RateLimitEntry *entry = (RateLimitEntry *)calloc(1, sizeof(RateLimitEntry)); + if (!entry) + return NULL; + + snprintf(entry->client_key, sizeof(entry->client_key), "%s", client_key); + snprintf(entry->scope, sizeof(entry->scope), "%s", scope); + entry->window_start = now; + entry->last_seen = now; + entry->next = rate_limit_entries; + rate_limit_entries = entry; + return entry; +} + +RateLimitResult rate_limit_check(const char *scope, + const RateLimitConfig *config) { + RateLimitResult result = {.limited = 0, .retry_after_seconds = 0}; + + if (!scope || !config || config->max_requests <= 0 || + config->interval_seconds <= 0) { + return result; + } + + char client_key[64]; + time_t now = time(NULL); + + rate_limit_get_client_key(client_key, sizeof(client_key)); + + pthread_mutex_lock(&rate_limit_mutex); + + prune_stale_entries(now); + + RateLimitEntry *entry = find_entry(client_key, scope); + if (!entry) { + entry = create_entry(client_key, scope, now); + if (!entry) { + pthread_mutex_unlock(&rate_limit_mutex); + return result; + } + } + + entry->last_seen = now; + + if (now - entry->window_start >= config->interval_seconds) { + entry->window_start = now; + entry->count = 0; + } + + if (entry->count >= config->max_requests) { + result.limited = 1; + result.retry_after_seconds = + config->interval_seconds - (int)(now - entry->window_start); + if (result.retry_after_seconds < 1) { + result.retry_after_seconds = 1; + } + pthread_mutex_unlock(&rate_limit_mutex); + return result; + } + + entry->count++; + pthread_mutex_unlock(&rate_limit_mutex); + return result; +} diff --git a/src/Limiter/RateLimit.h b/src/Limiter/RateLimit.h new file mode 100644 index 0000000..fabd05d --- /dev/null +++ b/src/Limiter/RateLimit.h @@ -0,0 +1,20 @@ +#ifndef RATE_LIMIT_H +#define RATE_LIMIT_H + +#include <stddef.h> + +typedef struct { + int max_requests; + int interval_seconds; +} RateLimitConfig; + +typedef struct { + int limited; + int retry_after_seconds; +} RateLimitResult; + +void rate_limit_get_client_key(char *client_key, size_t client_key_size); +RateLimitResult rate_limit_check(const char *scope, + const RateLimitConfig *config); + +#endif @@ -55,7 +55,11 @@ int main() { .cache_ttl_search = DEFAULT_CACHE_TTL_SEARCH, .cache_ttl_infobox = DEFAULT_CACHE_TTL_INFOBOX, .cache_ttl_image = DEFAULT_CACHE_TTL_IMAGE, - .engines = ""}; + .engines = "", + .rate_limit_search_requests = 0, + .rate_limit_search_interval = 0, + .rate_limit_images_requests = 0, + .rate_limit_images_interval = 0}; if (load_config("config.ini", &cfg) != 0) { fprintf(stderr, "[WARN] Could not load config file, using defaults\n"); diff --git a/src/Routes/Images.c b/src/Routes/Images.c index 03eb280..98fd3f4 100644 --- a/src/Routes/Images.c +++ b/src/Routes/Images.c @@ -1,10 +1,21 @@ #include "Images.h" +#include "../Cache/Cache.h" +#include "../Limiter/RateLimit.h" #include "../Scraping/ImageScraping.h" #include "../Utility/Unescape.h" #include "../Utility/Utility.h" #include "Config.h" +static char *build_images_request_cache_key(const char *query, int page, + const char *client_key) { + char scope_key[BUFFER_SIZE_MEDIUM]; + snprintf(scope_key, sizeof(scope_key), "images_request:%s", + client_key ? client_key : "unknown"); + return cache_compute_key(query, page, scope_key); +} + int images_handler(UrlParams *params) { + extern Config global_config; TemplateContext ctx = new_context(); char *raw_query = ""; int page = 1; @@ -52,12 +63,55 @@ int images_handler(UrlParams *params) { return -1; } + char client_key[BUFFER_SIZE_SMALL]; + rate_limit_get_client_key(client_key, sizeof(client_key)); + + char *request_cache_key = + build_images_request_cache_key(raw_query, page, client_key); + int request_is_cached = 0; + + if (request_cache_key && get_cache_ttl_image() > 0) { + char *cached_marker = NULL; + size_t cached_marker_size = 0; + + if (cache_get(request_cache_key, (time_t)get_cache_ttl_image(), + &cached_marker, &cached_marker_size) == 0) { + request_is_cached = 1; + } + + free(cached_marker); + } + + if (!request_is_cached) { + RateLimitConfig rate_limit_config = { + .max_requests = global_config.rate_limit_images_requests, + .interval_seconds = global_config.rate_limit_images_interval, + }; + RateLimitResult rate_limit_result = + rate_limit_check("images", &rate_limit_config); + if (rate_limit_result.limited) { + char response[256]; + snprintf(response, sizeof(response), + "<h1>Slow down!</h1><p>Too many image searches from you!</p>"); + send_response(response); + free(request_cache_key); + free(display_query); + free_context(&ctx); + return -1; + } + + if (request_cache_key && get_cache_ttl_image() > 0) { + cache_set(request_cache_key, "1", 1); + } + } + ImageResult *results = NULL; int result_count = 0; if (scrape_images(raw_query, page, &results, &result_count) != 0 || !results) { send_response("<h1>Error fetching images</h1>"); + free(request_cache_key); free(display_query); free_context(&ctx); return -1; @@ -72,6 +126,7 @@ int images_handler(UrlParams *params) { if (inner_counts) free(inner_counts); free_image_results(results, result_count); + free(request_cache_key); free(display_query); free_context(&ctx); return -1; @@ -106,6 +161,7 @@ int images_handler(UrlParams *params) { free(inner_counts); free_image_results(results, result_count); + free(request_cache_key); free(display_query); free_context(&ctx); diff --git a/src/Routes/Search.c b/src/Routes/Search.c index 81e43d4..170b488 100644 --- a/src/Routes/Search.c +++ b/src/Routes/Search.c @@ -1,9 +1,11 @@ #include "Search.h" +#include "../Cache/Cache.h" #include "../Infobox/Calculator.h" #include "../Infobox/CurrencyConversion.h" #include "../Infobox/Dictionary.h" #include "../Infobox/UnitConversion.h" #include "../Infobox/Wikipedia.h" +#include "../Limiter/RateLimit.h" #include "../Scraping/Scraping.h" #include "../Utility/Display.h" #include "../Utility/Unescape.h" @@ -378,7 +380,17 @@ static char *build_search_href(const char *query, const char *engine_id, return href; } +static char *build_search_request_cache_key(const char *query, + const char *engine_id, int page, + const char *client_key) { + char scope_key[BUFFER_SIZE_MEDIUM]; + snprintf(scope_key, sizeof(scope_key), "search_request:%s:%s", + engine_id ? engine_id : "all", client_key ? client_key : "unknown"); + return cache_compute_key(query, page, scope_key); +} + int results_handler(UrlParams *params) { + extern Config global_config; TemplateContext ctx = new_context(); char *raw_query = ""; const char *selected_engine_id = "all"; @@ -474,6 +486,47 @@ int results_handler(UrlParams *params) { } } + char client_key[BUFFER_SIZE_SMALL]; + rate_limit_get_client_key(client_key, sizeof(client_key)); + + char *request_cache_key = build_search_request_cache_key( + raw_query, selected_engine_id, page, client_key); + int request_is_cached = 0; + + if (request_cache_key && get_cache_ttl_search() > 0) { + char *cached_marker = NULL; + size_t cached_marker_size = 0; + + if (cache_get(request_cache_key, (time_t)get_cache_ttl_search(), + &cached_marker, &cached_marker_size) == 0) { + request_is_cached = 1; + } + + free(cached_marker); + } + + if (engine_idx > 0 && !request_is_cached) { + RateLimitConfig rate_limit_config = { + .max_requests = global_config.rate_limit_search_requests, + .interval_seconds = global_config.rate_limit_search_interval, + }; + RateLimitResult rate_limit_result = + rate_limit_check("search", &rate_limit_config); + if (rate_limit_result.limited) { + char response[256]; + snprintf(response, sizeof(response), + "<h1>Slow down!</h1><p>Too many searches from you!</p>"); + send_response(response); + free(request_cache_key); + free_context(&ctx); + return -1; + } + + if (request_cache_key && get_cache_ttl_search() > 0) { + cache_set(request_cache_key, "1", 1); + } + } + int filter_engine_count = 0; for (int i = 0; i < ENGINE_COUNT; i++) { if (ENGINE_REGISTRY[i].enabled) @@ -551,6 +604,7 @@ int results_handler(UrlParams *params) { } } } + free(request_cache_key); free_context(&ctx); if (redirect_url) { send_redirect(redirect_url); @@ -569,6 +623,7 @@ int results_handler(UrlParams *params) { } } } + free(request_cache_key); free_context(&ctx); send_response("<h1>No results found</h1>"); return 0; @@ -668,6 +723,7 @@ int results_handler(UrlParams *params) { } } } + free(request_cache_key); free_context(&ctx); return 0; } @@ -817,6 +873,8 @@ int results_handler(UrlParams *params) { } } + free(request_cache_key); + if (page == 1) { for (int i = 0; i < HANDLER_COUNT; i++) { if (infobox_data[i].success) { |
