#include #include #include #include #include #ifdef __linux__ #include #else #include #endif #include #include "ginxsom.h" // Database path (consistent with main.c) #define DB_PATH "db/ginxsom.db" // Function declarations (moved from admin_api.h) void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated); void handle_stats_api(void); void handle_config_get_api(void); void handle_config_put_api(void); void handle_config_key_put_api(const char* key); void handle_files_api(void); void handle_health_api(void); int authenticate_admin_request(const char* auth_header); int is_admin_enabled(void); int verify_admin_pubkey(const char* event_pubkey); void send_json_response(int status, const char* json_content); void send_json_error(int status, const char* error, const char* message); int parse_query_params(const char* query_string, char params[][256], int max_params); // Forward declarations for local utility functions static int admin_nip94_get_origin(char* out, size_t out_size); static void admin_nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size); static const char* admin_mime_to_extension(const char* mime_type); // Local utility functions (from main.c but implemented here for admin API) static int admin_nip94_get_origin(char* out, size_t out_size) { if (!out || out_size == 0) { return 0; } sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { // Default on DB error strncpy(out, "http://localhost:9001", out_size - 1); out[out_size - 1] = '\0'; return 1; } const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char* value = (const char*)sqlite3_column_text(stmt, 0); if (value) { strncpy(out, value, out_size - 1); out[out_size - 1] = '\0'; sqlite3_finalize(stmt); sqlite3_close(db); return 1; } } sqlite3_finalize(stmt); } sqlite3_close(db); // Default fallback strncpy(out, "http://localhost:9001", out_size - 1); out[out_size - 1] = '\0'; return 1; } static void admin_nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size) { if (!origin || !sha256 || !out || out_size == 0) { return; } // Use local admin implementation for extension mapping const char* extension = admin_mime_to_extension(mime_type); snprintf(out, out_size, "%s/%s%s", origin, sha256, extension); } // Centralized MIME type to file extension mapping (from main.c) static const char* admin_mime_to_extension(const char* mime_type) { if (!mime_type) { return ".bin"; } if (strstr(mime_type, "image/jpeg")) { return ".jpg"; } else if (strstr(mime_type, "image/webp")) { return ".webp"; } else if (strstr(mime_type, "image/png")) { return ".png"; } else if (strstr(mime_type, "image/gif")) { return ".gif"; } else if (strstr(mime_type, "video/mp4")) { return ".mp4"; } else if (strstr(mime_type, "video/webm")) { return ".webm"; } else if (strstr(mime_type, "audio/mpeg")) { return ".mp3"; } else if (strstr(mime_type, "audio/ogg")) { return ".ogg"; } else if (strstr(mime_type, "text/plain")) { return ".txt"; } else if (strstr(mime_type, "application/pdf")) { return ".pdf"; } else { return ".bin"; } } // Main API request handler void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated) { const char* path = uri + 4; // Skip "/api" // Check if admin interface is enabled if (!is_admin_enabled()) { send_json_error(503, "admin_disabled", "Admin interface is disabled"); return; } // Authentication now handled by centralized validation system // Health endpoint is exempt from authentication requirement if (strcmp(path, "/health") != 0) { if (!is_authenticated || !validated_pubkey) { send_json_error(401, "admin_auth_required", "Valid admin authentication required"); return; } // Verify the authenticated pubkey has admin privileges if (!verify_admin_pubkey(validated_pubkey)) { send_json_error(403, "admin_forbidden", "Admin privileges required"); return; } } // Route to appropriate handler if (strcmp(method, "GET") == 0) { if (strcmp(path, "/stats") == 0) { handle_stats_api(); } else if (strcmp(path, "/config") == 0) { handle_config_get_api(); } else if (strncmp(path, "/files", 6) == 0) { handle_files_api(); } else if (strcmp(path, "/health") == 0) { handle_health_api(); } else { send_json_error(404, "not_found", "API endpoint not found"); } } else if (strcmp(method, "PUT") == 0) { if (strcmp(path, "/config") == 0) { handle_config_put_api(); } else if (strncmp(path, "/config/", 8) == 0) { const char* key = path + 8; // Skip "/config/" if (strlen(key) > 0) { handle_config_key_put_api(key); } else { send_json_error(400, "invalid_key", "Configuration key cannot be empty"); } } else { send_json_error(405, "method_not_allowed", "Method not allowed"); } } else { send_json_error(405, "method_not_allowed", "Method not allowed"); } } // Admin authentication functions int authenticate_admin_request(const char* auth_header) { if (!auth_header) { return 0; // No auth header } // NOTE: Authentication now handled by centralized validation system in main.c // This function is kept for compatibility but should receive validation results // from the centralized system rather than performing duplicate validation // TODO: Modify to accept validation results from centralized system // For now, assume validation was successful if we reach here // and extract pubkey from global context or parameters return 0; // Temporarily disabled - needs integration with centralized system } int verify_admin_pubkey(const char* event_pubkey) { if (!event_pubkey) { return 0; } sqlite3* db; sqlite3_stmt* stmt; int rc, is_admin = 0; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 0; } const char* sql = "SELECT value FROM config WHERE key = 'admin_pubkey'"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char* admin_pubkey = (const char*)sqlite3_column_text(stmt, 0); if (admin_pubkey && strcmp(event_pubkey, admin_pubkey) == 0) { is_admin = 1; } } sqlite3_finalize(stmt); } sqlite3_close(db); return is_admin; } int is_admin_enabled(void) { sqlite3* db; sqlite3_stmt* stmt; int rc, enabled = 0; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 0; // Default disabled if can't access DB } const char* sql = "SELECT value FROM config WHERE key = 'admin_enabled'"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char* value = (const char*)sqlite3_column_text(stmt, 0); enabled = (value && strcmp(value, "true") == 0) ? 1 : 0; } sqlite3_finalize(stmt); } sqlite3_close(db); return enabled; } // Individual endpoint handlers void handle_stats_api(void) { sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { send_json_error(500, "database_error", "Failed to open database"); return; } // Create consolidated statistics view if it doesn't exist const char* create_view = "CREATE VIEW IF NOT EXISTS storage_stats AS " "SELECT " " COUNT(*) as total_blobs, " " SUM(size) as total_bytes, " " AVG(size) as avg_blob_size, " " COUNT(DISTINCT uploader_pubkey) as unique_uploaders, " " MIN(uploaded_at) as first_upload, " " MAX(uploaded_at) as last_upload " "FROM blobs"; rc = sqlite3_exec(db, create_view, NULL, NULL, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); send_json_error(500, "database_error", "Failed to create stats view"); return; } // Query storage_stats view const char* sql = "SELECT total_blobs, total_bytes, avg_blob_size, " "unique_uploaders, first_upload, last_upload FROM storage_stats"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); send_json_error(500, "database_error", "Failed to prepare query"); return; } cJSON* response = cJSON_CreateObject(); cJSON* data = cJSON_CreateObject(); cJSON_AddStringToObject(response, "status", "success"); cJSON_AddItemToObject(response, "data", data); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { int total_files = sqlite3_column_int(stmt, 0); long long total_bytes = sqlite3_column_int64(stmt, 1); double avg_size = sqlite3_column_double(stmt, 2); int unique_uploaders = sqlite3_column_int(stmt, 3); cJSON_AddNumberToObject(data, "total_files", total_files); cJSON_AddNumberToObject(data, "total_bytes", (double)total_bytes); cJSON_AddNumberToObject(data, "total_size_mb", (double)total_bytes / (1024 * 1024)); cJSON_AddNumberToObject(data, "unique_uploaders", unique_uploaders); cJSON_AddNumberToObject(data, "first_upload", sqlite3_column_int64(stmt, 4)); cJSON_AddNumberToObject(data, "last_upload", sqlite3_column_int64(stmt, 5)); cJSON_AddNumberToObject(data, "avg_file_size", avg_size); // Get file type distribution sqlite3_stmt* type_stmt; const char* type_sql = "SELECT type, COUNT(*) FROM blobs GROUP BY type ORDER BY COUNT(*) DESC LIMIT 5"; cJSON* file_types = cJSON_CreateObject(); rc = sqlite3_prepare_v2(db, type_sql, -1, &type_stmt, NULL); if (rc == SQLITE_OK) { while (sqlite3_step(type_stmt) == SQLITE_ROW) { const char* type_name = (const char*)sqlite3_column_text(type_stmt, 0); int count = sqlite3_column_int(type_stmt, 1); cJSON_AddNumberToObject(file_types, type_name ? type_name : "unknown", count); } sqlite3_finalize(type_stmt); } cJSON_AddItemToObject(data, "file_types", file_types); } else { // No data - return zeros cJSON_AddNumberToObject(data, "total_files", 0); cJSON_AddNumberToObject(data, "total_bytes", 0); cJSON_AddNumberToObject(data, "total_size_mb", 0.0); cJSON_AddNumberToObject(data, "unique_uploaders", 0); cJSON_AddNumberToObject(data, "avg_file_size", 0); cJSON_AddItemToObject(data, "file_types", cJSON_CreateObject()); } sqlite3_finalize(stmt); sqlite3_close(db); char* response_str = cJSON_PrintUnformatted(response); send_json_response(200, response_str); free(response_str); cJSON_Delete(response); } void handle_config_get_api(void) { sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { send_json_error(500, "database_error", "Failed to open database"); return; } cJSON* response = cJSON_CreateObject(); cJSON* data = cJSON_CreateObject(); cJSON_AddStringToObject(response, "status", "success"); cJSON_AddItemToObject(response, "data", data); // Query all config settings const char* sql = "SELECT key, value FROM config ORDER BY key"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { const char* key = (const char*)sqlite3_column_text(stmt, 0); const char* value = (const char*)sqlite3_column_text(stmt, 1); if (key && value) { cJSON_AddStringToObject(data, key, value); } } sqlite3_finalize(stmt); } sqlite3_close(db); char* response_str = cJSON_PrintUnformatted(response); send_json_response(200, response_str); free(response_str); cJSON_Delete(response); } void handle_config_put_api(void) { // Read request body const char* content_length_str = getenv("CONTENT_LENGTH"); if (!content_length_str) { send_json_error(411, "length_required", "Content-Length header required"); return; } long content_length = atol(content_length_str); if (content_length <= 0 || content_length > 4096) { send_json_error(400, "invalid_content_length", "Invalid content length"); return; } char* json_body = malloc(content_length + 1); if (!json_body) { send_json_error(500, "memory_error", "Failed to allocate memory"); return; } size_t bytes_read = fread(json_body, 1, content_length, stdin); if (bytes_read != (size_t)content_length) { free(json_body); send_json_error(400, "incomplete_body", "Failed to read complete request body"); return; } json_body[content_length] = '\0'; // Parse JSON cJSON* config_data = cJSON_Parse(json_body); if (!config_data) { free(json_body); send_json_error(400, "invalid_json", "Invalid JSON in request body"); return; } // Update database sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); if (rc) { free(json_body); cJSON_Delete(config_data); send_json_error(500, "database_error", "Failed to open database"); return; } // Collect updated keys for response cJSON* updated_keys = cJSON_CreateArray(); // Update each config value const char* update_sql = "INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)"; cJSON* item = NULL; cJSON_ArrayForEach(item, config_data) { if (cJSON_IsString(item) && item->string) { rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, item->string, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(item), -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 3, time(NULL)); rc = sqlite3_step(stmt); if (rc == SQLITE_DONE) { cJSON_AddItemToArray(updated_keys, cJSON_CreateString(item->string)); } sqlite3_finalize(stmt); } } } free(json_body); cJSON_Delete(config_data); sqlite3_close(db); // Send response cJSON* response = cJSON_CreateObject(); cJSON_AddStringToObject(response, "status", "success"); cJSON_AddStringToObject(response, "message", "Configuration updated successfully"); cJSON_AddItemToObject(response, "updated_keys", updated_keys); char* response_str = cJSON_PrintUnformatted(response); send_json_response(200, response_str); free(response_str); cJSON_Delete(response); // Force cache refresh after configuration update nostr_request_validator_force_cache_refresh(); } void handle_config_key_put_api(const char* key) { if (!key || strlen(key) == 0) { send_json_error(400, "invalid_key", "Configuration key cannot be empty"); return; } // Read request body const char* content_length_str = getenv("CONTENT_LENGTH"); if (!content_length_str) { send_json_error(411, "length_required", "Content-Length header required"); return; } long content_length = atol(content_length_str); if (content_length <= 0 || content_length > 4096) { send_json_error(400, "invalid_content_length", "Invalid content length"); return; } char* json_body = malloc(content_length + 1); if (!json_body) { send_json_error(500, "memory_error", "Failed to allocate memory"); return; } size_t bytes_read = fread(json_body, 1, content_length, stdin); if (bytes_read != (size_t)content_length) { free(json_body); send_json_error(400, "incomplete_body", "Failed to read complete request body"); return; } json_body[content_length] = '\0'; // Parse JSON - expect {"value": "..."} cJSON* request_data = cJSON_Parse(json_body); if (!request_data) { free(json_body); send_json_error(400, "invalid_json", "Invalid JSON in request body"); return; } cJSON* value_item = cJSON_GetObjectItem(request_data, "value"); if (!cJSON_IsString(value_item)) { free(json_body); cJSON_Delete(request_data); send_json_error(400, "missing_value", "Request must contain 'value' field"); return; } const char* value = cJSON_GetStringValue(value_item); if (!value) { free(json_body); cJSON_Delete(request_data); send_json_error(400, "invalid_value", "Value must be a string"); return; } // Make a safe copy of the value string BEFORE deleting cJSON object char safe_value[256]; strncpy(safe_value, value, sizeof(safe_value) - 1); safe_value[sizeof(safe_value) - 1] = '\0'; // Update database sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); if (rc) { free(json_body); cJSON_Delete(request_data); send_json_error(500, "database_error", "Failed to open database"); return; } // Update or insert the config value const char* update_sql = "INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)"; rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { free(json_body); cJSON_Delete(request_data); sqlite3_close(db); send_json_error(500, "database_error", "Failed to prepare update statement"); return; } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, safe_value, -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 3, time(NULL)); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); sqlite3_close(db); free(json_body); cJSON_Delete(request_data); if (rc != SQLITE_DONE) { send_json_error(500, "database_error", "Failed to update configuration"); return; } cJSON* response = cJSON_CreateObject(); cJSON_AddStringToObject(response, "status", "success"); cJSON_AddStringToObject(response, "message", "Configuration updated successfully"); cJSON_AddStringToObject(response, "key", key); cJSON_AddStringToObject(response, "value", safe_value); char* response_str = cJSON_PrintUnformatted(response); send_json_response(200, response_str); free(response_str); cJSON_Delete(response); // Force cache refresh after configuration update nostr_request_validator_force_cache_refresh(); } void handle_files_api(void) { // Parse query parameters for pagination const char* query_string = getenv("QUERY_STRING"); int limit = 50; int offset = 0; if (query_string) { char params[10][256]; int param_count = parse_query_params(query_string, params, 10); for (int i = 0; i < param_count; i++) { char* key = params[i]; char* value = strchr(key, '='); if (value) { *value++ = '\0'; if (strcmp(key, "limit") == 0) { limit = atoi(value); if (limit <= 0 || limit > 200) limit = 50; } else if (strcmp(key, "offset") == 0) { offset = atoi(value); if (offset < 0) offset = 0; } } } } sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { send_json_error(500, "database_error", "Failed to open database"); return; } // Query recent files with pagination const char* sql = "SELECT sha256, size, type, uploaded_at, uploader_pubkey, filename " "FROM blobs ORDER BY uploaded_at DESC LIMIT ? OFFSET ?"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); send_json_error(500, "database_error", "Failed to prepare query"); return; } sqlite3_bind_int(stmt, 1, limit); sqlite3_bind_int(stmt, 2, offset); cJSON* response = cJSON_CreateObject(); cJSON* data = cJSON_CreateObject(); cJSON* files_array = cJSON_CreateArray(); cJSON_AddStringToObject(response, "status", "success"); cJSON_AddItemToObject(response, "data", data); cJSON_AddItemToObject(data, "files", files_array); cJSON_AddNumberToObject(data, "limit", limit); cJSON_AddNumberToObject(data, "offset", offset); int total_count = 0; while (sqlite3_step(stmt) == SQLITE_ROW) { total_count++; cJSON* file_obj = cJSON_CreateObject(); cJSON_AddItemToArray(files_array, file_obj); const char* sha256 = (const char*)sqlite3_column_text(stmt, 0); const char* type = (const char*)sqlite3_column_text(stmt, 2); const char* filename = (const char*)sqlite3_column_text(stmt, 5); cJSON_AddStringToObject(file_obj, "sha256", sha256 ? sha256 : ""); cJSON_AddNumberToObject(file_obj, "size", sqlite3_column_int64(stmt, 1)); cJSON_AddStringToObject(file_obj, "type", type ? type : ""); cJSON_AddNumberToObject(file_obj, "uploaded_at", sqlite3_column_int64(stmt, 3)); const char* uploader = (const char*)sqlite3_column_text(stmt, 4); cJSON_AddStringToObject(file_obj, "uploader_pubkey", uploader ? uploader : ""); cJSON_AddStringToObject(file_obj, "filename", filename ? filename : ""); // Build URL for file char url[1024]; if (type && sha256) { // Use local admin implementation for URL building char origin_url[256]; admin_nip94_get_origin(origin_url, sizeof(origin_url)); admin_nip94_build_blob_url(origin_url, sha256, type, url, sizeof(url)); cJSON_AddStringToObject(file_obj, "url", url); } } // Get total count for pagination info const char* count_sql = "SELECT COUNT(*) FROM blobs"; sqlite3_stmt* count_stmt; rc = sqlite3_prepare_v2(db, count_sql, -1, &count_stmt, NULL); if (rc == SQLITE_OK) { rc = sqlite3_step(count_stmt); if (rc == SQLITE_ROW) { int total = sqlite3_column_int(count_stmt, 0); cJSON_AddNumberToObject(data, "total", total); } sqlite3_finalize(count_stmt); } sqlite3_finalize(stmt); sqlite3_close(db); char* response_str = cJSON_PrintUnformatted(response); send_json_response(200, response_str); free(response_str); cJSON_Delete(response); } void handle_health_api(void) { cJSON* response = cJSON_CreateObject(); cJSON* data = cJSON_CreateObject(); cJSON_AddStringToObject(response, "status", "success"); cJSON_AddItemToObject(response, "data", data); // Check database connection sqlite3* db; int rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc == SQLITE_OK) { cJSON_AddStringToObject(data, "database", "connected"); sqlite3_close(db); } else { cJSON_AddStringToObject(data, "database", "disconnected"); } // Check blob directory struct stat st; if (stat("blobs", &st) == 0 && S_ISDIR(st.st_mode)) { cJSON_AddStringToObject(data, "blob_directory", "accessible"); } else { cJSON_AddStringToObject(data, "blob_directory", "inaccessible"); } // Get disk usage cJSON* disk_usage = cJSON_CreateObject(); struct statvfs vfs; if (statvfs(".", &vfs) == 0) { unsigned long long total_bytes = (unsigned long long)vfs.f_blocks * vfs.f_frsize; unsigned long long free_bytes = (unsigned long long)vfs.f_bavail * vfs.f_frsize; unsigned long long used_bytes = total_bytes - free_bytes; double usage_percent = (double)used_bytes / (double)total_bytes * 100.0; cJSON_AddNumberToObject(disk_usage, "total_bytes", (double)total_bytes); cJSON_AddNumberToObject(disk_usage, "used_bytes", (double)used_bytes); cJSON_AddNumberToObject(disk_usage, "available_bytes", (double)free_bytes); cJSON_AddNumberToObject(disk_usage, "usage_percent", usage_percent); } cJSON_AddItemToObject(data, "disk_usage", disk_usage); // Add server info cJSON_AddNumberToObject(data, "server_time", (double)time(NULL)); cJSON_AddNumberToObject(data, "uptime", 0); // Would need to track process start time char* response_str = cJSON_PrintUnformatted(response); send_json_response(200, response_str); free(response_str); cJSON_Delete(response); } // Utility functions void send_json_response(int status, const char* json_content) { printf("Status: %d OK\r\n", status); printf("Content-Type: application/json\r\n"); printf("Cache-Control: no-cache\r\n"); printf("\r\n"); printf("%s\n", json_content); } void send_json_error(int status, const char* error, const char* message) { cJSON* response = cJSON_CreateObject(); cJSON_AddStringToObject(response, "status", "error"); cJSON_AddStringToObject(response, "error", error); cJSON_AddStringToObject(response, "message", message); char* response_str = cJSON_PrintUnformatted(response); printf("Status: %d %s\r\n", status, status == 400 ? "Bad Request" : status == 401 ? "Unauthorized" : status == 403 ? "Forbidden" : status == 404 ? "Not Found" : status == 500 ? "Internal Server Error" : status == 503 ? "Service Unavailable" : "Error"); printf("Content-Type: application/json\r\n"); printf("Cache-Control: no-cache\r\n"); printf("\r\n"); printf("%s\n", response_str); free(response_str); cJSON_Delete(response); } int parse_query_params(const char* query_string, char params[][256], int max_params) { if (!query_string || !params) return 0; size_t query_len = strlen(query_string); char* query_copy = malloc(query_len + 1); if (!query_copy) return 0; memcpy(query_copy, query_string, query_len + 1); int count = 0; char* token = strtok(query_copy, "&"); while (token && count < max_params) { if (strlen(token) >= sizeof(params[0])) { token[sizeof(params[0]) - 1] = '\0'; } strcpy(params[count], token); count++; token = strtok(NULL, "&"); } free(query_copy); return count; }