v0.7.8 - Fully static builds implemented with musl-gcc

This commit is contained in:
Your Name
2025-10-11 10:51:03 -04:00
parent d449513861
commit ecd7095123
23 changed files with 3801 additions and 89 deletions

View File

@@ -86,6 +86,13 @@ int is_event_expired(cJSON* event, time_t current_time);
int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss);
int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss);
// Forward declarations for rate limiting
int is_client_rate_limited_for_malformed_requests(struct per_session_data *pss);
void record_malformed_request(struct per_session_data *pss);
// Forward declarations for filter validation
int validate_filter_array(cJSON* filters, char* error_message, size_t error_size);
// Forward declarations for NOTICE message support
void send_notice_message(struct lws* wsi, const char* message);
@@ -264,6 +271,12 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
case LWS_CALLBACK_RECEIVE:
if (len > 0) {
// Check if client is rate limited for malformed requests
if (is_client_rate_limited_for_malformed_requests(pss)) {
send_notice_message(wsi, "error: too many malformed requests - temporarily blocked");
return 0;
}
char *message = malloc(len + 1);
if (message) {
memcpy(message, in, len);
@@ -677,15 +690,62 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
free(message);
return 0;
}
// Handle REQ message
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
if (sub_id && cJSON_IsString(sub_id)) {
const char* subscription_id = cJSON_GetStringValue(sub_id);
// Validate subscription ID before processing
if (!subscription_id) {
send_notice_message(wsi, "error: invalid subscription ID");
log_warning("REQ rejected: NULL subscription ID");
record_malformed_request(pss);
cJSON_Delete(json);
free(message);
return 0;
}
// Check subscription ID format and length
size_t id_len = strlen(subscription_id);
if (id_len == 0 || id_len >= SUBSCRIPTION_ID_MAX_LENGTH) {
send_notice_message(wsi, "error: subscription ID too long or empty");
log_warning("REQ rejected: invalid subscription ID length");
cJSON_Delete(json);
free(message);
return 0;
}
// Validate characters in subscription ID
int valid_id = 1;
for (size_t i = 0; i < id_len; i++) {
char c = subscription_id[i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '-')) {
valid_id = 0;
break;
}
}
if (!valid_id) {
send_notice_message(wsi, "error: invalid characters in subscription ID");
log_warning("REQ rejected: invalid characters in subscription ID");
cJSON_Delete(json);
free(message);
return 0;
}
// Create array of filter objects from position 2 onwards
cJSON* filters = cJSON_CreateArray();
if (!filters) {
send_notice_message(wsi, "error: failed to process filters");
log_error("REQ failed: could not create filters array");
cJSON_Delete(json);
free(message);
return 0;
}
int json_size = cJSON_GetArraySize(json);
for (int i = 2; i < json_size; i++) {
cJSON* filter = cJSON_GetArrayItem(json, i);
@@ -693,29 +753,46 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1));
}
}
// Validate filters before processing
char filter_error[512] = {0};
if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) {
send_notice_message(wsi, filter_error);
log_warning("REQ rejected: invalid filters");
record_malformed_request(pss);
cJSON_Delete(filters);
cJSON_Delete(json);
free(message);
return 0;
}
handle_req_message(subscription_id, filters, wsi, pss);
// Clean up the filters array we created
cJSON_Delete(filters);
// Send EOSE (End of Stored Events)
cJSON* eose_response = cJSON_CreateArray();
cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE"));
cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id));
char *eose_str = cJSON_Print(eose_response);
if (eose_str) {
size_t eose_len = strlen(eose_str);
unsigned char *buf = malloc(LWS_PRE + eose_len);
if (buf) {
memcpy(buf + LWS_PRE, eose_str, eose_len);
lws_write(wsi, buf + LWS_PRE, eose_len, LWS_WRITE_TEXT);
free(buf);
if (eose_response) {
cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE"));
cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id));
char *eose_str = cJSON_Print(eose_response);
if (eose_str) {
size_t eose_len = strlen(eose_str);
unsigned char *buf = malloc(LWS_PRE + eose_len);
if (buf) {
memcpy(buf + LWS_PRE, eose_str, eose_len);
lws_write(wsi, buf + LWS_PRE, eose_len, LWS_WRITE_TEXT);
free(buf);
}
free(eose_str);
}
free(eose_str);
cJSON_Delete(eose_response);
}
cJSON_Delete(eose_response);
} else {
send_notice_message(wsi, "error: missing or invalid subscription ID in REQ");
log_warning("REQ rejected: missing or invalid subscription ID");
}
} else if (strcmp(msg_type, "COUNT") == 0) {
// Check NIP-42 authentication for COUNT requests if required
@@ -747,6 +824,18 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
}
}
// Validate filters before processing
char filter_error[512] = {0};
if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) {
send_notice_message(wsi, filter_error);
log_warning("COUNT rejected: invalid filters");
record_malformed_request(pss);
cJSON_Delete(filters);
cJSON_Delete(json);
free(message);
return 0;
}
handle_count_message(subscription_id, filters, wsi, pss);
// Clean up the filters array we created
@@ -757,14 +846,52 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
if (sub_id && cJSON_IsString(sub_id)) {
const char* subscription_id = cJSON_GetStringValue(sub_id);
// Validate subscription ID before processing
if (!subscription_id) {
send_notice_message(wsi, "error: invalid subscription ID in CLOSE");
log_warning("CLOSE rejected: NULL subscription ID");
cJSON_Delete(json);
free(message);
return 0;
}
// Check subscription ID format and length
size_t id_len = strlen(subscription_id);
if (id_len == 0 || id_len >= SUBSCRIPTION_ID_MAX_LENGTH) {
send_notice_message(wsi, "error: subscription ID too long or empty in CLOSE");
log_warning("CLOSE rejected: invalid subscription ID length");
cJSON_Delete(json);
free(message);
return 0;
}
// Validate characters in subscription ID
int valid_id = 1;
for (size_t i = 0; i < id_len; i++) {
char c = subscription_id[i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '-')) {
valid_id = 0;
break;
}
}
if (!valid_id) {
send_notice_message(wsi, "error: invalid characters in subscription ID for CLOSE");
log_warning("CLOSE rejected: invalid characters in subscription ID");
cJSON_Delete(json);
free(message);
return 0;
}
// Remove from global manager
remove_subscription_from_manager(subscription_id, wsi);
// Remove from session list if present
if (pss) {
pthread_mutex_lock(&pss->session_lock);
struct subscription** current = &pss->subscriptions;
while (*current) {
if (strcmp((*current)->id, subscription_id) == 0) {
@@ -775,11 +902,14 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
}
current = &((*current)->session_next);
}
pthread_mutex_unlock(&pss->session_lock);
}
// Subscription closed
} else {
send_notice_message(wsi, "error: missing or invalid subscription ID in CLOSE");
log_warning("CLOSE rejected: missing or invalid subscription ID");
}
} else if (strcmp(msg_type, "AUTH") == 0) {
// Handle NIP-42 AUTH message
@@ -1511,3 +1641,270 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
return total_count;
}
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// RATE LIMITING FUNCTIONS
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
/**
* Check if a client is currently rate limited for malformed requests
*/
int is_client_rate_limited_for_malformed_requests(struct per_session_data *pss) {
if (!pss) {
return 0;
}
time_t now = time(NULL);
// Check if currently blocked
if (pss->malformed_request_blocked_until > now) {
return 1;
}
// Reset block if expired
if (pss->malformed_request_blocked_until > 0 && pss->malformed_request_blocked_until <= now) {
pss->malformed_request_blocked_until = 0;
pss->malformed_request_count = 0;
pss->malformed_request_window_start = now;
}
// Check if within current hour window
if (pss->malformed_request_window_start == 0 ||
(now - pss->malformed_request_window_start) >= 3600) { // 1 hour
// Start new window
pss->malformed_request_window_start = now;
pss->malformed_request_count = 0;
}
// Check if exceeded limit
if (pss->malformed_request_count >= MAX_MALFORMED_REQUESTS_PER_HOUR) {
// Block for the specified duration
pss->malformed_request_blocked_until = now + MALFORMED_REQUEST_BLOCK_DURATION;
log_warning("Client rate limited for malformed requests");
return 1;
}
return 0;
}
/**
* Record a malformed request for rate limiting purposes
*/
void record_malformed_request(struct per_session_data *pss) {
if (!pss) {
return;
}
time_t now = time(NULL);
// Initialize window if needed
if (pss->malformed_request_window_start == 0) {
pss->malformed_request_window_start = now;
pss->malformed_request_count = 0;
}
// Reset window if hour has passed
if ((now - pss->malformed_request_window_start) >= 3600) {
pss->malformed_request_window_start = now;
pss->malformed_request_count = 0;
}
// Increment count
pss->malformed_request_count++;
}
/**
* Validate if a string is valid hexadecimal of specified length
*/
int is_valid_hex_string(const char* str, size_t expected_len) {
if (!str || strlen(str) != expected_len) {
return 0;
}
for (size_t i = 0; i < expected_len; i++) {
char c = str[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
return 0;
}
}
return 1;
}
/**
* Validate a filter array for REQ and COUNT messages
*/
int validate_filter_array(cJSON* filters, char* error_message, size_t error_size) {
if (!filters || !cJSON_IsArray(filters)) {
snprintf(error_message, error_size, "error: filters must be an array");
return 0;
}
int filter_count = cJSON_GetArraySize(filters);
if (filter_count > MAX_FILTERS_PER_REQUEST) {
snprintf(error_message, error_size, "error: too many filters (max %d)", MAX_FILTERS_PER_REQUEST);
return 0;
}
// Validate each filter object
for (int i = 0; i < filter_count; i++) {
cJSON* filter = cJSON_GetArrayItem(filters, i);
if (!filter || !cJSON_IsObject(filter)) {
snprintf(error_message, error_size, "error: filter %d is not an object", i);
return 0;
}
// Validate filter fields
cJSON* filter_item = NULL;
cJSON_ArrayForEach(filter_item, filter) {
const char* key = filter_item->string;
if (!key) continue;
// Validate authors array
if (strcmp(key, "authors") == 0) {
if (!cJSON_IsArray(filter_item)) {
snprintf(error_message, error_size, "error: authors must be an array");
return 0;
}
int author_count = cJSON_GetArraySize(filter_item);
if (author_count > MAX_AUTHORS_PER_FILTER) {
snprintf(error_message, error_size, "error: too many authors (max %d)", MAX_AUTHORS_PER_FILTER);
return 0;
}
for (int j = 0; j < author_count; j++) {
cJSON* author = cJSON_GetArrayItem(filter_item, j);
if (!cJSON_IsString(author)) {
snprintf(error_message, error_size, "error: author %d is not a string", j);
return 0;
}
const char* author_str = cJSON_GetStringValue(author);
if (!is_valid_hex_string(author_str, 64)) {
snprintf(error_message, error_size, "error: invalid author hex string");
return 0;
}
}
}
// Validate ids array
else if (strcmp(key, "ids") == 0) {
if (!cJSON_IsArray(filter_item)) {
snprintf(error_message, error_size, "error: ids must be an array");
return 0;
}
int id_count = cJSON_GetArraySize(filter_item);
if (id_count > MAX_IDS_PER_FILTER) {
snprintf(error_message, error_size, "error: too many ids (max %d)", MAX_IDS_PER_FILTER);
return 0;
}
for (int j = 0; j < id_count; j++) {
cJSON* id = cJSON_GetArrayItem(filter_item, j);
if (!cJSON_IsString(id)) {
snprintf(error_message, error_size, "error: id %d is not a string", j);
return 0;
}
const char* id_str = cJSON_GetStringValue(id);
if (!is_valid_hex_string(id_str, 64)) {
snprintf(error_message, error_size, "error: invalid id hex string");
return 0;
}
}
}
// Validate kinds array
else if (strcmp(key, "kinds") == 0) {
if (!cJSON_IsArray(filter_item)) {
snprintf(error_message, error_size, "error: kinds must be an array");
return 0;
}
int kind_count = cJSON_GetArraySize(filter_item);
if (kind_count > MAX_KINDS_PER_FILTER) {
snprintf(error_message, error_size, "error: too many kinds (max %d)", MAX_KINDS_PER_FILTER);
return 0;
}
for (int j = 0; j < kind_count; j++) {
cJSON* kind = cJSON_GetArrayItem(filter_item, j);
if (!cJSON_IsNumber(kind)) {
snprintf(error_message, error_size, "error: kind %d is not a number", j);
return 0;
}
int kind_val = (int)cJSON_GetNumberValue(kind);
if (kind_val < 0 || kind_val > MAX_KIND_VALUE) {
snprintf(error_message, error_size, "error: invalid kind value %d", kind_val);
return 0;
}
}
}
// Validate since/until timestamps
else if (strcmp(key, "since") == 0 || strcmp(key, "until") == 0) {
if (!cJSON_IsNumber(filter_item)) {
snprintf(error_message, error_size, "error: %s must be a number", key);
return 0;
}
double timestamp = cJSON_GetNumberValue(filter_item);
if (timestamp < 0 || timestamp > MAX_TIMESTAMP_VALUE) {
snprintf(error_message, error_size, "error: invalid %s timestamp", key);
return 0;
}
}
// Validate limit
else if (strcmp(key, "limit") == 0) {
if (!cJSON_IsNumber(filter_item)) {
snprintf(error_message, error_size, "error: limit must be a number");
return 0;
}
int limit_val = (int)cJSON_GetNumberValue(filter_item);
if (limit_val < 0 || limit_val > MAX_LIMIT_VALUE) {
snprintf(error_message, error_size, "error: invalid limit value %d", limit_val);
return 0;
}
}
// Validate search term
else if (strcmp(key, "search") == 0) {
if (!cJSON_IsString(filter_item)) {
snprintf(error_message, error_size, "error: search must be a string");
return 0;
}
const char* search_str = cJSON_GetStringValue(filter_item);
size_t search_len = strlen(search_str);
if (search_len > MAX_SEARCH_LENGTH) {
snprintf(error_message, error_size, "error: search term too long (max %d)", MAX_SEARCH_LENGTH);
return 0;
}
// Check for SQL injection characters
if (strchr(search_str, ';') || strstr(search_str, "--") || strstr(search_str, "/*") || strstr(search_str, "*/")) {
snprintf(error_message, error_size, "error: invalid characters in search term");
return 0;
}
}
// Validate tag filters (#e, #p, #t, etc.)
else if (key[0] == '#' && strlen(key) > 1) {
if (!cJSON_IsArray(filter_item)) {
snprintf(error_message, error_size, "error: %s must be an array", key);
return 0;
}
int tag_count = cJSON_GetArraySize(filter_item);
if (tag_count > MAX_TAG_VALUES_PER_FILTER) {
snprintf(error_message, error_size, "error: too many %s values (max %d)", key, MAX_TAG_VALUES_PER_FILTER);
return 0;
}
for (int j = 0; j < tag_count; j++) {
cJSON* tag_value = cJSON_GetArrayItem(filter_item, j);
if (!cJSON_IsString(tag_value)) {
snprintf(error_message, error_size, "error: %s[%d] is not a string", key, j);
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, "error: %s value too long (max %d)", key, MAX_TAG_VALUE_LENGTH);
return 0;
}
}
}
// Unknown filter keys are allowed but ignored
}
}
return 1; // All filters valid
}