feat(app-catalog): serve the signed catalog from the node first
This commit is contained in:
@@ -145,6 +145,19 @@ impl ApiHandler {
|
|||||||
/// URL so the App Store still renders on nodes that haven't persisted
|
/// URL so the App Store still renders on nodes that haven't persisted
|
||||||
/// a registry config yet. 15s total timeout.
|
/// a registry config yet. 15s total timeout.
|
||||||
async fn handle_app_catalog_proxy(&self) -> Result<Response<hyper::Body>> {
|
async fn handle_app_catalog_proxy(&self) -> Result<Response<hyper::Body>> {
|
||||||
|
// The daemon already refreshes and verifies releases/app-catalog.json.
|
||||||
|
// Serve that release-root-anchored cache first so a newly published app
|
||||||
|
// appears immediately, without a frontend release. The old external UI
|
||||||
|
// catalog below is emergency compatibility only; it must never override
|
||||||
|
// a healthy signed catalog (Cuprate was invisible for exactly that reason).
|
||||||
|
if let Ok(body) = crate::container::app_catalog::verified_catalog_body(&self.config.data_dir).await {
|
||||||
|
return Ok(Response::builder()
|
||||||
|
.status(hyper::StatusCode::OK)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Cache-Control", "no-cache")
|
||||||
|
.body(hyper::Body::from(body))?);
|
||||||
|
}
|
||||||
|
|
||||||
let mut upstreams: Vec<String> = Vec::new();
|
let mut upstreams: Vec<String> = Vec::new();
|
||||||
if let Ok(config) = crate::container::registry::load_registries(&self.config.data_dir).await
|
if let Ok(config) = crate::container::registry::load_registries(&self.config.data_dir).await
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
//! Unknown fields are ignored (no `deny_unknown_fields`), so adding fields on the
|
//! Unknown fields are ignored (no `deny_unknown_fields`), so adding fields on the
|
||||||
//! publisher side never breaks older nodes.
|
//! publisher side never breaks older nodes.
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -194,6 +195,25 @@ fn entry_for(app_id: &str) -> Option<AppCatalogEntry> {
|
|||||||
load_catalog().apps.get(app_id).cloned()
|
load_catalog().apps.get(app_id).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the cached catalog bytes only when they carry a signature anchored
|
||||||
|
/// to the release root. This is the browser App Store's source: newly signed
|
||||||
|
/// apps must appear without waiting for a frontend OTA, while unsigned or
|
||||||
|
/// self-signed registry data must never become an install button.
|
||||||
|
pub async fn verified_catalog_body(data_dir: &Path) -> anyhow::Result<String> {
|
||||||
|
let path = data_dir.join(APP_CATALOG_FILE);
|
||||||
|
let body = tokio::fs::read_to_string(&path)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("read signed app catalog {}", path.display()))?;
|
||||||
|
let raw: serde_json::Value = serde_json::from_str(&body)?;
|
||||||
|
match crate::trust::verify_detached(&raw)? {
|
||||||
|
crate::trust::SignatureStatus::Verified { anchored: true, .. } => Ok(body),
|
||||||
|
crate::trust::SignatureStatus::Verified { anchored: false, .. } => {
|
||||||
|
anyhow::bail!("app catalog signer is not anchored to the release root")
|
||||||
|
}
|
||||||
|
crate::trust::SignatureStatus::Unsigned => anyhow::bail!("app catalog is unsigned"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Primary image for an app per the remote catalog, if covered.
|
/// Primary image for an app per the remote catalog, if covered.
|
||||||
pub fn catalog_primary_image(app_id: &str) -> Option<String> {
|
pub fn catalog_primary_image(app_id: &str) -> Option<String> {
|
||||||
entry_for(app_id).and_then(|e| e.image)
|
entry_for(app_id).and_then(|e| e.image)
|
||||||
|
|||||||
Reference in New Issue
Block a user