diff options
Diffstat (limited to 'main/relay_selector.c')
| -rw-r--r-- | main/relay_selector.c | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/main/relay_selector.c b/main/relay_selector.c new file mode 100644 index 0000000..7c443fe --- /dev/null +++ b/main/relay_selector.c | |||
| @@ -0,0 +1,270 @@ | |||
| 1 | #include "relay_selector.h" | ||
| 2 | #include "config.h" | ||
| 3 | #include "esp_log.h" | ||
| 4 | #include "esp_http_client.h" | ||
| 5 | #include "esp_tls.h" | ||
| 6 | #include "esp_crt_bundle.h" | ||
| 7 | #include "esp_timer.h" | ||
| 8 | #include "cJSON.h" | ||
| 9 | #include "freertos/FreeRTOS.h" | ||
| 10 | #include "freertos/semphr.h" | ||
| 11 | #include <string.h> | ||
| 12 | #include <stdlib.h> | ||
| 13 | |||
| 14 | static const char *TAG = "relay_sel"; | ||
| 15 | static const int MAX_REDIRECTS = 3; | ||
| 16 | static const int PROBE_TIMEOUT_MS = 5000; | ||
| 17 | static const int MAX_FAILURES = 3; | ||
| 18 | |||
| 19 | static int compare_relays(const void *a, const void *b) | ||
| 20 | { | ||
| 21 | const relay_info_t *ra = (const relay_info_t *)a; | ||
| 22 | const relay_info_t *rb = (const relay_info_t *)b; | ||
| 23 | |||
| 24 | if (ra->alive && !rb->alive) return -1; | ||
| 25 | if (!ra->alive && rb->alive) return 1; | ||
| 26 | |||
| 27 | int score_a = (ra->supports_nip77 ? 1000 : 0) - ra->consecutive_failures * 100; | ||
| 28 | int score_b = (rb->supports_nip77 ? 1000 : 0) - rb->consecutive_failures * 100; | ||
| 29 | if (score_a != score_b) return score_b - score_a; | ||
| 30 | |||
| 31 | return (int)ra->latency_ms - (int)rb->latency_ms; | ||
| 32 | } | ||
| 33 | |||
| 34 | esp_err_t relay_selector_init(relay_selector_t *sel) | ||
| 35 | { | ||
| 36 | memset(sel, 0, sizeof(relay_selector_t)); | ||
| 37 | sel->primary_idx = -1; | ||
| 38 | sel->fallback_idx = -1; | ||
| 39 | sel->lock = xSemaphoreCreateMutex(); | ||
| 40 | if (!sel->lock) return ESP_ERR_NO_MEM; | ||
| 41 | return ESP_OK; | ||
| 42 | } | ||
| 43 | |||
| 44 | void relay_selector_destroy(relay_selector_t *sel) | ||
| 45 | { | ||
| 46 | if (sel->lock) { vSemaphoreDelete(sel->lock); sel->lock = NULL; } | ||
| 47 | } | ||
| 48 | |||
| 49 | static esp_err_t probe_nip11(const char *wss_url, relay_info_t *info) | ||
| 50 | { | ||
| 51 | char http_url[192]; | ||
| 52 | const char *host_start = wss_url; | ||
| 53 | if (strncmp(wss_url, "wss://", 6) == 0) host_start = wss_url + 6; | ||
| 54 | else if (strncmp(wss_url, "ws://", 5) == 0) host_start = wss_url + 5; | ||
| 55 | |||
| 56 | snprintf(http_url, sizeof(http_url), "https://%s/", host_start); | ||
| 57 | |||
| 58 | char response[4096]; | ||
| 59 | int total_len = 0; | ||
| 60 | |||
| 61 | esp_http_client_config_t http_cfg = { | ||
| 62 | .url = http_url, | ||
| 63 | .method = HTTP_METHOD_GET, | ||
| 64 | .timeout_ms = PROBE_TIMEOUT_MS, | ||
| 65 | .crt_bundle_attach = esp_crt_bundle_attach, | ||
| 66 | .max_redirection_count = MAX_REDIRECTS, | ||
| 67 | .disable_auto_redirect = false, | ||
| 68 | }; | ||
| 69 | |||
| 70 | esp_http_client_handle_t client = esp_http_client_init(&http_cfg); | ||
| 71 | if (!client) return ESP_FAIL; | ||
| 72 | |||
| 73 | esp_http_client_set_header(client, "Accept", "application/nostr+json"); | ||
| 74 | |||
| 75 | int64_t start_time = esp_timer_get_time(); | ||
| 76 | esp_err_t err = esp_http_client_open(client, 0); | ||
| 77 | if (err != ESP_OK) { | ||
| 78 | esp_http_client_cleanup(client); | ||
| 79 | info->alive = false; | ||
| 80 | return err; | ||
| 81 | } | ||
| 82 | |||
| 83 | int content_length = esp_http_client_fetch_headers(client); | ||
| 84 | int status = esp_http_client_get_status_code(client); | ||
| 85 | |||
| 86 | if (status != 200) { | ||
| 87 | esp_http_client_close(client); | ||
| 88 | esp_http_client_cleanup(client); | ||
| 89 | info->alive = (status > 0); | ||
| 90 | return ESP_FAIL; | ||
| 91 | } | ||
| 92 | |||
| 93 | int max_read = content_length > 0 ? content_length : (int)sizeof(response) - 1; | ||
| 94 | if (max_read > (int)sizeof(response) - 1) max_read = (int)sizeof(response) - 1; | ||
| 95 | |||
| 96 | while (total_len < max_read) { | ||
| 97 | int read_len = esp_http_client_read(client, response + total_len, | ||
| 98 | max_read - total_len); | ||
| 99 | if (read_len <= 0) break; | ||
| 100 | total_len += read_len; | ||
| 101 | } | ||
| 102 | response[total_len] = '\0'; | ||
| 103 | |||
| 104 | int64_t end_time = esp_timer_get_time(); | ||
| 105 | info->latency_ms = (uint32_t)((end_time - start_time) / 1000); | ||
| 106 | |||
| 107 | esp_http_client_close(client); | ||
| 108 | esp_http_client_cleanup(client); | ||
| 109 | |||
| 110 | info->alive = true; | ||
| 111 | info->consecutive_failures = 0; | ||
| 112 | |||
| 113 | cJSON *root = cJSON_Parse(response); | ||
| 114 | if (!root) return ESP_OK; | ||
| 115 | |||
| 116 | cJSON *name = cJSON_GetObjectItem(root, "name"); | ||
| 117 | if (name && cJSON_IsString(name)) | ||
| 118 | strncpy(info->name, name->valuestring, sizeof(info->name) - 1); | ||
| 119 | |||
| 120 | cJSON *nips = cJSON_GetObjectItem(root, "supported_nips"); | ||
| 121 | if (nips && cJSON_IsArray(nips)) { | ||
| 122 | info->nips_count = cJSON_GetArraySize(nips); | ||
| 123 | if (info->nips_count > 32) info->nips_count = 32; | ||
| 124 | info->supports_nip77 = false; | ||
| 125 | for (size_t i = 0; i < info->nips_count; i++) { | ||
| 126 | cJSON *nip = cJSON_GetArrayItem(nips, i); | ||
| 127 | if (nip) { | ||
| 128 | info->supported_nips[i] = (uint8_t)nip->valueint; | ||
| 129 | if (nip->valueint == 77) info->supports_nip77 = true; | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | cJSON_Delete(root); | ||
| 135 | return ESP_OK; | ||
| 136 | } | ||
| 137 | |||
| 138 | static void select_primary_fallback(relay_selector_t *sel) | ||
| 139 | { | ||
| 140 | relay_info_t sorted[RELAY_SELECTOR_MAX_RELAYS]; | ||
| 141 | size_t sorted_count = 0; | ||
| 142 | |||
| 143 | for (size_t i = 0; i < sel->count; i++) { | ||
| 144 | if (sel->relays[i].alive) { | ||
| 145 | sorted[sorted_count++] = sel->relays[i]; | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | if (sorted_count == 0) { | ||
| 150 | sel->primary_idx = -1; | ||
| 151 | sel->fallback_idx = -1; | ||
| 152 | return; | ||
| 153 | } | ||
| 154 | |||
| 155 | qsort(sorted, sorted_count, sizeof(relay_info_t), compare_relays); | ||
| 156 | |||
| 157 | for (size_t i = 0; i < sel->count; i++) { | ||
| 158 | if (strcmp(sel->relays[i].url, sorted[0].url) == 0) { | ||
| 159 | sel->primary_idx = (int)i; | ||
| 160 | break; | ||
| 161 | } | ||
| 162 | } | ||
| 163 | |||
| 164 | if (sorted_count > 1) { | ||
| 165 | for (size_t i = 0; i < sel->count; i++) { | ||
| 166 | if (strcmp(sel->relays[i].url, sorted[1].url) == 0) { | ||
| 167 | sel->fallback_idx = (int)i; | ||
| 168 | break; | ||
| 169 | } | ||
| 170 | } | ||
| 171 | } else { | ||
| 172 | sel->fallback_idx = -1; | ||
| 173 | } | ||
| 174 | |||
| 175 | ESP_LOGI(TAG, "Primary: %s (latency=%lums, NIP-77=%s)", | ||
| 176 | sel->primary_idx >= 0 ? sel->relays[sel->primary_idx].url : "none", | ||
| 177 | sel->primary_idx >= 0 ? (unsigned long)sel->relays[sel->primary_idx].latency_ms : 0, | ||
| 178 | sel->primary_idx >= 0 && sel->relays[sel->primary_idx].supports_nip77 ? "yes" : "no"); | ||
| 179 | } | ||
| 180 | |||
| 181 | esp_err_t relay_selector_probe_all(relay_selector_t *sel) | ||
| 182 | { | ||
| 183 | xSemaphoreTake(sel->lock, portMAX_DELAY); | ||
| 184 | |||
| 185 | ESP_LOGI(TAG, "Probing %zu relays via NIP-11...", sel->count); | ||
| 186 | |||
| 187 | for (size_t i = 0; i < sel->count; i++) { | ||
| 188 | ESP_LOGI(TAG, "Probing %s...", sel->relays[i].url); | ||
| 189 | esp_err_t err = probe_nip11(sel->relays[i].url, &sel->relays[i]); | ||
| 190 | if (err != ESP_OK) { | ||
| 191 | sel->relays[i].consecutive_failures++; | ||
| 192 | ESP_LOGW(TAG, "Probe failed for %s (failures=%d)", | ||
| 193 | sel->relays[i].url, sel->relays[i].consecutive_failures); | ||
| 194 | if (sel->relays[i].consecutive_failures >= MAX_FAILURES) { | ||
| 195 | sel->relays[i].alive = false; | ||
| 196 | } | ||
| 197 | } | ||
| 198 | vTaskDelay(pdMS_TO_TICKS(100)); | ||
| 199 | } | ||
| 200 | |||
| 201 | select_primary_fallback(sel); | ||
| 202 | |||
| 203 | int64_t now = (int64_t)(xTaskGetTickCount() / configTICK_RATE_HZ); | ||
| 204 | sel->last_full_probe = (uint32_t)now; | ||
| 205 | |||
| 206 | xSemaphoreGive(sel->lock); | ||
| 207 | return ESP_OK; | ||
| 208 | } | ||
| 209 | |||
| 210 | const relay_info_t *relay_selector_get_primary(relay_selector_t *sel) | ||
| 211 | { | ||
| 212 | if (sel->primary_idx < 0 || sel->primary_idx >= (int)sel->count) return NULL; | ||
| 213 | return &sel->relays[sel->primary_idx]; | ||
| 214 | } | ||
| 215 | |||
| 216 | const relay_info_t *relay_selector_get_fallback(relay_selector_t *sel, int idx) | ||
| 217 | { | ||
| 218 | if (idx == 0) { | ||
| 219 | if (sel->fallback_idx < 0) return NULL; | ||
| 220 | return &sel->relays[sel->fallback_idx]; | ||
| 221 | } | ||
| 222 | for (size_t i = 0; i < sel->count; i++) { | ||
| 223 | if ((int)i != sel->primary_idx && (int)i != sel->fallback_idx) { | ||
| 224 | if (sel->relays[i].alive) { | ||
| 225 | if (idx <= 0) return &sel->relays[i]; | ||
| 226 | idx--; | ||
| 227 | } | ||
| 228 | } | ||
| 229 | } | ||
| 230 | return NULL; | ||
| 231 | } | ||
| 232 | |||
| 233 | void relay_selector_report_disconnect(relay_selector_t *sel, const char *url) | ||
| 234 | { | ||
| 235 | xSemaphoreTake(sel->lock, portMAX_DELAY); | ||
| 236 | for (size_t i = 0; i < sel->count; i++) { | ||
| 237 | if (strcmp(sel->relays[i].url, url) == 0) { | ||
| 238 | sel->relays[i].consecutive_failures++; | ||
| 239 | ESP_LOGW(TAG, "Disconnect reported for %s (failures=%d)", | ||
| 240 | url, sel->relays[i].consecutive_failures); | ||
| 241 | if (sel->relays[i].consecutive_failures >= MAX_FAILURES) { | ||
| 242 | sel->relays[i].alive = false; | ||
| 243 | ESP_LOGW(TAG, "Relay %s marked dead, triggering re-probe", url); | ||
| 244 | select_primary_fallback(sel); | ||
| 245 | } | ||
| 246 | break; | ||
| 247 | } | ||
| 248 | } | ||
| 249 | xSemaphoreGive(sel->lock); | ||
| 250 | } | ||
| 251 | |||
| 252 | esp_err_t relay_selector_seed_from_config(relay_selector_t *sel) | ||
| 253 | { | ||
| 254 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 255 | xSemaphoreTake(sel->lock, portMAX_DELAY); | ||
| 256 | |||
| 257 | sel->count = 0; | ||
| 258 | for (int i = 0; i < cfg->nostr_seed_relay_count && sel->count < RELAY_SELECTOR_MAX_RELAYS; i++) { | ||
| 259 | if (cfg->nostr_seed_relays[i][0] != '\0') { | ||
| 260 | strncpy(sel->relays[sel->count].url, cfg->nostr_seed_relays[i], | ||
| 261 | RELAY_SELECTOR_URL_LEN - 1); | ||
| 262 | sel->relays[sel->count].alive = true; | ||
| 263 | sel->count++; | ||
| 264 | } | ||
| 265 | } | ||
| 266 | |||
| 267 | xSemaphoreGive(sel->lock); | ||
| 268 | ESP_LOGI(TAG, "Seeded %zu relays from config", sel->count); | ||
| 269 | return ESP_OK; | ||
| 270 | } | ||