From e22b703580dc61ff186c0e067220a3f6f569078a Mon Sep 17 00:00:00 2001 From: Danny Hadley Date: Wed, 10 Jan 2024 15:54:20 -0500 Subject: [PATCH] firmware wifi updates (#29) * login page update + better error on boot for bad srv config * fsm wifi event stabilization * remove vim specific file * more diagnostic logging for job queueing * try playing with merge_bins * add a version string to common logs * temporary escaping workaround * add reminder, fix ca in CI * quoted host ca --- .../firmware-instructions/flashing-xiao.txt | 6 + .github/workflows/build-and-publish.yml | 16 +- src/beetle-pio-tls-tester/platformio.ini | 2 +- src/beetle-pio-tls-tester/src/main.cpp | 4 +- src/beetle-pio/.gitignore | 2 + src/beetle-pio/Makefile | 35 ++ .../lib/redis-events/redis-events.hpp | 62 ++- .../lib/wifi-events/wifi-events.cpp | 367 ------------- .../lib/wifi-events/wifi-events.hpp | 486 ++++++++++++++---- src/beetle-pio/load_env.py | 6 + src/beetle-pio/src/engine.cpp | 15 +- src/beetle-pio/src/engine.hpp | 2 + src/beetle-pio/src/main.cpp | 16 +- src/beetle-srv/env.example.toml | 7 +- src/beetle-srv/src/api/worker.rs | 82 +-- src/beetle-srv/src/bin/beetle-cli.rs | 11 +- src/beetle-srv/src/bin/beetle-registrar.rs | 7 +- src/beetle-srv/src/bin/beetle-web.rs | 7 +- src/beetle-srv/src/bin/cli/acls.rs | 54 +- src/beetle-srv/src/bin/cli/mod.rs | 2 +- src/beetle-srv/src/redis.rs | 9 +- src/beetle-srv/src/registrar/jobs.rs | 15 + src/beetle-ui/src/Icon.elm | 4 + src/beetle-ui/src/Route.elm | 10 +- src/beetle-ui/src/boot.ts | 5 +- src/beetle-ui/src/main.css | 11 + 26 files changed, 686 insertions(+), 557 deletions(-) create mode 100644 src/beetle-pio/Makefile delete mode 100644 src/beetle-pio/lib/wifi-events/wifi-events.cpp diff --git a/.automation/firmware-instructions/flashing-xiao.txt b/.automation/firmware-instructions/flashing-xiao.txt index 444e8e4..2420208 100644 --- a/.automation/firmware-instructions/flashing-xiao.txt +++ b/.automation/firmware-instructions/flashing-xiao.txt @@ -17,3 +17,9 @@ python esptool.py \ 0xe000 boot_app0.bin \ 0x10000 .pio/build/xiao/firmware.bin ``` + +Alternatively, the merged binary has been included: + +``` +esptool.py --chip esp32c3 write_flash 0x0 ./beetle-merged.bin +``` diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index 477f3fd..cc6dbc2 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -47,6 +47,7 @@ jobs: REDIS_PORT: "${{ secrets.PIO_REDIS_PORT }}" REDIS_AUTH_USERNAME: "${{ secrets.PIO_REDIS_AUTH_USERNAME }}" REDIS_AUTH_PASSWORD: "${{ secrets.PIO_REDIS_AUTH_PASSWORD }}" + REDIS_HOST_ROOT_CA: "${{ secrets.REDIS_HOST_ROOT_CA }}" DISTRIBUTABLE_DIRECTORY_NAME: "beetle-pio-dist" defaults: run: @@ -58,6 +59,7 @@ jobs: id: vars run: | echo "SHORT_SHA=$(echo $GITHUB_SHA | head -c 7)">>$GITHUB_OUTPUT + echo "BEETLE_VERSION=$(echo $GITHUB_SHA | head -c 7)">>$GITHUB_ENV - name: "import gpg" run: | @@ -72,7 +74,10 @@ jobs: run: pip install --upgrade platformio - name: "env prep: fill redis ca" - run: echo $REDIS_HOST_ROOT_CA > embeds/redis_host_root_ca.pem + run: | + echo -n "$REDIS_HOST_ROOT_CA" > embeds/redis_host_root_ca.pem + echo -n "$REDIS_HOST_ROOT_CA" | wc -l + wc -l embeds/redis_host_root_ca.pem - name: "pio: check" run: pio check @@ -86,9 +91,18 @@ jobs: - name: "pio(xiao): run xiao" run: pio run -e xiao + - name: "pio(xiao): make merged" + run: make + - name: "bundle(xiao): prepare-dir" run: mkdir -p $DISTRIBUTABLE_DIRECTORY_NAME/xiao + - name: "bundle(xiao): copy-merged" + run: | + gpg --trust-model always -e -r $BEETLE_GPG_KEY_ID -o \ + $DISTRIBUTABLE_DIRECTORY_NAME/xiao/beetle-merged.bin.pgp \ + beetle-merged-flash.bin + - name: "bundle(xiao): copy-bin" run: | gpg --trust-model always -e -r $BEETLE_GPG_KEY_ID -o \ diff --git a/src/beetle-pio-tls-tester/platformio.ini b/src/beetle-pio-tls-tester/platformio.ini index d6c1dcf..b123e38 100644 --- a/src/beetle-pio-tls-tester/platformio.ini +++ b/src/beetle-pio-tls-tester/platformio.ini @@ -1,5 +1,5 @@ [platformio] -default_envs = firebeetle +default_envs = xiao [env] framework=arduino diff --git a/src/beetle-pio-tls-tester/src/main.cpp b/src/beetle-pio-tls-tester/src/main.cpp index 4d2dd44..1737979 100644 --- a/src/beetle-pio-tls-tester/src/main.cpp +++ b/src/beetle-pio-tls-tester/src/main.cpp @@ -18,12 +18,10 @@ void listNetworks() { Serial.print("number of available networks:"); Serial.println(numSsid); for (int thisNet = 0; thisNet < numSsid; thisNet++) { - Serial.print(thisNet); - Serial.print(") "); Serial.print(WiFi.SSID(thisNet)); Serial.print("\tSignal: "); Serial.print(WiFi.RSSI(thisNet)); - Serial.print(" dBm"); + Serial.println(" dBm"); } } diff --git a/src/beetle-pio/.gitignore b/src/beetle-pio/.gitignore index 2e029c4..bbb8d49 100644 --- a/src/beetle-pio/.gitignore +++ b/src/beetle-pio/.gitignore @@ -1,4 +1,6 @@ /.pio +beetle-merged-flash.bin +.ycm_extra_conf.py /env /*.log /embeds/*.pem diff --git a/src/beetle-pio/Makefile b/src/beetle-pio/Makefile new file mode 100644 index 0000000..81bdadd --- /dev/null +++ b/src/beetle-pio/Makefile @@ -0,0 +1,35 @@ +PYTHON=python3 + +ESPTOOL=$(HOME)/.platformio/packages/tool-esptoolpy/esptool.py +BOOT_APP0=$(HOME)/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin + +# TODO: consult either esptool.py or platformio. they handle this gracefully. +PORT=/dev/cu.usbmodem101 + +BINS=.pio/build/xiao/bootloader.bin .pio/build/xiao/partitions.bin .pio/build/xiao/firmware.bin +MERGED_BIN=beetle-merged-flash.bin + +.PHONY: clean flash-merged tool + +all: $(MERGED_BIN) + +clean: + rm $(BINS) + +$(BINS): + pio run + +$(MERGED_BIN): $(BINS) + $(PYTHON) $(ESPTOOL) --chip esp32c3 merge_bin -o $@ \ + --flash_size 4MB \ + 0x0000 .pio/build/xiao/bootloader.bin \ + 0x8000 .pio/build/xiao/partitions.bin \ + 0xe000 $(BOOT_APP0) \ + 0x10000 .pio/build/xiao/firmware.bin + +help: + $(PYTHON) $(ESPTOOL) --help + +flash-merged: $(MERGED_BIN) + echo $< + $(PYTHON) $(ESPTOOL) --chip esp32c3 --port $(PORT) write_flash --flash_mode dio --flash_freq 80m 0x0 $< diff --git a/src/beetle-pio/lib/redis-events/redis-events.hpp b/src/beetle-pio/lib/redis-events/redis-events.hpp index 03735a7..48d46e6 100644 --- a/src/beetle-pio/lib/redis-events/redis-events.hpp +++ b/src/beetle-pio/lib/redis-events/redis-events.hpp @@ -1,6 +1,7 @@ #pragma once #include + #include "redis-config.hpp" #include "redis-event.hpp" #include "redis-reader.hpp" @@ -32,7 +33,7 @@ class Events final { std::optional &wifi, std::shared_ptr> buffer, uint32_t time) { auto visitor = StateVisitor{_context, &wifi, buffer, time, _reader}; - auto[next, message] = std::visit(visitor, _state); + auto [next, message] = std::visit(visitor, _state); _state = next; return message; } @@ -154,7 +155,7 @@ class Events final { context->device_id_len, context->device_id, context->device_id_len, context->device_id); context->client.print(context->outbound); - log_d("wrote auth: '%s'", context->outbound); + log_i("wrote auth: '%s'", context->outbound); connected.authorization_stage = AuthorizationStage::AuthorizationAttempted; @@ -192,6 +193,10 @@ class Events final { bool pending_burnin_auth = connected.authorization_stage == AuthorizationStage::AuthorizationRequested; + if (connected.last_read == 0) { + connected.last_read = time; + } + while (context->client.available()) { auto token = (char)context->client.read(); auto event = reader->fill(token, buffer); @@ -208,6 +213,8 @@ class Events final { : AuthorizationStage::FullyAuthorized; } } + + connected.last_read = time; } if (connected.authorization_stage == @@ -215,6 +222,14 @@ class Events final { return std::make_pair(connected, Authorized{}); } + if (time - connected.last_read > 5000) { + log_e("expected OK from redis but none was received in time, aborting"); + // important: explicitly stopping the client frees up internal memory + // used on the next connection attempt. + context->client.stop(); + return std::make_pair(Connected{false}, FailedConnection{}); + } + return std::make_pair(connected, std::nullopt); } @@ -232,7 +247,8 @@ class Events final { if (result != 1) { log_e("unable to establish connection - %d", result); - return std::make_pair(Disconnected{}, FailedConnection{}); + context->client.stop(); + return std::make_pair(Disconnected{time + 5000}, FailedConnection{}); } log_i("redis connection established successfully"); @@ -256,7 +272,7 @@ class Events final { context->device_id_len, context->device_id, context->device_id_len, context->device_id); context->client.print(context->outbound); - log_d("wrote auth: '%s'", context->outbound); + log_d("wrote auth: '%s'; clearing internal buffer", context->outbound); connected.authorization_stage = AuthorizationStage::AuthorizationAttempted; return std::make_pair(connected, IdentificationReceived{}); @@ -392,7 +408,8 @@ class Events final { context->device_id_len + 3, context->device_id); } - log_i("writing message (heartbeat? %d)", sending_heartbeat); + log_i("id[%s] writing message (heartbeat? %d)", context->device_id, + sending_heartbeat); context->client.print(context->outbound); } @@ -402,6 +419,7 @@ class Events final { std::pair, std::optional> operator()(Connected connected) { if (*wifi_message == wifievents::Events::EMessage::Disconnected) { + context->client.stop(); return std::make_pair(Disconnected{}, std::nullopt); } @@ -413,6 +431,7 @@ class Events final { if (connected.paused) { if (*wifi_message == wifievents::Events::EMessage::ConnectionResumed) { + context->client.stop(); return std::make_pair(Disconnected{}, std::nullopt); } @@ -431,6 +450,9 @@ class Events final { case AuthorizationStage::AuthorizationRequested: case AuthorizationStage::AuthorizationAttempted: + // TODO: it is likely that we should be ensuring the buffer is cleared + // out _before_ moving into either of these authorization states. + buffer->fill('\0'); return read_ok(connected); case AuthorizationStage::NotRequested: @@ -442,16 +464,37 @@ class Events final { std::pair, std::optional> operator()(Disconnected d) { - if (*wifi_message == wifievents::Events::EMessage::Connected) { + bool reconnect = + *wifi_message == wifievents::Events::EMessage::Connected || + *wifi_message == wifievents::Events::EMessage::ConnectionResumed; + + if (d.reconnect_after > 0 && time > d.reconnect_after) { + log_i("explicit redis reconnection attempt"); + return std::make_pair(Connected{false}, std::nullopt); + } + + if (reconnect) { log_i("redis events moving into connection attempt"); return std::make_pair(Connected{false}, std::nullopt); } - return std::make_pair(Disconnected{}, std::nullopt); + if (d.last_debug == 0) { + d.last_debug = time; + } + + if (time - d.last_debug > 3000) { + log_e("redis events disconnected; no connected wifi events received"); + d.last_debug = time; + } + + return std::make_pair(d, std::nullopt); } }; - struct Disconnected final {}; + struct Disconnected final { + uint32_t reconnect_after = 0; + uint32_t last_debug = 0; + }; struct ReceivingHeartbeatAck final {}; @@ -474,6 +517,7 @@ class Events final { struct Connected final { bool paused = false; uint32_t last_write = 0; + uint32_t last_read = 0; AuthorizationStage authorization_stage = AuthorizationStage::NotRequested; std::variant state = NotReceiving{true}; @@ -483,4 +527,4 @@ class Events final { std::variant _state; std::shared_ptr> _reader; }; -} +} // namespace redisevents diff --git a/src/beetle-pio/lib/wifi-events/wifi-events.cpp b/src/beetle-pio/lib/wifi-events/wifi-events.cpp deleted file mode 100644 index f67fe90..0000000 --- a/src/beetle-pio/lib/wifi-events/wifi-events.cpp +++ /dev/null @@ -1,367 +0,0 @@ -#include "wifi-events.hpp" - -namespace wifievents { -Events::Events(std::tuple ap) - : _last_mode(0), - _ap_config(ap), - _mode(std::in_place_type_t()) {} - -uint8_t Events::attempt(void) { - if (_mode.index() == 2) { - return std::get_if(&_mode)->_attempts; - } - return 0; -} - -std::optional Events::update(uint32_t current_time) { - auto timer_result = _timer.update(current_time); - - if (timer_result != 1) { - return std::nullopt; - } - - unsigned int modi = _mode.index(); - -#ifndef RELEASE - if (_last_mode != modi) { - switch (modi) { - case 0: - log_d("active"); - break; - case 1: - log_d("waiting for configration"); - break; - case 2: - log_d("connecting to network"); - break; - } - - _last_mode = modi; - } -#endif - - switch (modi) { - case 0: { - ActiveConnection *active = std::get_if(&_mode); - uint8_t previous = active->_disconnected; - - active->_disconnected = - WiFi.status() == WL_CONNECTED ? 0 : active->_disconnected + 1; - - // If we're now disconnected, sent an interruption message. - if (active->_disconnected == 1) { - log_e("wifi connection interrupted, attempting to reconnect"); - WiFi.reconnect(); - return Events::EMessage::ConnectionInterruption; - } - - if (active->_disconnected > 1) { - log_e("wifi still disconnected at %d attempts", active->_disconnected); - WiFi.reconnect(); - } - - // If we're no longer disconnected, but were previously, we've been - // resumed. - if (active->_disconnected == 0 && previous != 0) { - return Events::EMessage::ConnectionResumed; - } - - if (active->_disconnected > MAX_CONNECTION_INTERRUPTS) { - log_e("wifi manager disonncted after %d attempts", - active->_disconnected); - - _mode.emplace(); - return Events::EMessage::Disconnected; - } - - break; - } - - /** - * Configuration Mode - * - * When the `_mode` variant is a `WiFiServer`, we are waiting for someone to - * load the index html page and enter in the wifi credentials. - */ - case 1: { - PendingConfiguration *server = std::get_if(&_mode); - - // TODO: using stack allocated char arrays of a preset max size here over - // dynamically allocated memory. Not clear right now which is better. - char ssid[MAX_SSID_LENGTH]; - memset(ssid, '\0', MAX_SSID_LENGTH); - char password[MAX_PASSWORD_LENGTH]; - memset(password, '\0', MAX_PASSWORD_LENGTH); - - size_t stored_ssid_len = - _checked_stored_values == false && _preferences.isKey("ssid") - ? _preferences.getString("ssid", ssid, MAX_SSID_LENGTH) - : 0; - size_t stored_password_len = - _checked_stored_values == false && _preferences.isKey("password") - ? _preferences.getString("password", password, - MAX_PASSWORD_LENGTH) - : 0; - - if (_checked_stored_values == false) { - _checked_stored_values = true; - log_i("stored ssid len - %d (password %d)", stored_ssid_len, - stored_password_len); - } - - // If we have nothing stored, try to read from our http server. - if (stored_ssid_len == 0 || stored_password_len == 0) { - memset(ssid, '\0', MAX_SSID_LENGTH); - memset(password, '\0', MAX_PASSWORD_LENGTH); - - // If the server _also_ doesn't have an ssid or password available for - // us, break - // out of this arm of our state switch statement. - if (server->update(ssid, password) == false) { - break; - } - } - - // Terminate our hotspot, we have everything we need to make an attempt to - // establish a connection with the wifi network. - WiFi.softAPdisconnect(true); - WiFi.disconnect(true, true); - - // Move ourselves into the pending connection state. This will terminate - // our - // wifi server. - _mode.emplace(ssid, password); - - WiFi.mode(WIFI_STA); - return Events::EMessage::Connecting; - } - - /** - * Connection Mode - * - * During this phase, we have an ssid + password ready, we just need to - * attempt - * to boot the wifi module and wait for it to be connected. - */ - case 2: { - PendingConnection *pending = std::get_if(&_mode); - - if (pending->_attempts % 3 == 0) { - log_i("attempting to connect to wifi [%d]", pending->_attempts); - } - - if (pending->_attempts == 0) { - log_i("connecting to wifi"); - WiFi.setHostname("orient-beetle"); - WiFi.begin(pending->_ssid, pending->_password); - } - - // If we have a connection, move out of this mode - if (WiFi.status() == WL_CONNECTED) { - log_i("wifi is connected"); - _mode.emplace(0); - - size_t stored_ssid_len = _preferences.putString("ssid", pending->_ssid); - size_t stored_password_len = - _preferences.putString("password", pending->_password); - log_d("stored ssid len %d, password len %d", stored_ssid_len, - stored_password_len); - - return Events::EMessage::Connected; - } - - pending->_attempts += 1; - - // If we have seen too many frames without establishing a connection to - // the - // network provided by the user, move back into the AP/configuration mode. - if (pending->_attempts > MAX_PENDING_CONNECTION_ATTEMPTS) { - log_i("too many connections failed, resetting"); - - // Clear the stored ssid/password. - _preferences.remove("ssid"); - _preferences.remove("password"); - - // Clear out our connection attempt garbage. - WiFi.disconnect(true, true); - - // Prepare the wifi server. - _mode.emplace(); - - // Enter into AP mode and start the server. - begin(); - return Events::EMessage::FailedConnection; - } - - break; - } - default: - log_i("unknown state"); - break; - } - - return std::nullopt; -} - -void Events::begin(void) { - log_i("starting preferences"); - _preferences.begin("beetle-wifi", false); - - if (_mode.index() == 1) { - WiFi.softAP(std::get<0>(_ap_config), std::get<1>(_ap_config), 7, 0, 1); - IPAddress address = WiFi.softAPIP(); - - log_i("AP IP address: %s", address.toString()); - std::get_if<1>(&_mode)->begin(address); - return; - } - - log_d("soft ap not started"); -} - -bool Events::PendingConfiguration::update(char *ssid, char *password) { - WiFiClient client = available(); - - // If we are running in AP mode and have no http connection to our server, - // move right along. - if (!client) { - return false; - } - - // TODO: figure out how to decouple this so that consumers can provide their - // own index html. - // Currently, this is used to avoid the RAM cost associated with carrying - // around the char[] - extern const char index_html[] asm("_binary_embeds_index_http_start"); - extern const char index_end[] asm("_binary_embeds_index_http_end"); - - log_d("loaded index (%d bytes)", index_end - index_html); - - unsigned int cursor = 0, field = 0; - unsigned char noreads = 0; - - ERequestParsingMode method = ERequestParsingMode::None; - - // stack-allocated space with immediate initialization? - char buffer[SERVER_BUFFER_CAPACITY]; - memset(buffer, '\0', SERVER_BUFFER_CAPACITY); - - while ( - client.connected() && cursor < SERVER_BUFFER_CAPACITY - 1 && - noreads < MAX_CLIENT_BLANK_READS && - (method != ERequestParsingMode::None ? true : cursor < MAX_HEADER_SIZE)) { - // If there is no pending data in our buffer, increment our noop count and - // move on. If that count exceeds a threshold, we will stop reading. - if (!client.available()) { - noreads += 1; - delay(10); - continue; - } - - // Reset our message-less count. - noreads = 0; - - // Pull the next character off our client. - buffer[cursor] = client.read(); - - if (method == ERequestParsingMode::Network && buffer[cursor] == '+') { - buffer[cursor] = ' '; - } - - if (cursor < 3 || method == ERequestParsingMode::Done) { - cursor += 1; - continue; - } - - if (method == ERequestParsingMode::None && - strcmp(buffer, CONNECTION_PREFIX) == 0) { - log_i("found connection request, preparing for ssid parsing"); - - method = ERequestParsingMode::Network; - cursor += 1; - field = cursor; - continue; - } - - if (PendingConfiguration::termination(method) == buffer[cursor]) { - unsigned char offset = 0; - const char *value = buffer + offset + field; - - while ((offset + field) < cursor && *value != '=') { - value = buffer + offset + field; - offset += 1; - } - - switch (method) { - case ERequestParsingMode::Network: - method = ERequestParsingMode::Password; - memcpy(ssid, buffer + field + offset, cursor - (field + offset)); - break; - case ERequestParsingMode::Password: - method = ERequestParsingMode::Done; - memcpy(password, buffer + field + offset, cursor - (field + offset)); - break; - default: - break; - } - - cursor += 1; - field = cursor; - continue; - } - - cursor += 1; - } - - if (method != ERequestParsingMode::Done) { - log_i("non-connect request:\n%s", buffer); - - client.write(index_html, index_end - index_html); - delay(10); - client.stop(); - return false; - } - - log_i("[wifi_manager] ssid(%s) | password(%s)", ssid, password); - - client.write(index_html, index_end - index_html); - delay(10); - client.stop(); - - return true; -} - -/** - * When parsing the statusline of a request, this function will return the - * character - * that is expected to terminate a given parsing mode. - */ -inline char Events::PendingConfiguration::termination( - ERequestParsingMode mode) { - switch (mode) { - case ERequestParsingMode::Network: - return '&'; - case ERequestParsingMode::Password: - return ' '; - default: - return '\0'; - } -} - -void Events::PendingConfiguration::begin(IPAddress addr) { - _server.begin(); - _dns.start(53, "*", addr); -} - -WiFiClient Events::PendingConfiguration::available(void) { - _dns.processNextRequest(); - return _server.available(); -} - -Events::PendingConfiguration::~PendingConfiguration() { - log_i("wifi_manager::pending_configuration", "exiting pending configuration"); - - _server.stop(); - _dns.stop(); -} -} diff --git a/src/beetle-pio/lib/wifi-events/wifi-events.hpp b/src/beetle-pio/lib/wifi-events/wifi-events.hpp index db353ad..6b95415 100644 --- a/src/beetle-pio/lib/wifi-events/wifi-events.hpp +++ b/src/beetle-pio/lib/wifi-events/wifi-events.hpp @@ -1,9 +1,5 @@ #pragma once -#include -#include -#include "esp32-hal-log.h" - #include #include #include @@ -11,25 +7,26 @@ #include #include -#include "microtim.hpp" +#include +#include + +#include "esp32-hal-log.h" namespace wifievents { +constexpr static const char *CONNECTION_PREFIX = "GET /connect?"; +constexpr static uint8_t MAX_CLIENT_BLANK_READS = 5; +constexpr static uint16_t SERVER_BUFFER_CAPACITY = 1024; +constexpr static uint16_t MAX_HEADER_SIZE = 512; +constexpr static uint16_t MAX_NETWORK_CREDENTIAL_SIZE = 256; +constexpr static uint16_t MAX_PENDING_CONNECTION_ATTEMPTS = 20; + class Events final { public: - explicit Events(std::tuple); - ~Events() = default; - - // Disable Copy - Events(const Events &) = delete; - Events &operator=(const Events &) = delete; - - // Disable Move - Events(Events &&) = delete; - Events &operator=(Events &&) = delete; - + // The things that can happen during an update frame enum EMessage { - Connecting = 0, + AttemptingConnection = 0, + WaitingForCredentials, Connected, FailedConnection, Disconnected, @@ -37,100 +34,403 @@ class Events final { ConnectionResumed, }; - void begin(void); - std::optional update(uint32_t); - uint8_t attempt(void); + explicit Events(std::tuple ap) + : _mode(std::in_place_type_t()), + _ap_config(ap), + _ssid( + std::make_shared>()), + _password( + std::make_shared>()), + _preferences(std::make_shared()), + _visitor(Visitor{&_ap_config, 0, _ssid, _password, _preferences}) { + _ssid->fill('\0'); + _password->fill('\0'); + } + + ~Events() = default; + + void begin(void) { + log_i("wifi events preparing non-volatile storage"); + _preferences->begin("beetle-wifi", false); + } + + std::optional update(uint32_t current_time) { + _visitor._time = current_time; + auto [next, update] = std::visit(_visitor, std::move(_mode)); + + _mode = std::move(next); + + // TODO + return update; + } + + uint8_t attempt(void) { return 0; } private: - constexpr static const char *CONNECTION_PREFIX = "GET /connect?"; - constexpr static uint16_t SERVER_BUFFER_CAPACITY = 1024; - constexpr static uint8_t MAX_CLIENT_BLANK_READS = 5; - constexpr static uint16_t MAX_PENDING_CONNECTION_ATTEMPTS = 100; - constexpr static uint16_t MAX_CONNECTION_INTERRUPTS = 500; - constexpr static uint16_t MAX_HEADER_SIZE = 512; - - constexpr static uint8_t MAX_SSID_LENGTH = 60; - constexpr static uint8_t MAX_PASSWORD_LENGTH = 30; - - enum ERequestParsingMode { - None = 0, - Network = 1, - Password = 2, - Done = 3, - Failed = 4, + class Visitor; + + class Connecting final { + friend class Visitor; + uint32_t attempt = 0; + uint32_t attempt_time = 0; }; - // Initially, we do not have the necessary information to connect to a - // wifi network. While in this state, we will run both an http server - // as well as a dns server to create a "captive portal" - struct PendingConfiguration final { + struct Active final { + bool ok = false; + uint16_t interrupts = 0; + }; + + // Whenever we are without an active connection attempt or established + // connection, the underlying state will deal with creating an access point + // that responds with a "capture portal" where the user will enter in their + // real access point credentials. + class Configuring final { + friend class Visitor; + public: - PendingConfiguration() : _server(80) {} - ~PendingConfiguration(); + Configuring() + : _server(std::make_unique(80)), + _dns(std::make_unique()) {} + + ~Configuring() { + if (_server) { + log_e("wifi manager terminating server state"); + _server->stop(); + } + if (_dns) { + log_e("wifi manager terminating dns state"); + _dns->stop(); + } + } - PendingConfiguration(const PendingConfiguration &) = delete; - PendingConfiguration &operator=(const PendingConfiguration &) = delete; + // This is not a copyable resource; copying the wifi server is not defined + // behavior. + Configuring(Configuring &) = delete; + Configuring &operator=(Configuring &) = delete; + Configuring(const Configuring &) = delete; + Configuring &operator=(const Configuring &) = delete; - bool update(char *, char *); - void begin(IPAddress addr); + Configuring(Configuring &&c) = default; + Configuring &operator=(Configuring &&c) = default; private: - WiFiClient available(void); - inline static char termination(ERequestParsingMode); + // While receiving data on our wifi server, these states represent what we + // are in the process of doing. + enum ERequestParsingMode { + None = 0, + StartNetwork = 1, + NetworkValue = 2, + PasswordStart = 3, + PasswordValue = 3, + Done = 4, + Failed = 5, + }; - WiFiServer _server; - DNSServer _dns; + std::unique_ptr _server; + std::unique_ptr _dns; + bool _initialized = false; }; - // Once the user submits their wifi network configuration settings, we'll - // attempt to connect via `WiFi.begin(...)` and wait a defined number of - // frames before aborting back to configuration. - struct PendingConnection final { - uint8_t _attempts = 0; - - // TODO: unsure if using pointers here vs arrays with constant sizes is - // more "proper". Since we're dealing with a small amount (max 60 + 40 - // bytes) of data, it might be easier to use array members. - char *_ssid; - char *_password; - - PendingConnection(const char *ssid, const char *password) - : _attempts(0), - _ssid((char *)malloc(sizeof(char) * MAX_SSID_LENGTH)), - _password((char *)malloc(sizeof(char) * MAX_PASSWORD_LENGTH)) { - memcpy(_ssid, ssid, MAX_SSID_LENGTH); - memcpy(_password, password, MAX_PASSWORD_LENGTH); + using State = std::variant; + + struct Visitor final { + std::tuple> operator()( + Configuring &&configuring) { + uint8_t fields_set = 0; + + std::optional initial = + configuring._initialized + ? std::nullopt + : std::make_optional(EMessage::WaitingForCredentials); + + if (_preferences->isKey("ssid") && _preferences->isKey("password")) { + _preferences->getString("ssid", _ssid->data(), + MAX_NETWORK_CREDENTIAL_SIZE); + _preferences->getString("password", _password->data(), + MAX_NETWORK_CREDENTIAL_SIZE); + + log_i("wifi attempting stored credentials (ssid: %d, password: %d)", + strlen((char *)_ssid.get()), strlen((char *)_password.get())); + + return std::make_tuple(Connecting{}, EMessage::AttemptingConnection); + } + + if (!configuring._initialized) { + log_i("intializing configuration access point"); + const char *capture_ssid = std::get<0>(*_ap_config); + const char *capture_pass = std::get<1>(*_ap_config); + WiFi.softAP(capture_ssid, capture_pass, 7, 0, 1); + IPAddress address = WiFi.softAPIP(); + + log_i("access point (router) ip address: %s", address.toString()); + configuring._server->begin(); + configuring._dns->start(53, "*", address); + configuring._initialized = true; + } + + configuring._dns->processNextRequest(); + WiFiClient client = configuring._server->available(); + + if (!client) { + if (_time - _last_debug > 3000) { + log_i("no client connected for configuration yet (%d vs %d)", _time, + _last_debug); + _last_debug = _time; + } + + return std::make_tuple(State(std::move(configuring)), initial); + } + + _ssid->fill('\0'); + _password->fill('\0'); + + extern const char index_html[] asm("_binary_embeds_index_http_start"); + extern const char index_end[] asm("_binary_embeds_index_http_end"); + log_d("loaded index (%d bytes)", index_end - index_html); + + uint16_t cursor = 0, field = 0; + uint8_t noreads = 0; + + Configuring::ERequestParsingMode method = + Configuring::ERequestParsingMode::None; + + char buffer[SERVER_BUFFER_CAPACITY]; + memset(buffer, '\0', SERVER_BUFFER_CAPACITY); + + while (client.connected() && cursor < SERVER_BUFFER_CAPACITY - 1 && + noreads < MAX_CLIENT_BLANK_READS && + (method == Configuring::ERequestParsingMode::None + ? cursor < MAX_HEADER_SIZE + : true)) { + // If there is no pending data in our buffer, increment our noop count + // and move on. If that count exceeds a threshold, we will stop reading. + if (!client.available()) { + noreads += 1; + continue; + } + + noreads = 0; + + char token = client.read(); + buffer[cursor] = token; + cursor += 1; + + // TODO: what is this doing? + if (cursor < 3 || method == Configuring::ERequestParsingMode::Done) { + continue; + } + + // If we have not started to parse any response, and the client received + // a get request to the connect endpoint, we're going to want to send + // the capture portal html data. + if (method == Configuring::ERequestParsingMode::None && + strcmp(buffer, CONNECTION_PREFIX) == 0) { + method = Configuring::ERequestParsingMode::StartNetwork; + field = cursor; + continue; + } + } + + if (method == Configuring::ERequestParsingMode::StartNetwork) { + log_i("attempting to parse url parameters starting at %d (of %d)", + field, cursor); + + bool terminating = false; + uint8_t field_start = field; + + for (uint16_t start = field; + start < cursor && + method != Configuring::ERequestParsingMode::Failed; + start++) { + if (terminating && buffer[start] == '\n') { + break; + } + + if (buffer[start] == '\r') { + terminating = true; + continue; + } + + if (buffer[start] == '=' && + method == Configuring::ERequestParsingMode::StartNetwork) { + field_start = start + 1; + method = Configuring::ERequestParsingMode::NetworkValue; + continue; + } + + if (buffer[start] == '&' && + method == Configuring::ERequestParsingMode::NetworkValue) { + uint16_t len = start - field_start; + + if (len < MAX_NETWORK_CREDENTIAL_SIZE) { + fields_set += 1; + memcpy(_ssid.get(), buffer + field_start, len); + log_i("terminated SSID name value parsing: %d", + strlen((char *)_ssid.get())); + + // HACK: url decoding + for (uint16_t i = 0; i < len; i++) { + if (_ssid->at(i) == '+') { + std::array *mem = + _ssid.get(); + mem->at(i) = ' '; + // _ssid.get()[i] = ' '; + } + } + + method = Configuring::ERequestParsingMode::PasswordStart; + } else { + log_e("parsed ssid name too long: %d", len); + method = Configuring::ERequestParsingMode::Failed; + } + + continue; + } + + if (buffer[start] == '=' && + method == Configuring::ERequestParsingMode::PasswordStart) { + field_start = start + 1; + method = Configuring::ERequestParsingMode::PasswordValue; + continue; + } + + if (buffer[start] == ' ' && + method == Configuring::ERequestParsingMode::PasswordValue) { + uint16_t len = start - field_start; + + if (len < MAX_NETWORK_CREDENTIAL_SIZE) { + fields_set += 1; + memcpy(_password.get(), buffer + field_start, len); + log_i("terminated SSID password value parsing: %d", + strlen((char *)_password.get())); + } else { + log_e("parsed ssid password too long: %d", len); + method = Configuring::ERequestParsingMode::Failed; + } + + // method = Configuring::ERequestParsingMode::Done; + continue; + } + } + } + + // For now, always respond to clients with the same html response. + client.write(index_html, index_end - index_html); + delay(10); + client.stop(); + + if (fields_set == 2) { + log_i("wifi credentials ready ('%s' '%s')", _ssid.get(), + _password.get()); + + WiFi.softAPdisconnect(true); + WiFi.disconnect(true, true); + + return std::make_tuple(Connecting{}, EMessage::AttemptingConnection); + } + + // If we finished reading all the data available and we're not done, this + // is where we will write the html data. + log_e("non-connect request after %d bytes:\n%s", cursor, buffer); + + return std::make_tuple(State(std::move(configuring)), initial); } - ~PendingConnection() { - log_d("[MEMORY OPERATION] freeing memory used by pending connection"); - free(_ssid); - free(_password); + std::tuple> operator()( + Connecting &&connecting) { + if (connecting.attempt == 0) { + log_i("wifi attempting (ssid: %d, password: %d)", + strlen((char *)_ssid.get()), strlen((char *)_password.get())); + + WiFi.mode(WIFI_STA); + WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); + WiFi.setHostname("orient-beetle"); + WiFi.begin(_ssid->data(), _password->data()); + } + + if (WiFi.status() == WL_CONNECTED) { + log_i("wifi is connected"); + _preferences->putString("ssid", (char *)_ssid->data()); + _preferences->putString("password", (char *)_password->data()); + return std::make_tuple(Active{true}, EMessage::Connected); + } + + if (_time - _last_connecting_inc > 1000) { + log_i("wifi events incrementing pending connection attempt %d", + connecting.attempt); + _last_connecting_inc = _time; + connecting.attempt += 1; + } + + if (connecting.attempt > MAX_PENDING_CONNECTION_ATTEMPTS) { + WiFi.disconnect(true, true); + return std::make_tuple(Configuring{}, EMessage::FailedConnection); + } + + return std::make_tuple(connecting, std::nullopt); } - PendingConnection(const PendingConnection &) = delete; - PendingConnection &operator=(const PendingConnection &) = delete; - }; + std::tuple> operator()(Active &&active) { + uint8_t still_connected = WiFi.status() == WL_CONNECTED ? 1 : 0; - // After we're connected via `WiFi.status(...)` returns a connected state, - // we'll move into this active connection state where each frame checks - // the current connection information and disconnects after some number of - // frames. - struct ActiveConnection final { - explicit ActiveConnection(uint8_t d) : _disconnected(d) {} - uint8_t _disconnected = 0; - }; + if (_time - _last_debug > 3000) { + _last_debug = _time; + + if (still_connected) { + IPAddress address = WiFi.localIP(); + log_i("wifi events still active: (%s)", address.toString()); + } + } + + if (still_connected == 0 && active.ok) { + active.ok = false; + log_e("wifi connection interrupted"); + return std::make_tuple(active, EMessage::ConnectionInterruption); + } - uint8_t _last_mode; + if (still_connected == 1 && !active.ok) { + active.ok = true; + active.interrupts = 0; + log_i("wifi connection recovered after %d", active.interrupts); + return std::make_tuple(active, EMessage::ConnectionResumed); + } + if (still_connected == 0 && !active.ok && + _time - _last_connecting_inc > 100) { + _last_connecting_inc = _time; + active.interrupts += 1; + log_i("wifi connection still interrupted after: %d", active.interrupts); + } + + if (active.interrupts > MAX_PENDING_CONNECTION_ATTEMPTS) { + log_e("wifi connection being destroyed after: %d", active.interrupts); + + _preferences->remove("ssid"); + _preferences->remove("password"); + + return std::make_tuple(Configuring{}, EMessage::Disconnected); + } + + return std::make_tuple(active, std::nullopt); + } + + const std::tuple *_ap_config; + uint32_t _time; + std::shared_ptr> _ssid; + std::shared_ptr> _password; + std::shared_ptr _preferences; + uint32_t _last_debug = 0; + uint32_t _last_connecting_inc = 0; + }; + + State _mode; std::tuple _ap_config; - std::variant _mode; - Preferences _preferences; - - // When we're in our pending state, we only want to check the flash memory - // using - // the preferences library once. - bool _checked_stored_values = false; - microtim::MicroTimer _timer = microtim::MicroTimer(100); + std::shared_ptr> _ssid; + std::shared_ptr> _password; + std::shared_ptr _preferences; + Visitor _visitor; + uint32_t _last_time = 0; }; -} + +} // namespace wifievents + diff --git a/src/beetle-pio/load_env.py b/src/beetle-pio/load_env.py index be1af33..9ef1d79 100644 --- a/src/beetle-pio/load_env.py +++ b/src/beetle-pio/load_env.py @@ -4,6 +4,7 @@ redis_port = None redis_host = None +beetle_version = "dev" redis_auth_username = None redis_auth_password = None @@ -39,6 +40,9 @@ def get_value(line): if "REDIS_AUTH_PASSWORD" in os.environ: redis_auth_password = os.environ["REDIS_AUTH_PASSWORD"] +if "BEETLE_VERSION" in os.environ: + beetle_version = os.environ["BEETLE_VERSION"] + if "REDIS_PORT" in os.environ: redis_port = os.environ["REDIS_PORT"] @@ -56,8 +60,10 @@ def get_value(line): flag_str = "-DREDIS_HOST='\"{redis_host}\"' \ -DREDIS_PORT='{redis_port}' \ + -DBEETLE_VERSION='\"{beetle_version}\"' \ -DREDIS_AUTH_PASSWORD='\"{redis_auth_password}\"' \ -DREDIS_AUTH_USERNAME='\"{redis_auth_username}\"'".format(redis_host = redis_host, + beetle_version = beetle_version, redis_port = redis_port, redis_auth_username = redis_auth_username, redis_auth_password = redis_auth_password); diff --git a/src/beetle-pio/src/engine.cpp b/src/beetle-pio/src/engine.cpp index 38a565a..bc9ef37 100644 --- a/src/beetle-pio/src/engine.cpp +++ b/src/beetle-pio/src/engine.cpp @@ -1,5 +1,9 @@ #include "engine.hpp" +#ifndef BEETLE_VERSION +#define BEETLE_VERSION "dev" +#endif + Engine::Engine(std::tuple ap_config, std::shared_ptr redis_config) : _buffer(std::make_shared>()), @@ -22,19 +26,28 @@ states::State Engine::update(states::State&& current, uint32_t current_time) { if (wifi_update != std::nullopt) { switch (*wifi_update) { - case wifievents::Events::EMessage::Connecting: + case wifievents::Events::EMessage::AttemptingConnection: + log_i("received connection attempt event from wifi"); next = states::Connecting{}; break; case wifievents::Events::EMessage::ConnectionResumed: case wifievents::Events::EMessage::Connected: + log_i("received connection established event from wifi"); next = states::Connected{}; break; case wifievents::Events::EMessage::FailedConnection: case wifievents::Events::EMessage::ConnectionInterruption: case wifievents::Events::EMessage::Disconnected: + log_i("received error event from wifi"); next = states::Unknown{}; break; + case wifievents::Events::EMessage::WaitingForCredentials: + log_i("acknowlegement of wifi waiting for credentials from user"); + break; } + } else if (current_time - _last_time > 3000) { + log_i("(v. %s) no update from wifi events", BEETLE_VERSION); + _last_time = current_time; } auto redis_update = _redis.update(wifi_update, _buffer, current_time); diff --git a/src/beetle-pio/src/engine.hpp b/src/beetle-pio/src/engine.hpp index d59fabd..0040a66 100644 --- a/src/beetle-pio/src/engine.hpp +++ b/src/beetle-pio/src/engine.hpp @@ -2,6 +2,7 @@ #define _ENGINE_H 1 #include + #include "redis-config.hpp" #include "redis-events.hpp" #include "state.hpp" @@ -26,6 +27,7 @@ class Engine final { std::shared_ptr> _buffer; wifievents::Events _wifi; redisevents::Events _redis; + uint32_t _last_time = 0; }; #endif diff --git a/src/beetle-pio/src/main.cpp b/src/beetle-pio/src/main.cpp index c11c8b6..92b726b 100644 --- a/src/beetle-pio/src/main.cpp +++ b/src/beetle-pio/src/main.cpp @@ -12,6 +12,7 @@ static_assert(1 = 0, #endif #include + #include "esp32-hal-log.h" #ifdef XIAO @@ -35,17 +36,17 @@ lighting::Lighting lights; #include "wifi-events.hpp" // Configuration files -#include "redis_config.hpp" -#include "wifi_config.hpp" - #include "engine.hpp" +#include "redis_config.hpp" #include "state.hpp" +#include "wifi_config.hpp" extern const char* ap_ssid; extern const char* ap_password; extern const char* redis_host; extern const uint32_t redis_port; + extern const char* redis_auth_username; extern const char* redis_auth_password; @@ -67,12 +68,16 @@ Adafruit_VCNL4010 vcnl; microtim::MicroTimer _debug_timer(5000); #endif +// DEPRECATED: Proximity sensors were removed on the hardware migration to using +// e-ink displays with the xiao esp32c3. +#ifndef DISABLE_PROXIMITY microtim::MicroTimer _prox_timer(5000); bool _prox_state = true; +bool prox_ready = false; +#endif uint32_t last_frame = 0; bool failed = false; -bool prox_ready = false; void setup(void) { #ifdef XIAO @@ -104,12 +109,9 @@ void setup(void) { failed = true; } #else - prox_ready = false; log_e("[notice] proximity functionality disabled at compile time"); #endif - // log_i("boot complete, redis-config. host: %s | port: %d", redis_host, - // redis_port); eng.begin(); } diff --git a/src/beetle-srv/env.example.toml b/src/beetle-srv/env.example.toml index fa57d87..c4b248b 100644 --- a/src/beetle-srv/env.example.toml +++ b/src/beetle-srv/env.example.toml @@ -26,14 +26,15 @@ interval_delay_ms = 500 active_device_chunk_size = 10 device_schedule_refresh_interval_seconds = 15 -[registrar.analytics_configuration] -kind = "" -content = { api_key = "", account_id = "" } +# [registrar.analytics_configuration] +# kind = "" +# content = { api_key = "", account_id = "" } [web] cookie_domain = "" session_secret = "" session_cookie = "" +temp_file_storage = ".tmp" ui_redirect = "" [google] diff --git a/src/beetle-srv/src/api/worker.rs b/src/beetle-srv/src/api/worker.rs index b4ed5bc..a30679a 100644 --- a/src/beetle-srv/src/api/worker.rs +++ b/src/beetle-srv/src/api/worker.rs @@ -37,7 +37,9 @@ impl Worker { let mongo = mongodb::Client::with_options(mongo_options) .map_err(|error| Error::new(ErrorKind::Other, format!("failed mongodb connection - {error}")))?; - let redis = crate::redis::connect(&config.redis).await?; + let redis = crate::redis::connect(&config.redis) + .await + .map_err(|error| Error::new(ErrorKind::Other, format!("unable to connect to redis - {error}")))?; let redis_pool = Arc::new(Mutex::new(Some(redis))); @@ -63,6 +65,7 @@ impl Worker { async fn queue_job(&self, job: crate::registrar::RegistrarJob) -> Result { // TODO: this is where id generation should happen, not in the job construction itself. let id = job.id.clone(); + let label = job.label(); let serialized = job.encrypt(&self.registrar_configuration)?; @@ -71,6 +74,7 @@ impl Worker { Error::new(ErrorKind::Other, "job-serialize") })?; + let now = std::time::Instant::now(); self .command(&kramer::Command::Hashes(kramer::HashCommand::Set( crate::constants::REGISTRAR_JOB_RESULTS, @@ -87,6 +91,8 @@ impl Worker { ))) .await?; + log::debug!("job '{label}' took {}ms to queue", now.elapsed().as_millis()); + Ok(id) } @@ -149,66 +155,20 @@ impl Worker { S: std::fmt::Display, V: std::fmt::Display, { - let mut now = std::time::Instant::now(); - let mut lock_result = self.redis_pool.lock().await; - log::trace!("redis 'pool' lock in in {}ms", now.elapsed().as_millis()); - now = std::time::Instant::now(); - - #[allow(unused_assignments)] - let mut result = Err(Error::new(ErrorKind::Other, "failed send")); - - let mut retry_count = 0; - - 'retries: loop { - *lock_result = match lock_result.take() { - Some(mut connection) => { - log::trace!("redis lock taken in in {}ms", now.elapsed().as_millis()); - result = kramer::execute(&mut connection, command).await; - Some(connection) - } - None => { - log::warn!("no existing redis connection, establishing now"); - - let mut connection = crate::redis::connect(&self.redis_configuration) - .await - .map_err(|error| { - log::warn!("unable to connect to redis from previous disconnect - {error}"); - error - })?; - - result = kramer::execute(&mut connection, command).await; - Some(connection) - } - }; - - // TODO: add a redis connection retry configuration value that can be used here. - if retry_count > 0 { - log::warn!("exceeded redis retry count, breaking with current result"); - break; - } + let now = std::time::Instant::now(); + log::debug!("attempting to aquire redis pool lock"); + let mut redis_connection = self.get_redis_lock().await.map_err(|error| { + log::error!("redis connection lock failed - {error}"); + error + })?; + log::debug!("redis 'pool' lock in in {}ms", now.elapsed().as_millis()); - match result { - // If we were successful, there is nothing more to do here, exit the loop - Ok(_) => break, - - // If we failed due to a broken pipe, clear out our connection and try one more time. - Err(error) if error.kind() == ErrorKind::BrokenPipe => { - log::warn!("detected broken pipe, re-trying"); - retry_count += 1; - lock_result.take(); - continue 'retries; - } - - Err(error) => { - log::warn!("redis command failed for ({:?}) ({:?}), no retry", error, error.kind()); - retry_count += 1; - lock_result.take(); - continue 'retries; - } - } + if let Some(ref mut connection) = *redis_connection { + return kramer::execute(connection, command).await; } - result + log::warn!("unable to aquire redis connection from lock!"); + Err(Error::new(ErrorKind::Other, "unable to connect to redis")) } /// This is an associated, helper function for routes to require that a request has a valid user @@ -293,7 +253,11 @@ impl Worker { /// Attempts to aquire a lock, filling the contents with either a new connection, or just /// re-using the existing one. async fn get_redis_lock(&self) -> Result>> { - let mut lock_result = self.redis_pool.lock().await; + let mut lock_result = + match async_std::future::timeout(std::time::Duration::from_secs(1), self.redis_pool.lock()).await { + Ok(r) => r, + Err(_error) => return Err(Error::new(ErrorKind::Other, "detected deadlock on redis connection!")), + }; match lock_result.take() { Some(connection) => { diff --git a/src/beetle-srv/src/bin/beetle-cli.rs b/src/beetle-srv/src/bin/beetle-cli.rs index ab813a3..23aa69c 100644 --- a/src/beetle-srv/src/bin/beetle-cli.rs +++ b/src/beetle-srv/src/bin/beetle-cli.rs @@ -22,6 +22,9 @@ enum CommandLineCommand { /// re-authenticate from a fresh set of available ids. InvalidateAcls, + /// Prints out the acls found in redis. + ListAcls, + /// Removes devices that have not been heard from within the amount of time that we consider /// active. CleanDisconnects, @@ -105,6 +108,7 @@ async fn run(config: cli::CommandLineConfig, command: CommandLineCommand) -> io: Ok(()) } CommandLineCommand::InvalidateAcls => cli::invalidate_acls(&config).await, + CommandLineCommand::ListAcls => cli::print_acls(&config).await, CommandLineCommand::CleanDisconnects => cli::clean_disconnects(&config).await, CommandLineCommand::Provision(command) => cli::provision(&config, command).await, CommandLineCommand::PrintConnected => cli::print_connected(&config).await, @@ -166,7 +170,12 @@ fn main() -> io::Result<()> { log::info!("environment + logger ready."); let options = CommandLineOptions::parse(); - let contents = std::fs::read_to_string(&options.config)?; + let contents = std::fs::read_to_string(&options.config).map_err(|error| { + io::Error::new( + io::ErrorKind::Other, + format!("unable to load config file - '{}' ({error})", options.config), + ) + })?; let config = toml::from_str::(&contents).map_err(|error| { log::warn!("invalid toml config file - {error}"); io::Error::new(io::ErrorKind::Other, "bad-config") diff --git a/src/beetle-srv/src/bin/beetle-registrar.rs b/src/beetle-srv/src/bin/beetle-registrar.rs index 8b97040..a6b3a97 100644 --- a/src/beetle-srv/src/bin/beetle-registrar.rs +++ b/src/beetle-srv/src/bin/beetle-registrar.rs @@ -69,7 +69,12 @@ fn main() -> Result<()> { log::info!("environment + logger ready."); let args = CommandLineArguments::parse(); - let contents = std::fs::read_to_string(args.config)?; + let contents = std::fs::read_to_string(&args.config).map_err(|error| { + Error::new( + ErrorKind::Other, + format!("unable to load config '{}' - {error}", args.config), + ) + })?; let config = toml::from_str::(&contents).map_err(|error| { log::warn!("invalid toml config file - {error}"); diff --git a/src/beetle-srv/src/bin/beetle-web.rs b/src/beetle-srv/src/bin/beetle-web.rs index 9211762..7f36a1c 100644 --- a/src/beetle-srv/src/bin/beetle-web.rs +++ b/src/beetle-srv/src/bin/beetle-web.rs @@ -30,7 +30,12 @@ fn main() -> io::Result<()> { env_logger::init(); let options = CommandLineOptions::parse(); - let contents = std::fs::read_to_string(&options.config)?; + let contents = std::fs::read_to_string(&options.config).map_err(|error| { + io::Error::new( + io::ErrorKind::Other, + format!("unable to load config file '{}' - {error}", options.config), + ) + })?; let config = toml::from_str::(&contents).map_err(|error| { log::warn!("invalid toml config file - {error}"); io::Error::new(io::ErrorKind::Other, "bad-config") diff --git a/src/beetle-srv/src/bin/cli/acls.rs b/src/beetle-srv/src/bin/cli/acls.rs index 0283fbc..c874e08 100644 --- a/src/beetle-srv/src/bin/cli/acls.rs +++ b/src/beetle-srv/src/bin/cli/acls.rs @@ -23,13 +23,13 @@ fn id_from_acl_entry(entry: &str) -> Option<&str> { pub async fn provision(config: &super::CommandLineConfig, command: ProvisionCommand) -> io::Result<()> { let ProvisionCommand { user, password } = command; let mut stream = beetle::redis::connect(&config.redis).await?; - log::info!("provisioning redis environment with burn-in auth information"); let password = password.or(config.registrar.id_consumer_password.clone()); let user = user.or(config.registrar.id_consumer_username.clone()); + println!("provisioning redis environment with burn-in auth information (p: {password:?}, u: {user:?})"); match (user, password) { - (Some(ref user), Some(ref pass)) => { + (Some(ref user), Some(ref pass)) if !user.is_empty() && !pass.is_empty() => { let command = kramer::Command::Acl::<&str, &str>(kramer::acl::AclCommand::SetUser(kramer::acl::SetUser { name: user, password: Some(pass), @@ -37,8 +37,9 @@ pub async fn provision(config: &super::CommandLineConfig, command: ProvisionComm commands: Some(vec!["lpop", "blpop"]), })); + log::debug!("sending {command:?}"); let result = kramer::execute(&mut stream, &command).await; - log::debug!("result from {command:?} - {result:?}"); + log::info!("acl provisioning result - {result:?}"); println!("ok"); } _ => { @@ -52,6 +53,53 @@ pub async fn provision(config: &super::CommandLineConfig, command: ProvisionComm Ok(()) } +/// Finds all acl entries on our redis instance and prints them. +pub async fn print_acls(config: &super::CommandLineConfig) -> io::Result<()> { + let mut stream = beetle::redis::connect(&config.redis).await?; + let allowed: std::collections::hash_set::HashSet = match &config.registrar.acl_user_allowlist { + Some(ref list) => std::collections::hash_set::HashSet::from_iter(list.iter().cloned()), + None => { + log::warn!("no acl allowlist configured"); + std::collections::hash_set::HashSet::new() + } + }; + log::debug!("looking for acl entries to destroy, skipping {allowed:?}"); + let list = kramer::execute(&mut stream, kramer::Command::Acl::(kramer::AclCommand::List)).await; + + let values = match list { + Ok(kramer::Response::Array(inner)) => inner, + _ => return Err(io::Error::new(io::ErrorKind::Other, "")), + }; + + let names = values + .into_iter() + .filter_map(|entry| match entry { + kramer::ResponseValue::String(v) => { + let id = id_from_acl_entry(&v)?; + log::trace!("found id {id}"); + + if allowed.contains(id) { + None + } else { + Some(id.to_string()) + } + } + _ => None, + }) + .collect::>(); + + if names.is_empty() { + println!("no matching acl entries to delete"); + return Ok(()); + } + + for name in names { + println!("{name}"); + } + + Ok(()) +} + /// Finds all acl entries on our redis instance and deletes them. pub async fn invalidate_acls(config: &super::CommandLineConfig) -> io::Result<()> { let mut stream = beetle::redis::connect(&config.redis).await?; diff --git a/src/beetle-srv/src/bin/cli/mod.rs b/src/beetle-srv/src/bin/cli/mod.rs index 208bd62..cd945b1 100644 --- a/src/beetle-srv/src/bin/cli/mod.rs +++ b/src/beetle-srv/src/bin/cli/mod.rs @@ -45,7 +45,7 @@ pub mod migrate; /// Commands associated with device permissions + authentication. mod acls; -pub use acls::{invalidate_acls, provision, ProvisionCommand}; +pub use acls::{invalidate_acls, print_acls, provision, ProvisionCommand}; /// Commands associated with device messaging. mod messages; diff --git a/src/beetle-srv/src/redis.rs b/src/beetle-srv/src/redis.rs index 1a5b4d9..4695c3d 100644 --- a/src/beetle-srv/src/redis.rs +++ b/src/beetle-srv/src/redis.rs @@ -22,12 +22,14 @@ where #[cfg(not(feature = "redis-insecure"))] pub async fn connect(config: &crate::config::RedisConfiguration) -> Result { let connector = async_tls::TlsConnector::default(); + log::debug!("attempting secure redis connection"); let mut stream = connector .connect( &config.host, async_std::net::TcpStream::connect(format!("{}:{}", config.host, config.port)).await?, ) - .await?; + .await + .map_err(|error| Error::new(ErrorKind::Other, format!("unable to connect to redis - {error}")))?; match &config.auth { Some(auth) => { @@ -51,7 +53,10 @@ pub async fn connect(config: &crate::config::RedisConfiguration) -> Result Result { - let mut stream = async_std::net::TcpStream::connect(format!("{}:{}", &config.host, &config.port)).await?; + log::warn!("redis connecting over unencrypted stream"); + let mut stream = async_std::net::TcpStream::connect(format!("{}:{}", &config.host, &config.port)) + .await + .map_err(|error| Error::new(ErrorKind::Other, format!("unable to connect to redis - {error}")))?; match &config.auth { Some(auth) => { diff --git a/src/beetle-srv/src/registrar/jobs.rs b/src/beetle-srv/src/registrar/jobs.rs index df25152..e561e37 100644 --- a/src/beetle-srv/src/registrar/jobs.rs +++ b/src/beetle-srv/src/registrar/jobs.rs @@ -101,6 +101,21 @@ pub struct RegistrarJob { } impl RegistrarJob { + /// Returns a string that can be used to label what kind of job is being executed for logging + /// purposes. This could be handled as a macro instead, probably. + pub fn label(&self) -> &'static str { + match self.job { + RegistrarJobKind::MutateDeviceState(_) => "MutateDeviceState", + RegistrarJobKind::Ownership(_) => "Ownership", + RegistrarJobKind::OwnershipChange(_) => "OwnershipChange", + RegistrarJobKind::Rename(_) => "Rename", + RegistrarJobKind::Renders(_) => "Render", + RegistrarJobKind::RunDeviceSchedule { .. } => "RunDeviceSchedule", + RegistrarJobKind::ToggleDefaultSchedule { .. } => "ToggleDefaultSchedule", + RegistrarJobKind::UserAccessTokenRefresh { .. } => "UserAccessTokenRefresh", + } + } + /// Serializes and encrypts a job. pub fn encrypt(self, config: &crate::config::RegistrarConfiguration) -> io::Result { // TODO(job_encryption): using jwt here for ease, not the fact that it is the best. The diff --git a/src/beetle-ui/src/Icon.elm b/src/beetle-ui/src/Icon.elm index 8e3df3b..9a2af66 100644 --- a/src/beetle-ui/src/Icon.elm +++ b/src/beetle-ui/src/Icon.elm @@ -26,11 +26,15 @@ type Icon | Send | Refresh | EllipsisH + | Google view : Icon -> Html.Html a view icon = case icon of + Google -> + Html.i [ A.class "icon-google" ] [] + Refresh -> Html.i [ A.class "icon-refresh" ] [] diff --git a/src/beetle-ui/src/Route.elm b/src/beetle-ui/src/Route.elm index f841717..9b877f1 100644 --- a/src/beetle-ui/src/Route.elm +++ b/src/beetle-ui/src/Route.elm @@ -5,6 +5,7 @@ import Html import Html.Attributes import Html.Parser import Html.Parser.Util as HTP +import Icon import Route.Account import Route.Device import Route.DeviceRegistration @@ -292,10 +293,15 @@ renderLogin env returnState = [ Html.Attributes.href env.configuration.loginUrl , Html.Attributes.rel "noopener" , Html.Attributes.target "_self" + , Html.Attributes.class "google-login" + ] + [ Html.div [ Html.Attributes.class "flex items-center" ] + [ Icon.view Icon.Google + , Html.span [ Html.Attributes.class "block ml-3" ] [ Html.text "Sign in with Google" ] + ] ] - [ Html.text "Login" ] ] ] - , Html.div [ Html.Attributes.class "lg:flex-1 lg:pr-3 flex-1 flex flex-col h-full relative" ] + , Html.div [ Html.Attributes.class "lg:pr-3 flex-2 flex flex-col h-full relative" ] loginContentDom ] diff --git a/src/beetle-ui/src/boot.ts b/src/beetle-ui/src/boot.ts index 23c0bef..4a1d550 100644 --- a/src/beetle-ui/src/boot.ts +++ b/src/beetle-ui/src/boot.ts @@ -13,6 +13,7 @@ type Environment = { }; const REPO_URL = 'https://github.com/dadleyy/orient-beetle'; +const DEMO_URL = 'https://www.youtube.com/embed/Fgkh3P7V6Mo?si=eSraH7IkpJrTcb_x'; // TODO: incorporate this into the project somewhere else. Would probably require some additional // build tooling. Skimping on that for now. @@ -36,8 +37,8 @@ const LOCALIZATION = [