// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use crate::auth_tokens::AuthTokens; use crate::colors; use crate::http_cache::HttpCache; use crate::http_util::fetch_once; use crate::http_util::CacheSemantics; use crate::http_util::FetchOnceArgs; use crate::http_util::FetchOnceResult; use crate::text_encoding; use crate::version::get_user_agent; use data_url::DataUrl; use deno_ast::MediaType; use deno_core::anyhow::anyhow; use deno_core::error::custom_error; use deno_core::error::generic_error; use deno_core::error::uri_error; use deno_core::error::AnyError; use deno_core::futures; use deno_core::futures::future::FutureExt; use deno_core::parking_lot::Mutex; use deno_core::ModuleSpecifier; use deno_runtime::deno_fetch::create_http_client; use deno_runtime::deno_fetch::reqwest; use deno_runtime::deno_tls::rustls; use deno_runtime::deno_tls::rustls::RootCertStore; use deno_runtime::deno_tls::rustls_native_certs::load_native_certs; use deno_runtime::deno_tls::rustls_pemfile; use deno_runtime::deno_tls::webpki_roots; use deno_runtime::deno_web::BlobStore; use deno_runtime::permissions::Permissions; use log::debug; use std::borrow::Borrow; use std::collections::HashMap; use std::env; use std::fs; use std::future::Future; use std::io::BufReader; use std::io::Read; use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; use std::time::SystemTime; pub const SUPPORTED_SCHEMES: [&str; 5] = ["data", "blob", "file", "http", "https"]; /// A structure representing a source file. #[derive(Debug, Clone, Eq, PartialEq)] pub struct File { /// The path to the local version of the source file. For local files this /// will be the direct path to that file. For remote files, it will be the /// path to the file in the HTTP cache. pub local: PathBuf, /// For remote files, if there was an `X-TypeScript-Type` header, the parsed /// out value of that header. pub maybe_types: Option, /// The resolved media type for the file. pub media_type: MediaType, /// The source of the file as a string. pub source: Arc, /// The _final_ specifier for the file. The requested specifier and the final /// specifier maybe different for remote files that have been redirected. pub specifier: ModuleSpecifier, pub maybe_headers: Option>, } /// Simple struct implementing in-process caching to prevent multiple /// fs reads/net fetches for same file. #[derive(Debug, Clone, Default)] struct FileCache(Arc>>); impl FileCache { pub fn get(&self, specifier: &ModuleSpecifier) -> Option { let cache = self.0.lock(); cache.get(specifier).cloned() } pub fn insert(&self, specifier: ModuleSpecifier, file: File) -> Option { let mut cache = self.0.lock(); cache.insert(specifier, file) } } /// Indicates how cached source files should be handled. #[derive(Debug, Clone, Eq, PartialEq)] pub enum CacheSetting { /// Only the cached files should be used. Any files not in the cache will /// error. This is the equivalent of `--cached-only` in the CLI. Only, /// No cached source files should be used, and all files should be reloaded. /// This is the equivalent of `--reload` in the CLI. ReloadAll, /// Only some cached resources should be used. This is the equivalent of /// `--reload=https://deno.land/std` or /// `--reload=https://deno.land/std,https://deno.land/x/example`. ReloadSome(Vec), /// The usability of a cached value is determined by analyzing the cached /// headers and other metadata associated with a cached response, reloading /// any cached "non-fresh" cached responses. RespectHeaders, /// The cached source files should be used for local modules. This is the /// default behavior of the CLI. Use, } impl CacheSetting { /// Returns if the cache should be used for a given specifier. pub fn should_use( &self, specifier: &ModuleSpecifier, http_cache: &HttpCache, ) -> bool { match self { CacheSetting::ReloadAll => false, CacheSetting::Use | CacheSetting::Only => true, CacheSetting::RespectHeaders => { if let Ok((_, headers, cache_time)) = http_cache.get(specifier) { let cache_semantics = CacheSemantics::new(headers, cache_time, SystemTime::now()); cache_semantics.should_use() } else { false } } CacheSetting::ReloadSome(list) => { let mut url = specifier.clone(); url.set_fragment(None); if list.contains(&url.as_str().to_string()) { return false; } url.set_query(None); let mut path = PathBuf::from(url.as_str()); loop { if list.contains(&path.to_str().unwrap().to_string()) { return false; } if !path.pop() { break; } } true } } } } /// Fetch a source file from the local file system. fn fetch_local(specifier: &ModuleSpecifier) -> Result { let local = specifier.to_file_path().map_err(|_| { uri_error(format!("Invalid file path.\n Specifier: {}", specifier)) })?; let bytes = fs::read(local.clone())?; let charset = text_encoding::detect_charset(&bytes).to_string(); let source = get_source_from_bytes(bytes, Some(charset))?; let media_type = MediaType::from(specifier); Ok(File { local, maybe_types: None, media_type, source: source.into(), specifier: specifier.clone(), maybe_headers: None, }) } /// 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_file: Option, ) -> Result { let mut root_cert_store = RootCertStore::empty(); let ca_stores: Vec = maybe_ca_stores .or_else(|| { let env_ca_store = env::var("DENO_TLS_CA_STORE").ok()?; Some( env_ca_store .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(), ) }) .unwrap_or_else(|| vec!["mozilla".to_string()]); for store in ca_stores.iter() { match store.as_str() { "mozilla" => { root_cert_store.add_server_trust_anchors( webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| { rustls::OwnedTrustAnchor::from_subject_spki_name_constraints( ta.subject, ta.spki, ta.name_constraints, ) }), ); } "system" => { let roots = load_native_certs().expect("could not load platform certs"); for root in roots { root_cert_store .add(&rustls::Certificate(root.0)) .expect("Failed to add platform cert to root cert store"); } } _ => { return Err(anyhow!("Unknown certificate store \"{}\" specified (allowed: \"system,mozilla\")", store)); } } } let ca_file = maybe_ca_file.or_else(|| env::var("DENO_CERT").ok()); if let Some(ca_file) = ca_file { let ca_file = if let Some(root) = &maybe_root_path { root.join(&ca_file) } else { PathBuf::from(ca_file) }; let certfile = fs::File::open(&ca_file)?; let mut reader = BufReader::new(certfile); match rustls_pemfile::certs(&mut reader) { Ok(certs) => { root_cert_store.add_parsable_certificates(&certs); } Err(e) => { return Err(anyhow!( "Unable to add pem file to certificate store: {}", e )); } } } Ok(root_cert_store) } /// Returns the decoded body and content-type of a provided /// data URL. pub fn get_source_from_data_url( specifier: &ModuleSpecifier, ) -> Result<(String, String), AnyError> { let data_url = DataUrl::process(specifier.as_str()) .map_err(|e| uri_error(format!("{:?}", e)))?; let mime = data_url.mime_type(); let charset = mime.get_parameter("charset").map(|v| v.to_string()); let (bytes, _) = data_url .decode_to_vec() .map_err(|e| uri_error(format!("{:?}", e)))?; Ok((get_source_from_bytes(bytes, charset)?, format!("{}", mime))) } /// Given a vector of bytes and optionally a charset, decode the bytes to a /// string. pub fn get_source_from_bytes( bytes: Vec, maybe_charset: Option, ) -> Result { let source = if let Some(charset) = maybe_charset { text_encoding::convert_to_utf8(&bytes, &charset)?.to_string() } else { String::from_utf8(bytes)? }; Ok(source) } /// Return a validated scheme for a given module specifier. fn get_validated_scheme( specifier: &ModuleSpecifier, ) -> Result { let scheme = specifier.scheme(); if !SUPPORTED_SCHEMES.contains(&scheme) { Err(generic_error(format!( "Unsupported scheme \"{}\" for module \"{}\". Supported schemes: {:#?}", scheme, specifier, SUPPORTED_SCHEMES ))) } else { Ok(scheme.to_string()) } } /// Resolve a media type and optionally the charset from a module specifier and /// the value of a content type header. pub fn map_content_type( specifier: &ModuleSpecifier, maybe_content_type: Option, ) -> (MediaType, Option) { if let Some(content_type) = maybe_content_type { let mut content_types = content_type.split(';'); let content_type = content_types.next().unwrap(); let media_type = MediaType::from_content_type(specifier, content_type); let charset = content_types .map(str::trim) .find_map(|s| s.strip_prefix("charset=")) .map(String::from); (media_type, charset) } else { (MediaType::from(specifier), None) } } /// A structure for resolving, fetching and caching source files. #[derive(Debug, Clone)] pub struct FileFetcher { auth_tokens: AuthTokens, allow_remote: bool, cache: FileCache, cache_setting: CacheSetting, pub http_cache: HttpCache, http_client: reqwest::Client, blob_store: BlobStore, download_log_level: log::Level, } impl FileFetcher { pub fn new( http_cache: HttpCache, cache_setting: CacheSetting, allow_remote: bool, root_cert_store: Option, blob_store: BlobStore, unsafely_ignore_certificate_errors: Option>, ) -> Result { Ok(Self { auth_tokens: AuthTokens::new(env::var("DENO_AUTH_TOKENS").ok()), allow_remote, cache: Default::default(), cache_setting, http_cache, http_client: create_http_client( get_user_agent(), root_cert_store, vec![], None, unsafely_ignore_certificate_errors, None, )?, blob_store, download_log_level: log::Level::Info, }) } /// Sets the log level to use when outputting the download message. pub fn set_download_log_level(&mut self, level: log::Level) { self.download_log_level = level; } /// Creates a `File` structure for a remote file. fn build_remote_file( &self, specifier: &ModuleSpecifier, bytes: Vec, headers: &HashMap, ) -> Result { let local = self .http_cache .get_cache_filename(specifier) .ok_or_else(|| { generic_error("Cannot convert specifier to cached filename.") })?; let maybe_content_type = headers.get("content-type").cloned(); let (media_type, maybe_charset) = map_content_type(specifier, maybe_content_type); let source = get_source_from_bytes(bytes, maybe_charset)?; let maybe_types = match media_type { MediaType::JavaScript | MediaType::Cjs | MediaType::Mjs | MediaType::Jsx => headers.get("x-typescript-types").cloned(), _ => None, }; Ok(File { local, maybe_types, media_type, source: source.into(), specifier: specifier.clone(), maybe_headers: Some(headers.clone()), }) } /// Fetch cached remote file. /// /// This is a recursive operation if source file has redirections. pub fn fetch_cached( &self, specifier: &ModuleSpecifier, redirect_limit: i64, ) -> Result