Skip to content

Commit

Permalink
Verify checksum when installing Node.js (#140)
Browse files Browse the repository at this point in the history
* Verify checksum when installing Node.js

* Implement Installer for TailwindCSS
  • Loading branch information
JunichiSugiura authored Dec 4, 2022
1 parent b95fe8e commit 2e1ff86
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 100 deletions.
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ keywords = ["declarative-ui", "ecs", "bevy", "dioxus", "cross-platform"]
anyhow = "1.0"
bevy = { version = "0.8", default-features = false }
bevy_ecs = "0.8"
bytes = "1"
cmd_lib = "1"
config = "0.13"
convert_case = "0.5"
Expand All @@ -39,13 +40,15 @@ dip_macro = { path = "./plugins/macro" }
dip_task = { path = "./plugins/task" }
dirs = "4.0"
flate2 = "1.0"
hex = "0.4"
pathdiff = "0.2"
reqwest = { version = "0.11", features = ["json", "blocking"] }
tar = "0.4"
tokio = { version = "1.18", features = ["rt-multi-thread", "sync", "macros", "fs"], default-features = false }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
sha2 = "0.10"
tempfile = "3"
walkdir = "2"

Expand Down
2 changes: 1 addition & 1 deletion docs/handbook/Internal
5 changes: 4 additions & 1 deletion plugins/bundle/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@ keywords.workspace = true
[dependencies]
anyhow.workspace = true
bevy.workspace = true
bytes.workspace = true
dip_core.workspace = true
dip_macro.workspace = true
dirs.workspace = true
cmd_lib.workspace = true
config.workspace = true
convert_case.workspace = true
flate2.workspace = true
hex.workspace = true
pathdiff.workspace = true
reqwest.workspace = true
serde.workspace = true
sha2.workspace = true
tar.workspace = true
tempfile.workspace = true
tokio.workspace = true
Expand All @@ -29,7 +33,6 @@ walkdir.workspace = true
[features]
default = ["full"]
full = ["brew", "dotfiles", "nodejs", "scripts", "tailwindcss", "vm"]
# full = ["vm", "nodejs", "tailwindcss"]
brew = []
dotfiles = []
nodejs = []
Expand Down
83 changes: 83 additions & 0 deletions plugins/bundle/src/installer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use anyhow::{bail, Context};
use bytes::Bytes;
use flate2::read::GzDecoder;
use reqwest::{header, StatusCode};
use sha2::{Digest, Sha256};
use std::{fs, os::unix::fs::PermissionsExt, path::PathBuf};
use tar::Archive;

pub trait Installer {
/// 1. Download package: e.g. https://nodejs.org/dist/v16.18.1/node-v16.18.1-darwin-arm64.tar.gz
/// 2. Verify checkum
/// 3. Unpack to path: e.g. /User/Application Support/dip/bundle/installs/nodejs/16.18.1/
fn install(
&self,
download_url: &String,
install_path: &PathBuf,
file_name: &String,
checksum: Option<&String>,
) -> anyhow::Result<()> {
let res = reqwest::blocking::get(download_url)
.context("Failed to download. Check internet connection.")?;

match res.status() {
StatusCode::OK => {
match res.headers()[header::CONTENT_TYPE].to_str()? {
"application/gzip" => {
let bytes = res.bytes()?;

if let Some(checksum) = checksum {
self.verify_checksum(&bytes, checksum)?;
}

let mut cloned_path = install_path.clone();
cloned_path.pop();

let tar = GzDecoder::new(&bytes[..]);
let mut archive = Archive::new(tar);

archive.unpack(&cloned_path)?;

fs::rename(
// e.g. /User/Application Support/dip/bundle/installs/nodejs/node-v16.18.1-darwin-arm64
cloned_path.join(&file_name),
// e.g. /User/Application Support/dip/bundle/installs/nodejs/16.18.1/
&install_path,
)?;
Ok(())
}
"application/octet-stream" => {
let file_path = &install_path.join(file_name);

fs::create_dir_all(&install_path)?;
fs::write(&file_path, &res.bytes()?)?;
fs::set_permissions(&file_path, fs::Permissions::from_mode(0o755))?;

Ok(())
}
unsupported_content_type => {
bail!(
"Content-Type is not supported: {}",
unsupported_content_type
);
}
}
}
StatusCode::NOT_FOUND => {
bail!("Download URL not found: {download_url}");
}
_ => {
bail!("Fail to download binary")
}
}
}

fn verify_checksum(&self, bytes: &Bytes, checksum: &String) -> anyhow::Result<()> {
let hash = Sha256::digest(&bytes);
if hash[..] == hex::decode(checksum)? {
Ok(())
} else {
bail!("Checksum doesn't match")
}
}
}
2 changes: 2 additions & 0 deletions plugins/bundle/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
mod config;
mod installer;
mod platform;
mod schedule;
mod tool;

pub use crate::{
config::{BundleConfig, BundleConfigPlugin},
installer::Installer,
schedule::{BundleSchedulePlugin, BundleStage},
tool::{InstallTools, ToolPlugin},
};
Expand Down
27 changes: 21 additions & 6 deletions plugins/bundle/src/tool/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use nodejs::NodeJSPlugin;
#[cfg(feature = "tailwindcss")]
use tailwindcss::TailwindCSSPlugin;

use crate::Bundler;
use crate::{Bundler, Installer};
use anyhow::Context;
use bevy::app::{App, Plugin};
use std::{fs, path::PathBuf};
Expand All @@ -26,7 +26,15 @@ impl Plugin for VersionManagerPlugin {
}
}

pub trait VersionManager: Bundler {
pub trait VersionManager: Bundler + Installer {
fn file_name(&self, version: &String) -> String;

fn file_name_without_ext(&self, version: &String) -> String;

fn download_file_name(&self, version: &String) -> String;

fn download_url(&self, version: &String) -> String;

fn installs_dir(&self) -> PathBuf {
self.bundle_config().install_root().join(Self::key())
}
Expand All @@ -41,9 +49,9 @@ pub trait VersionManager: Bundler {
self.installs_dir().join(version)
}

fn download_url(&self, version: &String) -> String;

fn install(&self, version: &String) -> anyhow::Result<()>;
fn install_path(&self, version: &String) -> PathBuf {
self.installs_dir().join(version)
}

fn list_shims() -> Vec<&'static str>;

Expand Down Expand Up @@ -80,7 +88,12 @@ pub trait VersionManager: Bundler {
continue;
}

self.install(v)?;
self.install(
&self.download_url(v),
&self.install_path(v),
&self.download_file_name(v),
self.checksum(v)?.to_owned().as_ref(),
)?;
println!("Installed: {}", &p.display());
}

Expand All @@ -94,6 +107,8 @@ pub trait VersionManager: Bundler {
Ok(())
}

fn checksum(&self, version: &String) -> anyhow::Result<Option<String>>;

/// Iterate over each versions currently installed but removed from the user bundle config
fn clean_all(&self) -> anyhow::Result<()> {
if self.installs_dir().is_dir() {
Expand Down
99 changes: 45 additions & 54 deletions plugins/bundle/src/tool/vm/nodejs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ use bevy::{
app::{App, Plugin},
ecs::{event::EventReader, system::Res},
};
use flate2::read::GzDecoder;
use reqwest::StatusCode;
use dip_macro::Installer;
use std::{fs, io::Write, os::unix::fs::PermissionsExt};
use tar::Archive;

pub struct NodeJSPlugin;

Expand Down Expand Up @@ -49,6 +47,7 @@ fn apply(mut events: EventReader<ApplyBundle>, config: Res<BundleConfig>) {
});
}

#[derive(Installer)]
struct NodeJS {
bundle_config: BundleConfig,
platform: Platform,
Expand All @@ -62,20 +61,8 @@ impl NodeJS {
}
}

fn file_name_without_ext(&self, version: &String) -> String {
format!(
"node-v{version}-{name}-{arch}",
name = self.platform.name(),
arch = Platform::arch(),
)
}

fn file_name(&self, version: &String) -> String {
format!(
"{}{archive_ext}",
self.file_name_without_ext(version),
archive_ext = self.platform.archive_ext(),
)
fn version_url(&self, version: &String) -> String {
format!("https://nodejs.org/dist/v{version}")
}
}

Expand All @@ -94,53 +81,57 @@ impl Bundler for NodeJS {
}

impl VersionManager for NodeJS {
fn versions(&self) -> &Vec<String> {
&self.bundle_config().runtime().nodejs
fn file_name(&self, version: &String) -> String {
format!(
"{}{archive_ext}",
self.file_name_without_ext(version),
archive_ext = self.platform.archive_ext(),
)
}

fn file_name_without_ext(&self, version: &String) -> String {
format!(
"node-v{version}-{name}-{arch}",
name = self.platform.name(),
arch = Platform::arch(),
)
}

fn download_file_name(&self, version: &String) -> String {
self.file_name_without_ext(version)
}

fn download_url(&self, version: &String) -> String {
format!(
"https://nodejs.org/dist/v{version}/{file_name}",
"{version_url}/{file_name}",
version_url = &self.version_url(version),
file_name = &self.file_name(&version),
)
}

fn install(&self, version: &String) -> anyhow::Result<()> {
let download_url = self.download_url(version);

let res = reqwest::blocking::get(&download_url)
.context("Failed to download. Check internet connection.")?;
fn versions(&self) -> &Vec<String> {
&self.bundle_config().runtime().nodejs
}

match res.status() {
StatusCode::NOT_FOUND => {
bail!("Download URL not found: {download_url}");
}
StatusCode::OK => {
if res.status() == StatusCode::NOT_FOUND {
bail!("Download URL not found: {download_url}");
}
let bytes = res.bytes()?;

if cfg!(unix) {
let tar = GzDecoder::new(&bytes[..]);
let mut archive = Archive::new(tar);

archive.unpack(&self.installs_dir())?;
fs::rename(
&self
.installs_dir()
.join(&self.file_name_without_ext(&version)),
&self.installs_dir().join(&version),
)?;
} else if cfg!(windows) {
// win: zip
todo!("Implement zip extraction logic for Windows");
}

Ok(())
fn checksum(&self, version: &String) -> anyhow::Result<Option<String>> {
let url = format!(
"{version_url}/SHASUMS256.txt",
version_url = &self.version_url(version)
);
let res = reqwest::blocking::get(&url)
.context("Failed to fetch checksum. Check internet connection.")?;

match res
.text()?
.lines()
.find(|ln| ln.contains(&self.file_name(version)))
{
Some(ln) => {
let checksum = ln.split(" ").next().context("Cannot find checksum")?;
Ok(Some(checksum.to_string()))
}
_ => {
bail!("Fail to download binary")
None => {
bail!("Cannot find checksum");
}
}
}
Expand Down
Loading

1 comment on commit 2e1ff86

@vercel
Copy link

@vercel vercel bot commented on 2e1ff86 Dec 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

dip – ./

dip-git-main-diptools.vercel.app
dip.tools
dip-diptools.vercel.app

Please sign in to comment.