diff --git a/Cargo.lock b/Cargo.lock index 378a93e91df240..8afcd1f638451b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2209,6 +2209,21 @@ version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +[[package]] +name = "git2" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf97ba92db08df386e10c8ede66a2a0369bd277090afd8710e19e38de9ec0cd" +dependencies = [ + "bitflags 2.3.3", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url 2.4.1", +] + [[package]] name = "glob" version = "0.3.0" @@ -2447,6 +2462,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.8.0" @@ -2521,6 +2542,25 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "hyper-staticfile" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "318ca89e4827e7fe4ddd2824f52337239796ae8ecc761a663324407dc3d8d7e7" +dependencies = [ + "futures-util", + "http", + "http-range", + "httpdate", + "hyper", + "mime_guess", + "percent-encoding 2.3.0", + "rand 0.8.5", + "tokio", + "url 2.4.1", + "winapi 0.3.9", +] + [[package]] name = "hyper-timeout" version = "0.4.1" @@ -2872,6 +2912,20 @@ version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +[[package]] +name = "libgit2-sys" +version = "0.16.1+1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2951,6 +3005,20 @@ dependencies = [ "libsecp256k1-core", ] +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-sys" version = "1.1.3" @@ -2958,6 +3026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" dependencies = [ "cc", + "libc", "pkg-config", "vcpkg", ] @@ -3113,6 +3182,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "min-max-heap" version = "1.3.0" @@ -3939,7 +4018,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "toml", + "toml 0.5.8", ] [[package]] @@ -3949,7 +4028,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" dependencies = [ "thiserror", - "toml", + "toml 0.5.8", ] [[package]] @@ -4784,6 +4863,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5471,6 +5559,35 @@ dependencies = [ "tar", ] +[[package]] +name = "solana-cargo-registry" +version = "1.18.0" +dependencies = [ + "clap 2.33.3", + "flate2", + "git2", + "hyper", + "hyper-staticfile", + "log", + "rustc_version 0.4.0", + "serde", + "serde_json", + "solana-clap-utils", + "solana-cli", + "solana-cli-config", + "solana-cli-output", + "solana-logger", + "solana-remote-wallet", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-sdk", + "solana-version", + "tar", + "tempfile", + "tokio", + "toml 0.8.2", +] + [[package]] name = "solana-cargo-test-bpf" version = "1.18.0" @@ -8371,6 +8488,40 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.0.2", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.9.2" @@ -9057,6 +9208,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winnow" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index 05c6241523883a..509ffb6047ac9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "bench-tps", "bloom", "bucket_map", + "cargo-registry", "clap-utils", "clap-v3-utils", "cli", @@ -205,6 +206,7 @@ gag = "1.0.0" generic-array = { version = "0.14.7", default-features = false } gethostname = "0.2.3" getrandom = "0.2.10" +git2 = "0.18.1" goauth = "0.13.1" hex = "0.4.3" hidapi = { version = "2.4.1", default-features = false } @@ -214,6 +216,7 @@ http = "0.2.9" humantime = "2.0.1" hyper = "0.14.27" hyper-proxy = "0.9.1" +hyper-staticfile = "0.9.5" im = "15.1.0" index_list = "0.2.7" indexmap = "2.0.2" @@ -311,6 +314,7 @@ solana-bench-tps = { path = "bench-tps", version = "=1.18.0" } solana-bloom = { path = "bloom", version = "=1.18.0" } solana-bpf-loader-program = { path = "programs/bpf_loader", version = "=1.18.0" } solana-bucket-map = { path = "bucket_map", version = "=1.18.0" } +solana-cargo-registry = { path = "cargo-registry", version = "=1.18.0" } solana-connection-cache = { path = "connection-cache", version = "=1.18.0", default-features = false } solana-clap-utils = { path = "clap-utils", version = "=1.18.0" } solana-clap-v3-utils = { path = "clap-v3-utils", version = "=1.18.0" } @@ -405,6 +409,7 @@ tokio-serde = "0.8" tokio-stream = "0.1.14" tokio-tungstenite = "0.20.1" tokio-util = "0.6" +toml = "0.8.0" tonic = "0.9.2" tonic-build = "0.9.2" trees = "0.4.2" diff --git a/cargo-registry/Cargo.toml b/cargo-registry/Cargo.toml new file mode 100644 index 00000000000000..43aed1f4fa2097 --- /dev/null +++ b/cargo-registry/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "solana-cargo-registry" +description = "Solana cargo registry" +documentation = "https://docs.rs/solana-cargo-registry" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] +clap = { workspace = true } +flate2 = { workspace = true } +git2 = { workspace = true } +hyper = { workspace = true, features = ["full"] } +hyper-staticfile = { workspace = true } +log = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +solana-clap-utils = { workspace = true } +solana-cli = { workspace = true } +solana-cli-config = { workspace = true } +solana-cli-output = { workspace = true } +solana-logger = { workspace = true } +solana-remote-wallet = { workspace = true, features = ["default"] } +solana-rpc-client = { workspace = true, features = ["default"] } +solana-rpc-client-api = { workspace = true } +solana-sdk = { workspace = true } +solana-version = { workspace = true } +tar = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true, features = ["full"] } +toml = { workspace = true } + +[dev-dependencies] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[build-dependencies] +rustc_version = { workspace = true } + +[features] +dev-context-only-utils = [] diff --git a/cargo-registry/src/client.rs b/cargo-registry/src/client.rs new file mode 100644 index 00000000000000..17432f0ebe27cd --- /dev/null +++ b/cargo-registry/src/client.rs @@ -0,0 +1,209 @@ +use { + clap::{crate_description, crate_name, value_t_or_exit, App, Arg, ArgMatches}, + solana_clap_utils::{ + hidden_unless_forced, + input_validators::is_url_or_moniker, + keypair::{DefaultSigner, SignerIndex}, + }, + solana_cli::{ + cli::{DEFAULT_CONFIRM_TX_TIMEOUT_SECONDS, DEFAULT_RPC_TIMEOUT_SECONDS}, + program_v4::ProgramV4CommandConfig, + }, + solana_cli_config::{Config, ConfigInput}, + solana_cli_output::OutputFormat, + solana_rpc_client::rpc_client::RpcClient, + solana_sdk::{ + commitment_config, + signature::{read_keypair_file, Keypair}, + }, + std::{error, sync::Arc, time::Duration}, +}; + +pub struct ClientConfig<'a>(pub ProgramV4CommandConfig<'a>); + +impl<'a> ClientConfig<'a> { + pub fn new(client: &'a Client) -> Self { + Self(ProgramV4CommandConfig { + websocket_url: &client.websocket_url, + commitment: client.commitment, + payer: &client.cli_signers[0], + authority: &client.cli_signers[client.authority_signer_index], + output_format: &OutputFormat::Display, + use_quic: true, + }) + } +} + +pub struct Client { + pub rpc_client: Arc, + pub port: u16, + websocket_url: String, + commitment: commitment_config::CommitmentConfig, + cli_signers: Vec, + authority_signer_index: SignerIndex, +} + +impl Client { + fn get_keypair( + matches: &ArgMatches<'_>, + config_path: &str, + name: &str, + ) -> Result> { + let (_, default_signer_path) = ConfigInput::compute_keypair_path_setting( + matches.value_of(name).unwrap_or(""), + config_path, + ); + + let default_signer = DefaultSigner::new(name, default_signer_path); + + read_keypair_file(default_signer.path) + } + + fn get_clap_app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, 'v> { + App::new(name) + .about(about) + .version(version) + .arg( + Arg::with_name("config_file") + .short("C") + .long("config") + .value_name("FILEPATH") + .takes_value(true) + .global(true) + .help("Configuration file to use"), + ) + .arg( + Arg::with_name("json_rpc_url") + .short("u") + .long("url") + .value_name("URL_OR_MONIKER") + .takes_value(true) + .global(true) + .validator(is_url_or_moniker) + .help( + "URL for Solana's JSON RPC or moniker (or their first letter): \ + [mainnet-beta, testnet, devnet, localhost]", + ), + ) + .arg( + Arg::with_name("keypair") + .short("k") + .long("keypair") + .value_name("KEYPAIR") + .global(true) + .takes_value(true) + .help("Filepath or URL to a keypair"), + ) + .arg( + Arg::with_name("authority") + .short("a") + .long("authority") + .value_name("KEYPAIR") + .global(true) + .takes_value(true) + .help("Filepath or URL to program authority keypair"), + ) + .arg( + Arg::with_name("port") + .short("p") + .long("port") + .value_name("PORT") + .global(true) + .takes_value(true) + .help("Cargo registry's local TCP port. The server will bind to this port and wait for requests."), + ) + .arg( + Arg::with_name("commitment") + .long("commitment") + .takes_value(true) + .possible_values(&[ + "processed", + "confirmed", + "finalized", + ]) + .value_name("COMMITMENT_LEVEL") + .hide_possible_values(true) + .global(true) + .help("Return information at the selected commitment level [possible values: processed, confirmed, finalized]"), + ) + .arg( + Arg::with_name("rpc_timeout") + .long("rpc-timeout") + .value_name("SECONDS") + .takes_value(true) + .default_value(DEFAULT_RPC_TIMEOUT_SECONDS) + .global(true) + .hidden(hidden_unless_forced()) + .help("Timeout value for RPC requests"), + ) + .arg( + Arg::with_name("confirm_transaction_initial_timeout") + .long("confirm-timeout") + .value_name("SECONDS") + .takes_value(true) + .default_value(DEFAULT_CONFIRM_TX_TIMEOUT_SECONDS) + .global(true) + .hidden(hidden_unless_forced()) + .help("Timeout value for initial transaction status"), + ) + } + + pub fn new() -> Result> { + let matches = Self::get_clap_app( + crate_name!(), + crate_description!(), + solana_version::version!(), + ) + .get_matches(); + + let config = if let Some(config_file) = matches.value_of("config_file") { + Config::load(config_file).unwrap_or_default() + } else { + Config::default() + }; + + let (_, json_rpc_url) = ConfigInput::compute_json_rpc_url_setting( + matches.value_of("json_rpc_url").unwrap_or(""), + &config.json_rpc_url, + ); + + let (_, websocket_url) = ConfigInput::compute_websocket_url_setting( + matches.value_of("websocket_url").unwrap_or(""), + &config.websocket_url, + matches.value_of("json_rpc_url").unwrap_or(""), + &config.json_rpc_url, + ); + + let (_, commitment) = ConfigInput::compute_commitment_config( + matches.value_of("commitment").unwrap_or(""), + &config.commitment, + ); + + let rpc_timeout = value_t_or_exit!(matches, "rpc_timeout", u64); + let rpc_timeout = Duration::from_secs(rpc_timeout); + + let confirm_transaction_initial_timeout = + value_t_or_exit!(matches, "confirm_transaction_initial_timeout", u64); + let confirm_transaction_initial_timeout = + Duration::from_secs(confirm_transaction_initial_timeout); + + let payer_keypair = Self::get_keypair(&matches, &config.keypair_path, "keypair")?; + let authority_keypair = Self::get_keypair(&matches, &config.keypair_path, "authority")?; + + let port = value_t_or_exit!(matches, "port", u16); + + Ok(Client { + rpc_client: Arc::new(RpcClient::new_with_timeouts_and_commitment( + json_rpc_url.to_string(), + rpc_timeout, + commitment, + confirm_transaction_initial_timeout, + )), + port, + websocket_url, + commitment, + cli_signers: vec![payer_keypair, authority_keypair], + authority_signer_index: 1, + }) + } +} diff --git a/cargo-registry/src/dummy_git_index.rs b/cargo-registry/src/dummy_git_index.rs new file mode 100644 index 00000000000000..1b36f485ebff3e --- /dev/null +++ b/cargo-registry/src/dummy_git_index.rs @@ -0,0 +1,119 @@ +use { + git2::{IndexAddOption, Repository}, + serde::{Deserialize, Serialize}, + std::{ + fs::{self, create_dir_all}, + io::ErrorKind, + net::SocketAddr, + path::PathBuf, + process::Command, + }, +}; + +#[derive(Debug, Default, Deserialize, Serialize)] +struct RegistryConfig { + dl: String, + api: Option, +} + +pub struct DummyGitIndex {} + +impl DummyGitIndex { + pub fn create_or_update_git_repo(root_dir: PathBuf, server_addr: &SocketAddr) { + create_dir_all(&root_dir).expect("Failed to create root directory"); + + let expected_config = serde_json::to_string(&RegistryConfig { + dl: format!( + "http://{}/api/v1/crates/{{crate}}/{{version}}/download", + server_addr + ), + api: Some(format!("http://{}", server_addr)), + }) + .expect("Failed to create expected config"); + + let config_path = root_dir.join("config.json"); + let config_written = if let Ok(config) = fs::read_to_string(&config_path) { + if config != expected_config { + fs::write(config_path, expected_config).expect("Failed to update config"); + true + } else { + false + } + } else { + fs::write(config_path, expected_config).expect("Failed to write config"); + true + }; + + #[cfg(unix)] + use std::os::unix::fs::symlink; + #[cfg(windows)] + use std::os::windows::fs::symlink_dir as symlink; + + let new_symlink = match symlink(".", root_dir.join("index")) { + Ok(()) => true, + Err(ref err) if err.kind() == ErrorKind::AlreadyExists => false, + Err(err) => panic!("Failed to create a symlink: {}", err), + }; + + let new_git_symlink = match symlink(".git", root_dir.join("git")) { + Ok(()) => true, + Err(ref err) if err.kind() == ErrorKind::AlreadyExists => false, + Err(err) => panic!("Failed to create git symlink: {}", err), + }; + + let repository = Repository::init(&root_dir).expect("Failed to GIT init"); + + let empty = repository + .is_empty() + .expect("Failed to check if GIT repo is empty"); + + if empty || config_written || new_symlink || new_git_symlink { + let mut index = repository.index().expect("cannot get the Index file"); + index + .add_all(["*"].iter(), IndexAddOption::DEFAULT, None) + .expect("Failed to add modified files to git index"); + index.write().expect("Failed to update the git index"); + + let tree = index + .write_tree() + .and_then(|tree_id| repository.find_tree(tree_id)) + .expect("Failed to get tree"); + + let signature = repository.signature().expect("Failed to get signature"); + + if empty { + repository.commit( + Some("HEAD"), + &signature, + &signature, + "Created new repo", + &tree, + &[], + ) + } else { + let oid = repository + .refname_to_id("HEAD") + .expect("Failed to get HEAD ref"); + let parent = repository + .find_commit(oid) + .expect("Failed to find parent commit"); + + repository.commit( + Some("HEAD"), + &signature, + &signature, + "Updated GIT repo", + &tree, + &[&parent], + ) + } + .expect("Failed to commit the changes"); + } + + Command::new("git") + .current_dir(&root_dir) + .arg("update-server-info") + .status() + .expect("git update-server-info failed"); + } +} diff --git a/cargo-registry/src/main.rs b/cargo-registry/src/main.rs new file mode 100644 index 00000000000000..0749875824c072 --- /dev/null +++ b/cargo-registry/src/main.rs @@ -0,0 +1,327 @@ +//! The `registry_service` module implements the Solana cargo registry service. +use { + crate::{ + client::Client, + dummy_git_index::DummyGitIndex, + publisher::{Error, Publisher}, + }, + hyper::{ + body, + service::{make_service_fn, service_fn}, + Method, Server, + }, + hyper_staticfile::Static, + log::*, + std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + path::PathBuf, + sync::Arc, + }, +}; + +mod client; +mod dummy_git_index; +mod publisher; + +const PATH_PREFIX: &str = "/api/v1/crates"; + +pub struct CargoRegistryService {} + +impl CargoRegistryService { + fn error_response(status: hyper::StatusCode, msg: &str) -> hyper::Response { + error!("{}", msg); + hyper::Response::builder() + .status(status) + .body(hyper::Body::from( + serde_json::json!({ + "errors" : [ + {"details": msg} + ] + }) + .to_string(), + )) + .unwrap() + } + + fn success_response() -> hyper::Response { + hyper::Response::builder() + .status(hyper::StatusCode::OK) + .body(hyper::Body::from("")) + .unwrap() + } + + async fn handle_publish_request( + request: hyper::Request, + client: Arc, + ) -> hyper::Response { + info!("Handling request to publish the crate"); + let bytes = body::to_bytes(request.into_body()).await; + + match bytes { + Ok(data) => { + let Ok(result) = + tokio::task::spawn_blocking(move || Publisher::publish_crate(data, client)) + .await + else { + return Self::error_response( + hyper::StatusCode::INTERNAL_SERVER_ERROR, + "Internal error. Failed to wait for program deployment", + ); + }; + + if result.is_ok() { + info!("Published the crate successfully. {:?}", result); + Self::success_response() + } else { + Self::error_response( + hyper::StatusCode::BAD_REQUEST, + format!("Failed to publish the crate. {:?}", result).as_str(), + ) + } + } + Err(_) => Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Failed to receive the crate data from the client.", + ), + } + } + + fn get_crate_name_and_version(path: &str) -> Option<(&str, &str, &str)> { + path.rsplit_once('/').and_then(|(remainder, version)| { + remainder + .rsplit_once('/') + .map(|(remainder, name)| (remainder, name, version)) + }) + } + + fn handle_yank_request( + path: &str, + _request: &hyper::Request, + ) -> hyper::Response { + let Some((path, _crate_name, _version)) = Self::get_crate_name_and_version(path) else { + return Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Failed to parse the request.", + ); + }; + + if path.len() != PATH_PREFIX.len() { + return Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Request length is incorrect", + ); + } + + Self::error_response( + hyper::StatusCode::NOT_IMPLEMENTED, + "This command is not implemented yet", + ) + } + + fn handle_unyank_request( + path: &str, + _request: &hyper::Request, + ) -> hyper::Response { + let Some((path, _crate_name, _version)) = Self::get_crate_name_and_version(path) else { + return Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Failed to parse the request.", + ); + }; + + if path.len() != PATH_PREFIX.len() { + return Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Request length is incorrect", + ); + } + + Self::error_response( + hyper::StatusCode::NOT_IMPLEMENTED, + "This command is not implemented yet", + ) + } + + fn get_crate_name(path: &str) -> Option<(&str, &str)> { + path.rsplit_once('/') + } + + fn handle_get_owners_request( + path: &str, + _request: &hyper::Request, + ) -> hyper::Response { + let Some((path, _crate_name)) = Self::get_crate_name(path) else { + return Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Failed to parse the request.", + ); + }; + + if path.len() != PATH_PREFIX.len() { + return Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Request length is incorrect", + ); + } + + Self::error_response( + hyper::StatusCode::NOT_IMPLEMENTED, + "This command is not implemented yet", + ) + } + + fn handle_add_owners_request( + path: &str, + _request: &hyper::Request, + ) -> hyper::Response { + let Some((path, _crate_name)) = Self::get_crate_name(path) else { + return Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Failed to parse the request.", + ); + }; + + if path.len() != PATH_PREFIX.len() { + return Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Request length is incorrect", + ); + } + + Self::error_response( + hyper::StatusCode::NOT_IMPLEMENTED, + "This command is not implemented yet", + ) + } + + fn handle_delete_owners_request( + path: &str, + _request: &hyper::Request, + ) -> hyper::Response { + let Some((path, _crate_name)) = Self::get_crate_name(path) else { + return Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Failed to parse the request.", + ); + }; + + if path.len() != PATH_PREFIX.len() { + return Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Request length is incorrect", + ); + } + + Self::error_response( + hyper::StatusCode::NOT_IMPLEMENTED, + "This command is not implemented yet", + ) + } + + fn handle_get_crates_request( + path: &str, + _request: &hyper::Request, + ) -> hyper::Response { + // The endpoint for this type of request is `/api/v1/crates` (same as PATH_PREFIX). + // The `crates` substring has already been extracted out of the endpoint string. + // So the path should only contain `/api/v1". The caller already checked that the + // full path started with PATH_PREFIX. So it's sufficient to check that provided + // path is smaller than PATH_PREFIX. + if path.len() >= PATH_PREFIX.len() { + return Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Request length is incorrect", + ); + } + + Self::error_response( + hyper::StatusCode::NOT_IMPLEMENTED, + "This command is not implemented yet", + ) + } + + async fn handler( + request: hyper::Request, + client: Arc, + ) -> Result, Error> { + let path = request.uri().path(); + if path.starts_with("/git") { + return Static::new("/tmp/dummy-git") + .serve(request) + .await + .or_else(|_| { + Ok(Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Failed to serve git index", + )) + }); + } + + if !path.starts_with(PATH_PREFIX) { + return Ok(Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Invalid path for the request", + )); + } + + let Some((path, endpoint)) = path.rsplit_once('/') else { + return Ok(Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Invalid endpoint in the path", + )); + }; + + Ok(match *request.method() { + Method::PUT => match endpoint { + "new" => { + if path.len() != PATH_PREFIX.len() { + Self::error_response( + hyper::StatusCode::BAD_REQUEST, + "Invalid length of the request.", + ) + } else { + Self::handle_publish_request(request, client.clone()).await + } + } + "unyank" => Self::handle_unyank_request(path, &request), + "owners" => Self::handle_add_owners_request(path, &request), + _ => Self::error_response(hyper::StatusCode::METHOD_NOT_ALLOWED, "Unknown request"), + }, + Method::GET => match endpoint { + "crates" => Self::handle_get_crates_request(path, &request), + "owners" => Self::handle_get_owners_request(path, &request), + _ => Self::error_response(hyper::StatusCode::METHOD_NOT_ALLOWED, "Unknown request"), + }, + Method::DELETE => match endpoint { + "yank" => Self::handle_yank_request(path, &request), + "owners" => Self::handle_delete_owners_request(path, &request), + _ => Self::error_response(hyper::StatusCode::METHOD_NOT_ALLOWED, "Unknown request"), + }, + _ => Self::error_response(hyper::StatusCode::METHOD_NOT_ALLOWED, "Unknown request"), + }) + } +} + +#[tokio::main] +async fn main() { + solana_logger::setup_with_default("solana=info"); + let client = Arc::new(Client::new().expect("Failed to get RPC Client instance")); + let port = client.port; + + let registry_service = make_service_fn(move |_| { + let client_inner = client.clone(); + async move { + Ok::<_, Error>(service_fn(move |request| { + CargoRegistryService::handler(request, client_inner.clone()) + })) + } + }); + + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), port); + DummyGitIndex::create_or_update_git_repo(PathBuf::from("/tmp/dummy-git"), &addr); + + let server = Server::bind(&addr).serve(registry_service); + info!("Server running on on http://{}", addr); + + let _ = server.await; +} diff --git a/cargo-registry/src/publisher.rs b/cargo-registry/src/publisher.rs new file mode 100644 index 00000000000000..a712da35895e1c --- /dev/null +++ b/cargo-registry/src/publisher.rs @@ -0,0 +1,160 @@ +use { + crate::client::{Client, ClientConfig}, + flate2::read::GzDecoder, + hyper::body::Bytes, + log::*, + serde::{Deserialize, Serialize}, + serde_json::from_slice, + solana_cli::program_v4::{process_deploy_program, read_and_verify_elf}, + solana_sdk::{ + signature::{Keypair, Signer}, + signer::EncodableKey, + }, + std::{ + collections::BTreeMap, + fs, + mem::size_of, + ops::Deref, + path::{Path, PathBuf}, + sync::Arc, + }, + tar::Archive, + tempfile::{tempdir, TempDir}, +}; + +pub type Error = Box; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +enum DependencyType { + Dev, + Build, + Normal, +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +struct Dependency { + name: String, + version_req: String, + features: Vec, + optional: bool, + default_features: bool, + target: Option, + kind: DependencyType, + registry: Option, + explicit_name_in_toml: Option, +} + +#[derive(Debug, Deserialize)] +#[allow(unused)] +struct PackageMetaData { + name: String, + vers: String, + deps: Vec, + features: BTreeMap>, + authors: Vec, + description: Option, + documentation: Option, + homepage: Option, + readme: Option, + readme_file: Option, + keywords: Vec, + categories: Vec, + license: Option, + license_file: Option, + repository: Option, + badges: BTreeMap>, + links: Option, + rust_version: Option, +} + +impl PackageMetaData { + fn new(bytes: &Bytes) -> serde_json::Result<(PackageMetaData, usize)> { + let (json_length, sizeof_length) = Self::read_u32_length(bytes)?; + let end_of_meta_data = sizeof_length.saturating_add(json_length as usize); + let json_body = bytes.slice(sizeof_length..end_of_meta_data); + from_slice::(json_body.deref()).map(|data| (data, end_of_meta_data)) + } + + fn read_u32_length(bytes: &Bytes) -> serde_json::Result<(u32, usize)> { + let sizeof_length = size_of::(); + let length_le = bytes.slice(0..sizeof_length); + let length = + u32::from_le_bytes(length_le.deref().try_into().expect("Failed to read length")); + Ok((length, sizeof_length)) + } +} + +pub struct Publisher {} + +impl Publisher { + fn make_path>(tempdir: &TempDir, meta: &PackageMetaData, append: P) -> PathBuf { + let mut path = tempdir.path().to_path_buf(); + path.push(format!("{}-{}/", meta.name, meta.vers)); + path.push(append); + path + } + + fn program_library_name(tempdir: &TempDir, meta: &PackageMetaData) -> Result { + let toml_content = fs::read_to_string(Self::make_path(tempdir, meta, "Cargo.toml.orig"))?; + let toml = toml_content.parse::()?; + let library_name = toml + .get("lib") + .and_then(|v| v.get("name")) + .and_then(|v| v.as_str()) + .ok_or("Failed to get module name")?; + Ok(library_name.to_string()) + } + + pub(crate) fn publish_crate(bytes: Bytes, client: Arc) -> Result<(), Error> { + let (meta_data, offset) = PackageMetaData::new(&bytes)?; + + let (_crate_file_length, length_size) = + PackageMetaData::read_u32_length(&bytes.slice(offset..))?; + let crate_bytes = bytes.slice(offset.saturating_add(length_size)..); + + let decoder = GzDecoder::new(crate_bytes.as_ref()); + let mut archive = Archive::new(decoder); + + let tempdir = tempdir()?; + archive.unpack(tempdir.path())?; + + let config = ClientConfig::new(client.as_ref()); + + let lib_name = Self::program_library_name(&tempdir, &meta_data)?; + + let program_path = Self::make_path(&tempdir, &meta_data, format!("out/{}.so", lib_name)) + .into_os_string() + .into_string() + .map_err(|_| "Failed to get program file path")?; + + let program_data = read_and_verify_elf(program_path.as_ref()) + .map_err(|e| format!("failed to read the program: {}", e))?; + + let program_keypair = Keypair::read_from_file(Self::make_path( + &tempdir, + &meta_data, + format!("out/{}-keypair.json", lib_name), + )) + .map_err(|e| format!("Failed to get keypair from the file: {}", e))?; + + info!("Deploying program at {:?}", program_keypair.pubkey()); + + process_deploy_program( + client.rpc_client.clone(), + &config.0, + &program_data, + program_data.len() as u32, + &program_keypair.pubkey(), + Some(&program_keypair), + ) + .map_err(|e| { + error!("Failed to deploy the program: {}", e); + format!("Failed to deploy the program: {}", e) + })?; + + info!("Successfully deployed the program"); + Ok(()) + } +} diff --git a/cli/src/program_v4.rs b/cli/src/program_v4.rs index b1e0d60fa9a39c..b676656fedfa11 100644 --- a/cli/src/program_v4.rs +++ b/cli/src/program_v4.rs @@ -311,7 +311,7 @@ pub fn parse_program_v4_subcommand( Ok(response) } -fn read_and_verify_elf(program_location: &str) -> Result, Box> { +pub fn read_and_verify_elf(program_location: &str) -> Result, Box> { let mut file = File::open(program_location) .map_err(|err| format!("Unable to open program file: {err}"))?; let mut program_data = Vec::new(); @@ -427,7 +427,7 @@ pub fn process_program_v4_subcommand( // * Redeploy a program using a buffer account // - buffer_signer argument must contain the temporary buffer account information // (program_address must contain program ID and must NOT be same as buffer_signer.pubkey()) -fn process_deploy_program( +pub fn process_deploy_program( rpc_client: Arc, config: &ProgramV4CommandConfig, program_data: &[u8],