5#include <ArduinoJson.h>
7#include <esp_http_client.h>
13#ifdef SENSESP_SSL_SUPPORT
14#include <mbedtls/pem.h>
15#include <mbedtls/sha256.h>
16#include <mbedtls/ssl.h>
17#include <mbedtls/x509_crt.h>
26#include "elapsedMillis.h"
27#include "esp_arduino_version.h"
53static const char* kRequestPermission =
"readwrite";
55#ifdef SENSESP_SSL_SUPPORT
57static void sha256_to_hex(
const uint8_t* sha256,
char* hex) {
58 for (
int i = 0; i < 32; i++) {
59 sprintf(hex + (i * 2),
"%02x", sha256[i]);
65static String cert_fingerprint(
const mbedtls_x509_crt* crt) {
67 mbedtls_sha256_context ctx;
68 mbedtls_sha256_init(&ctx);
69 mbedtls_sha256_starts(&ctx, 0);
70 mbedtls_sha256_update(&ctx, crt->raw.p, crt->raw.len);
71 mbedtls_sha256_finish(&ctx, sha256);
72 mbedtls_sha256_free(&ctx);
74 sha256_to_hex(sha256, hex);
79static constexpr size_t kMaxPinCnLen = 64;
84static String cert_common_name(
const mbedtls_x509_crt* crt) {
86 int len = mbedtls_x509_dn_gets(dn,
sizeof(dn), &crt->subject);
90 const char* cn = strstr(dn,
"CN=");
96 for (
size_t i = 0; i < kMaxPinCnLen && cn[i] !=
'\0' && cn[i] !=
','; i++) {
98 if (c >= 0x20 && c < 0x7f && c !=
'"' && c !=
'\\') {
110static String cert_to_pem(
const mbedtls_x509_crt* crt) {
117 constexpr size_t kPemBufSize = 4096;
118 std::unique_ptr<unsigned char[]> pem_buf(
119 new (std::nothrow)
unsigned char[kPemBufSize]);
121 ESP_LOGE(
"SKWSClient",
"TOFU: PEM buffer allocation failed");
125 int r = mbedtls_pem_write_buffer(
126 "-----BEGIN CERTIFICATE-----\n",
"-----END CERTIFICATE-----\n",
127 crt->raw.p, crt->raw.len, pem_buf.get(), kPemBufSize, &olen);
129 ESP_LOGE(
"SKWSClient",
"TOFU: PEM encode failed (-0x%x)", -r);
132 return String(
reinterpret_cast<const char*
>(pem_buf.get()));
140static String cert_dns_sans(
const mbedtls_x509_crt* crt) {
141 std::set<String> names;
142 for (
const mbedtls_x509_sequence* cur = &crt->subject_alt_names;
143 cur !=
nullptr && cur->buf.p !=
nullptr; cur = cur->next) {
144 mbedtls_x509_subject_alternative_name san;
145 memset(&san, 0,
sizeof(san));
146 if (mbedtls_x509_parse_subject_alt_name(&cur->buf, &san) != 0) {
149 if (san.type == MBEDTLS_X509_SAN_DNS_NAME &&
150 san.san.unstructured_name.p !=
nullptr &&
151 san.san.unstructured_name.len > 0) {
152 size_t n = san.san.unstructured_name.len;
158 for (
size_t i = 0; i < n; i++) {
159 char c =
static_cast<char>(san.san.unstructured_name.p[i]);
160 if (c >=
'A' && c <=
'Z') {
161 c =
static_cast<char>(c + (
'a' -
'A'));
167 mbedtls_x509_free_subject_alt_name(&san);
170 for (
const String& s : names) {
171 if (!out.isEmpty()) {
182static int tofu_verify_callback(
void* ctx, mbedtls_x509_crt* crt,
int depth,
184 SKWSClient* client =
static_cast<SKWSClient*
>(ctx);
185 if (client ==
nullptr) {
186 ESP_LOGW(
"SKWSClient",
"TOFU: no client context, allowing connection");
191 if (!client->is_tofu_enabled()) {
200 if (client->has_tofu_ca()) {
202 ESP_LOGE(
"SKWSClient",
"TOFU: certificate failed CA validation (0x%lx)",
203 (
unsigned long)*flags);
204 client->flag_cert_error();
205 return MBEDTLS_ERR_X509_CERT_VERIFY_FAILED;
211 if (depth == 0 && !client->get_tofu_san().isEmpty()) {
212 if (cert_dns_sans(crt) != client->get_tofu_san()) {
213 ESP_LOGE(
"SKWSClient",
"TOFU: leaf identity (SAN) mismatch, rejecting");
214 client->flag_cert_error();
215 return MBEDTLS_ERR_X509_CERT_VERIFY_FAILED;
235 bool is_ca = crt->MBEDTLS_PRIVATE(ca_istrue) != 0;
236 if (is_ca && !client->has_pending_ca()) {
237 String ca_pem = cert_to_pem(crt);
240 if (!ca_pem.isEmpty()) {
241 client->stash_pending_ca(ca_pem, cert_common_name(crt));
249 String leaf_fp = cert_fingerprint(crt);
250 String leaf_san = cert_dns_sans(crt);
252 client->has_tofu_fingerprint() && client->get_tofu_fingerprint() == leaf_fp;
254 client->has_tofu_fingerprint(), leaf_matches, client->has_pending_ca(),
255 !leaf_san.isEmpty());
259 ESP_LOGE(
"SKWSClient",
"TOFU: leaf fingerprint mismatch, rejecting");
260 client->flag_cert_error();
261 return MBEDTLS_ERR_X509_CERT_VERIFY_FAILED;
263 ESP_LOGI(
"SKWSClient",
"TOFU: first use, pinning leaf %s", leaf_fp.c_str());
264 client->stash_pending_leaf(leaf_fp, cert_common_name(crt));
267 ESP_LOGI(
"SKWSClient",
"TOFU: first use, pinning issuing CA (identity %s)",
269 client->set_pending_san(leaf_san);
275 client->clear_pending_tofu();
286static esp_err_t tofu_crt_bundle_attach(
void* conf) {
287 mbedtls_ssl_config* ssl_conf =
static_cast<mbedtls_ssl_config*
>(conf);
290 if (client !=
nullptr && client->is_tofu_enabled() && client->has_tofu_ca()) {
292 static mbedtls_x509_crt pinned_ca;
293 static bool pinned_ca_inited =
false;
294 if (pinned_ca_inited) {
295 mbedtls_x509_crt_free(&pinned_ca);
297 mbedtls_x509_crt_init(&pinned_ca);
298 pinned_ca_inited =
true;
299 const String& pem = client->get_tofu_ca();
300 int r = mbedtls_x509_crt_parse(
301 &pinned_ca,
reinterpret_cast<const unsigned char*
>(pem.c_str()),
304 mbedtls_ssl_conf_ca_chain(ssl_conf, &pinned_ca,
nullptr);
306 ESP_LOGD(
"SKWSClient",
"TOFU: pinned CA installed as trust anchor");
312 ESP_LOGE(
"SKWSClient",
313 "TOFU: stored CA failed to parse (-0x%x); connections will fail "
319 mbedtls_ssl_conf_authmode(ssl_conf, MBEDTLS_SSL_VERIFY_OPTIONAL);
322 mbedtls_ssl_conf_verify(ssl_conf, tofu_verify_callback, client);
327static void websocket_event_handler(
void* handler_args,
328 esp_event_base_t base,
329 int32_t event_id,
void* event_data) {
334 static_cast<uint32_t
>(
reinterpret_cast<uintptr_t
>(handler_args)) !=
338 esp_websocket_event_data_t* data = (esp_websocket_event_data_t*)event_data;
340 case WEBSOCKET_EVENT_CONNECTED:
343 case WEBSOCKET_EVENT_DISCONNECTED:
346 case WEBSOCKET_EVENT_CLOSED:
349 case WEBSOCKET_EVENT_DATA:
352 if (data->op_code <= 0x2) {
356 case WEBSOCKET_EVENT_ERROR:
363#ifdef SENSESP_SSL_SUPPORT
373 std::shared_ptr<SKDeltaQueue> sk_delta_queue,
374 const String& server_address, uint16_t server_port,
377 conf_server_address_{server_address},
378 conf_server_port_{server_port},
380 sk_delta_queue_{sk_delta_queue} {
400 ESP_LOGD(__FILENAME__,
"Starting SKWSClient");
413 MDNS.addService(
"signalk-sensesp",
"tcp", 80);
428 return handshake_status == kHttpUnauthorized;
449 ESP_LOGW(__FILENAME__,
"Token rejected (%d), requesting new access",
455 ESP_LOGW(__FILENAME__,
"Websocket client error.");
472 ESP_LOGI(__FILENAME__,
"Subscribing to Signal K listeners...");
483 bool output_available =
false;
484 JsonDocument subscription;
485 subscription[
"context"] =
"vessels.self";
490 if (listeners.size() > 0) {
491 output_available =
true;
492 JsonArray subscribe = subscription[
"subscribe"].to<JsonArray>();
500 std::map<String, int> path_period;
501 for (
size_t i = 0; i < listeners.size(); i++) {
502 auto* listener = listeners.at(i);
503 const String& sk_path = listener->get_sk_path();
504 int listen_delay = listener->get_listen_delay();
505 auto it = path_period.find(sk_path);
506 if (it == path_period.end() || listen_delay < it->second) {
507 path_period[sk_path] = listen_delay;
510 for (
const auto& [sk_path, listen_delay] : path_period) {
511 JsonObject subscribe_path = subscribe.add<JsonObject>();
512 subscribe_path[
"path"] = sk_path;
513 subscribe_path[
"period"] = listen_delay;
514 ESP_LOGI(__FILENAME__,
"Adding %s subscription with listen_delay %d\n",
515 sk_path.c_str(), listen_delay);
520 if (output_available &&
524 serializeJson(subscription, json_message);
525 ESP_LOGI(__FILENAME__,
"Subscription JSON message:\n %s",
526 json_message.c_str());
527 int result = esp_websocket_client_send_text(
528 client_.load(), json_message.c_str(), json_message.length(),
531 ESP_LOGE(__FILENAME__,
"Subscription send failed (result=%d)", result);
545 constexpr size_t kMaxWsMessageSize = 4096;
546 if (length > kMaxWsMessageSize) {
547 ESP_LOGW(__FILENAME__,
"WebSocket message too large (%u bytes), dropping",
551 std::unique_ptr<char[]> buf(
new char[length + 1]);
552 memcpy(buf.get(), payload, length);
555#ifdef SIGNALK_PRINT_RCV_DELTA
556 ESP_LOGD(__FILENAME__,
"Websocket payload received: %s", buf.get());
559 JsonDocument message;
560 auto error = deserializeJson(message, buf.get());
563 if (message[
"updates"].is<JsonVariant>()) {
567 if (message[
"put"].is<JsonVariant>()) {
572 if (message[
"requestId"].is<JsonVariant>() &&
573 !message[
"put"].is<JsonVariant>()) {
577 ESP_LOGE(__FILENAME__,
"deserializeJson error: %s", error.c_str());
590 JsonArray updates = message[
"updates"];
596 bool has_meta_listener =
false;
599 if (listener->wants_meta()) {
600 has_meta_listener =
true;
607 for (
size_t i = 0; i < updates.size(); i++) {
608 JsonObject update = updates[i];
610 JsonArray values = update[
"values"];
612 for (
size_t vi = 0; vi < values.size(); vi++) {
618 ru.
doc.set(values[vi]);
628 if (has_meta_listener) {
629 JsonArray meta_entries = update[
"meta"];
630 for (
size_t mi = 0; mi < meta_entries.size(); mi++) {
631 JsonObject entry = meta_entries[mi];
632 if (entry[
"path"].isNull() || entry[
"value"].isNull())
continue;
659 const bool is_meta = update.is_meta;
660 const size_t cap = is_meta ? kMaxReceivedMeta : kMaxReceivedValues;
662 size_t kind_count = 0;
664 if (ru.is_meta == is_meta) kind_count++;
667 while (kind_count >= cap) {
671 if (it->is_meta == is_meta) {
677 ESP_LOGW(__FILENAME__,
"Dropping oldest received %s update (queue full)",
678 is_meta ?
"meta" :
"value");
694 const std::vector<SKPutListener*>& put_listeners =
707 String path = ru.
doc[
"path"].as<String>();
711 std::shared_ptr<const JsonDocument> meta_doc =
712 std::make_shared<const JsonDocument>(std::move(ru.
doc));
713 for (
size_t i = 0; i < listeners.size(); i++) {
721 const char* path = ru.
doc[
"path"];
722 JsonObject value = ru.
doc.as<JsonObject>();
724 for (
size_t i = 0; i < listeners.size(); i++) {
731 for (
size_t i = 0; i < put_listeners.size(); i++) {
755 JsonArray puts = message[
"put"];
756 bool all_matched =
true;
757 for (
size_t i = 0; i < puts.size(); i++) {
758 JsonObject value = puts[i];
759 const char* path = value[
"path"];
760 bool matched =
false;
763 const std::vector<SKPutListener*>& listeners =
765 for (
size_t j = 0; j < listeners.size(); j++) {
786 JsonDocument put_response;
787 put_response[
"requestId"] = message[
"requestId"];
789 put_response[
"state"] =
"COMPLETED";
790 put_response[
"statusCode"] = 200;
792 put_response[
"state"] =
"FAILED";
793 put_response[
"statusCode"] = 405;
795 String response_text;
796 serializeJson(put_response, response_text);
797 int result = esp_websocket_client_send_text(
798 client_.load(), response_text.c_str(), response_text.length(),
801 ESP_LOGE(__FILENAME__,
"PUT response send failed (result=%d)", result);
815 int result = esp_websocket_client_send_text(
818 ESP_LOGE(__FILENAME__,
"sendTXT failed (result=%d)", result);
824 uint16_t& server_port) {
827 int num = MDNS.queryService(
"signalk-wss",
"tcp");
831 ESP_LOGI(__FILENAME__,
"Found Signal K server via mDNS (signalk-wss)");
834 num = MDNS.queryService(
"signalk-ws",
"tcp");
841 ESP_LOGI(__FILENAME__,
"Found Signal K server via mDNS (signalk-ws)");
844#if ESP_ARDUINO_VERSION_MAJOR < 3
845 server_address = MDNS.IP(0).toString();
847 server_address = MDNS.address(0).toString();
849 server_port = MDNS.port(0);
850 ESP_LOGI(__FILENAME__,
"Found server %s (port %d)", server_address.c_str(),
856static esp_err_t detect_ssl_event_handler(esp_http_client_event_t* evt) {
857 if (evt->event_id == HTTP_EVENT_ON_HEADER) {
859 if (strcasecmp(evt->header_key,
"Location") == 0) {
860 String* location =
static_cast<String*
>(evt->user_data);
861 *location = evt->header_value;
873 ESP_LOGD(__FILENAME__,
"Probing for SSL redirect at %s", url.c_str());
877 esp_http_client_config_t config = {};
878 config.url = url.c_str();
879 config.disable_auto_redirect =
true;
880 config.timeout_ms = 10000;
881 config.event_handler = detect_ssl_event_handler;
882 config.user_data = &location;
884 esp_http_client_handle_t client = esp_http_client_init(&config);
885 if (client ==
nullptr) {
886 ESP_LOGE(__FILENAME__,
"Failed to initialize HTTP client");
890 esp_err_t err = esp_http_client_perform(client);
891 int status_code = esp_http_client_get_status_code(client);
892 esp_http_client_cleanup(client);
895 ESP_LOGD(__FILENAME__,
"HTTP request failed: %s", esp_err_to_name(err));
899 if ((status_code == 301 || status_code == 302 ||
900 status_code == 307 || status_code == 308) &&
901 location.startsWith(
"https://")) {
902 ESP_LOGI(__FILENAME__,
"SSL redirect detected, enabling HTTPS/WSS");
929 if (
client_.load() !=
nullptr) {
950 if (!provisioner || !provisioner->is_connected()) {
951 ESP_LOGI(__FILENAME__,
952 "Network is not yet up. SignalK client connection will be "
953 "initiated when the link comes up.");
966 ESP_LOGE(__FILENAME__,
"connect worker spawn failed");
975 self->auth_job_running_.store(
false);
976 vTaskDelete(
nullptr);
980 ESP_LOGI(__FILENAME__,
"Initiating websocket connection with server...");
984 ESP_LOGE(__FILENAME__,
985 "No Signal K server found in network when using mDNS service!");
987 ESP_LOGI(__FILENAME__,
988 "Signal K server has been found at address %s:%d by mDNS.",
997 ESP_LOGD(__FILENAME__,
998 "Websocket is connecting to Signal K server on address %s:%d",
1007 ESP_LOGD(__FILENAME__,
1008 "Websocket is not connecting to Signal K server because host and "
1009 "port are not defined.");
1014 if (this->
polling_href_.length() > 0 && this->polling_href_.startsWith(
"/")) {
1023 ESP_LOGD(__FILENAME__,
"No prior authorization token present.");
1029#ifdef SENSESP_SSL_SUPPORT
1051#ifndef SENSESP_SSL_SUPPORT
1053 const uint16_t server_port) {
1054 String url = String(
"http://") + server_address +
":" + server_port +
1055 "/signalk/v1/stream";
1056 ESP_LOGD(__FILENAME__,
"Testing token with url %s", url.c_str());
1058 const String full_token = String(
"Bearer ") +
auth_token_;
1059 ESP_LOGD(__FILENAME__,
"Authorization: %.8s...[redacted]", full_token.c_str());
1061 esp_http_client_config_t config = {};
1062 config.url = url.c_str();
1063 config.timeout_ms = 10000;
1065 esp_http_client_handle_t client = esp_http_client_init(&config);
1066 if (client ==
nullptr) {
1067 ESP_LOGE(__FILENAME__,
"Failed to initialize HTTP client");
1072 esp_http_client_set_header(client,
"Authorization", full_token.c_str());
1075 esp_err_t err = esp_http_client_open(client, 0);
1076 if (err != ESP_OK) {
1077 ESP_LOGE(__FILENAME__,
"Failed to open HTTP connection: %s",
1078 esp_err_to_name(err));
1079 esp_http_client_cleanup(client);
1084 int content_length = esp_http_client_fetch_headers(client);
1085 int http_code = esp_http_client_get_status_code(client);
1087 ESP_LOGD(__FILENAME__,
"Testing resulted in http status %d", http_code);
1091 if (content_length > 0 && content_length < 4096) {
1092 char* buffer =
new char[content_length + 1];
1093 int read_len = esp_http_client_read(client, buffer, content_length);
1094 buffer[read_len > 0 ? read_len : 0] =
'\0';
1095 payload = String(buffer);
1101 while ((read_len = esp_http_client_read(client, buffer,
1102 sizeof(buffer) - 1)) > 0) {
1103 buffer[read_len] =
'\0';
1104 payload += String(buffer);
1105 if (payload.length() > 4096)
break;
1109 esp_http_client_close(client);
1110 esp_http_client_cleanup(client);
1112 if (payload.length() > 0) {
1113 ESP_LOGD(__FILENAME__,
"Returned payload (%d bytes): %s", payload.length(),
1117 if (http_code == 426) {
1120 ESP_LOGD(__FILENAME__,
"Attempting to connect to Signal K Websocket...");
1121 this->
connect_ws(server_address, server_port);
1122 }
else if (http_code == kHttpUnauthorized) {
1125 ESP_LOGW(__FILENAME__,
"Token rejected (401), requesting new access");
1129 }
else if (http_code > 0) {
1132 ESP_LOGE(__FILENAME__,
"HTTP request failed with code %d", http_code);
1139 const uint16_t server_port) {
1140 ESP_LOGD(__FILENAME__,
"Sending access request (client_id=%s, ssl=%d)",
1151 doc[
"description"] =
1153 doc[
"permissions"] = kRequestPermission;
1154 String json_req =
"";
1155 serializeJson(doc, json_req);
1157 ESP_LOGD(__FILENAME__,
"Access request: %s", json_req.c_str());
1159 String protocol =
ssl_enabled_ ?
"https://" :
"http://";
1160 String url = protocol + server_address +
":" + server_port +
1161 "/signalk/v1/access/requests";
1162 ESP_LOGD(__FILENAME__,
"Access request url: %s", url.c_str());
1164 esp_http_client_config_t config = {};
1165 config.url = url.c_str();
1166 config.method = HTTP_METHOD_POST;
1167 config.timeout_ms = 10000;
1168#ifdef SENSESP_SSL_SUPPORT
1170 config.crt_bundle_attach = tofu_crt_bundle_attach;
1171 config.skip_cert_common_name_check =
true;
1175 esp_http_client_handle_t client = esp_http_client_init(&config);
1176 if (client ==
nullptr) {
1177 ESP_LOGE(__FILENAME__,
"Failed to initialize HTTP client");
1183 esp_http_client_set_header(client,
"Content-Type",
"application/json");
1186 esp_err_t err = esp_http_client_open(client, json_req.length());
1187 if (err != ESP_OK) {
1188 ESP_LOGE(__FILENAME__,
"Failed to open HTTP connection: %s", esp_err_to_name(err));
1189 esp_http_client_cleanup(client);
1194 int write_len = esp_http_client_write(client, json_req.c_str(), json_req.length());
1195 if (write_len < 0 || write_len != (
int)json_req.length()) {
1196 ESP_LOGE(__FILENAME__,
"Failed to write request body (wrote %d of %d bytes)",
1197 write_len, json_req.length());
1198 esp_http_client_close(client);
1199 esp_http_client_cleanup(client);
1204 int content_length = esp_http_client_fetch_headers(client);
1205 int http_code = esp_http_client_get_status_code(client);
1207 ESP_LOGD(__FILENAME__,
"HTTP response: code=%d, content_length=%d", http_code, content_length);
1213 while ((read_len = esp_http_client_read(client, buffer,
sizeof(buffer) - 1)) > 0) {
1214 buffer[read_len] =
'\0';
1215 payload += String(buffer);
1216 if (payload.length() > 4096)
break;
1218 ESP_LOGD(__FILENAME__,
"Response payload (%d bytes): %s",
1219 payload.length(), payload.c_str());
1221 esp_http_client_close(client);
1222 esp_http_client_cleanup(client);
1225 deserializeJson(doc, payload.c_str());
1226 String state = doc[
"state"].is<
const char*>() ? doc[
"state"].as<String>() :
"";
1227 String href = doc[
"href"].is<
const char*>() ? doc[
"href"].as<String>() :
"";
1228 String message = doc[
"message"].is<
const char*>() ? doc[
"message"].as<String>() :
"";
1230 ESP_LOGD(__FILENAME__,
"Access request response: http=%d, state=%s, href=%s",
1231 http_code, state.c_str(), href.c_str());
1232 if (message.length() > 0) {
1233 ESP_LOGI(__FILENAME__,
"Server message: %s", message.c_str());
1238 if (http_code == 400 && href.length() > 0 && href.startsWith(
"/")) {
1239 ESP_LOGI(__FILENAME__,
"Existing request found, will poll href: %s", href.c_str());
1248 if (http_code == 202 && href.length() > 0 && href.startsWith(
"/")) {
1257 if (http_code == 404) {
1258 ESP_LOGI(__FILENAME__,
1259 "Server security disabled (404 on access request) — connecting "
1262 this->
connect_ws(server_address, server_port);
1267 ESP_LOGW(__FILENAME__,
"Cannot handle response: http=%d, state=%s", http_code, state.c_str());
1272 const uint16_t server_port,
1273 const String href) {
1274 ESP_LOGD(__FILENAME__,
"Polling SK Server for authentication token");
1276 String protocol =
ssl_enabled_ ?
"https://" :
"http://";
1277 String url = protocol + server_address +
":" + server_port + href;
1279 esp_http_client_config_t config = {};
1280 config.url = url.c_str();
1281 config.timeout_ms = 10000;
1282#ifdef SENSESP_SSL_SUPPORT
1284 config.crt_bundle_attach = tofu_crt_bundle_attach;
1285 config.skip_cert_common_name_check =
true;
1289 esp_http_client_handle_t client = esp_http_client_init(&config);
1290 if (client ==
nullptr) {
1291 ESP_LOGE(__FILENAME__,
"Failed to initialize HTTP client");
1297 esp_err_t err = esp_http_client_open(client, 0);
1298 if (err != ESP_OK) {
1299 ESP_LOGE(__FILENAME__,
"Failed to open HTTP connection: %s", esp_err_to_name(err));
1300 esp_http_client_cleanup(client);
1305 int content_length = esp_http_client_fetch_headers(client);
1306 int http_code = esp_http_client_get_status_code(client);
1310 if (content_length > 0 && content_length < 4096) {
1311 char* buffer =
new char[content_length + 1];
1312 int read_len = esp_http_client_read(client, buffer, content_length);
1313 buffer[read_len > 0 ? read_len : 0] =
'\0';
1314 payload = String(buffer);
1320 while ((read_len = esp_http_client_read(client, buffer,
sizeof(buffer) - 1)) > 0) {
1321 buffer[read_len] =
'\0';
1322 payload += String(buffer);
1323 if (payload.length() > 4096)
break;
1330 ESP_LOGD(__FILENAME__,
"Poll response: http=%d, %d bytes", http_code,
1331 static_cast<int>(payload.length()));
1333 esp_http_client_close(client);
1334 esp_http_client_cleanup(client);
1336 if (http_code == 200 || http_code == 202) {
1338 auto error = deserializeJson(doc, payload.c_str());
1340 ESP_LOGW(__FILENAME__,
"WARNING: Could not deserialize http payload.");
1341 ESP_LOGW(__FILENAME__,
"DeserializationError: %s", error.c_str());
1345 String state = doc[
"state"];
1346 ESP_LOGD(__FILENAME__,
"%s", state.c_str());
1347 if (state ==
"PENDING") {
1351 if (state ==
"COMPLETED") {
1352 JsonObject access_req = doc[
"accessRequest"];
1353 String permission = access_req[
"permission"];
1358 if (permission ==
"DENIED") {
1359 ESP_LOGW(__FILENAME__,
"Permission denied");
1364 if (permission ==
"APPROVED") {
1365 ESP_LOGI(__FILENAME__,
"Permission granted");
1366 String token = access_req[
"token"];
1369 this->
connect_ws(server_address, server_port);
1374 if (http_code == 404 || http_code == 500) {
1379 ESP_LOGD(__FILENAME__,
1380 "Got %d polling access request — clearing stale href.",
1388 ESP_LOGW(__FILENAME__,
1389 "Can't handle response %d to pending access request.\n",
1405 ESP_LOGW(__FILENAME__,
"connect_ws: prior client not reaped; deferring");
1419 String path =
"/signalk/v1/stream?subscribe=none";
1421 String url = protocol +
"://" + host +
":" + String(port) + path;
1423 ESP_LOGD(__FILENAME__,
"Connecting WebSocket to %s", url.c_str());
1428 auth_header = String(
"Authorization: Bearer ") +
auth_token_ +
"\r\n";
1432 esp_websocket_client_config_t config = {};
1433 config.uri = url.c_str();
1436 if (auth_header.length() > 0) {
1437 config.headers = auth_header.c_str();
1440#ifdef SENSESP_SSL_SUPPORT
1444 config.crt_bundle_attach = tofu_crt_bundle_attach;
1445 config.skip_cert_common_name_check =
true;
1449 esp_websocket_client_handle_t h = esp_websocket_client_init(&config);
1451 ESP_LOGE(__FILENAME__,
"Failed to initialize WebSocket client");
1460 esp_websocket_register_events(
1461 h, WEBSOCKET_EVENT_ANY, websocket_event_handler,
1462 reinterpret_cast<void*
>(
1466 esp_err_t err = esp_websocket_client_start(h);
1467 if (err != ESP_OK) {
1468 ESP_LOGE(__FILENAME__,
"Failed to start WebSocket client: %s",
1469 esp_err_to_name(err));
1472 esp_websocket_client_destroy(h);
1477 ESP_LOGD(__FILENAME__,
"WebSocket client started, waiting for connection...");
1485struct WsTeardownArg {
1492 auto* a =
static_cast<WsTeardownArg*
>(arg);
1496 if (esp_websocket_client_stop(a->handle) == ESP_FAIL) {
1502 esp_websocket_client_destroy(a->handle);
1503 a->self->teardown_in_progress_.store(
false);
1505 vTaskDelete(
nullptr);
1509 if (old ==
nullptr) {
1512 auto* arg =
new WsTeardownArg{old,
this};
1514 nullptr) == pdPASS) {
1526 ESP_LOGW(__FILENAME__,
"teardown task spawn failed; will retry next cycle");
1532 esp_websocket_client_handle_t old =
client_.exchange(
nullptr);
1533 if (old ==
nullptr) {
1556 if (!esp_websocket_client_is_connected(
client_.load())) {
1557 ESP_LOGW(__FILENAME__,
"WebSocket client is no longer connected");
1562 std::vector<String> deltas;
1565 for (
const auto& delta : deltas) {
1577 uint32_t now = millis();
1580 ESP_LOGW(__FILENAME__,
1581 "Delta too large (%u B > %u buffer); dropped to keep the "
1582 "connection alive -- raise SENSESP_SK_WS_BUFFER_SIZE",
1583 (
unsigned)delta.length(),
1590 int send_result = esp_websocket_client_send_text(
1592 if (send_result < 0) {
1607 ESP_LOGW(__FILENAME__,
1608 "Delta send incomplete (result=%d); dropping rest of batch",
1642 if (config[
"sk_address"].is<String>()) {
1645 if (config[
"sk_port"].is<int>()) {
1648 if (config[
"use_mdns"].is<bool>()) {
1649 this->
use_mdns_ = config[
"use_mdns"].as<
bool>();
1651 if (config[
"token"].is<String>()) {
1654 if (config[
"client_id"].is<String>()) {
1655 this->
client_id_ = config[
"client_id"].as<String>();
1657 if (config[
"polling_href"].is<String>()) {
1658 String href = config[
"polling_href"].as<String>();
1663 if (config[
"ssl_enabled"].is<bool>()) {
1666 if (config[
"tofu_enabled"].is<bool>()) {
1672 if (config[
"tofu_fingerprint"].is<String>()) {
1675 if (config[
"tofu_ca_pem"].is<String>()) {
1676 this->
tofu_ca_pem_ = config[
"tofu_ca_pem"].as<String>();
1678 if (config[
"tofu_san"].is<String>()) {
1679 this->
tofu_san_ = config[
"tofu_san"].as<String>();
1681 if (config[
"tofu_pin_cn"].is<String>()) {
1682 this->
tofu_pin_cn_ = config[
"tofu_pin_cn"].as<String>();
1684 if (config[
"tofu_pin_is_ca"].is<bool>()) {
1687 if (config[
"send_meta_enabled"].is<bool>()) {
1703 return "Authorizing with SignalK";
1707 return "Connecting";
1709 return "Disconnected";
1711 return "Certificate verification failed";
virtual bool load() override
Load and populate the object from a persistent storage.
virtual bool save() override
Save the object to a persistent storage.
virtual void set(const C &input) override final
int attach(std::function< void()> observer)
Attach an observer callback.
An Obervable class that listens for Signal K stream deltas and notifies any observers of value change...
static bool take_semaphore(uint64_t timeout_ms=0)
static void release_semaphore()
virtual void parse_value(const JsonObject &json)
virtual bool matches(const String &path) const
virtual void parse_meta(const std::shared_ptr< const JsonDocument > &meta_doc)
static const std::vector< SKListener * > & get_listeners()
virtual bool wants_meta() const
An Obervable class that listens for Signal K PUT requests coming over the websocket connection and no...
static const std::vector< SKPutListener * > & get_listeners()
virtual void parse_value(const JsonObject &put)=0
static void handle_response(JsonDocument &response)
The websocket connection to the Signal K server.
void commit_pending_tofu()
Persist a stashed anchor after a successful (authenticated) connection. Called from on_connected().
void poll_access_request(const String host, const uint16_t port, const String href)
SKWSClient(const String &config_path, std::shared_ptr< SKDeltaQueue > sk_delta_queue, const String &server_address, uint16_t server_port, bool use_mdns=true)
void schedule_reconnect()
void on_receive_delta(uint8_t *payload, size_t length)
Called when the websocket receives a delta.
void process_received_updates()
Loop through the received updates and process them.
std::atomic< esp_websocket_client_handle_t > pending_teardown_
bool is_connect_due() const
std::atomic< bool > auth_job_running_
void connect_ws(const String &host, const uint16_t port)
Integrator< int, int > delta_tx_count_producer_
TaskQueueProducer< SKWSConnectionState > connection_state_
SKWSConnectionState get_connection_state()
std::atomic< bool > teardown_in_progress_
void on_receive_put(JsonDocument &message)
Called when a PUT event is received.
uint32_t client_generation() const
Generation tag of the currently-valid client. The event handler compares the generation it was regist...
void release_received_updates_semaphore()
void run_connect_attempt()
The (blocking) connect attempt — mDNS resolve, SSL detect, and the access-request / poll / connect_ws...
void send_access_request(const String host, const uint16_t port)
std::atomic< uint32_t > client_generation_
void on_error(int handshake_status)
Called when the websocket connection encounters an error.
Integrator< int, int > delta_rx_count_producer_
void clear_pending_tofu()
std::list< ReceivedUpdate > received_updates_
virtual bool to_json(JsonObject &root) override final
void detach_teardown()
Hand client_ to a detached one-shot task that stops+destroys it, so the blocking teardown never runs ...
void enqueue_received_update(ReceivedUpdate &&update)
Push a received delta entry onto the queue, enforcing a per-kind budget so a metadata burst (one meta...
void sendTXT(String &payload)
Send some processed data to the websocket.
uint32_t last_oversize_log_ms_
millis() timestamp of the last oversize-delta-drop warning, used to rate-limit it when a device keeps...
bool get_mdns_service(String &server_address, uint16_t &server_port)
std::atomic< esp_websocket_client_handle_t > client_
void on_disconnected()
Called when the websocket connection is disconnected.
void reap_async(esp_websocket_client_handle_t old)
Spawn the detached reaper for old; on spawn failure (OOM) stash it in pending_teardown_ for a later r...
void subscribe_listeners()
Subscribes the SK delta paths to the websocket.
void on_receive_updates(JsonDocument &message)
Called when a delta update is received.
uint16_t conf_server_port_
void test_token(const String host, const uint16_t port)
void set_connection_state(SKWSConnectionState state)
bool take_received_updates_semaphore(unsigned long int timeout_ms=0)
virtual bool from_json(const JsonObject &config) override final
String get_connection_status()
Get a String representation of the current connection state.
static void connect_worker(void *arg)
TaskQueueProducer< int > delta_tx_tick_producer_
Emits the number of deltas sent since last report.
static void teardown_task(void *arg)
Body of the detached teardown task (stop+destroy+self-delete).
void reset_reconnect_interval()
std::shared_ptr< SKDeltaQueue > sk_delta_queue_
String conf_server_address_
void restart()
Drop the current connection (detached, non-blocking teardown) and let the reconnect path rebuild it.
void on_connected()
Called when the websocket connection is established.
static std::shared_ptr< SensESPApp > get()
Get the singleton instance of the SensESPApp.
static String get_hostname()
Get the current hostname.
virtual void set(const T &value) override
virtual const T & get() const
void emit(const SKWSConnectionState &new_value)
std::enable_if< std::is_base_of< ValueConsumer< typenameVConsumer::input_type >, VConsumer >::value &&std::is_convertible< T, typenameVConsumer::input_type >::value, std::shared_ptr< VConsumer > >::type connect_to(std::shared_ptr< VConsumer > consumer)
Connect a producer to a transform with a different input type.
std::shared_ptr< reactesp::EventLoop > event_loop()
String generate_uuid4()
Generate a random UUIDv4 string.
constexpr int kWsClientTaskStackSize
bool sk_delta_exceeds_ws_buffer(size_t delta_length, size_t buffer_size)
True if a delta is too large to hand to esp_websocket_client as a single tx chunk,...
constexpr TickType_t kWsSendTimeoutTicks
constexpr TickType_t kWsSelfStoppedTaskGraceTicks
constexpr uint32_t kOversizeDropLogIntervalMs
constexpr int kWsTransportTaskStackSize
TofuCaptureDecision tofu_decide_capture(bool has_leaf_anchor, bool leaf_matches_anchor, bool ca_present, bool leaf_has_identity)
TofuCaptureDecision
Capture-mode decision for TOFU certificate pinning.
@ kCaptureLeaf
first use, no usable CA in the chain: pin the leaf
@ kAccept
leaf matches the stored fingerprint; nothing to capture
@ kCaptureCa
first use, usable CA present: pin the CA
@ kReject
leaf does not match the stored fingerprint
bool should_clear_token_on_status(int handshake_status)
Decide whether a failed WebSocket upgrade should clear the auth token.
constexpr TickType_t kWsDeltaSendTimeoutTicks
esp_websocket_client_handle_t handle
#define SENSESP_SK_WS_BUFFER_SIZE
#define SENSESP_MAX_RECEIVED_META_UPDATES
#define SENSESP_MAX_RECEIVED_VALUE_UPDATES
A single received delta entry awaiting dispatch on the main task.