diff --git a/cli/args/mod.rs b/cli/args/mod.rs index 00476dce1c..b5975536a1 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -13,6 +13,7 @@ use self::package_json::PackageJsonDeps; use ::import_map::ImportMap; use deno_core::resolve_url_or_path; use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; +use deno_runtime::deno_tls::RootCertStoreProvider; use deno_semver::npm::NpmPackageReqReference; use indexmap::IndexMap; @@ -52,6 +53,7 @@ use deno_runtime::deno_tls::webpki_roots; use deno_runtime::inspector_server::InspectorServer; use deno_runtime::permissions::PermissionsOptions; use once_cell::sync::Lazy; +use once_cell::sync::OnceCell; use std::collections::HashMap; use std::env; use std::io::BufReader; @@ -61,6 +63,7 @@ use std::num::NonZeroUsize; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +use thiserror::Error; use crate::cache::DenoDir; use crate::file_fetcher::FileFetcher; @@ -401,13 +404,62 @@ fn discover_package_json( Ok(None) } +struct CliRootCertStoreProvider { + cell: OnceCell, + maybe_root_path: Option, + maybe_ca_stores: Option>, + maybe_ca_data: Option, +} + +impl CliRootCertStoreProvider { + pub fn new( + maybe_root_path: Option, + maybe_ca_stores: Option>, + maybe_ca_data: Option, + ) -> Self { + Self { + cell: Default::default(), + maybe_root_path, + maybe_ca_stores, + maybe_ca_data, + } + } +} + +impl RootCertStoreProvider for CliRootCertStoreProvider { + fn get_or_try_init(&self) -> Result<&RootCertStore, AnyError> { + self + .cell + .get_or_try_init(|| { + get_root_cert_store( + self.maybe_root_path.clone(), + self.maybe_ca_stores.clone(), + self.maybe_ca_data.clone(), + ) + }) + .map_err(|e| e.into()) + } +} + +#[derive(Error, Debug, Clone)] +pub enum RootCertStoreLoadError { + #[error( + "Unknown certificate store \"{0}\" specified (allowed: \"system,mozilla\")" + )] + UnknownStore(String), + #[error("Unable to add pem file to certificate store: {0}")] + FailedAddPemFile(String), + #[error("Failed opening CA file: {0}")] + CaFileOpenError(String), +} + /// Create and populate a root cert store based on the passed options and /// environment. pub fn get_root_cert_store( maybe_root_path: Option, maybe_ca_stores: Option>, maybe_ca_data: Option, -) -> Result { +) -> Result { let mut root_cert_store = RootCertStore::empty(); let ca_stores: Vec = maybe_ca_stores .or_else(|| { @@ -444,7 +496,7 @@ pub fn get_root_cert_store( } } _ => { - return Err(anyhow!("Unknown certificate store \"{}\" specified (allowed: \"system,mozilla\")", store)); + return Err(RootCertStoreLoadError::UnknownStore(store.clone())); } } } @@ -459,7 +511,9 @@ pub fn get_root_cert_store( } else { PathBuf::from(ca_file) }; - let certfile = std::fs::File::open(ca_file)?; + let certfile = std::fs::File::open(ca_file).map_err(|err| { + RootCertStoreLoadError::CaFileOpenError(err.to_string()) + })?; let mut reader = BufReader::new(certfile); rustls_pemfile::certs(&mut reader) } @@ -474,10 +528,7 @@ pub fn get_root_cert_store( root_cert_store.add_parsable_certificates(&certs); } Err(e) => { - return Err(anyhow!( - "Unable to add pem file to certificate store: {}", - e - )); + return Err(RootCertStoreLoadError::FailedAddPemFile(e.to_string())); } } } @@ -799,12 +850,14 @@ impl CliOptions { .map(|path| ModuleSpecifier::from_directory_path(path).unwrap()) } - pub fn resolve_root_cert_store(&self) -> Result { - get_root_cert_store( + pub fn resolve_root_cert_store_provider( + &self, + ) -> Arc { + Arc::new(CliRootCertStoreProvider::new( None, self.flags.ca_stores.clone(), self.flags.ca_data.clone(), - ) + )) } pub fn resolve_ts_config_for_emit( diff --git a/cli/factory.rs b/cli/factory.rs index 69560cf544..73d0cb8ea9 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -45,7 +45,7 @@ use deno_core::parking_lot::Mutex; use deno_runtime::deno_node; use deno_runtime::deno_node::analyze::NodeCodeTranslator; use deno_runtime::deno_node::NodeResolver; -use deno_runtime::deno_tls::rustls::RootCertStore; +use deno_runtime::deno_tls::RootCertStoreProvider; use deno_runtime::deno_web::BlobStore; use deno_runtime::inspector_server::InspectorServer; use deno_semver::npm::NpmPackageReqReference; @@ -129,14 +129,14 @@ struct CliFactoryServices { dir: Deferred, caches: Deferred>, file_fetcher: Deferred>, - http_client: Deferred, + http_client: Deferred>, emit_cache: Deferred, emitter: Deferred>, graph_container: Deferred>, lockfile: Deferred>>>, maybe_import_map: Deferred>>, maybe_inspector_server: Deferred>>, - root_cert_store: Deferred, + root_cert_store_provider: Deferred>, blob_store: Deferred, parsed_source_cache: Deferred>, resolver: Deferred>, @@ -208,11 +208,11 @@ impl CliFactory { self.services.blob_store.get_or_init(BlobStore::default) } - pub fn root_cert_store(&self) -> Result<&RootCertStore, AnyError> { + pub fn root_cert_store_provider(&self) -> &Arc { self .services - .root_cert_store - .get_or_try_init(|| self.options.resolve_root_cert_store()) + .root_cert_store_provider + .get_or_init(|| self.options.resolve_root_cert_store_provider()) } pub fn text_only_progress_bar(&self) -> &ProgressBar { @@ -222,12 +222,12 @@ impl CliFactory { .get_or_init(|| ProgressBar::new(ProgressBarStyle::TextOnly)) } - pub fn http_client(&self) -> Result<&HttpClient, AnyError> { - self.services.http_client.get_or_try_init(|| { - HttpClient::new( - Some(self.root_cert_store()?.clone()), + pub fn http_client(&self) -> &Arc { + self.services.http_client.get_or_init(|| { + Arc::new(HttpClient::new( + Some(self.root_cert_store_provider().clone()), self.options.unsafely_ignore_certificate_errors().clone(), - ) + )) }) } @@ -237,7 +237,7 @@ impl CliFactory { HttpCache::new(&self.deno_dir()?.deps_folder_path()), self.options.cache_setting(), !self.options.no_remote(), - self.http_client()?.clone(), + self.http_client().clone(), self.blob_store().clone(), Some(self.text_only_progress_bar().clone()), ))) @@ -256,7 +256,7 @@ impl CliFactory { Ok(Arc::new(NpmCache::new( self.deno_dir()?.npm_folder_path(), self.options.cache_setting(), - self.http_client()?.clone(), + self.http_client().clone(), self.text_only_progress_bar().clone(), ))) }) @@ -267,7 +267,7 @@ impl CliFactory { Ok(Arc::new(CliNpmRegistryApi::new( CliNpmRegistryApi::default_url().to_owned(), self.npm_cache()?.clone(), - self.http_client()?.clone(), + self.http_client().clone(), self.text_only_progress_bar().clone(), ))) }) @@ -554,7 +554,7 @@ impl CliFactory { let options = self.cli_options().clone(); let main_worker_options = self.create_cli_main_worker_options()?; let node_fs = self.node_fs().clone(); - let root_cert_store = self.root_cert_store()?.clone(); + let root_cert_store_provider = self.root_cert_store_provider().clone(); let node_resolver = self.node_resolver().await?.clone(); let npm_resolver = self.npm_resolver().await?.clone(); let maybe_inspector_server = self.maybe_inspector_server().clone(); @@ -578,7 +578,7 @@ impl CliFactory { node_resolver.clone(), ), )), - root_cert_store.clone(), + root_cert_store_provider.clone(), node_fs.clone(), maybe_inspector_server.clone(), main_worker_options.clone(), @@ -609,7 +609,7 @@ impl CliFactory { node_resolver.clone(), ), )), - self.root_cert_store()?.clone(), + self.root_cert_store_provider().clone(), self.node_fs().clone(), self.maybe_inspector_server().clone(), self.create_cli_main_worker_options()?, diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index 38b96c72de..fd8c0f7939 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -178,7 +178,7 @@ pub struct FileFetcher { cache: FileCache, cache_setting: CacheSetting, pub http_cache: HttpCache, - http_client: HttpClient, + http_client: Arc, blob_store: BlobStore, download_log_level: log::Level, progress_bar: Option, @@ -189,7 +189,7 @@ impl FileFetcher { http_cache: HttpCache, cache_setting: CacheSetting, allow_remote: bool, - http_client: HttpClient, + http_client: Arc, blob_store: BlobStore, progress_bar: Option, ) -> Self { @@ -660,7 +660,7 @@ async fn fetch_once<'a>( http_client: &HttpClient, args: FetchOnceArgs<'a>, ) -> Result { - let mut request = http_client.get_no_redirect(args.url.clone()); + let mut request = http_client.get_no_redirect(args.url.clone())?; if let Some(etag) = args.maybe_etag { let if_none_match_val = HeaderValue::from_str(&etag)?; @@ -769,7 +769,7 @@ mod tests { HttpCache::new(&location), cache_setting, true, - HttpClient::new(None, None).unwrap(), + Arc::new(HttpClient::new(None, None)), blob_store.clone(), None, ); @@ -1207,7 +1207,7 @@ mod tests { HttpCache::new(&location), CacheSetting::ReloadAll, true, - HttpClient::new(None, None).unwrap(), + Arc::new(HttpClient::new(None, None)), BlobStore::default(), None, ); @@ -1232,7 +1232,7 @@ mod tests { HttpCache::new(&location), CacheSetting::Use, true, - HttpClient::new(None, None).unwrap(), + Arc::new(HttpClient::new(None, None)), BlobStore::default(), None, ); @@ -1257,7 +1257,7 @@ mod tests { HttpCache::new(&location), CacheSetting::Use, true, - HttpClient::new(None, None).unwrap(), + Arc::new(HttpClient::new(None, None)), BlobStore::default(), None, ); @@ -1398,7 +1398,7 @@ mod tests { HttpCache::new(&location), CacheSetting::Use, true, - HttpClient::new(None, None).unwrap(), + Arc::new(HttpClient::new(None, None)), BlobStore::default(), None, ); @@ -1426,7 +1426,7 @@ mod tests { HttpCache::new(&location), CacheSetting::Use, true, - HttpClient::new(None, None).unwrap(), + Arc::new(HttpClient::new(None, None)), BlobStore::default(), None, ); @@ -1525,7 +1525,7 @@ mod tests { HttpCache::new(&location), CacheSetting::Use, false, - HttpClient::new(None, None).unwrap(), + Arc::new(HttpClient::new(None, None)), BlobStore::default(), None, ); @@ -1550,7 +1550,7 @@ mod tests { HttpCache::new(&location), CacheSetting::Only, true, - HttpClient::new(None, None).unwrap(), + Arc::new(HttpClient::new(None, None)), BlobStore::default(), None, ); @@ -1558,7 +1558,7 @@ mod tests { HttpCache::new(&location), CacheSetting::Use, true, - HttpClient::new(None, None).unwrap(), + Arc::new(HttpClient::new(None, None)), BlobStore::default(), None, ); @@ -2021,15 +2021,24 @@ mod tests { #[ignore] // https://github.com/denoland/deno/issues/12561 async fn test_fetch_with_empty_certificate_store() { use deno_runtime::deno_tls::rustls::RootCertStore; + use deno_runtime::deno_tls::RootCertStoreProvider; + + struct ValueRootCertStoreProvider(RootCertStore); + + impl RootCertStoreProvider for ValueRootCertStoreProvider { + fn get_or_try_init(&self) -> Result<&RootCertStore, AnyError> { + Ok(&self.0) + } + } let _http_server_guard = test_util::http_server(); // Relies on external http server with a valid mozilla root CA cert. let url = Url::parse("https://deno.land").unwrap(); let client = HttpClient::new( - Some(RootCertStore::empty()), // no certs loaded at all + // no certs loaded at all + Some(Arc::new(ValueRootCertStoreProvider(RootCertStore::empty()))), None, - ) - .unwrap(); + ); let result = fetch_once( &client, diff --git a/cli/http_util.rs b/cli/http_util.rs index b01732ca97..7c17e8e1e5 100644 --- a/cli/http_util.rs +++ b/cli/http_util.rs @@ -15,8 +15,9 @@ use deno_runtime::deno_fetch::create_http_client; use deno_runtime::deno_fetch::reqwest; use deno_runtime::deno_fetch::reqwest::header::LOCATION; use deno_runtime::deno_fetch::reqwest::Response; -use deno_runtime::deno_tls::rustls::RootCertStore; +use deno_runtime::deno_tls::RootCertStoreProvider; use std::collections::HashMap; +use std::sync::Arc; use std::time::Duration; use std::time::SystemTime; @@ -217,34 +218,68 @@ impl CacheSemantics { } } -#[derive(Debug, Clone)] -pub struct HttpClient(reqwest::Client); +pub struct HttpClient { + root_cert_store_provider: Option>, + unsafely_ignore_certificate_errors: Option>, + cell: once_cell::sync::OnceCell, +} + +impl std::fmt::Debug for HttpClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HttpClient") + .field( + "unsafely_ignore_certificate_errors", + &self.unsafely_ignore_certificate_errors, + ) + .finish() + } +} impl HttpClient { pub fn new( - root_cert_store: Option, + root_cert_store_provider: Option>, unsafely_ignore_certificate_errors: Option>, - ) -> Result { - Ok(HttpClient::from_client(create_http_client( - get_user_agent(), - root_cert_store, - vec![], - None, + ) -> Self { + Self { + root_cert_store_provider, unsafely_ignore_certificate_errors, - None, - )?)) + cell: Default::default(), + } } + #[cfg(test)] pub fn from_client(client: reqwest::Client) -> Self { - Self(client) + let result = Self { + root_cert_store_provider: Default::default(), + unsafely_ignore_certificate_errors: Default::default(), + cell: Default::default(), + }; + result.cell.set(client).unwrap(); + result + } + + fn client(&self) -> Result<&reqwest::Client, AnyError> { + self.cell.get_or_try_init(|| { + create_http_client( + get_user_agent(), + match &self.root_cert_store_provider { + Some(provider) => Some(provider.get_or_try_init()?.clone()), + None => None, + }, + vec![], + None, + self.unsafely_ignore_certificate_errors.clone(), + None, + ) + }) } /// Do a GET request without following redirects. pub fn get_no_redirect( &self, url: U, - ) -> reqwest::RequestBuilder { - self.0.get(url) + ) -> Result { + Ok(self.client()?.get(url)) } pub async fn download_text( @@ -306,12 +341,13 @@ impl HttpClient { url: U, ) -> Result { let mut url = url.into_url()?; - let mut response = self.get_no_redirect(url.clone()).send().await?; + let mut response = self.get_no_redirect(url.clone())?.send().await?; let status = response.status(); if status.is_redirection() { for _ in 0..5 { let new_url = resolve_redirect_from_response(&url, &response)?; - let new_response = self.get_no_redirect(new_url.clone()).send().await?; + let new_response = + self.get_no_redirect(new_url.clone())?.send().await?; let status = new_response.status(); if status.is_redirection() { response = new_response; @@ -357,7 +393,7 @@ mod test { #[tokio::test] async fn test_http_client_download_redirect() { let _http_server_guard = test_util::http_server(); - let client = HttpClient::new(None, None).unwrap(); + let client = HttpClient::new(None, None); // make a request to the redirect server let text = client diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index d49a2559ca..83657a8ef4 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -12,6 +12,8 @@ use deno_core::ModuleSpecifier; use deno_runtime::deno_node; use deno_runtime::deno_node::NodeResolver; use deno_runtime::deno_node::PackageJson; +use deno_runtime::deno_tls::rustls::RootCertStore; +use deno_runtime::deno_tls::RootCertStoreProvider; use deno_runtime::deno_web::BlobStore; use import_map::ImportMap; use log::error; @@ -93,6 +95,14 @@ use crate::util::path::specifier_to_file_path; use crate::util::progress_bar::ProgressBar; use crate::util::progress_bar::ProgressBarStyle; +struct LspRootCertStoreProvider(RootCertStore); + +impl RootCertStoreProvider for LspRootCertStoreProvider { + fn get_or_try_init(&self) -> Result<&RootCertStore, AnyError> { + Ok(&self.0) + } +} + #[derive(Debug, Clone)] pub struct LanguageServer(Arc>); @@ -124,7 +134,7 @@ pub struct Inner { /// The collection of documents that the server is currently handling, either /// on disk or "open" within the client. pub documents: Documents, - http_client: HttpClient, + http_client: Arc, /// Handles module registries, which allow discovery of modules module_registries: ModuleRegistry, /// The path to the module registries cache @@ -420,7 +430,7 @@ impl LanguageServer { fn create_lsp_structs( dir: &DenoDir, - http_client: HttpClient, + http_client: Arc, ) -> ( Arc, Arc, @@ -469,10 +479,9 @@ impl Inner { let dir = DenoDir::new(maybe_custom_root).expect("could not access DENO_DIR"); let module_registries_location = dir.registries_folder_path(); - let http_client = HttpClient::new(None, None).unwrap(); + let http_client = Arc::new(HttpClient::new(None, None)); let module_registries = - ModuleRegistry::new(&module_registries_location, http_client.clone()) - .unwrap(); + ModuleRegistry::new(&module_registries_location, http_client.clone()); let location = dir.deps_folder_path(); let documents = Documents::new(&location, client.kind()); let deps_http_cache = HttpCache::new(&location); @@ -775,20 +784,22 @@ impl Inner { .root_uri .as_ref() .and_then(|uri| specifier_to_file_path(uri).ok()); - let root_cert_store = Some(get_root_cert_store( + let root_cert_store = get_root_cert_store( maybe_root_path, workspace_settings.certificate_stores, workspace_settings.tls_certificate.map(CaData::File), - )?); - let module_registries_location = dir.registries_folder_path(); - self.http_client = HttpClient::new( - root_cert_store, - workspace_settings.unsafely_ignore_certificate_errors, )?; + let root_cert_store_provider = + Arc::new(LspRootCertStoreProvider(root_cert_store)); + let module_registries_location = dir.registries_folder_path(); + self.http_client = Arc::new(HttpClient::new( + Some(root_cert_store_provider), + workspace_settings.unsafely_ignore_certificate_errors, + )); self.module_registries = ModuleRegistry::new( &module_registries_location, self.http_client.clone(), - )?; + ); self.module_registries_location = module_registries_location; ( self.npm_api, diff --git a/cli/lsp/registries.rs b/cli/lsp/registries.rs index f46aba44f2..b2f9bee2c3 100644 --- a/cli/lsp/registries.rs +++ b/cli/lsp/registries.rs @@ -35,6 +35,7 @@ use log::error; use once_cell::sync::Lazy; use std::collections::HashMap; use std::path::Path; +use std::sync::Arc; use tower_lsp::lsp_types as lsp; const CONFIG_PATH: &str = "/.well-known/deno-import-intellisense.json"; @@ -425,16 +426,13 @@ impl Default for ModuleRegistry { // custom root. let dir = DenoDir::new(None).unwrap(); let location = dir.registries_folder_path(); - let http_client = HttpClient::new(None, None).unwrap(); - Self::new(&location, http_client).unwrap() + let http_client = Arc::new(HttpClient::new(None, None)); + Self::new(&location, http_client) } } impl ModuleRegistry { - pub fn new( - location: &Path, - http_client: HttpClient, - ) -> Result { + pub fn new(location: &Path, http_client: Arc) -> Self { let http_cache = HttpCache::new(location); let mut file_fetcher = FileFetcher::new( http_cache, @@ -446,10 +444,10 @@ impl ModuleRegistry { ); file_fetcher.set_download_log_level(super::logging::lsp_log_level()); - Ok(Self { + Self { origins: HashMap::new(), file_fetcher, - }) + } } fn complete_literal( @@ -1251,8 +1249,7 @@ mod tests { let temp_dir = TempDir::new(); let location = temp_dir.path().join("registries"); let mut module_registry = - ModuleRegistry::new(&location, HttpClient::new(None, None).unwrap()) - .unwrap(); + ModuleRegistry::new(&location, Arc::new(HttpClient::new(None, None))); module_registry .enable("http://localhost:4545/") .await @@ -1313,8 +1310,7 @@ mod tests { let temp_dir = TempDir::new(); let location = temp_dir.path().join("registries"); let mut module_registry = - ModuleRegistry::new(&location, HttpClient::new(None, None).unwrap()) - .unwrap(); + ModuleRegistry::new(&location, Arc::new(HttpClient::new(None, None))); module_registry .enable("http://localhost:4545/") .await @@ -1537,8 +1533,7 @@ mod tests { let temp_dir = TempDir::new(); let location = temp_dir.path().join("registries"); let mut module_registry = - ModuleRegistry::new(&location, HttpClient::new(None, None).unwrap()) - .unwrap(); + ModuleRegistry::new(&location, Arc::new(HttpClient::new(None, None))); module_registry .enable_custom("http://localhost:4545/lsp/registries/deno-import-intellisense-key-first.json") .await @@ -1608,8 +1603,7 @@ mod tests { let temp_dir = TempDir::new(); let location = temp_dir.path().join("registries"); let mut module_registry = - ModuleRegistry::new(&location, HttpClient::new(None, None).unwrap()) - .unwrap(); + ModuleRegistry::new(&location, Arc::new(HttpClient::new(None, None))); module_registry .enable_custom("http://localhost:4545/lsp/registries/deno-import-intellisense-complex.json") .await @@ -1660,8 +1654,7 @@ mod tests { let temp_dir = TempDir::new(); let location = temp_dir.path().join("registries"); let module_registry = - ModuleRegistry::new(&location, HttpClient::new(None, None).unwrap()) - .unwrap(); + ModuleRegistry::new(&location, Arc::new(HttpClient::new(None, None))); let result = module_registry.check_origin("http://localhost:4545").await; assert!(result.is_ok()); } @@ -1672,8 +1665,7 @@ mod tests { let temp_dir = TempDir::new(); let location = temp_dir.path().join("registries"); let module_registry = - ModuleRegistry::new(&location, HttpClient::new(None, None).unwrap()) - .unwrap(); + ModuleRegistry::new(&location, Arc::new(HttpClient::new(None, None))); let result = module_registry.check_origin("https://example.com").await; assert!(result.is_err()); let err = result.unwrap_err().to_string(); diff --git a/cli/npm/cache.rs b/cli/npm/cache.rs index 0d88109de3..cda40fd172 100644 --- a/cli/npm/cache.rs +++ b/cli/npm/cache.rs @@ -4,6 +4,7 @@ use std::collections::HashSet; use std::fs; use std::path::Path; use std::path::PathBuf; +use std::sync::Arc; use deno_ast::ModuleSpecifier; use deno_core::anyhow::bail; @@ -295,7 +296,7 @@ impl ReadonlyNpmCache { pub struct NpmCache { readonly: ReadonlyNpmCache, cache_setting: CacheSetting, - http_client: HttpClient, + http_client: Arc, progress_bar: ProgressBar, /// ensures a package is only downloaded once per run previously_reloaded_packages: Mutex>, @@ -305,7 +306,7 @@ impl NpmCache { pub fn new( cache_dir_path: PathBuf, cache_setting: CacheSetting, - http_client: HttpClient, + http_client: Arc, progress_bar: ProgressBar, ) -> Self { Self { diff --git a/cli/npm/registry.rs b/cli/npm/registry.rs index ef050a7346..40d7f62191 100644 --- a/cli/npm/registry.rs +++ b/cli/npm/registry.rs @@ -63,7 +63,7 @@ impl CliNpmRegistryApi { pub fn new( base_url: Url, cache: Arc, - http_client: HttpClient, + http_client: Arc, progress_bar: ProgressBar, ) -> Self { Self(Some(Arc::new(CliNpmRegistryApiInner { @@ -172,7 +172,7 @@ struct CliNpmRegistryApiInner { force_reload_flag: AtomicFlag, mem_cache: Mutex>, previously_reloaded_packages: Mutex>, - http_client: HttpClient, + http_client: Arc, progress_bar: ProgressBar, } diff --git a/cli/standalone/mod.rs b/cli/standalone/mod.rs index 556346535b..2ef21d417e 100644 --- a/cli/standalone/mod.rs +++ b/cli/standalone/mod.rs @@ -32,6 +32,8 @@ use deno_core::ResolutionKind; use deno_graph::source::Resolver; use deno_runtime::deno_node; use deno_runtime::deno_node::NodeResolver; +use deno_runtime::deno_tls::rustls::RootCertStore; +use deno_runtime::deno_tls::RootCertStoreProvider; use deno_runtime::deno_web::BlobStore; use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsContainer; @@ -161,22 +163,37 @@ impl HasNodeSpecifierChecker for StandaloneHasNodeSpecifierChecker { } } +struct StandaloneRootCertStoreProvider { + ca_stores: Option>, + ca_data: Option, + cell: once_cell::sync::OnceCell, +} + +impl RootCertStoreProvider for StandaloneRootCertStoreProvider { + fn get_or_try_init(&self) -> Result<&RootCertStore, AnyError> { + self.cell.get_or_try_init(|| { + get_root_cert_store(None, self.ca_stores.clone(), self.ca_data.clone()) + .map_err(|err| err.into()) + }) + } +} + pub async fn run( eszip: eszip::EszipV2, metadata: Metadata, ) -> Result<(), AnyError> { let main_module = &metadata.entrypoint; let dir = DenoDir::new(None)?; - let root_cert_store = get_root_cert_store( - None, - metadata.ca_stores, - metadata.ca_data.map(CaData::Bytes), - )?; + let root_cert_store_provider = Arc::new(StandaloneRootCertStoreProvider { + ca_stores: metadata.ca_stores, + ca_data: metadata.ca_data.map(CaData::Bytes), + cell: Default::default(), + }); let progress_bar = ProgressBar::new(ProgressBarStyle::TextOnly); - let http_client = HttpClient::new( - Some(root_cert_store.clone()), + let http_client = Arc::new(HttpClient::new( + Some(root_cert_store_provider.clone()), metadata.unsafely_ignore_certificate_errors.clone(), - )?; + )); let npm_registry_url = CliNpmRegistryApi::default_url().to_owned(); let npm_cache = Arc::new(NpmCache::new( dir.npm_folder_path(), @@ -235,7 +252,7 @@ pub async fn run( Box::new(StandaloneHasNodeSpecifierChecker), BlobStore::default(), Box::new(module_loader_factory), - root_cert_store, + root_cert_store_provider, node_fs, None, CliMainWorkerOptions { diff --git a/cli/tools/installer.rs b/cli/tools/installer.rs index fb83c3cab9..07606d5f8d 100644 --- a/cli/tools/installer.rs +++ b/cli/tools/installer.rs @@ -133,7 +133,7 @@ pub async fn infer_name_from_url(url: &Url) -> Option { let mut url = url.clone(); if url.path() == "/" { - let client = HttpClient::new(None, None).unwrap(); + let client = HttpClient::new(None, None); if let Ok(res) = client.get_redirected_response(url.clone()).await { url = res.url().clone(); } diff --git a/cli/tools/run.rs b/cli/tools/run.rs index c6e706285b..99312d5b90 100644 --- a/cli/tools/run.rs +++ b/cli/tools/run.rs @@ -35,7 +35,7 @@ To grant permissions, set them before the script argument. For example: // map specified and bare specifier is used on the command line let factory = CliFactory::from_flags(flags).await?; let deno_dir = factory.deno_dir()?; - let http_client = factory.http_client()?; + let http_client = factory.http_client(); let cli_options = factory.cli_options(); // Run a background task that checks for available upgrades. If an earlier diff --git a/cli/tools/standalone.rs b/cli/tools/standalone.rs index 0e8d9ca733..d34e5da833 100644 --- a/cli/tools/standalone.rs +++ b/cli/tools/standalone.rs @@ -26,7 +26,7 @@ pub async fn compile( let factory = CliFactory::from_flags(flags).await?; let cli_options = factory.cli_options(); let file_fetcher = factory.file_fetcher()?; - let http_client = factory.http_client()?; + let http_client = factory.http_client(); let deno_dir = factory.deno_dir()?; let module_graph_builder = factory.module_graph_builder().await?; let parsed_source_cache = factory.parsed_source_cache()?; diff --git a/cli/tools/upgrade.rs b/cli/tools/upgrade.rs index c76d36777f..b5aefe4798 100644 --- a/cli/tools/upgrade.rs +++ b/cli/tools/upgrade.rs @@ -26,6 +26,7 @@ use std::ops::Sub; use std::path::Path; use std::path::PathBuf; use std::process::Command; +use std::sync::Arc; use std::time::Duration; static ARCHIVE_NAME: Lazy = @@ -50,13 +51,13 @@ trait UpdateCheckerEnvironment: Clone + Send + Sync { #[derive(Clone)] struct RealUpdateCheckerEnvironment { - http_client: HttpClient, + http_client: Arc, cache_file_path: PathBuf, current_time: chrono::DateTime, } impl RealUpdateCheckerEnvironment { - pub fn new(http_client: HttpClient, cache_file_path: PathBuf) -> Self { + pub fn new(http_client: Arc, cache_file_path: PathBuf) -> Self { Self { http_client, cache_file_path, @@ -183,7 +184,10 @@ fn print_release_notes(current_version: &str, new_version: &str) { } } -pub fn check_for_upgrades(http_client: HttpClient, cache_file_path: PathBuf) { +pub fn check_for_upgrades( + http_client: Arc, + cache_file_path: PathBuf, +) { if env::var("DENO_NO_UPDATE_CHECK").is_ok() { return; } @@ -264,7 +268,7 @@ pub async fn upgrade( upgrade_flags: UpgradeFlags, ) -> Result<(), AnyError> { let factory = CliFactory::from_flags(flags).await?; - let client = factory.http_client()?; + let client = factory.http_client(); let current_exe_path = std::env::current_exe()?; let metadata = fs::metadata(¤t_exe_path)?; let permissions = metadata.permissions(); diff --git a/cli/worker.rs b/cli/worker.rs index 64ce284776..ae8822fe40 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -21,7 +21,7 @@ use deno_runtime::deno_broadcast_channel::InMemoryBroadcastChannel; use deno_runtime::deno_node; use deno_runtime::deno_node::NodeResolution; use deno_runtime::deno_node::NodeResolver; -use deno_runtime::deno_tls::rustls::RootCertStore; +use deno_runtime::deno_tls::RootCertStoreProvider; use deno_runtime::deno_web::BlobStore; use deno_runtime::fmt_errors::format_js_error; use deno_runtime::inspector_server::InspectorServer; @@ -96,7 +96,7 @@ struct SharedWorkerState { shared_array_buffer_store: SharedArrayBufferStore, compiled_wasm_module_store: CompiledWasmModuleStore, module_loader_factory: Box, - root_cert_store: RootCertStore, + root_cert_store_provider: Arc, node_fs: Arc, maybe_inspector_server: Option>, } @@ -307,7 +307,7 @@ impl CliMainWorkerFactory { has_node_specifier_checker: Box, blob_store: BlobStore, module_loader_factory: Box, - root_cert_store: RootCertStore, + root_cert_store_provider: Arc, node_fs: Arc, maybe_inspector_server: Option>, options: CliMainWorkerOptions, @@ -324,7 +324,7 @@ impl CliMainWorkerFactory { shared_array_buffer_store: Default::default(), compiled_wasm_module_store: Default::default(), module_loader_factory, - root_cert_store, + root_cert_store_provider, node_fs, maybe_inspector_server, }), @@ -434,7 +434,7 @@ impl CliMainWorkerFactory { .options .unsafely_ignore_certificate_errors .clone(), - root_cert_store: Some(shared.root_cert_store.clone()), + root_cert_store_provider: Some(shared.root_cert_store_provider.clone()), seed: shared.options.seed, source_map_getter: maybe_source_map_getter, format_js_error_fn: Some(Arc::new(format_js_error)), @@ -562,7 +562,7 @@ fn create_web_worker_callback( .options .unsafely_ignore_certificate_errors .clone(), - root_cert_store: Some(shared.root_cert_store.clone()), + root_cert_store_provider: Some(shared.root_cert_store_provider.clone()), seed: shared.options.seed, create_web_worker_cb, preload_module_cb, @@ -616,7 +616,7 @@ mod tests { extensions: vec![], startup_snapshot: Some(crate::js::deno_isolate_init()), unsafely_ignore_certificate_errors: None, - root_cert_store: None, + root_cert_store_provider: None, seed: None, format_js_error_fn: None, source_map_getter: None, diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs index 17f30d8ed3..51688a6fcf 100644 --- a/ext/fetch/lib.rs +++ b/ext/fetch/lib.rs @@ -3,7 +3,16 @@ mod byte_stream; mod fs_fetch_handler; -use data_url::DataUrl; +use std::borrow::Cow; +use std::cell::RefCell; +use std::cmp::min; +use std::convert::From; +use std::path::Path; +use std::path::PathBuf; +use std::pin::Pin; +use std::rc::Rc; +use std::sync::Arc; + use deno_core::error::type_error; use deno_core::error::AnyError; use deno_core::futures::stream::Peekable; @@ -29,6 +38,9 @@ use deno_core::ResourceId; use deno_core::ZeroCopyBuf; use deno_tls::rustls::RootCertStore; use deno_tls::Proxy; +use deno_tls::RootCertStoreProvider; + +use data_url::DataUrl; use http::header::CONTENT_LENGTH; use http::Uri; use reqwest::header::HeaderMap; @@ -46,14 +58,6 @@ use reqwest::RequestBuilder; use reqwest::Response; use serde::Deserialize; use serde::Serialize; -use std::borrow::Cow; -use std::cell::RefCell; -use std::cmp::min; -use std::convert::From; -use std::path::Path; -use std::path::PathBuf; -use std::pin::Pin; -use std::rc::Rc; use tokio::sync::mpsc; // Re-export reqwest and data_url @@ -67,7 +71,7 @@ use crate::byte_stream::MpscByteStream; #[derive(Clone)] pub struct Options { pub user_agent: String, - pub root_cert_store: Option, + pub root_cert_store_provider: Option>, pub proxy: Option, pub request_builder_hook: Option Result>, @@ -76,11 +80,20 @@ pub struct Options { pub file_fetch_handler: Rc, } +impl Options { + pub fn root_cert_store(&self) -> Result, AnyError> { + Ok(match &self.root_cert_store_provider { + Some(provider) => Some(provider.get_or_try_init()?.clone()), + None => None, + }) + } +} + impl Default for Options { fn default() -> Self { Self { user_agent: "".to_string(), - root_cert_store: None, + root_cert_store_provider: None, proxy: None, request_builder_hook: None, unsafely_ignore_certificate_errors: None, @@ -111,18 +124,7 @@ deno_core::extension!(deno_fetch, options: Options, }, state = |state, options| { - state.put::(options.options.clone()); - state.put::({ - create_http_client( - &options.options.user_agent, - options.options.root_cert_store, - vec![], - options.options.proxy, - options.options.unsafely_ignore_certificate_errors, - options.options.client_cert_chain_and_key - ) - .unwrap() - }); + state.put::(options.options); }, ); @@ -189,6 +191,26 @@ pub struct FetchReturn { cancel_handle_rid: Option, } +pub fn get_or_create_client_from_state( + state: &mut OpState, +) -> Result { + if let Some(client) = state.try_borrow::() { + Ok(client.clone()) + } else { + let options = state.borrow::(); + let client = create_http_client( + &options.user_agent, + options.root_cert_store()?, + vec![], + options.proxy.clone(), + options.unsafely_ignore_certificate_errors.clone(), + options.client_cert_chain_and_key.clone(), + )?; + state.put::(client.clone()); + Ok(client) + } +} + #[op] pub fn op_fetch( state: &mut OpState, @@ -207,8 +229,7 @@ where let r = state.resource_table.get::(rid)?; r.client.clone() } else { - let client = state.borrow::(); - client.clone() + get_or_create_client_from_state(state)? }; let method = Method::from_bytes(&method)?; @@ -632,7 +653,7 @@ where let client = create_http_client( &options.user_agent, - options.root_cert_store.clone(), + options.root_cert_store()?, ca_certs, args.proxy, options.unsafely_ignore_certificate_errors.clone(), diff --git a/ext/net/lib.rs b/ext/net/lib.rs index ff67186b0c..912b0723ea 100644 --- a/ext/net/lib.rs +++ b/ext/net/lib.rs @@ -11,10 +11,12 @@ pub mod resolve_addr; use deno_core::error::AnyError; use deno_core::OpState; use deno_tls::rustls::RootCertStore; +use deno_tls::RootCertStoreProvider; use std::cell::RefCell; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; +use std::sync::Arc; pub trait NetPermissions { fn check_net>( @@ -67,7 +69,16 @@ pub fn get_declaration() -> PathBuf { #[derive(Clone)] pub struct DefaultTlsOptions { - pub root_cert_store: Option, + pub root_cert_store_provider: Option>, +} + +impl DefaultTlsOptions { + pub fn root_cert_store(&self) -> Result, AnyError> { + Ok(match &self.root_cert_store_provider { + Some(provider) => Some(provider.get_or_try_init()?.clone()), + None => None, + }) + } } /// `UnsafelyIgnoreCertificateErrors` is a wrapper struct so it can be placed inside `GothamState`; @@ -113,13 +124,13 @@ deno_core::extension!(deno_net, ], esm = [ "01_net.js", "02_tls.js" ], options = { - root_cert_store: Option, + root_cert_store_provider: Option>, unstable: bool, unsafely_ignore_certificate_errors: Option>, }, state = |state, options| { state.put(DefaultTlsOptions { - root_cert_store: options.root_cert_store, + root_cert_store_provider: options.root_cert_store_provider, }); state.put(UnstableChecker { unstable: options.unstable }); state.put(UnsafelyIgnoreCertificateErrors( diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs index 8a77570668..b9b37b3282 100644 --- a/ext/net/ops_tls.rs +++ b/ext/net/ops_tls.rs @@ -813,14 +813,10 @@ where .try_borrow::() .and_then(|it| it.0.clone()); - // TODO(@justinmchase): Ideally the certificate store is created once - // and not cloned. The store should be wrapped in Arc to reduce - // copying memory unnecessarily. let root_cert_store = state .borrow() .borrow::() - .root_cert_store - .clone(); + .root_cert_store()?; let resource_rc = state .borrow_mut() @@ -912,8 +908,7 @@ where let root_cert_store = state .borrow() .borrow::() - .root_cert_store - .clone(); + .root_cert_store()?; let hostname_dns = ServerName::try_from(&*addr.hostname) .map_err(|_| invalid_hostname(&addr.hostname))?; let connect_addr = resolve_addr(&addr.hostname, addr.port) diff --git a/ext/tls/lib.rs b/ext/tls/lib.rs index 123d35acf0..3034e2ae98 100644 --- a/ext/tls/lib.rs +++ b/ext/tls/lib.rs @@ -34,6 +34,14 @@ use std::io::Cursor; use std::sync::Arc; use std::time::SystemTime; +/// Lazily resolves the root cert store. +/// +/// This was done because the root cert store is not needed in all cases +/// and takes a bit of time to initialize. +pub trait RootCertStoreProvider: Send + Sync { + fn get_or_try_init(&self) -> Result<&RootCertStore, AnyError>; +} + // This extension has no runtime apis, it only exports some shared native functions. deno_core::extension!(deno_tls); diff --git a/ext/websocket/lib.rs b/ext/websocket/lib.rs index 2ce141fc92..e03a13789f 100644 --- a/ext/websocket/lib.rs +++ b/ext/websocket/lib.rs @@ -19,6 +19,7 @@ use deno_core::ZeroCopyBuf; use deno_net::raw::take_network_stream_resource; use deno_net::raw::NetworkStream; use deno_tls::create_client_config; +use deno_tls::RootCertStoreProvider; use http::header::CONNECTION; use http::header::UPGRADE; use http::HeaderName; @@ -54,7 +55,17 @@ use fastwebsockets::WebSocket; mod stream; #[derive(Clone)] -pub struct WsRootStore(pub Option); +pub struct WsRootStoreProvider(Option>); + +impl WsRootStoreProvider { + pub fn get_or_try_init(&self) -> Result, AnyError> { + Ok(match &self.0 { + Some(provider) => Some(provider.get_or_try_init()?.clone()), + None => None, + }) + } +} + #[derive(Clone)] pub struct WsUserAgent(pub String); @@ -181,7 +192,10 @@ where .borrow() .try_borrow::() .and_then(|it| it.0.clone()); - let root_cert_store = state.borrow().borrow::().0.clone(); + let root_cert_store = state + .borrow() + .borrow::() + .get_or_try_init()?; let user_agent = state.borrow().borrow::().0.clone(); let uri: Uri = url.parse()?; let mut request = Request::builder().method(Method::GET).uri( @@ -525,7 +539,7 @@ deno_core::extension!(deno_websocket, esm = [ "01_websocket.js", "02_websocketstream.js" ], options = { user_agent: String, - root_cert_store: Option, + root_cert_store_provider: Option>, unsafely_ignore_certificate_errors: Option> }, state = |state, options| { @@ -533,7 +547,7 @@ deno_core::extension!(deno_websocket, state.put(UnsafelyIgnoreCertificateErrors( options.unsafely_ignore_certificate_errors, )); - state.put::(WsRootStore(options.root_cert_store)); + state.put::(WsRootStoreProvider(options.root_cert_store_provider)); }, ); diff --git a/runtime/examples/hello_runtime.rs b/runtime/examples/hello_runtime.rs index f97c045b26..2e930a03f2 100644 --- a/runtime/examples/hello_runtime.rs +++ b/runtime/examples/hello_runtime.rs @@ -32,7 +32,7 @@ async fn main() -> Result<(), AnyError> { extensions: vec![], startup_snapshot: None, unsafely_ignore_certificate_errors: None, - root_cert_store: None, + root_cert_store_provider: None, seed: None, source_map_getter: None, format_js_error_fn: None, diff --git a/runtime/ops/web_worker/sync_fetch.rs b/runtime/ops/web_worker/sync_fetch.rs index 2049d5ab85..ba5f325d63 100644 --- a/runtime/ops/web_worker/sync_fetch.rs +++ b/runtime/ops/web_worker/sync_fetch.rs @@ -8,7 +8,6 @@ use deno_core::op; use deno_core::url::Url; use deno_core::OpState; use deno_fetch::data_url::DataUrl; -use deno_fetch::reqwest; use deno_web::BlobStore; use deno_websocket::DomExceptionNetworkError; use hyper::body::Bytes; @@ -41,7 +40,7 @@ pub fn op_worker_sync_fetch( let handle = state.borrow::().clone(); assert_eq!(handle.worker_type, WebWorkerType::Classic); - let client = state.borrow::().clone(); + let client = deno_fetch::get_or_create_client_from_state(state)?; // TODO(andreubotella) It's not good to throw an exception related to blob // URLs when none of the script URLs use the blob scheme. diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index 9bc5ba011f..b688aae8b3 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -37,7 +37,7 @@ use deno_core::SourceMapGetter; use deno_fs::StdFs; use deno_io::Stdio; use deno_kv::sqlite::SqliteDbHandler; -use deno_tls::rustls::RootCertStore; +use deno_tls::RootCertStoreProvider; use deno_web::create_entangled_message_port; use deno_web::BlobStore; use deno_web::MessagePort; @@ -329,7 +329,7 @@ pub struct WebWorkerOptions { pub extensions: Vec, pub startup_snapshot: Option, pub unsafely_ignore_certificate_errors: Option>, - pub root_cert_store: Option, + pub root_cert_store_provider: Option>, pub seed: Option, pub module_loader: Rc, pub node_fs: Option>, @@ -407,7 +407,7 @@ impl WebWorker { deno_fetch::deno_fetch::init_ops::( deno_fetch::Options { user_agent: options.bootstrap.user_agent.clone(), - root_cert_store: options.root_cert_store.clone(), + root_cert_store_provider: options.root_cert_store_provider.clone(), unsafely_ignore_certificate_errors: options .unsafely_ignore_certificate_errors .clone(), @@ -418,7 +418,7 @@ impl WebWorker { deno_cache::deno_cache::init_ops::(create_cache), deno_websocket::deno_websocket::init_ops::( options.bootstrap.user_agent.clone(), - options.root_cert_store.clone(), + options.root_cert_store_provider.clone(), options.unsafely_ignore_certificate_errors.clone(), ), deno_webstorage::deno_webstorage::init_ops(None).disable(), @@ -429,7 +429,7 @@ impl WebWorker { ), deno_ffi::deno_ffi::init_ops::(unstable), deno_net::deno_net::init_ops::( - options.root_cert_store.clone(), + options.root_cert_store_provider.clone(), unstable, options.unsafely_ignore_certificate_errors.clone(), ), diff --git a/runtime/worker.rs b/runtime/worker.rs index 56684e9925..0d68a4b51e 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -33,7 +33,7 @@ use deno_core::SourceMapGetter; use deno_fs::StdFs; use deno_io::Stdio; use deno_kv::sqlite::SqliteDbHandler; -use deno_tls::rustls::RootCertStore; +use deno_tls::RootCertStoreProvider; use deno_web::BlobStore; use log::debug; @@ -84,7 +84,7 @@ pub struct WorkerOptions { /// V8 snapshot that should be loaded on startup. pub startup_snapshot: Option, pub unsafely_ignore_certificate_errors: Option>, - pub root_cert_store: Option, + pub root_cert_store_provider: Option>, pub seed: Option, /// Implementation of `ModuleLoader` which will be @@ -163,7 +163,7 @@ impl Default for WorkerOptions { cache_storage_dir: Default::default(), broadcast_channel: Default::default(), source_map_getter: Default::default(), - root_cert_store: Default::default(), + root_cert_store_provider: Default::default(), node_fs: Default::default(), npm_resolver: Default::default(), blob_store: Default::default(), @@ -228,7 +228,7 @@ impl MainWorker { deno_fetch::deno_fetch::init_ops::( deno_fetch::Options { user_agent: options.bootstrap.user_agent.clone(), - root_cert_store: options.root_cert_store.clone(), + root_cert_store_provider: options.root_cert_store_provider.clone(), unsafely_ignore_certificate_errors: options .unsafely_ignore_certificate_errors .clone(), @@ -239,7 +239,7 @@ impl MainWorker { deno_cache::deno_cache::init_ops::(create_cache), deno_websocket::deno_websocket::init_ops::( options.bootstrap.user_agent.clone(), - options.root_cert_store.clone(), + options.root_cert_store_provider.clone(), options.unsafely_ignore_certificate_errors.clone(), ), deno_webstorage::deno_webstorage::init_ops( @@ -252,7 +252,7 @@ impl MainWorker { ), deno_ffi::deno_ffi::init_ops::(unstable), deno_net::deno_net::init_ops::( - options.root_cert_store.clone(), + options.root_cert_store_provider.clone(), unstable, options.unsafely_ignore_certificate_errors.clone(), ),