OpenDTU-old/src/HttpGetter.cpp
Bernhard Kirchen 20ecf2a66b powermeter refactor: add and use HttpGetter class
this new class uses the newly introduced HttpRequestConfig and performs
HTTP requests using this config. it will be reused for other power
meters (SML over HTTP(S)) and may be reused by other features in the
future (battery provider, solar power provider, etc.).
2024-06-27 22:18:41 +02:00

232 lines
7.9 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
#include "HttpGetter.h"
#include <WiFiClientSecure.h>
#include "mbedtls/sha256.h"
#include <base64.h>
#include <ESPmDNS.h>
template<typename... Args>
void HttpGetter::logError(char const* format, Args... args) {
snprintf(_errBuffer, sizeof(_errBuffer), format, args...);
}
bool HttpGetter::init()
{
String url(_config.Url);
int index = url.indexOf(':');
if (index < 0) {
logError("failed to parse URL protocol: no colon in URL");
return false;
}
String protocol = url.substring(0, index);
if (protocol != "http" && protocol != "https") {
logError("failed to parse URL protocol: '%s' is neither 'http' nor 'https'", protocol.c_str());
return false;
}
_useHttps = (protocol == "https");
// initialize port to default values for http or https.
// port will be overwritten below in case port is explicitly defined
_port = _useHttps ? 443 : 80;
String slashes = url.substring(index + 1, index + 3);
if (slashes != "//") {
logError("expected two forward slashes after first colon in URL");
return false;
}
_uri = url.substring(index + 3); // without protocol identifier
index = _uri.indexOf('/');
if (index == -1) {
index = _uri.length();
_uri += '/';
}
_host = _uri.substring(0, index);
_uri.remove(0, index); // remove host part
index = _host.indexOf('@');
if (index >= 0) {
// basic authentication is only supported through setting username
// and password using the respective inputs, not embedded into the URL.
// to avoid regressions, we remove username and password from the host
// part of the URL.
_host.remove(0, index + 1); // remove auth part including @
}
// get port
index = _host.indexOf(':');
if (index >= 0) {
_host = _host.substring(0, index); // up until colon
_port = _host.substring(index + 1).toInt(); // after colon
}
if (_useHttps) {
auto secureWifiClient = std::make_shared<WiFiClientSecure>();
secureWifiClient->setInsecure();
_spWiFiClient = std::move(secureWifiClient);
} else {
_spWiFiClient = std::make_shared<WiFiClient>();
}
return true;
}
HttpRequestResult HttpGetter::performGetRequest()
{
// hostByName in WiFiGeneric fails to resolve local names. issue described at
// https://github.com/espressif/arduino-esp32/issues/3822 and in analyzed in
// depth at https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300
// in conclusion: we cannot rely on _upHttpClient->begin(*wifiClient, url) to resolve
// IP adresses. have to do it manually.
IPAddress ipaddr((uint32_t)0);
if (!ipaddr.fromString(_host)) {
// host is not an IP address, so try to resolve the name to an address.
// first try locally via mDNS, then via DNS. WiFiGeneric::hostByName()
// will spam the console if done the other way around.
ipaddr = INADDR_NONE;
if (Configuration.get().Mdns.Enabled) {
ipaddr = MDNS.queryHost(_host); // INADDR_NONE if failed
}
if (ipaddr == INADDR_NONE && !WiFiGenericClass::hostByName(_host.c_str(), ipaddr)) {
logError("failed to resolve host '%s' via DNS", _host.c_str());
return { false };
}
}
auto upTmpHttpClient = std::make_unique<HTTPClient>();
if (!upTmpHttpClient->begin(*_spWiFiClient, ipaddr.toString(), _port, _uri, _useHttps)) {
logError("HTTP client begin() failed for %s://%s",
(_useHttps ? "https" : "http"), _host.c_str());
return { false };
}
upTmpHttpClient->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
upTmpHttpClient->setUserAgent("OpenDTU-OnBattery");
upTmpHttpClient->setConnectTimeout(_config.Timeout);
upTmpHttpClient->setTimeout(_config.Timeout);
for (auto const& h : _additionalHeaders) {
upTmpHttpClient->addHeader(h.first.c_str(), h.second.c_str());
}
if (strlen(_config.HeaderKey) > 0) {
upTmpHttpClient->addHeader(_config.HeaderKey, _config.HeaderValue);
}
using Auth_t = HttpRequestConfig::Auth;
switch (_config.AuthType) {
case Auth_t::None:
break;
case Auth_t::Basic: {
String credentials = String(_config.Username) + ":" + _config.Password;
String authorization = "Basic " + base64::encode(credentials);
upTmpHttpClient->addHeader("Authorization", authorization);
break;
}
case Auth_t::Digest: {
const char *headers[1] = {"WWW-Authenticate"};
upTmpHttpClient->collectHeaders(headers, 1);
break;
}
}
int httpCode = upTmpHttpClient->GET();
if (httpCode == HTTP_CODE_UNAUTHORIZED && _config.AuthType == Auth_t::Digest) {
if (!upTmpHttpClient->hasHeader("WWW-Authenticate")) {
logError("Cannot perform digest authentication as server did "
"not send a WWW-Authenticate header");
return { false };
}
String authReq = upTmpHttpClient->header("WWW-Authenticate");
String authorization = getAuthDigest(authReq, 1);
upTmpHttpClient->addHeader("Authorization", authorization);
httpCode = upTmpHttpClient->GET();
}
if (httpCode <= 0) {
logError("HTTP Error: %s", upTmpHttpClient->errorToString(httpCode).c_str());
return { false };
}
if (httpCode != HTTP_CODE_OK) {
logError("Bad HTTP code: %d", httpCode);
return { false };
}
return { true, std::move(upTmpHttpClient), _spWiFiClient };
}
static String sha256(const String& data) {
uint8_t hash[32];
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0); // select SHA256
mbedtls_sha256_update(&ctx, reinterpret_cast<const unsigned char*>(data.c_str()), data.length());
mbedtls_sha256_finish(&ctx, hash);
mbedtls_sha256_free(&ctx);
char res[sizeof(hash) * 2 + 1];
for (int i = 0; i < sizeof(hash); i++) {
snprintf(res + (i*2), sizeof(res) - (i*2), "%02x", hash[i]);
}
return res;
}
static String extractParam(String const& authReq, String const& param, char delimiter) {
auto begin = authReq.indexOf(param);
if (begin == -1) { return ""; }
auto end = authReq.indexOf(delimiter, begin + param.length());
return authReq.substring(begin + param.length(), end);
}
static String getcNonce(int len) {
static const char alphanum[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
String s = "";
for (int i = 0; i < len; ++i) { s += alphanum[rand() % (sizeof(alphanum) - 1)]; }
return s;
}
String HttpGetter::getAuthDigest(String const& authReq, unsigned int counter) {
// extracting required parameters for RFC 2617 Digest
String realm = extractParam(authReq, "realm=\"", '"');
String nonce = extractParam(authReq, "nonce=\"", '"');
String cNonce = getcNonce(8);
char nc[9];
snprintf(nc, sizeof(nc), "%08x", counter);
// sha256 of the user:realm:password
String ha1 = sha256(String(_config.Username) + ":" + realm + ":" + _config.Password);
// sha256 of method:uri
String ha2 = sha256("GET:" + _uri);
// sha256 of h1:nonce:nc:cNonce:auth:h2
String response = sha256(ha1 + ":" + nonce + ":" + String(nc) +
":" + cNonce + ":" + "auth" + ":" + ha2);
// Final authorization String
return String("Digest username=\"") + _config.Username +
"\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" +
_uri + "\", cnonce=\"" + cNonce + "\", nc=" + nc +
", qop=auth, response=\"" + response + "\", algorithm=SHA-256";
}
void HttpGetter::addHeader(char const* key, char const* value)
{
_additionalHeaders.push_back({ key, value });
}