Public Access
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:
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
namespace roro {
|
||||
|
||||
// The roots this device trusts for what it fetches over HTTPS (docs/milestones/R1.md, Q162): the two
|
||||
// ISRG roots Let's Encrypt chains end in, not the framework's bundle of about 130 CAs. Public
|
||||
// certificates, from https://letsencrypt.org/certs/ (SHA-256 fingerprints below).
|
||||
//
|
||||
// ISRG Root X1 RSA 4096 valid until 2035-06-04
|
||||
// 96:BC:EC:06:26:49:76:F3:74:60:77:9A:CF:28:C5:A7:CF:E8:A3:C0:AA:E1:1A:8F:FC:EE:05:C0:BD:DF:08:C6
|
||||
// ISRG Root X2 ECDSA P-384 valid until 2040-09-17
|
||||
// 69:72:9B:8E:15:A8:6E:FC:17:7A:57:AF:B7:17:1D:FC:64:AD:D2:8C:2F:CA:8C:F1:50:7E:34:45:3C:CB:14:70
|
||||
//
|
||||
// If the server's CA ever changes, the next firmware has to carry the new root and come from the PC.
|
||||
inline constexpr char kTrustedRootsPem[] =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"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\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw\n"
|
||||
"CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg\n"
|
||||
"R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00\n"
|
||||
"MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT\n"
|
||||
"ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw\n"
|
||||
"EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW\n"
|
||||
"+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9\n"
|
||||
"ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T\n"
|
||||
"AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI\n"
|
||||
"zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW\n"
|
||||
"tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1\n"
|
||||
"/q4AaOeMSQ+2b1tbFfLn\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
} // namespace roro
|
||||
@@ -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
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <NetworkClientSecure.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "http_head.h"
|
||||
#include "platform/counted_client.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// One HTTPS GET, read as a stream: the connection checks the server's chain and name against the
|
||||
// roots in ca_roots.h, the head is parsed, and the body comes out whole whether the server sent it
|
||||
// with a length or in chunks. Used by the Update Service to read Gitea's answers and to download a
|
||||
// release. Redirects aren't followed: the device only goes where it was told to (Q163).
|
||||
class HttpsGet {
|
||||
public:
|
||||
explicit HttpsGet(net::User user) : tls_(user) {}
|
||||
~HttpsGet() { close(); }
|
||||
|
||||
// Connects and reads the head. "" when a 200 is on its way, or why not, in words for the screen.
|
||||
std::string open(const std::string& host, const std::string& path, const char* accept);
|
||||
long contentLength() const { return head_.contentLength; } // -1: not known
|
||||
|
||||
// Body bytes into `buf`: how many, 0 at the end, -1 when the connection broke or stalled
|
||||
// before the end.
|
||||
int read(uint8_t* buf, size_t len);
|
||||
void close();
|
||||
|
||||
private:
|
||||
static constexpr size_t kRaw = 1024;
|
||||
static constexpr uint32_t kStallMs = 10000;
|
||||
|
||||
int readRaw(uint8_t* into, size_t len); // from the connection, waiting up to kStallMs
|
||||
|
||||
Counted<NetworkClientSecure> tls_;
|
||||
release::HttpHead head_;
|
||||
release::ChunkedDecoder chunks_;
|
||||
std::unique_ptr<uint8_t[]> raw_;
|
||||
std::string early_; // body bytes that arrived with the head
|
||||
size_t earlyAt_ = 0;
|
||||
long left_ = -1; // body bytes still to come, if the server said how many
|
||||
bool done_ = false;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
Reference in New Issue
Block a user