diff --git a/include/HttpPowerMeter.h b/include/HttpPowerMeter.h index 16ee85851..8530f4cda 100644 --- a/include/HttpPowerMeter.h +++ b/include/HttpPowerMeter.h @@ -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); diff --git a/src/HttpPowerMeter.cpp b/src/HttpPowerMeter.cpp index f58ba0cfb..c232ce8a1 100644 --- a/src/HttpPowerMeter.cpp +++ b/src/HttpPowerMeter.cpp @@ -25,10 +25,6 @@ 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; @@ -36,7 +32,7 @@ bool HttpPowerMeterClass::updateValues() } 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); @@ -47,7 +43,7 @@ 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 @@ -55,34 +51,39 @@ bool HttpPowerMeterClass::queryPhase(int phase, const String& urlProtocol, const //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; - if (urlProtocol == "https") { + bool https = protocol == "https"; + if (https) { auto secureWifiClient = std::make_unique(); secureWifiClient->setInsecure(); wifiClient = std::move(secureWifiClient); @@ -90,18 +91,15 @@ bool HttpPowerMeterClass::queryPhase(int phase, const String& urlProtocol, const wifiClient = std::make_unique(); } - 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; } @@ -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; } @@ -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; } diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index d5958668f..8e8a4e652 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -258,14 +258,8 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) char response[256]; - String urlProtocol; - String urlHostname; - String urlUri; - - HttpPowerMeter.extractUrlComponents(root[F("url")].as().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().c_str(), root[F("auth_type")].as(), root[F("username")].as().c_str(), root[F("password")].as().c_str(), root[F("header_key")].as().c_str(), root[F("header_value")].as().c_str(), root[F("timeout")].as(), root[F("json_path")].as().c_str())) {