Add secure host CLI bridge and harden OTA workflows
Expose the Full Companion terminal over TCP and add an authenticated, replay-resistant host command service with documented CPU temperature, reboot, and allowlisted program examples.\n\nImprove LoRa OTA diagnostics and persistent CLI handling, reduce clock correction drift to ten minutes, and make mOTA stream flush behavior an explicit transport policy with regression coverage.
This commit is contained in:
1 parent
83c1dd95fe
commit
b699e552ec
46 files changed
+4364
-359
No files matched your search
@@ -116,13 +116,31 @@ inline TerminalArgumentCommandMatch parseTerminalArgumentCommand(
|
||||
return TerminalArgumentCommandMatch::Valid;
|
||||
}
|
||||
|
||||
// Return true once terminal input has reached a login password. The caller
|
||||
// can still retain the real bytes for command handling while echoing '*'.
|
||||
inline bool shouldMaskTerminalInput(const char* line) {
|
||||
// Return the first password byte once terminal input has reached a secret
|
||||
// value. The caller can retain the real bytes for command handling while
|
||||
// masking both the live echo and any backspace redraw.
|
||||
inline const char* terminalPasswordInput(const char* line) {
|
||||
line = skipRecentRepeaterSpaces(line);
|
||||
const char* password = nullptr;
|
||||
return parseTerminalArgumentCommand(line, "login", password)
|
||||
== TerminalArgumentCommandMatch::Valid;
|
||||
if (parseTerminalArgumentCommand(line, "login", password)
|
||||
== TerminalArgumentCommandMatch::Valid) {
|
||||
return password;
|
||||
}
|
||||
|
||||
const char* setting = nullptr;
|
||||
if (parseTerminalArgumentCommand(line, "set", setting)
|
||||
!= TerminalArgumentCommandMatch::Valid) {
|
||||
return nullptr;
|
||||
}
|
||||
if (parseTerminalArgumentCommand(setting, "wifi.pwd", password)
|
||||
== TerminalArgumentCommandMatch::Valid) {
|
||||
return password;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline bool shouldMaskTerminalInput(const char* line) {
|
||||
return terminalPasswordInput(line) != nullptr;
|
||||
}
|
||||
|
||||
// Remove one complete UTF-8 code point from a terminal input buffer. Invalid
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace mesh {
|
||||
|
||||
static constexpr uint8_t CLOCK_SYNC_SAMPLE_SOURCE_SIGNED_ADVERT = 1;
|
||||
static constexpr uint8_t CLOCK_SYNC_SAMPLE_SOURCE_PUBLIC_CHANNEL = 2;
|
||||
// Used only when /clock_sync has no valid saved threshold; persisted operator settings win.
|
||||
static constexpr uint32_t CLOCK_SYNC_DRIFT_DEFAULT_SECONDS = 10UL * 60UL;
|
||||
|
||||
inline bool clockSyncRequiresUniquePath(bool edge_mode) {
|
||||
return !edge_mode;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "AlertReporter.h" // for alertReporterBannedChannelMatch()
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include "AtomicFileWriter.h"
|
||||
#include "ota/OtaFlashLayout_nrf52.h"
|
||||
#endif
|
||||
#include <RTClib.h>
|
||||
#include <ctype.h>
|
||||
@@ -50,11 +51,16 @@ static void resetToUf2Bootloader() {
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <WiFi.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_partition.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#else
|
||||
#include <malloc.h> // mallinfo() for the `memory` command on nRF52/RP2040
|
||||
#endif
|
||||
#if defined(STM32_PLATFORM)
|
||||
#include <InternalFileSystem.h>
|
||||
#endif
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
#include "bridges/MQTTBridge.h"
|
||||
#include "CommonPrefsRecovery.h"
|
||||
@@ -2717,6 +2723,135 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
}
|
||||
}
|
||||
|
||||
bool CommonCLI::handleStorageLayoutGetCmd(const char* config, char* reply) {
|
||||
static const char key[] = "storage.layout";
|
||||
const size_t key_len = sizeof(key) - 1;
|
||||
if (strncmp(config, key, key_len) != 0 ||
|
||||
(config[key_len] != 0 && config[key_len] != ' ')) {
|
||||
return false;
|
||||
}
|
||||
if (config[key_len] != 0) {
|
||||
strcpy(reply, "Error: use get storage.layout");
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(ESP32_PLATFORM)
|
||||
snprintf(reply, 160, "> int:esp32=%luK ext:none; ",
|
||||
(unsigned long)(ESP.getFlashChipSize() / 1024UL));
|
||||
|
||||
const esp_partition_t* running = esp_ota_get_running_partition();
|
||||
esp_partition_iterator_t iterator = esp_partition_find(
|
||||
ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
|
||||
bool first = true;
|
||||
bool truncated = false;
|
||||
while (iterator != NULL) {
|
||||
const esp_partition_t* partition = esp_partition_get(iterator);
|
||||
char entry[48];
|
||||
snprintf(entry, sizeof(entry), "%s%s%s@0x%lX+%luK",
|
||||
first ? "" : ",", partition->label,
|
||||
running != NULL && partition->address == running->address ? "*" : "",
|
||||
(unsigned long)partition->address,
|
||||
(unsigned long)(partition->size / 1024UL));
|
||||
|
||||
const size_t used = strlen(reply);
|
||||
const size_t entry_len = strlen(entry);
|
||||
if (used + entry_len + 1 >= 160) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
memcpy(reply + used, entry, entry_len + 1);
|
||||
first = false;
|
||||
iterator = esp_partition_next(iterator);
|
||||
}
|
||||
if (iterator != NULL) esp_partition_iterator_release(iterator);
|
||||
if (first) {
|
||||
snprintf(reply + strlen(reply), 160 - strlen(reply), "partitions=none");
|
||||
} else if (truncated) {
|
||||
const size_t used = strlen(reply);
|
||||
if (used + 4 < 160) memcpy(reply + used, ",...", 5);
|
||||
}
|
||||
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
const uint32_t flash_bytes =
|
||||
(uint32_t)NRF_FICR->CODEPAGESIZE * (uint32_t)NRF_FICR->CODESIZE;
|
||||
const uint32_t app_start = mesh::ota::mota_nrf52_app_base();
|
||||
const uint32_t app_end =
|
||||
(uint32_t)(uintptr_t)mesh::ota::__flash_arduino_end;
|
||||
#if defined(NRF52840_XXAA)
|
||||
const uint32_t internal_fs_start = 0x000ED000UL;
|
||||
#else
|
||||
const uint32_t internal_fs_start = 0x0006D000UL;
|
||||
#endif
|
||||
const uint32_t internal_fs_size = 7UL * 4096UL;
|
||||
snprintf(reply, 160,
|
||||
"> int:nrf52=%luK app=0x%lX-0x%lX ifs=0x%lX+%luK",
|
||||
(unsigned long)(flash_bytes / 1024UL),
|
||||
(unsigned long)app_start, (unsigned long)app_end,
|
||||
(unsigned long)internal_fs_start,
|
||||
(unsigned long)(internal_fs_size / 1024UL));
|
||||
|
||||
#if defined(ENABLE_OTA) && defined(OTA_QSPI_STORE)
|
||||
mesh::ota::OtaStoreQspiNrf52& store = mesh::ota::ota_ctx().fetch_store;
|
||||
const uint32_t capacity = store.capacity();
|
||||
if (capacity != 0) {
|
||||
snprintf(reply + strlen(reply), 160 - strlen(reply),
|
||||
"; ext:qspi=%luK owner=ota-raw id=%06lX",
|
||||
(unsigned long)(capacity / 1024UL),
|
||||
(unsigned long)store.jedec_id());
|
||||
} else {
|
||||
snprintf(reply + strlen(reply), 160 - strlen(reply),
|
||||
"; ext:qspi=error(%s)", store.last_error());
|
||||
}
|
||||
#elif defined(ENABLE_OTA) && defined(OTA_SD_STORE)
|
||||
uint64_t used_bytes = 0;
|
||||
uint64_t free_bytes = 0;
|
||||
mesh::ota::OtaStoreSdNrf52& store = mesh::ota::ota_ctx().fetch_store;
|
||||
if (store.getSpace(*_board, used_bytes, free_bytes)) {
|
||||
char total[24];
|
||||
char used[24];
|
||||
char free[24];
|
||||
formatSdCardBytes(total, sizeof(total), used_bytes + free_bytes);
|
||||
formatSdCardBytes(used, sizeof(used), used_bytes);
|
||||
formatSdCardBytes(free, sizeof(free), free_bytes);
|
||||
snprintf(reply + strlen(reply), 160 - strlen(reply),
|
||||
"; ext:sd=%s used=%s free=%s owner=ota",
|
||||
total, used, free);
|
||||
} else {
|
||||
snprintf(reply + strlen(reply), 160 - strlen(reply),
|
||||
"; ext:sd=error(%s)", store.last_error());
|
||||
}
|
||||
#elif defined(QSPIFLASH)
|
||||
snprintf(reply + strlen(reply), 160 - strlen(reply),
|
||||
"; ext:qspi=configured owner=littlefs");
|
||||
#else
|
||||
snprintf(reply + strlen(reply), 160 - strlen(reply), "; ext:none");
|
||||
#endif
|
||||
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
#if defined(PICO_FLASH_SIZE_BYTES) && defined(FS_START) && defined(FS_END)
|
||||
snprintf(reply, 160,
|
||||
"> int:rp2040=%luK ifs=%luK layout=fixed; ext:none",
|
||||
(unsigned long)(PICO_FLASH_SIZE_BYTES / 1024UL),
|
||||
(unsigned long)(((uintptr_t)FS_END - (uintptr_t)FS_START) / 1024UL));
|
||||
#elif defined(PICO_FLASH_SIZE_BYTES)
|
||||
snprintf(reply, 160, "> int:rp2040=%luK layout=fixed; ext:none",
|
||||
(unsigned long)(PICO_FLASH_SIZE_BYTES / 1024UL));
|
||||
#else
|
||||
strcpy(reply, "> int:rp2040 layout=fixed; ext:none");
|
||||
#endif
|
||||
|
||||
#elif defined(STM32_PLATFORM)
|
||||
snprintf(reply, 160,
|
||||
"> int:stm32=%luK ifs=0x%lX+%luK layout=fixed; ext:none",
|
||||
(unsigned long)((FLASH_END_ADDR - FLASH_BASE + 1UL) / 1024UL),
|
||||
(unsigned long)LFS_FLASH_ADDR_BASE,
|
||||
(unsigned long)(LFS_FLASH_TOTAL_SIZE / 1024UL));
|
||||
#else
|
||||
strcpy(reply, "Error: storage layout unsupported on this platform");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
bool CommonCLI::handleSdCardSetCmd(const char* config, char* reply) {
|
||||
if (strncmp(config, "sdcard", 6) != 0 ||
|
||||
@@ -3891,6 +4026,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
#endif
|
||||
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
|
||||
if (handleObserverGetCmd(sender_timestamp, config, reply)) return;
|
||||
if (handleStorageLayoutGetCmd(config, reply)) return;
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
if (handleSdCardGetCmd(config, reply)) return;
|
||||
#endif
|
||||
|
||||
@@ -678,6 +678,7 @@ class CommonCLI {
|
||||
void handleGetCmd(uint32_t sender_timestamp, char* command, char* reply);
|
||||
void handleSetCmd(uint32_t sender_timestamp, char* command, char* reply);
|
||||
void handleDelCmd(char* command, char* reply);
|
||||
bool handleStorageLayoutGetCmd(const char* config, char* reply);
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
bool handleSdCardGetCmd(const char* config, char* reply);
|
||||
bool handleSdCardSetCmd(const char* config, char* reply);
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
#pragma once
|
||||
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/UTF8Helpers.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// Line-oriented protocol used to hand an authenticated LoRa CLI request to a
|
||||
// service on the repeater's USB host. The payload is Base64URL encoded so text
|
||||
// cannot inject a second serial command or debug record.
|
||||
class HostCliBridge {
|
||||
public:
|
||||
static constexpr uint32_t SERVICE_CLAIM_TIMEOUT_MILLIS = 4000UL;
|
||||
static constexpr uint32_t SERVICE_CLAIM_EMIT_DELAY_MILLIS = 250UL;
|
||||
static constexpr uint32_t SERVICE_REPLY_TIMEOUT_MILLIS = 6000UL;
|
||||
static constexpr uint32_t SERVICE_TIMEOUT_MILLIS =
|
||||
SERVICE_CLAIM_TIMEOUT_MILLIS + SERVICE_REPLY_TIMEOUT_MILLIS;
|
||||
static constexpr size_t REMOTE_COMMAND_MAX = 10U * CIPHER_BLOCK_SIZE;
|
||||
static constexpr size_t REMOTE_REPLY_MAX =
|
||||
MAX_PACKET_PAYLOAD - CIPHER_MAC_SIZE - (CIPHER_BLOCK_SIZE - 1U) - 5U;
|
||||
static constexpr size_t REQUEST_VERB_BYTES = 5U; // "host "
|
||||
static constexpr size_t CORRELATION_BYTES = 3U; // legacy "XX|"
|
||||
static constexpr size_t REQUEST_TEXT_MAX =
|
||||
REMOTE_COMMAND_MAX - REQUEST_VERB_BYTES;
|
||||
static constexpr size_t REQUEST_TEXT_WITH_CORRELATION_MAX =
|
||||
REQUEST_TEXT_MAX - CORRELATION_BYTES;
|
||||
static constexpr size_t REQUEST_ENCODED_MAX =
|
||||
(REQUEST_TEXT_MAX * 4U + 2U) / 3U;
|
||||
static constexpr size_t USB_SIGNED_CONTENT_MAX =
|
||||
sizeof("HOSTCLI/1 REQUEST FFFFFFFF FFFFFFFFFFFFFFFF ") - 1U
|
||||
+ REQUEST_ENCODED_MAX;
|
||||
static constexpr size_t USB_RECORD_MAX =
|
||||
sizeof("DEBUG ") - 1U + USB_SIGNED_CONTENT_MAX + 1U
|
||||
+ SIGNATURE_SIZE * 2U;
|
||||
static constexpr size_t SERIAL_REPLY_COMMAND_MAX =
|
||||
sizeof("host.reply FFFFFFFF FFFFFFFFFFFFFFFF ") - 1U
|
||||
+ REMOTE_REPLY_MAX;
|
||||
static constexpr size_t SERVICE_CLAIM_PREFIX_SIZE =
|
||||
sizeof("@claim=") - 1U;
|
||||
static constexpr size_t SERVICE_CLAIM_TEXT_SIZE =
|
||||
SERVICE_CLAIM_PREFIX_SIZE + 16U;
|
||||
static constexpr size_t CLAIMED_SIGNED_CONTENT_MAX =
|
||||
sizeof("HOSTCLI/1 CLAIMED FFFFFFFF FFFFFFFFFFFFFFFF "
|
||||
"FFFFFFFFFFFFFFFF") - 1U;
|
||||
static constexpr size_t CLAIMED_USB_RECORD_MAX =
|
||||
sizeof("DEBUG ") - 1U + CLAIMED_SIGNED_CONTENT_MAX + 1U
|
||||
+ SIGNATURE_SIZE * 2U;
|
||||
|
||||
enum ParseResult : uint8_t {
|
||||
NOT_HOST_COMMAND = 0,
|
||||
INVALID_HOST_COMMAND,
|
||||
VALID_HOST_COMMAND,
|
||||
};
|
||||
|
||||
struct RequestView {
|
||||
const char* text;
|
||||
size_t text_len;
|
||||
const char* correlation;
|
||||
size_t correlation_len;
|
||||
|
||||
RequestView()
|
||||
: text(NULL), text_len(0), correlation(NULL), correlation_len(0) {}
|
||||
};
|
||||
|
||||
struct ReplyView {
|
||||
uint32_t request_id;
|
||||
uint64_t request_nonce;
|
||||
const char* text;
|
||||
size_t text_len;
|
||||
|
||||
ReplyView() : request_id(0), request_nonce(0), text(NULL), text_len(0) {}
|
||||
};
|
||||
|
||||
static ParseResult parseRequest(const char* command, RequestView& view) {
|
||||
view = RequestView();
|
||||
if (command == NULL) return NOT_HOST_COMMAND;
|
||||
|
||||
const size_t command_len = strlen(command);
|
||||
|
||||
const char* cursor = command;
|
||||
if (command_len >= CORRELATION_BYTES && command[2] == '|') {
|
||||
view.correlation = command;
|
||||
view.correlation_len = CORRELATION_BYTES;
|
||||
cursor += CORRELATION_BYTES;
|
||||
}
|
||||
|
||||
if (strncmp(cursor, "host", 4) != 0) return NOT_HOST_COMMAND;
|
||||
if (cursor[4] != 0 && cursor[4] != ' ' && cursor[4] != '\t') {
|
||||
return NOT_HOST_COMMAND;
|
||||
}
|
||||
if (command_len > REMOTE_COMMAND_MAX) return INVALID_HOST_COMMAND;
|
||||
|
||||
cursor += 4;
|
||||
while (*cursor == ' ' || *cursor == '\t') cursor++;
|
||||
const char* text_end = command + command_len;
|
||||
while (text_end > cursor
|
||||
&& (text_end[-1] == ' ' || text_end[-1] == '\t'
|
||||
|| text_end[-1] == '\r' || text_end[-1] == '\n')) {
|
||||
text_end--;
|
||||
}
|
||||
if (text_end == cursor) return INVALID_HOST_COMMAND;
|
||||
|
||||
const size_t text_len = (size_t)(text_end - cursor);
|
||||
const size_t max_text = view.correlation_len == 0
|
||||
? REQUEST_TEXT_MAX : REQUEST_TEXT_WITH_CORRELATION_MAX;
|
||||
if (text_len > max_text) return INVALID_HOST_COMMAND;
|
||||
|
||||
view.text = cursor;
|
||||
view.text_len = text_len;
|
||||
return VALID_HOST_COMMAND;
|
||||
}
|
||||
|
||||
static ParseResult parseReply(const char* command, ReplyView& view) {
|
||||
view = ReplyView();
|
||||
if (command == NULL) return NOT_HOST_COMMAND;
|
||||
const char* end = command + strlen(command);
|
||||
if (strncmp(command, "host.reply", 10) != 0) return NOT_HOST_COMMAND;
|
||||
if (command[10] != 0 && command[10] != ' ' && command[10] != '\t') {
|
||||
return NOT_HOST_COMMAND;
|
||||
}
|
||||
|
||||
const char* cursor = command + 10;
|
||||
while (*cursor == ' ' || *cursor == '\t') cursor++;
|
||||
|
||||
uint32_t request_id = 0;
|
||||
if ((size_t)(end - cursor) < 8U) return INVALID_HOST_COMMAND;
|
||||
for (size_t i = 0; i < 8; i++) {
|
||||
const int value = hexValue(cursor[i]);
|
||||
if (value < 0) return INVALID_HOST_COMMAND;
|
||||
request_id = (request_id << 4) | (uint32_t)value;
|
||||
}
|
||||
cursor += 8;
|
||||
if (*cursor != ' ' && *cursor != '\t') return INVALID_HOST_COMMAND;
|
||||
while (*cursor == ' ' || *cursor == '\t') cursor++;
|
||||
|
||||
uint64_t request_nonce = 0;
|
||||
if ((size_t)(end - cursor) < 16U) return INVALID_HOST_COMMAND;
|
||||
for (size_t i = 0; i < 16; i++) {
|
||||
const int value = hexValue(cursor[i]);
|
||||
if (value < 0) return INVALID_HOST_COMMAND;
|
||||
request_nonce = (request_nonce << 4) | (uint64_t)value;
|
||||
}
|
||||
cursor += 16;
|
||||
if (*cursor != ' ' && *cursor != '\t') return INVALID_HOST_COMMAND;
|
||||
while (*cursor == ' ' || *cursor == '\t') cursor++;
|
||||
if (*cursor == 0) return INVALID_HOST_COMMAND;
|
||||
|
||||
const size_t text_len = strlen(cursor);
|
||||
if (text_len > REMOTE_REPLY_MAX
|
||||
|| validUtf8PrefixLength(cursor, text_len) != text_len) {
|
||||
return INVALID_HOST_COMMAND;
|
||||
}
|
||||
for (size_t i = 0; i < text_len; i++) {
|
||||
const uint8_t byte = (uint8_t)cursor[i];
|
||||
if (byte < 0x20 || byte == 0x7F) return INVALID_HOST_COMMAND;
|
||||
}
|
||||
|
||||
view.request_id = request_id;
|
||||
view.request_nonce = request_nonce;
|
||||
view.text = cursor;
|
||||
view.text_len = text_len;
|
||||
return VALID_HOST_COMMAND;
|
||||
}
|
||||
|
||||
static bool formatUsbRequest(char* output, size_t output_size,
|
||||
uint32_t request_id, uint64_t request_nonce,
|
||||
const char* text, size_t text_len) {
|
||||
if (output == NULL || output_size == 0 || text == NULL
|
||||
|| text_len == 0 || text_len > REQUEST_TEXT_MAX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int prefix_len = snprintf(output, output_size,
|
||||
"HOSTCLI/1 REQUEST %08lX %08lX%08lX ",
|
||||
(unsigned long)request_id,
|
||||
(unsigned long)(uint32_t)(request_nonce >> 32),
|
||||
(unsigned long)(uint32_t)request_nonce);
|
||||
if (prefix_len < 0 || (size_t)prefix_len >= output_size) return false;
|
||||
return base64UrlEncode((const uint8_t*)text, text_len,
|
||||
output + prefix_len, output_size - prefix_len);
|
||||
}
|
||||
|
||||
static bool parseServiceClaim(const char* text, size_t text_len,
|
||||
uint64_t& challenge) {
|
||||
challenge = 0;
|
||||
if (text == NULL || text_len != SERVICE_CLAIM_TEXT_SIZE
|
||||
|| memcmp(text, "@claim=", SERVICE_CLAIM_PREFIX_SIZE) != 0) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < 16U; i++) {
|
||||
const int value = hexValue(text[SERVICE_CLAIM_PREFIX_SIZE + i]);
|
||||
if (value < 0) {
|
||||
challenge = 0;
|
||||
return false;
|
||||
}
|
||||
challenge = (challenge << 4) | (uint64_t)value;
|
||||
}
|
||||
return challenge != 0;
|
||||
}
|
||||
|
||||
static bool formatUsbClaim(char* output, size_t output_size,
|
||||
uint32_t request_id, uint64_t request_nonce,
|
||||
uint64_t challenge) {
|
||||
if (output == NULL || output_size == 0 || challenge == 0) return false;
|
||||
const int length = snprintf(
|
||||
output, output_size,
|
||||
"HOSTCLI/1 CLAIMED %08lX %08lX%08lX %08lX%08lX",
|
||||
(unsigned long)request_id,
|
||||
(unsigned long)(uint32_t)(request_nonce >> 32),
|
||||
(unsigned long)(uint32_t)request_nonce,
|
||||
(unsigned long)(uint32_t)(challenge >> 32),
|
||||
(unsigned long)(uint32_t)challenge);
|
||||
return length > 0 && (size_t)length < output_size;
|
||||
}
|
||||
|
||||
static size_t formatRemoteReply(char* output, size_t output_size,
|
||||
const char* original_command,
|
||||
const char* service_reply) {
|
||||
if (output == NULL || output_size == 0 || original_command == NULL
|
||||
|| service_reply == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t output_len = 0;
|
||||
const size_t hard_limit = output_size - 1U < REMOTE_REPLY_MAX
|
||||
? output_size - 1U : REMOTE_REPLY_MAX;
|
||||
if (strlen(original_command) >= CORRELATION_BYTES
|
||||
&& original_command[2] == '|'
|
||||
&& hard_limit >= CORRELATION_BYTES) {
|
||||
memcpy(output, original_command, CORRELATION_BYTES);
|
||||
output_len = CORRELATION_BYTES;
|
||||
}
|
||||
|
||||
const size_t body_len = validUtf8PrefixLength(
|
||||
service_reply, hard_limit - output_len);
|
||||
memcpy(output + output_len, service_reply, body_len);
|
||||
output_len += body_len;
|
||||
output[output_len] = 0;
|
||||
return output_len;
|
||||
}
|
||||
|
||||
private:
|
||||
static int hexValue(char value) {
|
||||
if (value >= '0' && value <= '9') return value - '0';
|
||||
if (value >= 'a' && value <= 'f') return value - 'a' + 10;
|
||||
if (value >= 'A' && value <= 'F') return value - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool base64UrlEncode(const uint8_t* input, size_t input_len,
|
||||
char* output, size_t output_size) {
|
||||
static const char alphabet[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
const size_t encoded_len = (input_len * 4U + 2U) / 3U;
|
||||
if (input == NULL || output == NULL || output_size <= encoded_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t input_pos = 0;
|
||||
size_t output_pos = 0;
|
||||
while (input_pos + 3U <= input_len) {
|
||||
const uint32_t value = ((uint32_t)input[input_pos] << 16)
|
||||
| ((uint32_t)input[input_pos + 1U] << 8)
|
||||
| (uint32_t)input[input_pos + 2U];
|
||||
output[output_pos++] = alphabet[(value >> 18) & 0x3F];
|
||||
output[output_pos++] = alphabet[(value >> 12) & 0x3F];
|
||||
output[output_pos++] = alphabet[(value >> 6) & 0x3F];
|
||||
output[output_pos++] = alphabet[value & 0x3F];
|
||||
input_pos += 3U;
|
||||
}
|
||||
|
||||
const size_t remaining = input_len - input_pos;
|
||||
if (remaining == 1U) {
|
||||
const uint32_t value = (uint32_t)input[input_pos] << 16;
|
||||
output[output_pos++] = alphabet[(value >> 18) & 0x3F];
|
||||
output[output_pos++] = alphabet[(value >> 12) & 0x3F];
|
||||
} else if (remaining == 2U) {
|
||||
const uint32_t value = ((uint32_t)input[input_pos] << 16)
|
||||
| ((uint32_t)input[input_pos + 1U] << 8);
|
||||
output[output_pos++] = alphabet[(value >> 18) & 0x3F];
|
||||
output[output_pos++] = alphabet[(value >> 12) & 0x3F];
|
||||
output[output_pos++] = alphabet[(value >> 6) & 0x3F];
|
||||
}
|
||||
|
||||
output[output_pos] = 0;
|
||||
return output_pos == encoded_len;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace mesh
|
||||
@@ -33,7 +33,6 @@ constexpr uint32_t SAMPLE_MAX_AGE_MILLIS = 2UL * 60UL * 60UL * 1000UL;
|
||||
constexpr uint32_t CONSENSUS_WINDOW_SECONDS = 600UL;
|
||||
constexpr uint32_t DRIFT_MIN_SECONDS = 30UL;
|
||||
constexpr uint32_t DRIFT_MAX_SECONDS = 86400UL;
|
||||
constexpr uint32_t DRIFT_DEFAULT_SECONDS = 3600UL;
|
||||
constexpr uint16_t VALID_YEARS = 10;
|
||||
|
||||
// Public-channel AES key, zero-padded to the shared-secret buffer width used
|
||||
@@ -215,7 +214,7 @@ void MeshClockSync::resetDefaults() {
|
||||
_last_fresh_count = 0;
|
||||
_last_required_count = REQUIRED_SAMPLES_DEFAULT;
|
||||
_required_samples = REQUIRED_SAMPLES_DEFAULT;
|
||||
_drift_seconds = DRIFT_DEFAULT_SECONDS;
|
||||
_drift_seconds = mesh::CLOCK_SYNC_DRIFT_DEFAULT_SECONDS;
|
||||
_last_estimate = 0;
|
||||
_last_abs_drift = 0;
|
||||
_next_attempt_uptime = STARTUP_DELAY_MILLIS;
|
||||
@@ -231,7 +230,7 @@ void MeshClockSync::loadPrefs() {
|
||||
_mesh_enabled = CLOCK_SYNC_MESH_DEFAULT_ENABLED != 0;
|
||||
_mesh_edge_enabled = CLOCK_SYNC_MESH_EDGE_DEFAULT_ENABLED != 0;
|
||||
_internet_enabled = false;
|
||||
_drift_seconds = DRIFT_DEFAULT_SECONDS;
|
||||
_drift_seconds = mesh::CLOCK_SYNC_DRIFT_DEFAULT_SECONDS;
|
||||
_required_samples = REQUIRED_SAMPLES_DEFAULT;
|
||||
|
||||
if (_fs != nullptr && _fs->exists(PREFS_FILE)) {
|
||||
|
||||
@@ -22,8 +22,10 @@ WiFiClient seeder_client;
|
||||
// WiFiClient::flush() clears received bytes rather than flushing TX. Disabling
|
||||
// the serial-only flush prevents a fast host reply from being discarded and
|
||||
// turning every block read into a three-second timeout/retry.
|
||||
SerialMotaSource seeder_source(seeder_client, 3000, false);
|
||||
FolderMotaStore folder_store(seeder_client, 3000, false);
|
||||
SerialMotaSource seeder_source(seeder_client,
|
||||
MotaStreamWritePolicy::NoFlush, 3000);
|
||||
FolderMotaStore folder_store(seeder_client,
|
||||
MotaStreamWritePolicy::NoFlush, 3000);
|
||||
bool listener_active = false;
|
||||
bool tcp_folder_attached = false;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ bool FolderMotaStore::txn(uint8_t op, const uint8_t* args, uint16_t arglen,
|
||||
if (arglen) { memcpy(frame + frame_len, args, arglen); frame_len += arglen; }
|
||||
frame[frame_len++] = xs;
|
||||
if (_io.write(frame, frame_len) != frame_len) return false;
|
||||
if (_flush_after_write) _io.flush();
|
||||
if (_write_policy == MotaStreamWritePolicy::FlushTransmit) _io.flush();
|
||||
|
||||
uint32_t t0 = millis(); bool got = false; uint8_t prev = 0;
|
||||
while ((millis() - t0) < _to) { // scan for response magic 'm' 's' (tolerate noise)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "MotaStreamWritePolicy.h"
|
||||
#include "OtaStore.h"
|
||||
|
||||
// An OtaStore that captures an in-transit `.mota` onto a HOST folder over the mota-seeder link (the WRITE
|
||||
@@ -22,9 +23,9 @@ namespace ota {
|
||||
|
||||
class FolderMotaStore : public OtaStore {
|
||||
public:
|
||||
explicit FolderMotaStore(Stream& io, uint32_t timeout_ms = 3000,
|
||||
bool flush_after_write = true)
|
||||
: _io(io), _to(timeout_ms), _flush_after_write(flush_after_write) {}
|
||||
explicit FolderMotaStore(Stream& io, MotaStreamWritePolicy write_policy,
|
||||
uint32_t timeout_ms = 3000)
|
||||
: _io(io), _to(timeout_ms), _write_policy(write_policy) {}
|
||||
|
||||
// The container being pulled - set from the chosen `ota pull` mid before begin()/reopen().
|
||||
void set_mid(const uint8_t mid[4]) { memcpy(_mid, mid, 4); }
|
||||
@@ -51,7 +52,7 @@ private:
|
||||
|
||||
Stream& _io;
|
||||
uint32_t _to;
|
||||
bool _flush_after_write;
|
||||
MotaStreamWritePolicy _write_policy;
|
||||
uint8_t _mid[4] = {0, 0, 0, 0};
|
||||
uint32_t _total = 0;
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ bool SerialMotaSource::txn(uint8_t op, const uint8_t* args, uint8_t arglen,
|
||||
if (arglen) { memcpy(frame + frame_len, args, arglen); frame_len += arglen; }
|
||||
frame[frame_len++] = xs;
|
||||
if (_io.write(frame, frame_len) != frame_len) return false;
|
||||
if (_flush_after_write) _io.flush();
|
||||
if (_write_policy == MotaStreamWritePolicy::FlushTransmit) _io.flush();
|
||||
|
||||
// scan for response magic 'm' 's' (tolerate leading noise)
|
||||
uint32_t t0 = millis(); bool got = false;
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "MotaStreamWritePolicy.h"
|
||||
#include "OtaSource.h"
|
||||
|
||||
// A MotaSource backed by a host "mota-seeder" daemon over a dedicated Stream (a spare UART / USB-UART).
|
||||
// The device pulls catalog + bytes on demand (MotaSeederProto.h); the folder image is never held on the
|
||||
// device - it streams through. Use a stream that is NOT the text-CLI console so the binary framing never
|
||||
// collides with command/log text. Reads block on the Stream up to `timeout_ms`; keep the daemon on a fast
|
||||
// link so a serial round-trip does not stall the primary transfer.
|
||||
// A MotaSource backed by a host "mota-seeder" daemon over a dedicated framed Stream, such as a spare
|
||||
// UART, USB CDC, or TCP client. The device pulls catalog + bytes on demand (MotaSeederProto.h); the folder
|
||||
// image is never held on the device - it streams through. Do not share the Stream with a text CLI because
|
||||
// command/log text would collide with the binary framing. Reads block up to `timeout_ms`; keep the host
|
||||
// endpoint responsive so a round-trip does not stall the primary transfer.
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
class SerialMotaSource : public MotaSource {
|
||||
public:
|
||||
// Hardware serial needs flush() to finish transmitting before waiting for a
|
||||
// reply. NetworkClient/WiFiClient gives flush() receive-side semantics and
|
||||
// discards bytes, so TCP users must pass flush_after_write=false.
|
||||
explicit SerialMotaSource(Stream& io, uint32_t timeout_ms = 400,
|
||||
bool flush_after_write = true)
|
||||
: _io(io), _to(timeout_ms), _flush_after_write(flush_after_write) {}
|
||||
explicit SerialMotaSource(Stream& io, MotaStreamWritePolicy write_policy,
|
||||
uint32_t timeout_ms = 400)
|
||||
: _io(io), _to(timeout_ms), _write_policy(write_policy) {}
|
||||
|
||||
uint8_t count() override;
|
||||
bool describe(uint8_t idx, MotaDesc& out) override;
|
||||
@@ -34,7 +32,7 @@ private:
|
||||
|
||||
Stream& _io;
|
||||
uint32_t _to;
|
||||
bool _flush_after_write;
|
||||
MotaStreamWritePolicy _write_policy;
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// Stream::flush() does not have consistent semantics across Arduino transports.
|
||||
// Hardware serial implementations wait for queued TX bytes, while ESP32
|
||||
// WiFiClient and nRF52 BLEUart discard queued RX bytes. Require every mOTA
|
||||
// stream caller to choose explicitly so a new transport cannot inherit an
|
||||
// unsafe default.
|
||||
enum class MotaStreamWritePolicy : uint8_t {
|
||||
FlushTransmit,
|
||||
NoFlush,
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -375,7 +375,8 @@ struct OtaContext {
|
||||
// then advertises + relays them alongside its own fw). Only built when OTA_FOLDER_SERIAL is configured.
|
||||
#if defined(OTA_FOLDER_SERIAL)
|
||||
bool attach_folder(char* msg, size_t cap) {
|
||||
static SerialMotaSource src(OTA_FOLDER_SERIAL_STREAM, 600);
|
||||
static SerialMotaSource src(OTA_FOLDER_SERIAL_STREAM,
|
||||
MotaStreamWritePolicy::FlushTransmit, 600);
|
||||
#ifdef OTA_FOLDER_SERIAL_BEGIN
|
||||
OTA_FOLDER_SERIAL_STREAM.begin(OTA_FOLDER_SERIAL_BAUD); // dedicated UART; console is already up
|
||||
#endif
|
||||
|
||||
Reference in new issue
Block a user