http_client: Add integrity checks for GitHub binaries using digest checks (#43737)
Generalizes the digest verification logic from `rust-analyzer` and `clangd` into a reusable helper function in `http_client::github_download`. This removes ~100 lines of duplicated code across the two language adapters and makes it easier for other language servers to adopt digest verification in the future. Closes #35201 Release Notes: - N/A
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use std::{path::Path, pin::Pin, task::Poll};
|
||||
use std::{future::Future, path::Path, pin::Pin, task::Poll};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
@@ -85,6 +85,65 @@ pub async fn download_server_binary(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fetch_github_binary_with_digest_check<ValidityCheck, ValidityCheckFuture>(
|
||||
binary_path: &Path,
|
||||
metadata_path: &Path,
|
||||
expected_digest: Option<String>,
|
||||
url: &str,
|
||||
asset_kind: AssetKind,
|
||||
download_destination: &Path,
|
||||
http_client: &dyn HttpClient,
|
||||
validity_check: ValidityCheck,
|
||||
) -> Result<()>
|
||||
where
|
||||
ValidityCheck: FnOnce() -> ValidityCheckFuture,
|
||||
ValidityCheckFuture: Future<Output = Result<()>>,
|
||||
{
|
||||
let metadata = GithubBinaryMetadata::read_from_file(metadata_path)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
if let Some(metadata) = metadata {
|
||||
let validity_check_result = validity_check().await;
|
||||
|
||||
if let (Some(actual_digest), Some(expected_digest_ref)) =
|
||||
(&metadata.digest, &expected_digest)
|
||||
{
|
||||
if actual_digest == expected_digest_ref {
|
||||
if validity_check_result.is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
log::info!(
|
||||
"SHA-256 mismatch for {binary_path:?} asset, downloading new asset. Expected: {expected_digest_ref}, Got: {actual_digest}"
|
||||
);
|
||||
}
|
||||
} else if validity_check_result.is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
download_server_binary(
|
||||
http_client,
|
||||
url,
|
||||
expected_digest.as_deref(),
|
||||
download_destination,
|
||||
asset_kind,
|
||||
)
|
||||
.await?;
|
||||
|
||||
GithubBinaryMetadata::write_to_file(
|
||||
&GithubBinaryMetadata {
|
||||
metadata_version: 1,
|
||||
digest: expected_digest,
|
||||
},
|
||||
metadata_path,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stream_response_archive(
|
||||
response: impl AsyncRead + Unpin,
|
||||
url: &str,
|
||||
|
||||
@@ -3,7 +3,7 @@ use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, AsyncApp};
|
||||
use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release};
|
||||
use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
|
||||
use http_client::github_download::fetch_github_binary_with_digest_check;
|
||||
pub use language::*;
|
||||
use lsp::{InitializeParams, LanguageServerBinary, LanguageServerName};
|
||||
use project::lsp_store::clangd_ext;
|
||||
@@ -85,56 +85,33 @@ impl LspInstaller for CLspAdapter {
|
||||
};
|
||||
|
||||
let metadata_path = version_dir.join("metadata");
|
||||
let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
|
||||
.await
|
||||
.ok();
|
||||
if let Some(metadata) = metadata {
|
||||
let validity_check = async || {
|
||||
|
||||
let binary_path_for_check = binary_path.clone();
|
||||
fetch_github_binary_with_digest_check(
|
||||
&binary_path,
|
||||
&metadata_path,
|
||||
expected_digest,
|
||||
&url,
|
||||
AssetKind::Zip,
|
||||
&container_dir,
|
||||
&*delegate.http_client(),
|
||||
|| async move {
|
||||
delegate
|
||||
.try_exec(LanguageServerBinary {
|
||||
path: binary_path.clone(),
|
||||
path: binary_path_for_check,
|
||||
arguments: vec!["--version".into()],
|
||||
env: None,
|
||||
})
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
log::warn!("Unable to run {binary_path:?} asset, redownloading: {err:#}",)
|
||||
log::warn!("Unable to run clangd asset, redownloading: {err:#}")
|
||||
})
|
||||
};
|
||||
if let (Some(actual_digest), Some(expected_digest)) =
|
||||
(&metadata.digest, &expected_digest)
|
||||
{
|
||||
if actual_digest == expected_digest {
|
||||
if validity_check().await.is_ok() {
|
||||
return Ok(binary);
|
||||
}
|
||||
} else {
|
||||
log::info!(
|
||||
"SHA-256 mismatch for {binary_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
|
||||
);
|
||||
}
|
||||
} else if validity_check().await.is_ok() {
|
||||
return Ok(binary);
|
||||
}
|
||||
}
|
||||
download_server_binary(
|
||||
&*delegate.http_client(),
|
||||
&url,
|
||||
expected_digest.as_deref(),
|
||||
&container_dir,
|
||||
AssetKind::Zip,
|
||||
)
|
||||
.await?;
|
||||
remove_matching(&container_dir, |entry| entry != version_dir).await;
|
||||
GithubBinaryMetadata::write_to_file(
|
||||
&GithubBinaryMetadata {
|
||||
metadata_version: 1,
|
||||
digest: expected_digest,
|
||||
},
|
||||
&metadata_path,
|
||||
)
|
||||
.await?;
|
||||
|
||||
remove_matching(&container_dir, |entry| entry != version_dir).await;
|
||||
|
||||
Ok(binary)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use futures::StreamExt;
|
||||
use gpui::{App, AppContext, AsyncApp, SharedString, Task};
|
||||
use http_client::github::AssetKind;
|
||||
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
|
||||
use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
|
||||
use http_client::github_download::fetch_github_binary_with_digest_check;
|
||||
pub use language::*;
|
||||
use lsp::{InitializeParams, LanguageServerBinary};
|
||||
use project::lsp_store::rust_analyzer_ext::CARGO_DIAGNOSTICS_SOURCE_NAME;
|
||||
@@ -514,64 +514,34 @@ impl LspInstaller for RustLspAdapter {
|
||||
AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe
|
||||
};
|
||||
|
||||
let binary = LanguageServerBinary {
|
||||
path: server_path.clone(),
|
||||
env: None,
|
||||
arguments: Default::default(),
|
||||
};
|
||||
|
||||
let metadata_path = destination_path.with_extension("metadata");
|
||||
let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
|
||||
.await
|
||||
.ok();
|
||||
if let Some(metadata) = metadata {
|
||||
let validity_check = async || {
|
||||
|
||||
let server_path_for_check = server_path.clone();
|
||||
fetch_github_binary_with_digest_check(
|
||||
&server_path,
|
||||
&metadata_path,
|
||||
expected_digest,
|
||||
&url,
|
||||
Self::GITHUB_ASSET_KIND,
|
||||
&destination_path,
|
||||
&*delegate.http_client(),
|
||||
|| async move {
|
||||
delegate
|
||||
.try_exec(LanguageServerBinary {
|
||||
path: server_path.clone(),
|
||||
path: server_path_for_check,
|
||||
arguments: vec!["--version".into()],
|
||||
env: None,
|
||||
})
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
|
||||
log::warn!("Unable to run rust-analyzer asset, redownloading: {err:#}")
|
||||
})
|
||||
};
|
||||
if let (Some(actual_digest), Some(expected_digest)) =
|
||||
(&metadata.digest, &expected_digest)
|
||||
{
|
||||
if actual_digest == expected_digest {
|
||||
if validity_check().await.is_ok() {
|
||||
return Ok(binary);
|
||||
}
|
||||
} else {
|
||||
log::info!(
|
||||
"SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
|
||||
);
|
||||
}
|
||||
} else if validity_check().await.is_ok() {
|
||||
return Ok(binary);
|
||||
}
|
||||
}
|
||||
|
||||
download_server_binary(
|
||||
&*delegate.http_client(),
|
||||
&url,
|
||||
expected_digest.as_deref(),
|
||||
&destination_path,
|
||||
Self::GITHUB_ASSET_KIND,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
make_file_executable(&server_path).await?;
|
||||
remove_matching(&container_dir, |path| path != destination_path).await;
|
||||
GithubBinaryMetadata::write_to_file(
|
||||
&GithubBinaryMetadata {
|
||||
metadata_version: 1,
|
||||
digest: expected_digest,
|
||||
},
|
||||
&metadata_path,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: server_path,
|
||||
|
||||
Reference in New Issue
Block a user