Reverted changes to PowerLimiter, adapted DNS and mDNS handling in HttpPowerMeter

For non IP address URLs, HttpPowerMeter now first tries DNS for resolution as done in WifiClient::connect, and only if that fails tries mDNS. For the latter to work mDNS needs to be enabled in settings. Log in console if mDNS is disabled. String building for Digest authorization still tries to avoid "+" for reasons outlined in https://cpp4arduino.com/2020/02/07/how-to-format-strings-without-the-string-class.html This should also be saver than just concatenating user input strings in preventing format string attacks. https://owasp.org/www-community/attacks/Format_string_attack
This commit is contained in:
Fribur 2024-01-05 10:13:16 -05:00
parent bc38ce344f
commit 9ed5a78818
3 changed files with 80 additions and 81 deletions

View File

@ -3,7 +3,7 @@
#include "HttpPowerMeter.h"
#include "MessageOutput.h"
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>//saves 20kB to not use FirebaseJson as ArduinoJson is used already elsewhere (e.g. in WebApi_powermeter)
#include <ArduinoJson.h>
#include <Crypto.h>
#include <SHA256.h>
#include <base64.h>
@ -59,19 +59,24 @@ bool HttpPowerMeterClass::queryPhase(int phase, const String& urlProtocol, const
//first check if the urlHostname is already an IP adress
if (!ipaddr.fromString(urlHostname))
{
//no it is not, so try to resolve the IP adress
//urlHostname is not an IP address so try to resolve the IP adress
//first, try DNS
if(!WiFiGenericClass::hostByName(urlHostname.c_str(), ipaddr))
{
//DNS failed, so now try mDNS
const bool mdnsEnabled = Configuration.get().Mdns.Enabled;
if (!mdnsEnabled) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Enable mDNS in Network Settings"));
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving url %s via DNS, try to enable mDNS in Network Settings"), urlHostname);
return false;
}
ipaddr = MDNS.queryHost(urlHostname);
if (ipaddr == INADDR_NONE){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving url %s"), urlHostname.c_str());
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving url %s via DNS and mDNS"), urlHostname.c_str());
return false;
}
}
}
// secureWifiClient MUST be created before HTTPClient
// see discussion: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/381
@ -84,8 +89,10 @@ bool HttpPowerMeterClass::queryPhase(int phase, const String& urlProtocol, const
} else {
wifiClient = std::make_unique<WiFiClient>();
}
return httpRequest(phase, *wifiClient, urlProtocol, ipaddr.toString(), uri, authType, username, password, httpHeader, httpValue, timeout, jsonPath);
}
bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& urlProtocol, const String& urlHostname, const String& uri, Auth authType, const char* username,
const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath)
{
@ -138,6 +145,7 @@ String HttpPowerMeterClass::extractParam(String& authReq, const String& param, c
if (_begin == -1) { return ""; }
return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length()));
}
void HttpPowerMeterClass::getcNonce(char* cNounce) {
static const char alphanum[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
@ -147,8 +155,9 @@ void HttpPowerMeterClass::getcNonce(char* cNounce) {
for (int i = 0; i < len; ++i) { cNounce[i] = alphanum[rand() % (sizeof(alphanum) - 1)]; }
}
String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter) {
// extracting required parameters for RFC 2069 simpler Digest
// extracting required parameters for RFC 2617 Digest
String realm = extractParam(authReq, "realm=\"", '"');
String nonce = extractParam(authReq, "nonce=\"", '"');
char cNonce[8];
@ -157,28 +166,28 @@ String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& usernam
char nc[9];
snprintf(nc, sizeof(nc), "%08x", counter);
// parameters for the Digest
// sha256 of the user:realm:user
char h1Prep[sizeof(username)+sizeof(realm)+sizeof(password)+2];
char h1Prep[1024];//can username+password be longer than 255 chars each?
snprintf(h1Prep, sizeof(h1Prep), "%s:%s:%s", username.c_str(),realm.c_str(), password.c_str());
String ha1 = sha256(h1Prep);
//sha256 of method:uri
char h2Prep[sizeof(method) + sizeof(uri) + 1];
char h2Prep[1024];//can uri be longer?
snprintf(h2Prep, sizeof(h2Prep), "%s:%s", method.c_str(),uri.c_str());
String ha2 = sha256(h2Prep);
//md5 of h1:nonce:nc:cNonce:auth:h2
char responsePrep[sizeof(ha1)+sizeof(nc)+sizeof(cNonce)+4+sizeof(ha2) + 5];
//sha256 of h1:nonce:nc:cNonce:auth:h2
char responsePrep[2048];//can nounce and cNounce be longer?
snprintf(responsePrep, sizeof(responsePrep), "%s:%s:%s:%s:auth:%s", ha1.c_str(),nonce.c_str(), nc, cNonce,ha2.c_str());
String response = sha256(responsePrep);
//Final authorization String;
char authorization[17 + sizeof(username) + 10 + sizeof(realm) + 10 + sizeof(nonce) + 8 + sizeof(uri) + 34 + sizeof(nc) + 10 + sizeof(cNonce) + 13 + sizeof(response)];
char authorization[2048];//can username+password be longer than 255 chars each? can uri be longer?
snprintf(authorization, sizeof(authorization), "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", algorithm=SHA-256, qop=auth, nc=%s, cnonce=\"%s\", response=\"%s\"", username.c_str(), realm.c_str(), nonce.c_str(), uri.c_str(), nc, cNonce, response.c_str());
return authorization;
}
bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, int httpCode, const char* jsonPath)
{
bool success = false;
@ -201,6 +210,7 @@ bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, int httpCode, cons
}
return success;
}
void HttpPowerMeterClass::extractUrlComponents(const String& url, String& protocol, String& hostname, String& uri) {
// Find protocol delimiter
int protocolEndIndex = url.indexOf(":");

View File

@ -187,8 +187,7 @@ void PowerLimiterClass::loop()
// a calculated power limit will always be limited to the reported
// device's max power. that upper limit is only known after the first
// DevInfoSimpleCommand succeeded.
auto maxPower = _inverter->DevInfo()->getMaxPower();
if (maxPower <= 0) {
if (_inverter->DevInfo()->getMaxPower() <= 0) {
return announceStatus(Status::InverterDevInfoPending);
}
@ -204,8 +203,6 @@ void PowerLimiterClass::loop()
return;
}
//instead of shutting down on PowerMeterTimeout, how about setting alternativly to a safe "low production" mode?
//Could be usefull when PowerMeter fails but we know for sure house consumption will never fall below a certain limit (say 200W)
if (millis() - PowerMeter.getLastPowerMeterUpdate() > (30 * 1000)) {
shutdown(Status::PowerMeterTimeout);
return;
@ -226,7 +223,6 @@ void PowerLimiterClass::loop()
return announceStatus(Status::InverterStatsPending);
}
if (PowerMeter.getLastPowerMeterUpdate() <= settlingEnd) {
return announceStatus(Status::PowerMeterPending);
}
@ -549,8 +545,8 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> inver
dcTotalChnls, dcProdChnls);
effPowerLimit = round(effPowerLimit * static_cast<float>(dcTotalChnls) / dcProdChnls);
}
auto maxPower = inverter->DevInfo()->getMaxPower();
effPowerLimit = std::min<int32_t>(effPowerLimit, maxPower);
effPowerLimit = std::min<int32_t>(effPowerLimit, inverter->DevInfo()->getMaxPower());
// Check if the new value is within the limits of the hysteresis
auto diff = std::abs(effPowerLimit - _lastRequestedPowerLimit);
@ -560,23 +556,16 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> inver
// staleness in case a power limit update was not received by the inverter.
auto ageMillis = millis() - _lastPowerLimitMillis;
//instead pushing limit to inverter every 60 seconds no matter what,
//why not query instead the currenty configured limit...and do nothing if not needed
int currentLimit = round(inverter->SystemConfigPara()->getLimitPercent() * maxPower / 100);
auto currentDiff = std::abs(effPowerLimit - currentLimit );
if (diff < hysteresis && currentDiff < hysteresis ){
//if (diff < hysteresis && ageMillis < 60 * 1000) {
//MessageOutput.printf("Keep limit: %d W, current limit %d W\r\n", effPowerLimit, currentLimit);
if (diff < hysteresis && ageMillis < 60 * 1000) {
if (_verboseLogging) {
MessageOutput.printf("[DPL::setNewPowerLimit] Keep current limit. (new calculated: %d W, last limit: %d W, diff: %d W, hysteresis: %d W, age: %ld ms)\r\n",
effPowerLimit, _lastRequestedPowerLimit, diff, hysteresis, ageMillis);
MessageOutput.printf("[DPL::setNewPowerLimit] requested: %d W, last limit: %d W, diff: %d W, hysteresis: %d W, age: %ld ms\r\n",
newPowerLimit, _lastRequestedPowerLimit, diff, hysteresis, ageMillis);
}
return false;
}
//if we end up here, it we will set new limit
if (_verboseLogging) {
MessageOutput.printf("[DPL::setNewPowerLimit] requested: %d W, sending limit: %d W\r\n",
MessageOutput.printf("[DPL::setNewPowerLimit] requested: %d W, (re-)sending limit: %d W\r\n",
newPowerLimit, effPowerLimit);
}

View File

@ -203,11 +203,11 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
response->setLength();
request->send(response);
// why reboot..WebApi_powerlimiter is also not rebooting
// yield();
// delay(1000);
// yield();
// ESP.restart();
// reboot requiered as per https://github.com/helgeerbe/OpenDTU-OnBattery/issues/565#issuecomment-1872552559
yield();
delay(1000);
yield();
ESP.restart();
}
void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)