#pragma once #include #include #include #include "gemini_response.h" #include "key_value_store.h" #include "services/storage_service.h" #include "text_buffer.h" class NetworkClientSecure; namespace roro { // What a fetch came back with. struct GeminiPage { enum class Outcome { Ok, Failed, CertificateChanged }; Outcome outcome = Outcome::Failed; std::string requested; // what was asked for std::string url; // where it ended up, after redirects gemini::Header header; // the final response's header gemini::TextBuffer text; // 2x only: the body as lines, up to kMaxBody bool truncated = false; // the body was cut: see truncatedWhy std::string truncatedWhy; std::string error; // Failed: why, for a human std::string host; // CertificateChanged: whose, and both fingerprints int port = 1965; std::string pinned, presented; std::string cachePath; // with a card: the whole body as received (Saved Pages copy it) size_t receivedBytes = 0; // the whole body, which may be more than `text` holds uint32_t ms = 0; }; // Gemini (docs/milestones/G1.md). Each fetch runs on a short-lived task of its own: a TLS // handshake can block for seconds, which the main loop's watchdog wouldn't allow, and an idle // client should cost no stack. Certificates are trusted on first use, per host and port (Q71). class GeminiService { public: static constexpr size_t kMaxBody = 64 * 1024; // Q74 static constexpr size_t kStartFloor = 55 * 1024; // Q86: no fetch below this static constexpr size_t kSteadyFloor = 40 * 1024; // Q86: what must be left once the page is in static constexpr size_t kTransientFloor = 20 * 1024; // Q86: while the TLS connection is open static constexpr int kMaxRedirects = 5; // Q72 static constexpr size_t kMaxOnCard = 1024 * 1024; // a text page larger than this is cut static constexpr const char* kCachePath = "/gemini/cache/page.gmi"; GeminiService(KeyValueStore& store, StorageService& storage); // Starts a fetch; false if one is running. The result comes from takeResult(). bool fetch(const std::string& url); bool busy() const { return busy_; } bool takeResult(GeminiPage& out); void cancel() { cancel_ = true; } // After the user accepts a changed certificate. void trust(const std::string& host, int port, const std::string& fingerprint); // `gemini get `: the same fetch, reported on the console instead of to the App. bool fetchToConsole(const std::string& url); private: static void taskEntry(void* self); void run(); void fetchOne(const std::string& url, GeminiPage& page); void report(const GeminiPage& page, size_t heapBefore, size_t heapLowest); std::string pinKey(const std::string& host, int port) const; bool receiveToCard(::NetworkClientSecure& tls, const char* first, size_t len, GeminiPage& page); void loadFromCard(GeminiPage& page, size_t freeBefore); KeyValueStore& store_; StorageService& storage_; SemaphoreHandle_t lock_; std::string pendingUrl_; GeminiPage result_; volatile bool busy_ = false, ready_ = false, cancel_ = false, toConsole_ = false; size_t lowest_ = 0; }; } // namespace roro