Skip to content

Commit

Permalink
HttpPowerMeterClass: change order of resolving hostname
Browse files Browse the repository at this point in the history
OpenDTU console gets spammed with "WifiGeneric::hostByName() error when first trying to resolve the hostname via DNS. So reverse order: first try mDNS, if that fails try DNS. Also ensure that https bool is passed correctly to HTTPClient::begin(). Lastly, concatenate strings for building Digest authorization using "+" and not via snprintf.
  • Loading branch information
Fribur committed Jan 5, 2024
1 parent 9ed5a78 commit 85d0f2a
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 60 deletions.
9 changes: 5 additions & 4 deletions include/HttpPowerMeter.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@ class HttpPowerMeterClass {
bool updateValues();
float getPower(int8_t phase);
char httpPowerMeterError[256];
bool queryPhase(int phase, const String& urlProtocol, const String& urlHostname, const String& uri, Auth authType, const char* username, const char* password,
bool queryPhase(int phase, const String& url, Auth authType, const char* username, const char* password,
const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath);
void extractUrlComponents(const String& url, String& protocol, String& hostname, String& uri);


private:
float power[POWERMETER_MAX_PHASES];
HTTPClient httpClient;
String httpResponse;
bool httpRequest(int phase, WiFiClient &wifiClient, const String& urlProtocol, const String& urlHostname, const String& urlUri, Auth authType, const char* username,
bool httpRequest(int phase, WiFiClient &wifiClient, const String& urlHostname, const String& uri, bool https, Auth authType, const char* username,
const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath);
void extractUrlComponents(const String& url, String& protocol, String& hostname, String& uri);
String extractParam(String& authReq, const String& param, const char delimit);
void getcNonce(char* cNounce);
String getcNonce(const int len);
String getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter);
bool tryGetFloatValueForPhase(int phase, int httpCode, const char* jsonPath);
void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue);
Expand Down
103 changes: 54 additions & 49 deletions src/HttpPowerMeter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,14 @@ bool HttpPowerMeterClass::updateValues()

for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
POWERMETER_HTTP_PHASE_CONFIG_T phaseConfig = config.PowerMeter.Http_Phase[i];
String urlProtocol;
String urlHostname;
String urlUri;
extractUrlComponents(phaseConfig.Url, urlProtocol, urlHostname, urlUri);

if (!phaseConfig.Enabled) {
power[i] = 0.0;
continue;
}

if (i == 0 || config.PowerMeter.HttpIndividualRequests) {
if (!queryPhase(i, urlProtocol, urlHostname, urlUri, phaseConfig.AuthType, phaseConfig.Username, phaseConfig.Password, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout,
if (!queryPhase(i, phaseConfig.Url, phaseConfig.AuthType, phaseConfig.Username, phaseConfig.Password, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout,
phaseConfig.JsonPath)) {
MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed.\r\n", i + 1);
MessageOutput.printf("%s\r\n", httpPowerMeterError);
Expand All @@ -47,61 +43,63 @@ bool HttpPowerMeterClass::updateValues()
return true;
}

bool HttpPowerMeterClass::queryPhase(int phase, const String& urlProtocol, const String& urlHostname, const String& uri, Auth authType, const char* username, const char* password,
bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType, const char* username, const char* password,
const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath)
{
//hostByName in WiFiGeneric fails to resolve local names. issue described in
//https://github.com/espressif/arduino-esp32/issues/3822
//and in depth analyzed in https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300
//in conclusion: we cannot rely on httpClient.begin(*wifiClient, url) to resolve IP adresses.
//have to do it manually here. Feels Hacky...
String protocol;
String host;
String uri;
extractUrlComponents(url, protocol, host, uri);

IPAddress ipaddr((uint32_t)0);
//first check if the urlHostname is already an IP adress
if (!ipaddr.fromString(urlHostname))
if (!ipaddr.fromString(host))
{
//urlHostname is not an IP address so try to resolve the IP adress
//first, try DNS
if(!WiFiGenericClass::hostByName(urlHostname.c_str(), ipaddr))
//first try locally via mDNS, then via DNS (WiFiGeneric::hostByName() will spam the console if done the otherway around)
const bool mdnsEnabled = Configuration.get().Mdns.Enabled;
if (!mdnsEnabled) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving url %s via DNS, try to enable mDNS in Network Settings"), url.c_str());
}
else
{
//DNS failed, so now try mDNS
const bool mdnsEnabled = Configuration.get().Mdns.Enabled;
if (!mdnsEnabled) {
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);
ipaddr = MDNS.queryHost(host);
if (ipaddr == INADDR_NONE){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving url %s via DNS and mDNS"), urlHostname.c_str());
return false;
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving url %s via mDNS"), url.c_str());
if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving url %s via DNS"), url.c_str());
}
}
}
}
}

// secureWifiClient MUST be created before HTTPClient
// see discussion: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/381
std::unique_ptr<WiFiClient> wifiClient;

if (urlProtocol == "https") {
bool https = protocol == "https";
if (https) {
auto secureWifiClient = std::make_unique<WiFiClientSecure>();
secureWifiClient->setInsecure();
wifiClient = std::move(secureWifiClient);
} else {
wifiClient = std::make_unique<WiFiClient>();
}

return httpRequest(phase, *wifiClient, urlProtocol, ipaddr.toString(), uri, authType, username, password, httpHeader, httpValue, timeout, jsonPath);
return httpRequest(phase, *wifiClient, ipaddr.toString(), uri, https, 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,
bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& urlHostname, const String& uri, bool https, Auth authType, const char* username,
const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath)
{
int port = 80;
if (urlProtocol == "https") {
port = 443;
}
if(!httpClient.begin(wifiClient, urlHostname, port, uri)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), urlProtocol.c_str(), urlHostname.c_str());
int port = (https ? 443 : 80);
if(!httpClient.begin(wifiClient, urlHostname, port, uri, https)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), urlHostname.c_str());
return false;
}

Expand All @@ -125,8 +123,8 @@ bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const S
String authReq = httpClient.header("WWW-Authenticate");
String authorization = getDigestAuth(authReq, String(username), String(password), "GET", String(uri), 1);
httpClient.end();
if(!httpClient.begin(wifiClient, urlHostname, port, uri)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s using digest auth"), urlProtocol.c_str(), urlHostname.c_str());
if(!httpClient.begin(wifiClient, urlHostname, port, uri, https)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s using digest auth"), (https ? "https" : "http"), urlHostname.c_str());
return false;
}

Expand All @@ -146,44 +144,51 @@ String HttpPowerMeterClass::extractParam(String& authReq, const String& param, c
return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length()));
}

void HttpPowerMeterClass::getcNonce(char* cNounce) {
String HttpPowerMeterClass::getcNonce(const int len) {
static const char alphanum[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
auto len=sizeof(cNounce);
String s = "";

for (int i = 0; i < len; ++i) { cNounce[i] = alphanum[rand() % (sizeof(alphanum) - 1)]; }
for (int i = 0; i < len; ++i) { s += alphanum[rand() % (sizeof(alphanum) - 1)]; }

return s;
}

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 2617 Digest
String realm = extractParam(authReq, "realm=\"", '"');
String nonce = extractParam(authReq, "nonce=\"", '"');
char cNonce[8];
getcNonce(cNonce);
String cNonce = getcNonce(8);

char nc[9];
snprintf(nc, sizeof(nc), "%08x", counter);

// sha256 of the user:realm:user
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 the user:realm:password
String ha1 = sha256(username + ":" + realm + ":" + password);

//sha256 of method:uri
char h2Prep[1024];//can uri be longer?
snprintf(h2Prep, sizeof(h2Prep), "%s:%s", method.c_str(),uri.c_str());
String ha2 = sha256(h2Prep);
String ha2 = sha256(method + ":" + uri);

//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);
//sha256 of h1:nonce:nc:cNonce:auth:h2
String response = sha256(ha1 + ":" + nonce + ":" + String(nc) + ":" + cNonce + ":" + "auth" + ":" + ha2);

//Final authorization String;
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());
String authorization = "Digest username=\"";
authorization += username;
authorization += "\", realm=\"";
authorization += realm;
authorization += "\", nonce=\"";
authorization += nonce;
authorization += "\", uri=\"";
authorization += uri;
authorization += "\", cnonce=\"";
authorization += cNonce;
authorization += "\", nc=";
authorization += String(nc);
authorization += ", qop=auth, response=\"";
authorization += response;
authorization += "\", algorithm=SHA-256";

return authorization;
}
Expand Down
8 changes: 1 addition & 7 deletions src/WebApi_powermeter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -258,14 +258,8 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)

char response[256];

String urlProtocol;
String urlHostname;
String urlUri;

HttpPowerMeter.extractUrlComponents(root[F("url")].as<String>().c_str(), urlProtocol, urlHostname, urlUri);

int phase = 0;//"absuing" index 0 of the float power[3] in HttpPowerMeter to store the result
if (HttpPowerMeter.queryPhase(phase, urlProtocol, urlHostname, urlUri,
if (HttpPowerMeter.queryPhase(phase, root[F("url")].as<String>().c_str(),
root[F("auth_type")].as<Auth>(), root[F("username")].as<String>().c_str(), root[F("password")].as<String>().c_str(),
root[F("header_key")].as<String>().c_str(), root[F("header_value")].as<String>().c_str(), root[F("timeout")].as<uint16_t>(),
root[F("json_path")].as<String>().c_str())) {
Expand Down

0 comments on commit 85d0f2a

Please sign in to comment.