|
|
|
|
@@ -5,7 +5,6 @@
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <time.h>
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <printf.h>
|
|
|
|
|
#include <pthread.h>
|
|
|
|
|
#include <libwebsockets.h>
|
|
|
|
|
#include "subscriptions.h"
|
|
|
|
|
@@ -21,6 +20,13 @@ const char* get_config_value(const char* key);
|
|
|
|
|
// Forward declarations for NIP-40 expiration functions
|
|
|
|
|
int is_event_expired(cJSON* event, time_t current_time);
|
|
|
|
|
|
|
|
|
|
// Forward declarations for filter validation
|
|
|
|
|
int validate_filter_values(cJSON* filter_json, char* error_message, size_t error_size);
|
|
|
|
|
int validate_hex_string(const char* str, size_t expected_len, const char* field_name, char* error_message, size_t error_size);
|
|
|
|
|
int validate_timestamp_range(long since, long until, char* error_message, size_t error_size);
|
|
|
|
|
int validate_numeric_limits(int limit, char* error_message, size_t error_size);
|
|
|
|
|
int validate_search_term(const char* search_term, char* error_message, size_t error_size);
|
|
|
|
|
|
|
|
|
|
// Global database variable
|
|
|
|
|
extern sqlite3* g_db;
|
|
|
|
|
|
|
|
|
|
@@ -42,7 +48,14 @@ subscription_filter_t* create_subscription_filter(cJSON* filter_json) {
|
|
|
|
|
if (!filter_json || !cJSON_IsObject(filter_json)) {
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Validate filter values before creating the filter
|
|
|
|
|
char error_message[512] = {0};
|
|
|
|
|
if (!validate_filter_values(filter_json, error_message, sizeof(error_message))) {
|
|
|
|
|
log_warning(error_message);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
subscription_filter_t* filter = calloc(1, sizeof(subscription_filter_t));
|
|
|
|
|
if (!filter) {
|
|
|
|
|
return NULL;
|
|
|
|
|
@@ -111,28 +124,66 @@ void free_subscription_filter(subscription_filter_t* filter) {
|
|
|
|
|
free(filter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate subscription ID format and length
|
|
|
|
|
static int validate_subscription_id(const char* sub_id) {
|
|
|
|
|
if (!sub_id) {
|
|
|
|
|
return 0; // NULL pointer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t len = strlen(sub_id);
|
|
|
|
|
if (len == 0 || len >= SUBSCRIPTION_ID_MAX_LENGTH) {
|
|
|
|
|
return 0; // Empty or too long
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for valid characters (alphanumeric, underscore, hyphen)
|
|
|
|
|
for (size_t i = 0; i < len; i++) {
|
|
|
|
|
char c = sub_id[i];
|
|
|
|
|
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
|
|
|
(c >= '0' && c <= '9') || c == '_' || c == '-')) {
|
|
|
|
|
return 0; // Invalid character
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 1; // Valid
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a new subscription
|
|
|
|
|
subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip) {
|
|
|
|
|
if (!sub_id || !wsi || !filters_array) {
|
|
|
|
|
log_error("create_subscription: NULL parameter(s)");
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Validate subscription ID
|
|
|
|
|
if (!validate_subscription_id(sub_id)) {
|
|
|
|
|
log_error("create_subscription: invalid subscription ID format or length");
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
subscription_t* sub = calloc(1, sizeof(subscription_t));
|
|
|
|
|
if (!sub) {
|
|
|
|
|
log_error("create_subscription: failed to allocate subscription");
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Copy subscription ID (truncate if too long)
|
|
|
|
|
strncpy(sub->id, sub_id, SUBSCRIPTION_ID_MAX_LENGTH - 1);
|
|
|
|
|
sub->id[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0';
|
|
|
|
|
|
|
|
|
|
// Copy subscription ID safely (already validated)
|
|
|
|
|
size_t id_len = strlen(sub_id);
|
|
|
|
|
memcpy(sub->id, sub_id, id_len);
|
|
|
|
|
sub->id[id_len] = '\0';
|
|
|
|
|
|
|
|
|
|
// Set WebSocket connection
|
|
|
|
|
sub->wsi = wsi;
|
|
|
|
|
|
|
|
|
|
// Set client IP
|
|
|
|
|
// Set client IP safely
|
|
|
|
|
if (client_ip) {
|
|
|
|
|
strncpy(sub->client_ip, client_ip, CLIENT_IP_MAX_LENGTH - 1);
|
|
|
|
|
sub->client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
|
|
|
|
|
size_t ip_len = strlen(client_ip);
|
|
|
|
|
if (ip_len >= CLIENT_IP_MAX_LENGTH) {
|
|
|
|
|
ip_len = CLIENT_IP_MAX_LENGTH - 1;
|
|
|
|
|
}
|
|
|
|
|
memcpy(sub->client_ip, client_ip, ip_len);
|
|
|
|
|
sub->client_ip[ip_len] = '\0';
|
|
|
|
|
} else {
|
|
|
|
|
sub->client_ip[0] = '\0'; // Ensure null termination
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set timestamps and state
|
|
|
|
|
@@ -215,42 +266,61 @@ int add_subscription_to_manager(subscription_t* sub) {
|
|
|
|
|
|
|
|
|
|
// Remove subscription from global manager (thread-safe)
|
|
|
|
|
int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
|
|
|
|
|
if (!sub_id) return -1;
|
|
|
|
|
|
|
|
|
|
if (!sub_id) {
|
|
|
|
|
log_error("remove_subscription_from_manager: NULL subscription ID");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate subscription ID format
|
|
|
|
|
if (!validate_subscription_id(sub_id)) {
|
|
|
|
|
log_error("remove_subscription_from_manager: invalid subscription ID format");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
subscription_t** current = &g_subscription_manager.active_subscriptions;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while (*current) {
|
|
|
|
|
subscription_t* sub = *current;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Match by ID and WebSocket connection
|
|
|
|
|
if (strcmp(sub->id, sub_id) == 0 && (!wsi || sub->wsi == wsi)) {
|
|
|
|
|
// Remove from list
|
|
|
|
|
*current = sub->next;
|
|
|
|
|
g_subscription_manager.total_subscriptions--;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Copy data needed for logging before unlocking
|
|
|
|
|
char client_ip_copy[CLIENT_IP_MAX_LENGTH];
|
|
|
|
|
int events_sent_copy = sub->events_sent;
|
|
|
|
|
char sub_id_copy[SUBSCRIPTION_ID_MAX_LENGTH];
|
|
|
|
|
|
|
|
|
|
memcpy(client_ip_copy, sub->client_ip, CLIENT_IP_MAX_LENGTH);
|
|
|
|
|
memcpy(sub_id_copy, sub->id, SUBSCRIPTION_ID_MAX_LENGTH);
|
|
|
|
|
client_ip_copy[CLIENT_IP_MAX_LENGTH - 1] = '\0';
|
|
|
|
|
sub_id_copy[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0';
|
|
|
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
|
|
|
|
|
|
|
|
|
// Log subscription closure to database
|
|
|
|
|
log_subscription_closed(sub_id, sub->client_ip, "closed");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Log subscription closure to database (now safe)
|
|
|
|
|
log_subscription_closed(sub_id_copy, client_ip_copy, "closed");
|
|
|
|
|
|
|
|
|
|
// Update events sent counter before freeing
|
|
|
|
|
update_subscription_events_sent(sub_id, sub->events_sent);
|
|
|
|
|
update_subscription_events_sent(sub_id_copy, events_sent_copy);
|
|
|
|
|
|
|
|
|
|
free_subscription(sub);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
current = &(sub->next);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
char debug_msg[256];
|
|
|
|
|
snprintf(debug_msg, sizeof(debug_msg), "Subscription '%s' not found for removal", sub_id);
|
|
|
|
|
log_warning(debug_msg);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -493,13 +563,28 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
|
|
|
|
temp_sub_t* temp = malloc(sizeof(temp_sub_t));
|
|
|
|
|
if (temp) {
|
|
|
|
|
temp->wsi = sub->wsi;
|
|
|
|
|
strncpy(temp->id, sub->id, SUBSCRIPTION_ID_MAX_LENGTH - 1);
|
|
|
|
|
temp->id[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0';
|
|
|
|
|
strncpy(temp->client_ip, sub->client_ip, CLIENT_IP_MAX_LENGTH - 1);
|
|
|
|
|
temp->client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
|
|
|
|
|
|
|
|
|
|
// Safely copy subscription ID
|
|
|
|
|
size_t id_len = strlen(sub->id);
|
|
|
|
|
if (id_len >= SUBSCRIPTION_ID_MAX_LENGTH) {
|
|
|
|
|
id_len = SUBSCRIPTION_ID_MAX_LENGTH - 1;
|
|
|
|
|
}
|
|
|
|
|
memcpy(temp->id, sub->id, id_len);
|
|
|
|
|
temp->id[id_len] = '\0';
|
|
|
|
|
|
|
|
|
|
// Safely copy client IP
|
|
|
|
|
size_t ip_len = strlen(sub->client_ip);
|
|
|
|
|
if (ip_len >= CLIENT_IP_MAX_LENGTH) {
|
|
|
|
|
ip_len = CLIENT_IP_MAX_LENGTH - 1;
|
|
|
|
|
}
|
|
|
|
|
memcpy(temp->client_ip, sub->client_ip, ip_len);
|
|
|
|
|
temp->client_ip[ip_len] = '\0';
|
|
|
|
|
|
|
|
|
|
temp->next = matching_subs;
|
|
|
|
|
matching_subs = temp;
|
|
|
|
|
matching_count++;
|
|
|
|
|
} else {
|
|
|
|
|
log_error("broadcast_event_to_subscriptions: failed to allocate temp subscription");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sub = sub->next;
|
|
|
|
|
@@ -884,3 +969,330 @@ int get_active_connections_for_ip(const char* client_ip) {
|
|
|
|
|
pthread_mutex_unlock(&g_subscription_manager.ip_tracking_lock);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
// FILTER VALIDATION FUNCTIONS
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate hex string format and length
|
|
|
|
|
*/
|
|
|
|
|
int validate_hex_string(const char* str, size_t expected_len, const char* field_name, char* error_message, size_t error_size) {
|
|
|
|
|
if (!str) {
|
|
|
|
|
snprintf(error_message, error_size, "%s: null value", field_name);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t len = strlen(str);
|
|
|
|
|
if (len != expected_len) {
|
|
|
|
|
snprintf(error_message, error_size, "%s: invalid length %zu, expected %zu", field_name, len, expected_len);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for valid hex characters
|
|
|
|
|
for (size_t i = 0; i < len; i++) {
|
|
|
|
|
char c = str[i];
|
|
|
|
|
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
|
|
|
|
|
snprintf(error_message, error_size, "%s: invalid hex character '%c' at position %zu", field_name, c, i);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate timestamp range (since/until)
|
|
|
|
|
*/
|
|
|
|
|
int validate_timestamp_range(long since, long until, char* error_message, size_t error_size) {
|
|
|
|
|
// Allow zero values (not set)
|
|
|
|
|
if (since == 0 && until == 0) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for reasonable timestamp bounds (1970-01-01 to 2100-01-01)
|
|
|
|
|
if (since != 0 && (since < MIN_TIMESTAMP || since > MAX_TIMESTAMP)) {
|
|
|
|
|
snprintf(error_message, error_size, "since: timestamp %ld out of valid range", since);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (until != 0 && (until < MIN_TIMESTAMP || until > MAX_TIMESTAMP)) {
|
|
|
|
|
snprintf(error_message, error_size, "until: timestamp %ld out of valid range", until);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check that since is before until if both are set
|
|
|
|
|
if (since > 0 && until > 0 && since >= until) {
|
|
|
|
|
snprintf(error_message, error_size, "since (%ld) must be before until (%ld)", since, until);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate numeric limits
|
|
|
|
|
*/
|
|
|
|
|
int validate_numeric_limits(int limit, char* error_message, size_t error_size) {
|
|
|
|
|
// Allow zero (no limit)
|
|
|
|
|
if (limit == 0) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for reasonable limits (1-10000)
|
|
|
|
|
if (limit < MIN_LIMIT || limit > MAX_LIMIT) {
|
|
|
|
|
snprintf(error_message, error_size, "limit: value %d out of valid range [%d, %d]", limit, MIN_LIMIT, MAX_LIMIT);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate search term for SQL injection and length
|
|
|
|
|
*/
|
|
|
|
|
int validate_search_term(const char* search_term, char* error_message, size_t error_size) {
|
|
|
|
|
if (!search_term) {
|
|
|
|
|
return 1; // NULL search terms are allowed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t len = strlen(search_term);
|
|
|
|
|
|
|
|
|
|
// Check maximum length
|
|
|
|
|
if (len > MAX_SEARCH_TERM_LENGTH) {
|
|
|
|
|
snprintf(error_message, error_size, "search: term too long (%zu characters, max %d)", len, (int)MAX_SEARCH_TERM_LENGTH);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for potentially dangerous characters that could cause SQL issues
|
|
|
|
|
// Allow alphanumeric, spaces, and common punctuation
|
|
|
|
|
for (size_t i = 0; i < len; i++) {
|
|
|
|
|
char c = search_term[i];
|
|
|
|
|
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
|
|
|
(c >= '0' && c <= '9') || c == ' ' || c == '-' || c == '_' ||
|
|
|
|
|
c == '.' || c == ',' || c == '!' || c == '?' || c == ':' ||
|
|
|
|
|
c == ';' || c == '"' || c == '\'' || c == '(' || c == ')' ||
|
|
|
|
|
c == '[' || c == ']' || c == '{' || c == '}' || c == '@' ||
|
|
|
|
|
c == '#' || c == '$' || c == '%' || c == '^' || c == '&' ||
|
|
|
|
|
c == '*' || c == '+' || c == '=' || c == '|' || c == '\\' ||
|
|
|
|
|
c == '/' || c == '<' || c == '>' || c == '~' || c == '`')) {
|
|
|
|
|
// Reject control characters and other potentially problematic chars
|
|
|
|
|
if (c < 32 || c == 127) {
|
|
|
|
|
snprintf(error_message, error_size, "search: invalid character (ASCII %d) at position %zu", (int)c, i);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate all filter values in a filter object
|
|
|
|
|
*/
|
|
|
|
|
int validate_filter_values(cJSON* filter_json, char* error_message, size_t error_size) {
|
|
|
|
|
if (!filter_json || !cJSON_IsObject(filter_json)) {
|
|
|
|
|
snprintf(error_message, error_size, "filter must be a JSON object");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate kinds array
|
|
|
|
|
cJSON* kinds = cJSON_GetObjectItem(filter_json, "kinds");
|
|
|
|
|
if (kinds) {
|
|
|
|
|
if (!cJSON_IsArray(kinds)) {
|
|
|
|
|
snprintf(error_message, error_size, "kinds must be an array");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int kinds_count = cJSON_GetArraySize(kinds);
|
|
|
|
|
if (kinds_count > MAX_KINDS_PER_FILTER) {
|
|
|
|
|
snprintf(error_message, error_size, "kinds array too large (%d items, max %d)", kinds_count, MAX_KINDS_PER_FILTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < kinds_count; i++) {
|
|
|
|
|
cJSON* kind_item = cJSON_GetArrayItem(kinds, i);
|
|
|
|
|
if (!cJSON_IsNumber(kind_item)) {
|
|
|
|
|
snprintf(error_message, error_size, "kinds[%d] must be a number", i);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int kind_val = (int)cJSON_GetNumberValue(kind_item);
|
|
|
|
|
if (kind_val < 0 || kind_val > 65535) { // Reasonable range for event kinds
|
|
|
|
|
snprintf(error_message, error_size, "kinds[%d]: invalid event kind %d", i, kind_val);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate authors array
|
|
|
|
|
cJSON* authors = cJSON_GetObjectItem(filter_json, "authors");
|
|
|
|
|
if (authors) {
|
|
|
|
|
if (!cJSON_IsArray(authors)) {
|
|
|
|
|
snprintf(error_message, error_size, "authors must be an array");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int authors_count = cJSON_GetArraySize(authors);
|
|
|
|
|
if (authors_count > MAX_AUTHORS_PER_FILTER) {
|
|
|
|
|
snprintf(error_message, error_size, "authors array too large (%d items, max %d)", authors_count, MAX_AUTHORS_PER_FILTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < authors_count; i++) {
|
|
|
|
|
cJSON* author_item = cJSON_GetArrayItem(authors, i);
|
|
|
|
|
if (!cJSON_IsString(author_item)) {
|
|
|
|
|
snprintf(error_message, error_size, "authors[%d] must be a string", i);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* author_str = cJSON_GetStringValue(author_item);
|
|
|
|
|
// Allow partial pubkeys (prefix matching), so validate hex but allow shorter lengths
|
|
|
|
|
size_t author_len = strlen(author_str);
|
|
|
|
|
if (author_len == 0 || author_len > 64) {
|
|
|
|
|
snprintf(error_message, error_size, "authors[%d]: invalid length %zu", i, author_len);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate hex characters (allow partial)
|
|
|
|
|
for (size_t j = 0; j < author_len; j++) {
|
|
|
|
|
char c = author_str[j];
|
|
|
|
|
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
|
|
|
|
|
snprintf(error_message, error_size, "authors[%d]: invalid hex character '%c'", i, c);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate ids array
|
|
|
|
|
cJSON* ids = cJSON_GetObjectItem(filter_json, "ids");
|
|
|
|
|
if (ids) {
|
|
|
|
|
if (!cJSON_IsArray(ids)) {
|
|
|
|
|
snprintf(error_message, error_size, "ids must be an array");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int ids_count = cJSON_GetArraySize(ids);
|
|
|
|
|
if (ids_count > MAX_IDS_PER_FILTER) {
|
|
|
|
|
snprintf(error_message, error_size, "ids array too large (%d items, max %d)", ids_count, MAX_IDS_PER_FILTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < ids_count; i++) {
|
|
|
|
|
cJSON* id_item = cJSON_GetArrayItem(ids, i);
|
|
|
|
|
if (!cJSON_IsString(id_item)) {
|
|
|
|
|
snprintf(error_message, error_size, "ids[%d] must be a string", i);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* id_str = cJSON_GetStringValue(id_item);
|
|
|
|
|
// Allow partial IDs (prefix matching)
|
|
|
|
|
size_t id_len = strlen(id_str);
|
|
|
|
|
if (id_len == 0 || id_len > 64) {
|
|
|
|
|
snprintf(error_message, error_size, "ids[%d]: invalid length %zu", i, id_len);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate hex characters
|
|
|
|
|
for (size_t j = 0; j < id_len; j++) {
|
|
|
|
|
char c = id_str[j];
|
|
|
|
|
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
|
|
|
|
|
snprintf(error_message, error_size, "ids[%d]: invalid hex character '%c'", i, c);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate since/until timestamps
|
|
|
|
|
long since_val = 0, until_val = 0;
|
|
|
|
|
|
|
|
|
|
cJSON* since = cJSON_GetObjectItem(filter_json, "since");
|
|
|
|
|
if (since) {
|
|
|
|
|
if (!cJSON_IsNumber(since)) {
|
|
|
|
|
snprintf(error_message, error_size, "since must be a number");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
since_val = (long)cJSON_GetNumberValue(since);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON* until = cJSON_GetObjectItem(filter_json, "until");
|
|
|
|
|
if (until) {
|
|
|
|
|
if (!cJSON_IsNumber(until)) {
|
|
|
|
|
snprintf(error_message, error_size, "until must be a number");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
until_val = (long)cJSON_GetNumberValue(until);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!validate_timestamp_range(since_val, until_val, error_message, error_size)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate limit
|
|
|
|
|
cJSON* limit = cJSON_GetObjectItem(filter_json, "limit");
|
|
|
|
|
if (limit) {
|
|
|
|
|
if (!cJSON_IsNumber(limit)) {
|
|
|
|
|
snprintf(error_message, error_size, "limit must be a number");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int limit_val = (int)cJSON_GetNumberValue(limit);
|
|
|
|
|
if (!validate_numeric_limits(limit_val, error_message, error_size)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate search term
|
|
|
|
|
cJSON* search = cJSON_GetObjectItem(filter_json, "search");
|
|
|
|
|
if (search) {
|
|
|
|
|
if (!cJSON_IsString(search)) {
|
|
|
|
|
snprintf(error_message, error_size, "search must be a string");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* search_term = cJSON_GetStringValue(search);
|
|
|
|
|
if (!validate_search_term(search_term, error_message, error_size)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate tag filters (#e, #p, #t, etc.)
|
|
|
|
|
cJSON* item = NULL;
|
|
|
|
|
cJSON_ArrayForEach(item, filter_json) {
|
|
|
|
|
const char* key = item->string;
|
|
|
|
|
if (key && strlen(key) >= 2 && key[0] == '#') {
|
|
|
|
|
if (!cJSON_IsArray(item)) {
|
|
|
|
|
snprintf(error_message, error_size, "%s must be an array", key);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int tag_count = cJSON_GetArraySize(item);
|
|
|
|
|
if (tag_count > MAX_TAG_VALUES_PER_FILTER) {
|
|
|
|
|
snprintf(error_message, error_size, "%s array too large (%d items, max %d)", key, tag_count, MAX_TAG_VALUES_PER_FILTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < tag_count; i++) {
|
|
|
|
|
cJSON* tag_value = cJSON_GetArrayItem(item, i);
|
|
|
|
|
if (!cJSON_IsString(tag_value)) {
|
|
|
|
|
snprintf(error_message, error_size, "%s[%d] must be a string", key, i);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* tag_str = cJSON_GetStringValue(tag_value);
|
|
|
|
|
size_t tag_len = strlen(tag_str);
|
|
|
|
|
if (tag_len > MAX_TAG_VALUE_LENGTH) {
|
|
|
|
|
snprintf(error_message, error_size, "%s[%d]: tag value too long (%zu characters, max %d)", key, i, tag_len, MAX_TAG_VALUE_LENGTH);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|