Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions apps/desktop/src-tauri/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,25 +52,54 @@ fn repo_to_raw_base(repo_url: &str, branch: &str, subpath: Option<&str>) -> Stri

#[tauri::command]
pub async fn get_registry_catalog(url: String) -> Result<RegistryCatalog, String> {
if url.starts_with("http://") || url.starts_with("https://") {
// Trim and lower-case the scheme before matching so inputs like
// ` HTTP://…` or `Https://…` still hit the intended branch instead of
// falling through to the local-file fallback with a confusing error.
let trimmed = url.trim();
let scheme_prefix: String = trimmed.chars().take_while(|c| *c != ':').collect();
let scheme_lc = scheme_prefix.to_ascii_lowercase();

// Plain `http://` (any casing/whitespace) is rejected outright: a MITM on
// the catalog can inject arbitrary `repository`/`data` entries that
// `install_extension` will then pass to `git clone`, escalating a network
// attack into code execution.
if scheme_lc == "http" {
return Err("Registry URL must use https://; plain HTTP is rejected to prevent MITM tampering of the catalog".to_string());
}

if scheme_lc == "https" {
let response = shared_client()
.get(&url)
.get(trimmed)
.timeout(DEFAULT_REQUEST_TIMEOUT)
.send()
.await
.map_err(|e| e.to_string())?;

// Surface the HTTP failure explicitly, otherwise a 404/500 HTML body
// turns into a "Failed to parse registry JSON" error that hides the
// real problem (typo'd URL, registry missing, proxy blocking, …).
let status = response.status();
if !status.is_success() {
return Err(format!(
"Failed to fetch registry from {}: HTTP {}",
trimmed, status
));
}

let body = read_text_capped(response, MAX_RESPONSE_BYTES).await?;
let catalog: RegistryCatalog = serde_json::from_str(&body).map_err(|e| {
let err = format!("Failed to parse registry JSON from {}: {}", url, e);
let err = format!("Failed to parse registry JSON from {}: {}", trimmed, e);
error!("{}", err);
redact_user_paths(&err)
})?;

return Ok(catalog);
}

// Fallback to local file reading for dev mode
// Fallback to local file reading for dev mode. Only reachable when the
// caller supplied something without an http/https scheme, so we pass the
// original (untrimmed) value through to keep error messages pointing at
// what the caller actually sent.
let content = std::fs::read_to_string(&url).map_err(|e| {
let err = format!("Failed to read local registry file {}: {}", url, e);
error!("{}", err);
Expand Down
4 changes: 1 addition & 3 deletions apps/desktop/src/context/ExtensionContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,9 @@ export const ExtensionProvider: React.FC<{ children: React.ReactNode }> = ({ chi
setError(null);
try {
// The Tauri process CWD is usually apps/desktop/src-tauri, so the repo root is ../../../
const registryUrl = "http://raw.githubusercontent.com/TrixtyAI/ide/main/registry/marketplace.json";
const registryUrl = "https://raw.githubusercontent.com/TrixtyAI/ide/main/registry/marketplace.json";
const devRegistryUrl = "../../../registry/marketplace.json";

// We will try devRegistryUrl, if it fails because it's a prod build, fallback to HTTP or viceversa.
// But for local test, let's just pass devRegistryUrl directly.
const targetUrl = process.env.NODE_ENV === "development" ? devRegistryUrl : registryUrl;

// 1. Fetch remote catalog raw list
Expand Down
Loading