From f38fe3c42ec01efe37820b0c00dd79a66c80c0de Mon Sep 17 00:00:00 2001 From: stab Date: Tue, 31 Mar 2026 04:57:15 +0300 Subject: Added rate limiting and settings fixes. --- example-config.ini | 11 +++ src/Config.c | 10 +++ src/Config.h | 4 + src/Limiter/RateLimit.c | 193 ++++++++++++++++++++++++++++++++++++++++++++++++ src/Limiter/RateLimit.h | 20 +++++ src/Main.c | 6 +- src/Routes/Images.c | 56 ++++++++++++++ src/Routes/Search.c | 58 +++++++++++++++ templates/settings.html | 6 ++ 9 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 src/Limiter/RateLimit.c create mode 100644 src/Limiter/RateLimit.h diff --git a/example-config.ini b/example-config.ini index fc6ea8d..5145d88 100644 --- a/example-config.ini +++ b/example-config.ini @@ -31,3 +31,14 @@ domain = https://search.example.com # Use *,-engine to exclude specific engines (e.g., *,-startpage) # Available engines: ddg, startpage, yahoo, mojeek engines="*" + +[rate_limit] +# Rate limit searches per interval + +# /search +#search_requests = 10 +#search_interval = 60 + +# /images +#images_requests = 20 +#images_interval = 60 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 + +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 diff --git a/src/Main.c b/src/Main.c index 326b5ae..c3a607a 100644 --- a/src/Main.c +++ b/src/Main.c @@ -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), + "

Slow down!

Too many image searches from you!

"); + 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("

Error fetching images

"); + 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), + "

Slow down!

Too many searches from you!

"); + 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("

No results found

"); 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) { diff --git a/templates/settings.html b/templates/settings.html index 780e438..a49a995 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -21,12 +21,17 @@

OmniSearch

+ {{if query != ""}}
+ {{endif}} + {{if query != ""}} + {{endif}} + {{if query != ""}} + {{endif}}
-- cgit v1.2.3