this allows us to add a valid Authorization header to each but the first GET request, saving us from performing two GET requests every time we want to perform the GET request. we still need a new client nonce and we need to increase the nonce counter, so we also need to calculate a whole new response, as we cannot just reuse the previous Authorization header (that would be a replay attack).
308 lines
11 KiB
C++
308 lines
11 KiB
C++
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
#include "HttpGetter.h"
|
|
#include <WiFiClientSecure.h>
|
|
#include "mbedtls/sha256.h"
|
|
#include "mbedtls/md5.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<HttpGetterClient>();
|
|
|
|
// use HTTP1.0 to avoid problems with chunked transfer encoding when the
|
|
// stream is later used to read the server's response.
|
|
upTmpHttpClient->useHTTP10(true);
|
|
|
|
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: {
|
|
// send "Connection: keep-alive" (despite using HTTP/1.0, where
|
|
// "Connection: close" is the default) so there is a chance to
|
|
// reuse the TCP connection when performing the second GET request.
|
|
upTmpHttpClient->setReuse(true);
|
|
|
|
const char *headers[2] = {"WWW-Authenticate", "Connection"};
|
|
upTmpHttpClient->collectHeaders(headers, 2);
|
|
|
|
// try with new auth response based on previous WWW-Authenticate
|
|
// header, which allows us to retrieve the resource without a
|
|
// second GET request. if the server decides that we reused the
|
|
// previous challenge too often, it will respond with HTTP401 and
|
|
// a new challenge, which we handle as if we had no challenge yet.
|
|
auto authorization = getAuthDigest();
|
|
if (authorization.first) {
|
|
upTmpHttpClient->addHeader("Authorization", authorization.second);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
int httpCode = upTmpHttpClient->GET();
|
|
|
|
if (httpCode == HTTP_CODE_UNAUTHORIZED && _config.AuthType == Auth_t::Digest) {
|
|
_wwwAuthenticate = "";
|
|
|
|
if (!upTmpHttpClient->hasHeader("WWW-Authenticate")) {
|
|
logError("Cannot perform digest authentication as server did "
|
|
"not send a WWW-Authenticate header");
|
|
return { false };
|
|
}
|
|
|
|
_wwwAuthenticate = upTmpHttpClient->header("WWW-Authenticate");
|
|
|
|
// using a new WWW-Authenticate challenge means
|
|
// we never used the server's nonce in a response
|
|
_nonceCounter = 0;
|
|
|
|
auto authorization = getAuthDigest();
|
|
if (!authorization.first) {
|
|
logError("Digest Error: %s", authorization.second.c_str());
|
|
return { false };
|
|
}
|
|
upTmpHttpClient->addHeader("Authorization", authorization.second);
|
|
|
|
// use a new TCP connection if the server sent "Connection: close".
|
|
bool restart = true;
|
|
if (upTmpHttpClient->hasHeader("Connection")) {
|
|
String connection = upTmpHttpClient->header("Connection");
|
|
connection.toLowerCase();
|
|
restart = connection.indexOf("keep-alive") == -1;
|
|
}
|
|
if (restart) { upTmpHttpClient->restartTCP(); }
|
|
|
|
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 };
|
|
}
|
|
|
|
template<size_t binLen>
|
|
static String bin2hex(uint8_t* hash) {
|
|
size_t constexpr kOutLen = binLen * 2 + 1;
|
|
char res[kOutLen];
|
|
for (int i = 0; i < binLen; i++) {
|
|
snprintf(res + (i*2), sizeof(res) - (i*2), "%02x", hash[i]);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
static String md5(const String& data) {
|
|
uint8_t hash[16];
|
|
|
|
mbedtls_md5_context ctx;
|
|
mbedtls_md5_init(&ctx);
|
|
mbedtls_md5_starts_ret(&ctx);
|
|
mbedtls_md5_update_ret(&ctx, reinterpret_cast<const unsigned char*>(data.c_str()), data.length());
|
|
mbedtls_md5_finish_ret(&ctx, hash);
|
|
mbedtls_md5_free(&ctx);
|
|
|
|
return bin2hex<sizeof(hash)>(hash);
|
|
}
|
|
|
|
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);
|
|
|
|
return bin2hex<sizeof(hash)>(hash);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
static std::pair<bool, String> getAlgo(String const& authReq) {
|
|
// the algorithm is NOT enclosed in double quotes, so we can't use extractParam
|
|
auto paramBegin = authReq.indexOf("algorithm=");
|
|
if (paramBegin == -1) { return { true, "MD5" }; } // default as per RFC2617
|
|
auto valueBegin = paramBegin + 10;
|
|
|
|
String algo = authReq.substring(valueBegin, valueBegin + 3);
|
|
if (algo == "MD5") { return { true, algo }; }
|
|
|
|
algo = authReq.substring(valueBegin, valueBegin + 7);
|
|
if (algo == "SHA-256") { return { true, algo }; }
|
|
|
|
return { false, "unsupported digest algorithm" };
|
|
}
|
|
|
|
std::pair<bool, String> HttpGetter::getAuthDigest() {
|
|
if (_wwwAuthenticate.isEmpty()) { return { false, "no digest challenge yet" }; }
|
|
|
|
// extracting required parameters for RFC 2617 Digest
|
|
String realm = extractParam(_wwwAuthenticate, "realm=\"", '"');
|
|
String nonce = extractParam(_wwwAuthenticate, "nonce=\"", '"');
|
|
String cNonce = getcNonce(8); // client nonce
|
|
|
|
char nc[9];
|
|
snprintf(nc, sizeof(nc), "%08x", ++_nonceCounter);
|
|
|
|
auto algo = getAlgo(_wwwAuthenticate);
|
|
if (!algo.first) { return { false, algo.second }; }
|
|
|
|
auto hash = (algo.second == "SHA-256") ? &sha256 : &md5;
|
|
String ha1 = hash(String(_config.Username) + ":" + realm + ":" + _config.Password);
|
|
String ha2 = hash("GET:" + _uri);
|
|
String response = hash(ha1 + ":" + nonce + ":" + String(nc) +
|
|
":" + cNonce + ":" + "auth" + ":" + ha2);
|
|
|
|
return { true, String("Digest username=\"") + _config.Username +
|
|
"\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" +
|
|
_uri + "\", cnonce=\"" + cNonce + "\", nc=" + nc +
|
|
", qop=auth, response=\"" + response + "\", algorithm=" + algo.second };
|
|
}
|
|
|
|
void HttpGetter::addHeader(char const* key, char const* value)
|
|
{
|
|
_additionalHeaders.push_back({ key, value });
|
|
}
|