From dc588dce075594fbf5da190ea779094ff221d9a7 Mon Sep 17 00:00:00 2001 From: Tobias Diedrich Date: Sat, 14 Sep 2024 14:41:49 +0200 Subject: [PATCH 1/3] Add UI for syslog configuration to network view --- include/Configuration.h | 8 ++++++++ include/WebApi_errors.h | 2 ++ include/defaults.h | 3 +++ src/Configuration.cpp | 10 ++++++++++ src/WebApi_network.cpp | 23 +++++++++++++++++++++++ webapp/src/locales/de.json | 5 ++++- webapp/src/locales/en.json | 5 ++++- webapp/src/types/NetworkConfig.ts | 3 +++ webapp/src/views/NetworkAdminView.vue | 23 +++++++++++++++++++++++ 9 files changed, 80 insertions(+), 2 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index 4a802e4e..9c2de125 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -11,6 +11,8 @@ #define WIFI_MAX_PASSWORD_STRLEN 64 #define WIFI_MAX_HOSTNAME_STRLEN 31 +#define SYSLOG_MAX_HOSTNAME_STRLEN 128 + #define NTP_MAX_SERVER_STRLEN 31 #define NTP_MAX_TIMEZONE_STRLEN 50 #define NTP_MAX_TIMEZONEDESCR_STRLEN 50 @@ -76,6 +78,12 @@ struct CONFIG_T { bool Enabled; } Mdns; + struct { + bool Enabled; + char Hostname[SYSLOG_MAX_HOSTNAME_STRLEN + 1]; + uint32_t Port; + } Syslog; + struct { char Server[NTP_MAX_SERVER_STRLEN + 1]; char Timezone[NTP_MAX_TIMEZONE_STRLEN + 1]; diff --git a/include/WebApi_errors.h b/include/WebApi_errors.h index c4cdaf21..89118c4d 100644 --- a/include/WebApi_errors.h +++ b/include/WebApi_errors.h @@ -70,6 +70,8 @@ enum WebApiError { NetworkDns1Invalid, NetworkDns2Invalid, NetworkApTimeoutInvalid, + NetworkSyslogHostnameLength, + NetworkSyslogPort, NtpBase = 9000, NtpServerLength, diff --git a/include/defaults.h b/include/defaults.h index ee3f7b2f..91843ddd 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -22,6 +22,9 @@ #define MDNS_ENABLED false +#define SYSLOG_ENABLED false +#define SYSLOG_PORT 514 + #define NTP_SERVER_OLD "pool.ntp.org" #define NTP_SERVER "opendtu.pool.ntp.org" #define NTP_TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3" diff --git a/src/Configuration.cpp b/src/Configuration.cpp index db47d9c8..44af07ca 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -47,6 +47,11 @@ bool ConfigurationClass::write() JsonObject mdns = doc["mdns"].to(); mdns["enabled"] = config.Mdns.Enabled; + JsonObject syslog = doc["syslog"].to(); + syslog["enabled"] = config.Syslog.Enabled; + syslog["hostname"] = config.Syslog.Hostname; + syslog["port"] = config.Syslog.Port; + JsonObject ntp = doc["ntp"].to(); ntp["server"] = config.Ntp.Server; ntp["timezone"] = config.Ntp.Timezone; @@ -222,6 +227,11 @@ bool ConfigurationClass::read() JsonObject mdns = doc["mdns"]; config.Mdns.Enabled = mdns["enabled"] | MDNS_ENABLED; + JsonObject syslog = doc["syslog"]; + config.Syslog.Enabled = syslog["enabled"] | SYSLOG_ENABLED; + strlcpy(config.Syslog.Hostname, syslog["hostname"] | "", sizeof(config.Syslog.Hostname)); + config.Syslog.Port = syslog["port"] | SYSLOG_PORT; + JsonObject ntp = doc["ntp"]; strlcpy(config.Ntp.Server, ntp["server"] | NTP_SERVER, sizeof(config.Ntp.Server)); strlcpy(config.Ntp.Timezone, ntp["timezone"] | NTP_TIMEZONE, sizeof(config.Ntp.Timezone)); diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index 75275755..98ebe23c 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -70,6 +70,9 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request) root["password"] = config.WiFi.Password; root["aptimeout"] = config.WiFi.ApTimeout; root["mdnsenabled"] = config.Mdns.Enabled; + root["syslogenabled"] = config.Syslog.Enabled; + root["sysloghostname"] = config.Syslog.Hostname; + root["syslogport"] = config.Syslog.Port; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } @@ -163,6 +166,23 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } + if (root["syslogenabled"].as()) { + if (root["sysloghostname"].as().length() == 0 || root["sysloghostname"].as().length() > SYSLOG_MAX_HOSTNAME_STRLEN) { + retMsg["message"] = "Syslog Server must between 1 and " STR(SYSLOG_MAX_HOSTNAME_STRLEN) " characters long!"; + retMsg["code"] = WebApiError::NetworkSyslogHostnameLength; + retMsg["param"]["max"] = SYSLOG_MAX_HOSTNAME_STRLEN; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + return; + } + + if (root["syslogport"].as() == 0 || root["syslogport"].as() > 65535) { + retMsg["message"] = "Port must be a number between 1 and 65535!"; + retMsg["code"] = WebApiError::NetworkSyslogPort; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + return; + } + + } CONFIG_T& config = Configuration.get(); config.WiFi.Ip[0] = ipaddress[0]; @@ -195,6 +215,9 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) } config.WiFi.ApTimeout = root["aptimeout"].as(); config.Mdns.Enabled = root["mdnsenabled"].as(); + config.Syslog.Enabled = root["syslogenabled"].as(); + strlcpy(config.Syslog.Hostname, root["sysloghostname"].as().c_str(), sizeof(config.Syslog.Hostname)); + config.Syslog.Port = root["syslogport"].as(); WebApi.writeConfig(retMsg); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 0bc17768..b0864bcc 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -444,7 +444,10 @@ "ApTimeoutHint": "Zeit die der AccessPoint offen gehalten wird. Ein Wert von 0 bedeutet unendlich.", "Minutes": "Minuten", "EnableMdns": "mDNS aktivieren", - "MdnsSettings": "mDNS-Einstellungen" + "MdnsSettings": "mDNS-Einstellungen", + "EnableSyslog": "Syslog aktivieren", + "SyslogSettings": "Syslog-Einstellungen", + "Port": "Port:" }, "mqttadmin": { "MqttSettings": "MQTT-Einstellungen", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index f760178f..2d0f56eb 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -444,7 +444,10 @@ "ApTimeoutHint": "Time which the AccessPoint is kept open. A value of 0 means infinite.", "Minutes": "minutes", "EnableMdns": "Enable mDNS", - "MdnsSettings": "mDNS Settings" + "MdnsSettings": "mDNS Settings", + "EnableSyslog": "Enable Syslog", + "SyslogSettings": "Syslog Settings", + "Port": "Port:" }, "mqttadmin": { "MqttSettings": "MQTT Settings", diff --git a/webapp/src/types/NetworkConfig.ts b/webapp/src/types/NetworkConfig.ts index da5ddd44..b71c6a38 100644 --- a/webapp/src/types/NetworkConfig.ts +++ b/webapp/src/types/NetworkConfig.ts @@ -10,4 +10,7 @@ export interface NetworkConfig { dns2: string; aptimeout: number; mdnsenabled: boolean; + syslogenabled: boolean; + sysloghostname: string; + syslogport: number; } diff --git a/webapp/src/views/NetworkAdminView.vue b/webapp/src/views/NetworkAdminView.vue index aeabec08..8c3da697 100644 --- a/webapp/src/views/NetworkAdminView.vue +++ b/webapp/src/views/NetworkAdminView.vue @@ -82,6 +82,29 @@ /> + + + + + + + + Date: Thu, 26 Sep 2024 23:01:06 +0200 Subject: [PATCH 2/3] Feature: Add syslog logger This implements RFC5424 version of the protocol. Doesn't use https://github.com/arcao/Syslog since the protocol itself is trivial and most of the libraries functionality is not needed here. The library also doesn't support setting the PROCID field, which is set to a random id to indicate a reboot here. Adds UI for syslog configuration to network admin view. --- include/Configuration.h | 2 +- include/SyslogLogger.h | 34 ++++++++++ src/MessageOutput.cpp | 5 ++ src/NetworkSettings.cpp | 5 ++ src/SyslogLogger.cpp | 138 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 include/SyslogLogger.h create mode 100644 src/SyslogLogger.cpp diff --git a/include/Configuration.h b/include/Configuration.h index 9c2de125..beed9f08 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -81,7 +81,7 @@ struct CONFIG_T { struct { bool Enabled; char Hostname[SYSLOG_MAX_HOSTNAME_STRLEN + 1]; - uint32_t Port; + uint16_t Port; } Syslog; struct { diff --git a/include/SyslogLogger.h b/include/SyslogLogger.h new file mode 100644 index 00000000..a6982e7b --- /dev/null +++ b/include/SyslogLogger.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once +#include +#include +#include + +class SyslogLogger { +public: + SyslogLogger(); + void init(Scheduler& scheduler); + void updateSettings(const String&& hostname); + void write(const uint8_t *buffer, size_t size); + +private: + void loop(); + void disable(); + void enable(); + bool resolveAndStart(); + bool isResolved() const { + return _address != INADDR_NONE; + } + + Task _loopTask; + std::mutex _mutex; + WiFiUDP _udp; + IPAddress _address; + String _syslog_hostname; + String _proc_id; + String _header; + uint16_t _port; + bool _enabled; +}; + +extern SyslogLogger Syslog; diff --git a/src/MessageOutput.cpp b/src/MessageOutput.cpp index 4ed13771..d9e88758 100644 --- a/src/MessageOutput.cpp +++ b/src/MessageOutput.cpp @@ -3,6 +3,7 @@ * Copyright (C) 2022-2024 Thomas Basler and others */ #include "MessageOutput.h" +#include "SyslogLogger.h" #include @@ -26,6 +27,8 @@ void MessageOutputClass::register_ws_output(AsyncWebSocket* output) size_t MessageOutputClass::write(uint8_t c) { + Syslog.write(&c, 1); + if (_buff_pos < BUFFER_SIZE) { std::lock_guard lock(_msgLock); _buffer[_buff_pos] = c; @@ -39,6 +42,8 @@ size_t MessageOutputClass::write(uint8_t c) size_t MessageOutputClass::write(const uint8_t* buffer, size_t size) { + Syslog.write(buffer, size); + std::lock_guard lock(_msgLock); if (_buff_pos + size < BUFFER_SIZE) { memcpy(&_buffer[_buff_pos], buffer, size); diff --git a/src/NetworkSettings.cpp b/src/NetworkSettings.cpp index 8f391973..506c60ba 100644 --- a/src/NetworkSettings.cpp +++ b/src/NetworkSettings.cpp @@ -5,6 +5,7 @@ #include "NetworkSettings.h" #include "Configuration.h" #include "MessageOutput.h" +#include "SyslogLogger.h" #include "PinMapping.h" #include "Utils.h" #include "__compiled_constants.h" @@ -52,6 +53,8 @@ void NetworkSettingsClass::init(Scheduler& scheduler) scheduler.addTask(_loopTask); _loopTask.enable(); + + Syslog.init(scheduler); } void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event, WiFiEventInfo_t info) @@ -292,6 +295,8 @@ void NetworkSettingsClass::applyConfig() } MessageOutput.println("done"); setStaticIp(); + + Syslog.updateSettings(getHostname()); } void NetworkSettingsClass::setHostname() diff --git a/src/SyslogLogger.cpp b/src/SyslogLogger.cpp new file mode 100644 index 00000000..0c7a8c11 --- /dev/null +++ b/src/SyslogLogger.cpp @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include +#include +#include "defaults.h" +#include "SyslogLogger.h" +#include "Configuration.h" +#include "MessageOutput.h" +#include "NetworkSettings.h" + +SyslogLogger::SyslogLogger() + : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&SyslogLogger::loop, this)) +{ +} + +void SyslogLogger::init(Scheduler& scheduler) +{ + // PROCID change indicates a restart. + _proc_id = String(esp_random(), HEX); + + scheduler.addTask(_loopTask); + _loopTask.enable(); +} + +void SyslogLogger::updateSettings(const String&& hostname) +{ + auto& config = Configuration.get().Syslog; + + // Disable logger while it is reconfigured. + disable(); + + if (!config.Enabled) { + MessageOutput.println("[SyslogLogger] Syslog not enabled"); + return; + } + + _port = config.Port; + _syslog_hostname = config.Hostname; + if (_syslog_hostname.isEmpty()) { + MessageOutput.println("[SyslogLogger] Hostname not configured"); + return; + } + + MessageOutput.printf("[SyslogLogger] Logging to %s!\r\n", _syslog_hostname.c_str()); + + _header = "<14>1 - "; // RFC5424: Facility USER, severity INFO, version 1, NIL timestamp. + _header += hostname; + _header += " OpenDTU "; + _header += _proc_id; + // NIL values for message id and structured data + _header += " - - "; + + // Enable logger. + enable(); +} + +void SyslogLogger::write(const uint8_t *buffer, size_t size) +{ + std::lock_guard lock(_mutex); + if (!_enabled || !isResolved()) { + return; + } + for (int i = 0; i < size; i++) { + uint8_t c = buffer[i]; + bool overflow = false; + if (c != '\r' && c != '\n') { + // Replace control and non-ASCII characters with '?'. + overflow = !_udp.write(c >= 0x20 && c < 0x7f ? c : '?'); + } + if (c == '\n' || overflow) { + _udp.endPacket(); + _udp.beginPacket(_address, _port); + _udp.print(_header); + } + } +} + +void SyslogLogger::disable() +{ + MessageOutput.println("[SyslogLogger] Disable"); + std::lock_guard lock(_mutex); + if (_enabled) { + _enabled = false; + _address = INADDR_NONE; + _udp.stop(); + } +} + +void SyslogLogger::enable() +{ + // Bind random source port. + if (!_udp.begin(0)) { + MessageOutput.println("[SyslogLogger] No sockets available"); + return; + } + + std::lock_guard lock(_mutex); + _enabled = true; +} + +bool SyslogLogger::resolveAndStart() +{ + if (Configuration.get().Mdns.Enabled) { + _address = MDNS.queryHost(_syslog_hostname); // INADDR_NONE if failed + } + if (_address != INADDR_NONE) { + if (!_udp.beginPacket(_address, _port)) { + return false; + } + } else { + if (!_udp.beginPacket(_syslog_hostname.c_str(), _port)) { + return false; + } + _address = _udp.remoteIP(); // Store resolved address. + } + _udp.print(_header); + _udp.print("[SyslogLogger] Logging to "); + _udp.print(_syslog_hostname); + _udp.endPacket(); + _udp.beginPacket(_address, _port); + _udp.print(_header); + return true; +} + +void SyslogLogger::loop() +{ + std::lock_guard lock(_mutex); + if (!_enabled || !NetworkSettings.isConnected() || isResolved()) { + return; + } + if (!resolveAndStart()) { + _enabled = false; + } +} + +SyslogLogger Syslog; From c159cc2b7844fd9009a045cba7a43f5157a76156 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Sat, 28 Sep 2024 21:35:18 +0200 Subject: [PATCH 3/3] webapp: optimize syslog settings * avoid duplicate id for hostname input * hide server and port inputs if syslog disabled --- webapp/src/locales/de.json | 3 ++- webapp/src/locales/en.json | 3 ++- webapp/src/views/NetworkAdminView.vue | 28 ++++++++++++++------------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index b0864bcc..5def6772 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -447,7 +447,8 @@ "MdnsSettings": "mDNS-Einstellungen", "EnableSyslog": "Syslog aktivieren", "SyslogSettings": "Syslog-Einstellungen", - "Port": "Port:" + "SyslogHostname": "Syslog Server", + "SyslogPort": "Port" }, "mqttadmin": { "MqttSettings": "MQTT-Einstellungen", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 2d0f56eb..905432b0 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -447,7 +447,8 @@ "MdnsSettings": "mDNS Settings", "EnableSyslog": "Enable Syslog", "SyslogSettings": "Syslog Settings", - "Port": "Port:" + "SyslogHostname": "Syslog Server", + "SyslogPort": "Port" }, "mqttadmin": { "MqttSettings": "MQTT Settings", diff --git a/webapp/src/views/NetworkAdminView.vue b/webapp/src/views/NetworkAdminView.vue index 8c3da697..058dfe32 100644 --- a/webapp/src/views/NetworkAdminView.vue +++ b/webapp/src/views/NetworkAdminView.vue @@ -89,20 +89,22 @@ type="checkbox" /> - +
+ - + +