aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorstab <stab@stab.ing>2026-03-31 04:57:15 +0300
committerfrosty <gabriel@bwaaa.monster>2026-03-31 05:10:22 +0300
commitf38fe3c42ec01efe37820b0c00dd79a66c80c0de (patch)
tree2af9e878661738d51efee43bbc9998bead3bf078 /src
parentc3ed9017385342944badec46de263560c6ab07c8 (diff)
downloadomnisearch-f38fe3c42ec01efe37820b0c00dd79a66c80c0de.tar.gz
Added rate limiting and settings fixes.
Diffstat (limited to 'src')
-rw-r--r--src/Config.c10
-rw-r--r--src/Config.h4
-rw-r--r--src/Limiter/RateLimit.c193
-rw-r--r--src/Limiter/RateLimit.h20
-rw-r--r--src/Main.c6
-rw-r--r--src/Routes/Images.c56
-rw-r--r--src/Routes/Search.c58
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
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),
+ "<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) {