Updates from Gitea, step 2: the connection (check and list work on the device)

The roots (ISRG X1 and X2), an HTTPS client that reads the answer as a
stream, GiteaReleases (the latest and a list of ten), the Update
Service's requests, install from Gitea through the existing install path,
the daily check's schedule, the Check for updates setting, and console
commands: update check | list | status | install <tag>.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-06 15:20:58 +02:00
co-authored by Claude Sonnet 5.5
parent b0e8226943
commit 5754ae5b57
12 changed files with 616 additions and 3 deletions
+112
View File
@@ -0,0 +1,112 @@
#include "platform/https_get.h"
#include <Arduino.h>
#include <esp_heap_caps.h>
#include <algorithm>
#include <ctime>
#include "platform/ca_roots.h"
#include "version.h"
namespace roro {
namespace {
constexpr size_t kFloor = 55 * 1024; // Q86, Q172: no connection below this
constexpr time_t kClockSetAfter = 1700000000; // 2023-11: anything earlier is the clock's default
// What mbedTLS says in the way it says it, for the cases a person can act on.
std::string whyNotConnected(NetworkClientSecure& tls, const std::string& host) {
char text[100] = "";
int err = tls.lastError(text, sizeof text);
std::string detail = text;
if (detail.find("X509") != std::string::npos || detail.find("certificate") != std::string::npos)
return "The certificate of " + host + " isn't accepted";
if (err != 0 && !detail.empty()) return "TLS to " + host + ": " + detail;
return "Can't connect to " + host;
}
} // namespace
std::string HttpsGet::open(const std::string& host, const std::string& path, const char* accept) {
close();
if (time(nullptr) < kClockSetAfter) return "The clock isn't set yet: certificates can't be checked";
if (esp_get_free_heap_size() < kFloor) return "Not enough memory: close IRC or a Gemini page";
tls_.setCACert(kTrustedRootsPem);
tls_.setTimeout(15);
if (!tls_.connect(host.c_str(), 443)) return whyNotConnected(tls_, host);
raw_.reset(new (std::nothrow) uint8_t[kRaw]);
if (!raw_) return "Not enough memory";
tls_.print(("GET " + path + " HTTP/1.1\r\nHost: " + host + "\r\nUser-Agent: roro9stack/" + versionString() +
"\r\nAccept: " + accept + "\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n")
.c_str());
release::HttpHeadParser parser;
while (!parser.complete()) {
int n = readRaw(raw_.get(), kRaw);
if (n <= 0) return "The server " + host + " stopped answering";
size_t used = parser.feed(reinterpret_cast<const char*>(raw_.get()), n);
if (parser.failed()) return host + " didn't answer with HTTP";
if (parser.complete() && used < static_cast<size_t>(n)) early_.assign(reinterpret_cast<const char*>(raw_.get()) + used, n - used);
}
head_ = parser.head();
if (head_.status != 200) return host + " answered " + std::to_string(head_.status) + (head_.status / 100 == 3 ? " (a redirect)" : "");
left_ = head_.chunked ? -1 : head_.contentLength;
return "";
}
int HttpsGet::readRaw(uint8_t* into, size_t len) {
uint32_t since = millis();
while (!tls_.available()) {
if (!tls_.connected()) return 0;
if (millis() - since > kStallMs) return -1;
delay(5);
}
return tls_.read(into, len);
}
int HttpsGet::read(uint8_t* buf, size_t len) {
if (done_ || len == 0) return 0;
if (!head_.chunked && left_ == 0) return done_ = true, 0;
for (;;) {
// Raw bytes: first those that came with the head, then the connection's.
size_t want = head_.chunked ? std::min(len, kRaw) : len;
if (!head_.chunked && left_ > 0) want = std::min<size_t>(want, left_);
int n;
if (earlyAt_ < early_.size()) {
n = static_cast<int>(std::min(want, early_.size() - earlyAt_));
std::copy(early_.data() + earlyAt_, early_.data() + earlyAt_ + n, head_.chunked ? raw_.get() : buf);
earlyAt_ += n;
} else {
n = readRaw(head_.chunked ? raw_.get() : buf, want);
}
if (n < 0) return -1;
if (n == 0) { // the server closed: the end, if it's where it said it would be
done_ = true;
bool whole = head_.chunked ? chunks_.done() : left_ <= 0;
return whole ? 0 : -1;
}
if (!head_.chunked) {
if (left_ > 0) left_ -= n;
return n;
}
size_t out = chunks_.decode(raw_.get(), n, buf);
if (chunks_.failed()) return -1;
if (out > 0) return static_cast<int>(out);
if (chunks_.done()) return done_ = true, 0;
}
}
void HttpsGet::close() {
tls_.stop();
raw_.reset();
std::string().swap(early_);
earlyAt_ = 0;
done_ = false;
head_ = release::HttpHead();
chunks_ = release::ChunkedDecoder();
}
} // namespace roro