2026-01-07 03:58:37 -05:00
|
|
|
#include "HttpDownloader.h"
|
|
|
|
|
|
|
|
|
|
#include <HTTPClient.h>
|
2026-02-13 12:16:39 +01:00
|
|
|
#include <Logging.h>
|
2026-02-22 11:31:33 +02:00
|
|
|
#include <NetworkClient.h>
|
|
|
|
|
#include <NetworkClientSecure.h>
|
2026-01-21 17:43:51 +03:00
|
|
|
#include <StreamString.h>
|
2026-01-27 06:02:38 -05:00
|
|
|
#include <base64.h>
|
2026-01-07 03:58:37 -05:00
|
|
|
|
2026-01-27 06:02:38 -05:00
|
|
|
#include <cstring>
|
2026-01-07 03:58:37 -05:00
|
|
|
#include <memory>
|
2026-02-28 13:39:09 +03:00
|
|
|
#include <utility>
|
2026-01-07 03:58:37 -05:00
|
|
|
|
2026-01-27 06:02:38 -05:00
|
|
|
#include "CrossPointSettings.h"
|
2026-01-14 06:54:14 -05:00
|
|
|
#include "util/UrlUtils.h"
|
|
|
|
|
|
2026-02-28 13:39:09 +03:00
|
|
|
namespace {
|
|
|
|
|
class FileWriteStream final : public Stream {
|
|
|
|
|
public:
|
|
|
|
|
FileWriteStream(FsFile& file, size_t total, HttpDownloader::ProgressCallback progress)
|
|
|
|
|
: file_(file), total_(total), progress_(std::move(progress)) {}
|
|
|
|
|
|
|
|
|
|
size_t write(uint8_t byte) override { return write(&byte, 1); }
|
|
|
|
|
|
|
|
|
|
size_t write(const uint8_t* buffer, size_t size) override {
|
|
|
|
|
// Write-through stream for HTTPClient::writeToStream with progress tracking.
|
|
|
|
|
const size_t written = file_.write(buffer, size);
|
|
|
|
|
if (written != size) {
|
|
|
|
|
writeOk_ = false;
|
|
|
|
|
}
|
|
|
|
|
downloaded_ += written;
|
|
|
|
|
if (progress_ && total_ > 0) {
|
|
|
|
|
progress_(downloaded_, total_);
|
|
|
|
|
}
|
|
|
|
|
return written;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int available() override { return 0; }
|
|
|
|
|
int read() override { return -1; }
|
|
|
|
|
int peek() override { return -1; }
|
|
|
|
|
void flush() override { file_.flush(); }
|
|
|
|
|
|
|
|
|
|
size_t downloaded() const { return downloaded_; }
|
|
|
|
|
bool ok() const { return writeOk_; }
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
FsFile& file_;
|
|
|
|
|
size_t total_;
|
|
|
|
|
size_t downloaded_ = 0;
|
|
|
|
|
bool writeOk_ = true;
|
|
|
|
|
HttpDownloader::ProgressCallback progress_;
|
|
|
|
|
};
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2026-01-21 17:43:51 +03:00
|
|
|
bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) {
|
2026-02-22 11:31:33 +02:00
|
|
|
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
|
|
|
|
|
std::unique_ptr<NetworkClient> client;
|
2026-01-14 06:54:14 -05:00
|
|
|
if (UrlUtils::isHttpsUrl(url)) {
|
2026-02-22 11:31:33 +02:00
|
|
|
auto* secureClient = new NetworkClientSecure();
|
2026-01-14 06:54:14 -05:00
|
|
|
secureClient->setInsecure();
|
|
|
|
|
client.reset(secureClient);
|
|
|
|
|
} else {
|
2026-02-22 11:31:33 +02:00
|
|
|
client.reset(new NetworkClient());
|
2026-01-14 06:54:14 -05:00
|
|
|
}
|
2026-01-07 03:58:37 -05:00
|
|
|
HTTPClient http;
|
|
|
|
|
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("HTTP", "Fetching: %s", url.c_str());
|
2026-01-07 03:58:37 -05:00
|
|
|
|
|
|
|
|
http.begin(*client, url.c_str());
|
|
|
|
|
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
|
|
|
|
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
|
|
|
|
|
2026-01-27 06:02:38 -05:00
|
|
|
// Add Basic HTTP auth if credentials are configured
|
|
|
|
|
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
|
|
|
|
|
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
|
|
|
|
|
String encoded = base64::encode(credentials.c_str());
|
|
|
|
|
http.addHeader("Authorization", "Basic " + encoded);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 03:58:37 -05:00
|
|
|
const int httpCode = http.GET();
|
|
|
|
|
if (httpCode != HTTP_CODE_OK) {
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_ERR("HTTP", "Fetch failed: %d", httpCode);
|
2026-01-07 03:58:37 -05:00
|
|
|
http.end();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 17:43:51 +03:00
|
|
|
http.writeToStream(&outContent);
|
|
|
|
|
|
2026-01-07 03:58:37 -05:00
|
|
|
http.end();
|
|
|
|
|
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("HTTP", "Fetch success");
|
2026-01-21 17:43:51 +03:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) {
|
|
|
|
|
StreamString stream;
|
|
|
|
|
if (!fetchUrl(url, stream)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
outContent = stream.c_str();
|
2026-01-07 03:58:37 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& url, const std::string& destPath,
|
|
|
|
|
ProgressCallback progress) {
|
2026-02-22 11:31:33 +02:00
|
|
|
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
|
|
|
|
|
std::unique_ptr<NetworkClient> client;
|
2026-01-14 06:54:14 -05:00
|
|
|
if (UrlUtils::isHttpsUrl(url)) {
|
2026-02-22 11:31:33 +02:00
|
|
|
auto* secureClient = new NetworkClientSecure();
|
2026-01-14 06:54:14 -05:00
|
|
|
secureClient->setInsecure();
|
|
|
|
|
client.reset(secureClient);
|
|
|
|
|
} else {
|
2026-02-22 11:31:33 +02:00
|
|
|
client.reset(new NetworkClient());
|
2026-01-14 06:54:14 -05:00
|
|
|
}
|
2026-01-07 03:58:37 -05:00
|
|
|
HTTPClient http;
|
|
|
|
|
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("HTTP", "Downloading: %s", url.c_str());
|
|
|
|
|
LOG_DBG("HTTP", "Destination: %s", destPath.c_str());
|
2026-01-07 03:58:37 -05:00
|
|
|
|
|
|
|
|
http.begin(*client, url.c_str());
|
|
|
|
|
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
|
|
|
|
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
|
|
|
|
|
2026-01-27 06:02:38 -05:00
|
|
|
// Add Basic HTTP auth if credentials are configured
|
|
|
|
|
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
|
|
|
|
|
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
|
|
|
|
|
String encoded = base64::encode(credentials.c_str());
|
|
|
|
|
http.addHeader("Authorization", "Basic " + encoded);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 03:58:37 -05:00
|
|
|
const int httpCode = http.GET();
|
|
|
|
|
if (httpCode != HTTP_CODE_OK) {
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_ERR("HTTP", "Download failed: %d", httpCode);
|
2026-01-07 03:58:37 -05:00
|
|
|
http.end();
|
|
|
|
|
return HTTP_ERROR;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 13:39:09 +03:00
|
|
|
const int64_t reportedLength = http.getSize();
|
|
|
|
|
const size_t contentLength = reportedLength > 0 ? static_cast<size_t>(reportedLength) : 0;
|
|
|
|
|
if (contentLength > 0) {
|
|
|
|
|
LOG_DBG("HTTP", "Content-Length: %zu", contentLength);
|
|
|
|
|
} else {
|
|
|
|
|
LOG_DBG("HTTP", "Content-Length: unknown");
|
|
|
|
|
}
|
2026-01-07 03:58:37 -05:00
|
|
|
|
|
|
|
|
// Remove existing file if present
|
2026-02-08 21:29:14 +01:00
|
|
|
if (Storage.exists(destPath.c_str())) {
|
|
|
|
|
Storage.remove(destPath.c_str());
|
2026-01-07 03:58:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open file for writing
|
|
|
|
|
FsFile file;
|
2026-02-08 21:29:14 +01:00
|
|
|
if (!Storage.openFileForWrite("HTTP", destPath.c_str(), file)) {
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_ERR("HTTP", "Failed to open file for writing");
|
2026-01-07 03:58:37 -05:00
|
|
|
http.end();
|
|
|
|
|
return FILE_ERROR;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 13:39:09 +03:00
|
|
|
// Let HTTPClient handle chunked decoding and stream body bytes into the file.
|
|
|
|
|
FileWriteStream fileStream(file, contentLength, progress);
|
|
|
|
|
const int writeResult = http.writeToStream(&fileStream);
|
|
|
|
|
|
|
|
|
|
file.close();
|
|
|
|
|
http.end();
|
|
|
|
|
|
|
|
|
|
if (writeResult < 0) {
|
|
|
|
|
LOG_ERR("HTTP", "writeToStream error: %d", writeResult);
|
2026-02-08 21:29:14 +01:00
|
|
|
Storage.remove(destPath.c_str());
|
2026-01-07 03:58:37 -05:00
|
|
|
return HTTP_ERROR;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 13:39:09 +03:00
|
|
|
const size_t downloaded = fileStream.downloaded();
|
|
|
|
|
LOG_DBG("HTTP", "Downloaded %zu bytes", downloaded);
|
2026-01-07 03:58:37 -05:00
|
|
|
|
2026-02-28 13:39:09 +03:00
|
|
|
// Guard against partial writes even if HTTPClient completes.
|
|
|
|
|
if (!fileStream.ok()) {
|
|
|
|
|
LOG_ERR("HTTP", "Write failed during download");
|
|
|
|
|
Storage.remove(destPath.c_str());
|
|
|
|
|
return FILE_ERROR;
|
2026-01-07 03:58:37 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 13:39:09 +03:00
|
|
|
if (contentLength == 0 && downloaded == 0) {
|
|
|
|
|
LOG_ERR("HTTP", "Download failed: no data received");
|
|
|
|
|
Storage.remove(destPath.c_str());
|
|
|
|
|
return HTTP_ERROR;
|
|
|
|
|
}
|
2026-01-07 03:58:37 -05:00
|
|
|
|
|
|
|
|
// Verify download size if known
|
|
|
|
|
if (contentLength > 0 && downloaded != contentLength) {
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_ERR("HTTP", "Size mismatch: got %zu, expected %zu", downloaded, contentLength);
|
2026-02-08 21:29:14 +01:00
|
|
|
Storage.remove(destPath.c_str());
|
2026-01-07 03:58:37 -05:00
|
|
|
return HTTP_ERROR;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return OK;
|
|
|
|
|
}
|