Added rate limiting and settings fixes.

This commit is contained in:
stab 2026-03-31 04:57:15 +03:00 committed by frosty
parent c3ed901738
commit f38fe3c42e
9 changed files with 363 additions and 1 deletions

View file

@ -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

View file

@ -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);
}
}
}
}

View file

@ -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);

193
src/Limiter/RateLimit.c Normal file
View file

@ -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;
}

20
src/Limiter/RateLimit.h Normal file
View file

@ -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

View file

@ -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");

View file

@ -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);

View file

@ -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) {

View file

@ -21,12 +21,17 @@
<h1>
Omni<span>Search</span>
</h1>
{{if query != ""}}
<form action="/search" method="GET" class="search-form">
<input name="q" type="text" class="search-box" autocomplete="off" placeholder="Search the web..."
value="{{query}}">
</form>
{{endif}}
{{if query != ""}}
<a href="/settings?q={{query}}" class="nav-settings-icon active" title="Settings"></a>
{{endif}}
</header>
{{if query != ""}}
<nav class="nav-tabs">
<div class="nav-container">
<a href="/search?q={{query}}">
@ -40,6 +45,7 @@
</a>
</div>
</nav>
{{endif}}
<div class="settings-layout">
<main class="settings-container">
<form action="/save_settings" method="GET">