Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
remote_update_fetch.hpp
Go to the documentation of this file.
1#pragma once
2
3// Opt-in remote UX update fetch — the network layer over evaluate_remote_ux_update.
4//
5// Default OFF. When a developer opts in, this fetches a signed UX pack over HTTPS,
6// hands the raw bytes to the pure fail-closed gate (remote_update.hpp), and — only
7// if the gate accepts — installs it content-addressed (pack_install.hpp), so the
8// loader reads immutable, re-verified files. The signature is the trust anchor: a
9// hostile or MITM'd host still cannot forge a signed pack.
10//
11// Two deliberate design properties the owner asked for:
12//
13// * Offline is a FIRST-CLASS outcome, never an error. An unreachable host, a
14// timeout, or a non-2xx response yields RemoteFetchStatus::Unavailable — the
15// caller simply keeps its current UI. Nothing here blocks or throws, so a dead
16// network never stalls the plugin. A developer may supply an offline_fallback
17// pack so the plugin still looks like THEIRS with no network.
18//
19// * Everything is customizable. The transport is an injected `fetcher` callable,
20// so any delivery "style" works — a CDN, an object store, a private server with
21// custom auth. `make_http_pack_fetcher()` is a convenience default over
22// runtime::http_*; override it freely.
23//
24// Privacy: an update check sends ONLY what the server needs to choose the right pack
25// — plugin id, channel, installed version — over HTTPS. No device id, no user id, no
26// telemetry. `build_update_check_url()` is pure so that contract is testable.
27
32#include <pulp/runtime/http.hpp>
35
36#include <cstdint>
37#include <filesystem>
38#include <functional>
39#include <string>
40#include <vector>
41
42namespace pulp::format::reload {
43
51
60
64 bool reachable = false;
65 std::string manifest_json;
66 std::filesystem::path local_root;
67 std::string detail;
68};
69
72inline std::string url_query_escape(std::string_view s) {
73 static const char* hex = "0123456789ABCDEF";
74 std::string out;
75 out.reserve(s.size());
76 for (unsigned char c : s) {
77 const bool unreserved = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
78 (c >= '0' && c <= '9') || c == '-' || c == '_' ||
79 c == '.' || c == '~';
80 if (unreserved) {
81 out.push_back(static_cast<char>(c));
82 } else {
83 out.push_back('%');
84 out.push_back(hex[c >> 4]);
85 out.push_back(hex[c & 0xF]);
86 }
87 }
88 return out;
89}
90
93inline std::string build_update_check_url(const std::string& base_url,
94 const std::string& plugin_id,
95 const std::string& channel,
96 std::uint64_t installed_version) {
97 const char sep = (base_url.find('?') == std::string::npos) ? '?' : '&';
98 return base_url + sep + "plugin=" + url_query_escape(plugin_id) +
99 "&channel=" + url_query_escape(channel) +
100 "&installed=" + std::to_string(installed_version);
101}
102
108inline bool is_safe_pack_member_path(const std::string& p) {
109 return runtime::is_safe_relative_path(std::filesystem::path(p));
110}
111
113 bool enabled = false;
114 std::string base_url;
115 std::string plugin_id;
116 std::string channel = "stable";
117 std::vector<std::uint8_t> trusted_public_key;
118 std::uint64_t installed_pack_version = 0;
120 std::filesystem::path install_base;
124 std::filesystem::path offline_fallback_root;
128 std::function<FetchedPack(const std::string& url)> fetcher;
129};
130
137 bool srl_fresh,
138 const SignedRevocationList* srl) {
139 if (!cfg.enabled)
140 return {RemoteFetchStatus::Disabled, {}, {}, "remote update disabled (opt-in)"};
141
142 auto offline = [&](std::string why) {
143 RemoteFetchResult r{RemoteFetchStatus::Unavailable, {}, {}, std::move(why)};
144 if (!cfg.offline_fallback_root.empty()) {
145 r.installed_root = cfg.offline_fallback_root;
146 r.detail += " — using offline fallback pack";
147 }
148 return r;
149 };
150
151 // Plaintext update channels are not allowed — the signature is the trust anchor,
152 // but HTTPS still protects the request's privacy + prevents trivial tampering.
153 if (cfg.base_url.rfind("https://", 0) != 0)
154 return offline("remote update requires an https:// endpoint");
155 if (!cfg.fetcher) return offline("no fetcher configured");
156
157 const std::string url =
159 const FetchedPack fetched = cfg.fetcher(url);
160 if (!fetched.reachable) return offline("offline: " + fetched.detail);
161
162 std::string err;
163 auto manifest = parse_swap_pack_manifest(fetched.manifest_json, err);
164 if (!manifest)
166 {RemoteUpdateVerdict::RejectSignature, "malformed manifest: " + err},
167 {}, "malformed manifest: " + err};
168
169 const auto decision =
171 cfg.installed_pack_version, cfg.host_granted, srl_fresh, srl);
172 if (!decision.ok()) {
173 // A server offering an equal/older version is benign "nothing newer" — not a
174 // security event. Everything else is a real rejection to surface loudly.
175 const bool benign = decision.verdict == RemoteUpdateVerdict::RejectDowngrade;
177 decision, {}, decision.detail};
178 }
179
180 // Accepted: install content-addressed — this re-verifies the exact bytes at the
181 // destination, so the loader never reads anything the gate did not clear.
182 const auto inst = install_verified_pack(fetched.local_root, *manifest,
184 if (!inst.ok())
185 return {RemoteFetchStatus::Rejected, decision, {}, "install failed: " + inst.detail};
186 return {RemoteFetchStatus::Applied, decision, inst.installed_root, "applied"};
187}
188
193inline std::function<FetchedPack(const std::string&)> make_http_pack_fetcher(
194 std::string files_base_url, std::filesystem::path download_dir, int timeout_seconds = 20) {
195 return [files_base_url = std::move(files_base_url),
196 download_dir = std::move(download_dir), timeout_seconds](const std::string& url) {
197 FetchedPack out;
198 const auto manifest_resp = runtime::http_get(url, timeout_seconds);
199 if (!manifest_resp.ok()) {
200 out.detail = manifest_resp.error.empty()
201 ? ("HTTP " + std::to_string(manifest_resp.status_code))
202 : manifest_resp.error;
203 return out; // reachable stays false → Unavailable
204 }
205 std::string err;
206 auto manifest = parse_swap_pack_manifest(manifest_resp.body, err);
207 if (!manifest) {
208 // Reachable, but the manifest is unusable — let the orchestrator reject it
209 // (a served-but-malformed manifest is a server problem, not offline).
210 out.reachable = true;
211 out.manifest_json = manifest_resp.body;
212 return out;
213 }
214 std::error_code ec;
215 std::filesystem::create_directories(download_dir, ec);
216 for (const auto& f : manifest->files) {
217 // The manifest is the UNTRUSTED server's word (the signature is checked
218 // later, by the gate). A hostile `path` must never let this write outside
219 // download_dir, so screen it BEFORE opening a destination.
220 // (install_verified_pack re-checks at publish time; this guards the
221 // download write itself.)
222 if (!is_safe_pack_member_path(f.path)) {
223 out.detail = "unsafe manifest path rejected: " + f.path;
224 return out; // fail closed — treat as unavailable, write nothing
225 }
226 const std::filesystem::path dest = download_dir / f.path;
227 if (!runtime::is_within_directory(download_dir, dest)) {
228 out.detail = "unsafe manifest path rejected: " + f.path;
229 return out;
230 }
231 std::filesystem::create_directories(dest.parent_path(), ec);
232 if (!runtime::http_download(files_base_url + "/" + f.path, dest.string(),
233 timeout_seconds)) {
234 out.detail = "download failed: " + f.path;
235 return out; // partial fetch → treat as offline/unavailable
236 }
237 }
238 out.reachable = true;
239 out.manifest_json = manifest_resp.body;
240 out.local_root = download_dir;
241 return out;
242 };
243}
244
245} // namespace pulp::format::reload
A granted-capability set.
Definition reload_capabilities.hpp:74
Definition build_fingerprint.hpp:23
RemoteUpdateResult evaluate_remote_ux_update(const std::filesystem::path &pack_root, const SwapPackManifest &m, const std::vector< std::uint8_t > &trusted_public_key, std::uint64_t installed_pack_version, const view::CapabilitySet &host_granted, bool srl_fresh, const SignedRevocationList *srl)
Definition remote_update.hpp:45
bool is_safe_pack_member_path(const std::string &p)
True only if p is safe to join onto a download directory, per runtime::is_safe_relative_path (no root...
Definition remote_update_fetch.hpp:108
RemoteFetchStatus
Definition remote_update_fetch.hpp:44
@ Unavailable
offline / unreachable / timeout / malformed transport — keep current UI
Definition remote_update_fetch.hpp:49
@ Applied
a newer signed pack passed the gate and was installed
Definition remote_update_fetch.hpp:46
@ UpToDate
reachable, but nothing newer to apply (server offered <= installed)
Definition remote_update_fetch.hpp:47
@ Disabled
opt-in feature is off (default) — no network is touched
Definition remote_update_fetch.hpp:45
@ Rejected
a pack was returned but the gate refused it (a security event)
Definition remote_update_fetch.hpp:48
RemoteFetchResult check_and_fetch_remote_ux_update(const RemoteUpdateConfig &cfg, bool srl_fresh, const SignedRevocationList *srl)
Orchestrate one opt-in remote-update check.
Definition remote_update_fetch.hpp:136
PackInstallResult install_verified_pack(const std::filesystem::path &source_root, const SwapPackManifest &manifest, const std::vector< std::uint8_t > &trusted_public_key, const std::filesystem::path &install_base, bool reject_unknown_kinds=true)
Verify source_root against trusted_public_key, then install the pack's files into install_base/<conte...
Definition pack_install.hpp:56
std::function< FetchedPack(const std::string &)> make_http_pack_fetcher(std::string files_base_url, std::filesystem::path download_dir, int timeout_seconds=20)
Convenience default transport over runtime::http_*: GET the manifest, then download each declared fil...
Definition remote_update_fetch.hpp:193
std::string url_query_escape(std::string_view s)
Percent-encode a query value (RFC 3986 unreserved kept; everything else escaped), so a plugin id / ch...
Definition remote_update_fetch.hpp:72
std::string build_update_check_url(const std::string &base_url, const std::string &plugin_id, const std::string &channel, std::uint64_t installed_version)
The privacy-preserving update-check URL: base + only (plugin, channel, installed version).
Definition remote_update_fetch.hpp:93
std::optional< SwapPackManifest > parse_swap_pack_manifest(std::string_view json, std::string &error)
Parse a swap-pack manifest from JSON.
Definition swap_pack.hpp:124
@ RejectSignature
bad signature or integrity (untrusted/tampered)
Definition remote_update.hpp:26
@ RejectDowngrade
pack_version <= the installed version
Definition remote_update.hpp:30
bool is_safe_relative_path(const std::filesystem::path &path)
True only if path is safe to join beneath a destination directory: a non-empty relative path with no ...
Definition safe_relative_path.hpp:25
bool http_download(std::string_view url, std::string_view output_path, int timeout_seconds=60)
Download a file from a URL to a local path. Returns true on success.
bool is_within_directory(const std::filesystem::path &directory, const std::filesystem::path &candidate)
True if candidate, compared lexically after normalization, is directory itself or lies beneath it.
Definition safe_relative_path.hpp:43
HttpResponse http_get(std::string_view url, int timeout_seconds=30)
Perform an HTTP GET request.
Signed Revocation List (SRL) + namespaced monotonic epoch floor.
Screening for untrusted relative paths that will be joined beneath a destination directory: archive e...
What an injected fetcher returns.
Definition remote_update_fetch.hpp:63
std::filesystem::path local_root
where the fetcher materialized the pack files
Definition remote_update_fetch.hpp:66
bool reachable
Definition remote_update_fetch.hpp:64
std::string detail
Definition remote_update_fetch.hpp:67
std::string manifest_json
the pack manifest as served
Definition remote_update_fetch.hpp:65
Definition remote_update_fetch.hpp:52
bool applied() const
Definition remote_update_fetch.hpp:58
RemoteUpdateResult decision
the gate verdict, when a pack was evaluated
Definition remote_update_fetch.hpp:54
RemoteFetchStatus status
Definition remote_update_fetch.hpp:53
std::string detail
Definition remote_update_fetch.hpp:57
std::filesystem::path installed_root
Applied: the immutable dir to load from; Unavailable: the offline fallback, if supplied.
Definition remote_update_fetch.hpp:55
Definition remote_update_fetch.hpp:112
std::filesystem::path offline_fallback_root
Optional developer-provided pack to fall back to when offline, so the plugin keeps THEIR look with no...
Definition remote_update_fetch.hpp:124
std::string base_url
HTTPS update-check endpoint.
Definition remote_update_fetch.hpp:114
std::string channel
Definition remote_update_fetch.hpp:116
view::CapabilitySet host_granted
the ceiling a pushed UX may not exceed
Definition remote_update_fetch.hpp:119
std::function< FetchedPack(const std::string &url)> fetcher
Transport.
Definition remote_update_fetch.hpp:128
std::string plugin_id
Definition remote_update_fetch.hpp:115
std::uint64_t installed_pack_version
Definition remote_update_fetch.hpp:118
std::vector< std::uint8_t > trusted_public_key
the pinned signer
Definition remote_update_fetch.hpp:117
bool enabled
opt-in; default OFF (no network)
Definition remote_update_fetch.hpp:113
std::filesystem::path install_base
where an accepted pack is installed
Definition remote_update_fetch.hpp:120
Definition remote_update.hpp:34
A signed revocation list.
Definition revocation.hpp:54
Sealed swap-pack manifest + per-file integrity.