#include "platform/https_get.h" #include #include #include #include #include "platform/ca_roots.h" #include "version.h" namespace roro { namespace { 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: can't check certificates"; if (esp_get_free_heap_size() < kNeedFree) return "Not enough memory for a secure connection"; 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(raw_.get()), n); if (parser.failed()) return host + " didn't answer with HTTP"; if (parser.complete() && used < static_cast(n)) early_.assign(reinterpret_cast(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(want, left_); int n; if (earlyAt_ < early_.size()) { n = static_cast(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(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