v0.7.8 - Fully static builds implemented with musl-gcc
This commit is contained in:
445
src/websockets.c
445
src/websockets.c
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user