#pragma once #ifdef ESP_PLATFORM #include #endif #include "ProtobufModule.h" #include "meshUtils.h" #include #if HAS_WIFI #include "mesh/wifi/WiFiAPClient.h" #endif /** * Datatype passed to Observers by AdminModule, to allow external handling of admin messages */ struct AdminModule_ObserverData { const meshtastic_AdminMessage *request; meshtastic_AdminMessage *response; AdminMessageHandleResult *result; }; /** * Admin module for admin messages */ class AdminModule : public ProtobufModule, public Observable { friend class AdminModuleTestShim; // test/support/AdminModuleTestShim.h - native tests reach the private handlers/state public: /** Constructor * name is for debugging output */ AdminModule(); protected: /** Called to handle a particular incoming message @return true if you've guaranteed you've handled this message and no other handlers should be considered for it */ virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *p) override; private: bool hasOpenEditTransaction = false; // Each deferred write restarts the clock, so this bounds the gap between writes, not the length // of the edit; a bulk import sends them milliseconds apart. static constexpr uint32_t EDIT_TRANSACTION_IDLE_MS = 60 * 1000; uint32_t editTransactionActivityMs = 0; // millis() of the last save this transaction deferred int deferredEditSegments = 0; // segments that transaction has touched but not yet saved /// Retire an open edit transaction whose client stopped talking, persisting what it applied. void expireStaleEditTransaction(); #ifdef PIO_UNIT_TESTING int lastSaveWhatForTest = 0; #endif uint8_t session_passkey[8] = {0}; uint32_t session_time = 0; // millis() when the current session passkey was issued bool sessionPasskeyValid = false; // separate flag: millis() 0 at boot is a valid issue time void saveChanges(int saveWhat, bool shouldReboot = true); /** * Getters * * Each of the NOINLINE ones below builds a whole meshtastic_AdminMessage (480 bytes) on the * stack. They are only ever reached from one case of handleReceivedProtobuf()'s switch, but * when the compiler inlines them the dispatcher's frame has to reserve a slot for every one of * them at once - measured at 3008 bytes on ESP32-S3, 37% of the 8 KB Arduino loopTask stack * that also has to carry PhoneAPI, the router, nanopb and LittleFS below it. Keeping them out * of line means only the request actually being served pays for its response buffer. * See issue #11237. */ void handleGetModuleConfigResponse(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *p); NOINLINE void handleGetOwner(const meshtastic_MeshPacket &req); NOINLINE void handleGetConfig(const meshtastic_MeshPacket &req, uint32_t configType); NOINLINE void handleGetModuleConfig(const meshtastic_MeshPacket &req, uint32_t configType); NOINLINE void handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex); NOINLINE void handleGetDeviceMetadata(const meshtastic_MeshPacket &req); NOINLINE void handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req); NOINLINE void handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &req); NOINLINE void handleGetDeviceUIConfig(const meshtastic_MeshPacket &req); /** * Setters */ void handleSetOwner(const meshtastic_User &o); void handleSetChannel(const meshtastic_Channel &cc); protected: void handleSetConfig(const meshtastic_Config &c, bool fromOthers); #ifdef PIO_UNIT_TESTING protected: #else private: #endif bool handleSetModuleConfig(const meshtastic_ModuleConfig &c); void handleSetChannel(); public: void handleSetHamMode(const meshtastic_HamParameters &req); /// Note an admin request leaving this node for a remote, so that remote's response is /// accepted. Called from the client-to-mesh path (MeshService::handleToRadio). void noteOutgoingAdminRequest(const meshtastic_MeshPacket &p); private: // An admin response has no session passkey and its sender need not hold an admin key, so a // request we sent is the only thing vouching for it. Track each request independently (its own // expiry and pinned key) so a later one can't extend or relax an earlier one. static constexpr size_t kOutstandingAdminRequests = 8; static constexpr uint32_t kOutstandingAdminRequestMs = 300 * 1000; // same window as the session passkey struct OutstandingAdminRequest { NodeNum to; // 0 = free slot uint32_t requestId; // our request's packet id; the response must echo it as request_id uint32_t sentAtMs; // millis() when this request went out pb_size_t expectedResponse; // the one response variant this request authorizes uint8_t moduleConfigType; // for get_module_config_request: which ModuleConfigType we asked uint8_t key[32]; // pinned destination key when the request goes out over PKC bool keyValid; }; OutstandingAdminRequest outstandingAdminRequests[kOutstandingAdminRequests] = {}; /// Whether a response (variant responseVariant, module-config subtype moduleConfigTag or 0) /// from mp.from answers a request we sent; consumes the matched request so it can't be replayed. bool responseIsSolicited(const meshtastic_MeshPacket &mp, pb_size_t responseVariant, pb_size_t moduleConfigTag); /// Offer an admin message we have no case for to the module API, and let observers (e.g. the UI) /// see every admin message. Both build a response on the stack, so like the getters above they /// stay out of line to keep handleReceivedProtobuf()'s frame small. NOINLINE void handleViaModuleApi(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *r); NOINLINE void handleViaObservers(const meshtastic_AdminMessage *r); void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg); void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent); void reboot(int32_t seconds); void setPassKey(meshtastic_AdminMessage *res); bool checkPassKey(meshtastic_AdminMessage *res); bool messageIsResponse(const meshtastic_AdminMessage *r); bool messageIsRequest(const meshtastic_AdminMessage *r); void sendWarning(const char *format, ...) __attribute__((format(printf, 2, 3))); void sendWarningAndLog(const char *format, ...) __attribute__((format(printf, 2, 3))); void warnOnLoraPresetChange(const meshtastic_Config_LoRaConfig &oldLora, const meshtastic_Config_LoRaConfig &newLora); void warnOnChannelSet(const meshtastic_Channel &cc); // Channel-configuration warnings are coalesced into a single client notification. // queueChannelWarning() records one warning for a channel; while an edit transaction // is open (begin/commit_edit_settings) the warnings accumulate and flushChannelWarnings() // emits one combined message at commit. Outside a transaction the caller flushes // immediately, so a single channel/preset edit still produces a single message. void queueChannelWarning(uint8_t channelIndex, bool nameIssue, bool pskIssue, const char *format, ...) __attribute__((format(printf, 5, 6))); // Emit the "licensed mode activated" notice, deferring to commit during an edit transaction // so repeated triggers (e.g. owner + several channels) produce a single message. void warnLicensedMode(); void flushChannelWarnings(); char pendingWarningText[250] = {}; // the lone queued message, used verbatim when only one fired uint32_t pendingWarningChannels = 0; // bitmask of channel indices with a queued warning uint8_t pendingWarningCount = 0; // number of queued warnings this transaction bool pendingWarningNameIssue = false; // any queued warning was about a channel name bool pendingWarningPskIssue = false; // any queued warning was about a PSK bool pendingLicenseWarning = false; // a licensed-mode notice is queued for this transaction }; static constexpr const char *licensedModeMessage = "Licensed mode activated, removing admin channel and encryption from all channels"; static constexpr const char *licensedIdentityMigrationMessage = "Licensed signing requires an identity key; this node identity will change after key generation"; static constexpr const char *publicChannelPrecisionMessage = "Precise position is not allowed on a public (open / known-key) channel; reduced to coarse precision"; extern AdminModule *adminModule; void disableBluetooth();