Added rate limiting and settings fixes.
This commit is contained in:
parent
c3ed901738
commit
f38fe3c42e
9 changed files with 363 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
10
src/Config.c
10
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
193
src/Limiter/RateLimit.c
Normal 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
20
src/Limiter/RateLimit.h
Normal 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
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue