fix(lsp): respect DENO_CERT and other options related to TLS certs (#13467)

Fixes #13437
This commit is contained in:
Kitson Kelly 2022-01-24 11:27:52 +11:00 committed by GitHub
parent 3959d9f2d2
commit 3ec248cff8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 393 additions and 101 deletions

View file

@ -12,6 +12,7 @@ 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;
@ -22,7 +23,11 @@ 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;
@ -31,6 +36,7 @@ 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;
@ -161,6 +167,80 @@ fn fetch_local(specifier: &ModuleSpecifier) -> Result<File, AnyError> {
})
}
/// Create and populate a root cert store based on the passed options and
/// environment.
pub(crate) fn get_root_cert_store(
maybe_root_path: Option<PathBuf>,
maybe_ca_stores: Option<Vec<String>>,
maybe_ca_file: Option<String>,
) -> Result<RootCertStore, AnyError> {
let mut root_cert_store = RootCertStore::empty();
let ca_stores: Vec<String> = 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(

View file

@ -33,6 +33,9 @@ impl CacheServer {
maybe_cache_path: Option<PathBuf>,
maybe_import_map: Option<Arc<ImportMap>>,
maybe_config_file: Option<ConfigFile>,
maybe_ca_stores: Option<Vec<String>>,
maybe_ca_file: Option<String>,
unsafely_ignore_certificate_errors: Option<Vec<String>>,
) -> Self {
let (tx, mut rx) = mpsc::unbounded_channel::<Request>();
let _join_handle = thread::spawn(move || {
@ -40,6 +43,9 @@ impl CacheServer {
runtime.block_on(async {
let ps = ProcState::build(Flags {
cache_path: maybe_cache_path,
ca_stores: maybe_ca_stores,
ca_file: maybe_ca_file,
unsafely_ignore_certificate_errors,
..Default::default()
})
.await

View file

@ -154,6 +154,10 @@ pub struct WorkspaceSettings {
/// cache/DENO_DIR for the language server.
pub cache: Option<String>,
/// Override the default stores used to validate certificates. This overrides
/// the environment variable `DENO_TLS_CA_STORE` if present.
pub certificate_stores: Option<Vec<String>>,
/// An option that points to a path string of the config file to apply to
/// code within the workspace.
pub config: Option<String>,
@ -179,6 +183,15 @@ pub struct WorkspaceSettings {
#[serde(default)]
pub suggest: CompletionSettings,
/// An option which sets the cert file to use when attempting to fetch remote
/// resources. This overrides `DENO_CERT` if present.
pub tls_certificate: Option<String>,
/// An option, if set, will unsafely ignore certificate errors when fetching
/// remote resources.
#[serde(default)]
pub unsafely_ignore_certificate_errors: Option<Vec<String>>,
#[serde(default)]
pub unstable: bool,
}
@ -485,6 +498,7 @@ mod tests {
WorkspaceSettings {
enable: false,
cache: None,
certificate_stores: None,
config: None,
import_map: None,
code_lens: CodeLensSettings {
@ -505,6 +519,8 @@ mod tests {
hosts: HashMap::new(),
}
},
tls_certificate: None,
unsafely_ignore_certificate_errors: None,
unstable: false,
}
);

View file

@ -48,7 +48,8 @@ use super::lsp_custom;
use super::parent_process_checker;
use super::performance::Performance;
use super::refactor;
use super::registries;
use super::registries::ModuleRegistry;
use super::registries::ModuleRegistryOptions;
use super::text;
use super::tsc;
use super::tsc::AssetDocument;
@ -96,7 +97,7 @@ pub(crate) struct Inner {
/// on disk or "open" within the client.
pub(crate) documents: Documents,
/// Handles module registries, which allow discovery of modules
module_registries: registries::ModuleRegistry,
module_registries: ModuleRegistry,
/// The path to the module registries cache
module_registries_location: PathBuf,
/// An optional path to the DENO_DIR which has been specified in the client
@ -139,8 +140,11 @@ impl Inner {
let dir = deno_dir::DenoDir::new(maybe_custom_root)
.expect("could not access DENO_DIR");
let module_registries_location = dir.root.join(REGISTRIES_PATH);
let module_registries =
registries::ModuleRegistry::new(&module_registries_location);
let module_registries = ModuleRegistry::new(
&module_registries_location,
ModuleRegistryOptions::default(),
)
.expect("could not create module registries");
let location = dir.root.join(CACHE_PATH);
let documents = Documents::new(&location);
let performance = Arc::new(Performance::default());
@ -425,11 +429,23 @@ impl Inner {
let maybe_custom_root = maybe_cache_path
.clone()
.or_else(|| env::var("DENO_DIR").map(String::into).ok());
let dir = deno_dir::DenoDir::new(maybe_custom_root)
.expect("could not access DENO_DIR");
let dir = deno_dir::DenoDir::new(maybe_custom_root)?;
let module_registries_location = dir.root.join(REGISTRIES_PATH);
self.module_registries =
registries::ModuleRegistry::new(&module_registries_location);
let workspace_settings = self.config.get_workspace_settings();
let maybe_root_path = self
.root_uri
.as_ref()
.and_then(|uri| fs_util::specifier_to_file_path(uri).ok());
self.module_registries = ModuleRegistry::new(
&module_registries_location,
ModuleRegistryOptions {
maybe_root_path,
maybe_ca_stores: workspace_settings.certificate_stores.clone(),
maybe_ca_file: workspace_settings.tls_certificate.clone(),
unsafely_ignore_certificate_errors: workspace_settings
.unsafely_ignore_certificate_errors,
},
)?;
self.module_registries_location = module_registries_location;
self.documents.set_location(dir.root.join(CACHE_PATH));
self.maybe_cache_path = maybe_cache_path;
@ -496,14 +512,23 @@ impl Inner {
async fn update_registries(&mut self) -> Result<(), AnyError> {
let mark = self.performance.mark("update_registries", None::<()>);
for (registry, enabled) in self
.config
.get_workspace_settings()
.suggest
.imports
.hosts
.iter()
{
let workspace_settings = self.config.get_workspace_settings();
let maybe_root_path = self
.root_uri
.as_ref()
.and_then(|uri| fs_util::specifier_to_file_path(uri).ok());
self.module_registries = ModuleRegistry::new(
&self.module_registries_location,
ModuleRegistryOptions {
maybe_root_path,
maybe_ca_stores: workspace_settings.certificate_stores.clone(),
maybe_ca_file: workspace_settings.tls_certificate.clone(),
unsafely_ignore_certificate_errors: workspace_settings
.unsafely_ignore_certificate_errors
.clone(),
},
)?;
for (registry, enabled) in workspace_settings.suggest.imports.hosts.iter() {
if *enabled {
lsp_log!("Enabling import suggestions for: {}", registry);
self.module_registries.enable(registry).await?;
@ -2583,6 +2608,9 @@ impl Inner {
self.maybe_cache_path.clone(),
self.maybe_import_map.clone(),
self.maybe_config_file.clone(),
None,
None,
None,
)
.await,
);
@ -2616,8 +2644,6 @@ impl Inner {
error!("Unable to remove registries cache: {}", err);
LspError::internal_error()
})?;
self.module_registries =
registries::ModuleRegistry::new(&self.module_registries_location);
self.update_registries().await.map_err(|err| {
error!("Unable to update registries: {}", err);
LspError::internal_error()

View file

@ -12,6 +12,7 @@ use super::path_to_regex::StringOrVec;
use super::path_to_regex::Token;
use crate::deno_dir;
use crate::file_fetcher::get_root_cert_store;
use crate::file_fetcher::CacheSetting;
use crate::file_fetcher::FileFetcher;
use crate::http_cache::HttpCache;
@ -37,6 +38,7 @@ use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
const CONFIG_PATH: &str = "/.well-known/deno-import-intellisense.json";
const COMPONENT: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
@ -406,11 +408,19 @@ enum VariableItems {
List(VariableItemsList),
}
#[derive(Debug, Default)]
pub(crate) struct ModuleRegistryOptions {
pub maybe_root_path: Option<PathBuf>,
pub maybe_ca_stores: Option<Vec<String>>,
pub maybe_ca_file: Option<String>,
pub unsafely_ignore_certificate_errors: Option<Vec<String>>,
}
/// A structure which holds the information about currently configured module
/// registries and can provide completion information for URLs that match
/// one of the enabled registries.
#[derive(Debug, Clone)]
pub struct ModuleRegistry {
pub(crate) struct ModuleRegistry {
origins: HashMap<String, Vec<RegistryConfiguration>>,
file_fetcher: FileFetcher,
}
@ -422,29 +432,35 @@ impl Default for ModuleRegistry {
// custom root.
let dir = deno_dir::DenoDir::new(None).unwrap();
let location = dir.root.join("registries");
Self::new(&location)
Self::new(&location, ModuleRegistryOptions::default()).unwrap()
}
}
impl ModuleRegistry {
pub fn new(location: &Path) -> Self {
pub fn new(
location: &Path,
options: ModuleRegistryOptions,
) -> Result<Self, AnyError> {
let http_cache = HttpCache::new(location);
let root_cert_store = Some(get_root_cert_store(
options.maybe_root_path,
options.maybe_ca_stores,
options.maybe_ca_file,
)?);
let mut file_fetcher = FileFetcher::new(
http_cache,
CacheSetting::RespectHeaders,
true,
None,
root_cert_store,
BlobStore::default(),
None,
)
.context("Error creating file fetcher in module registry.")
.unwrap();
options.unsafely_ignore_certificate_errors,
)?;
file_fetcher.set_download_log_level(super::logging::lsp_log_level());
Self {
Ok(Self {
origins: HashMap::new(),
file_fetcher,
}
})
}
fn complete_literal(
@ -1200,7 +1216,8 @@ mod tests {
let _g = test_util::http_server();
let temp_dir = TempDir::new().expect("could not create tmp");
let location = temp_dir.path().join("registries");
let mut module_registry = ModuleRegistry::new(&location);
let mut module_registry =
ModuleRegistry::new(&location, ModuleRegistryOptions::default()).unwrap();
module_registry
.enable("http://localhost:4545/")
.await
@ -1260,7 +1277,8 @@ mod tests {
let _g = test_util::http_server();
let temp_dir = TempDir::new().expect("could not create tmp");
let location = temp_dir.path().join("registries");
let mut module_registry = ModuleRegistry::new(&location);
let mut module_registry =
ModuleRegistry::new(&location, ModuleRegistryOptions::default()).unwrap();
module_registry
.enable("http://localhost:4545/")
.await
@ -1482,7 +1500,8 @@ mod tests {
let _g = test_util::http_server();
let temp_dir = TempDir::new().expect("could not create tmp");
let location = temp_dir.path().join("registries");
let mut module_registry = ModuleRegistry::new(&location);
let mut module_registry =
ModuleRegistry::new(&location, ModuleRegistryOptions::default()).unwrap();
module_registry
.enable_custom("http://localhost:4545/lsp/registries/deno-import-intellisense-key-first.json")
.await
@ -1551,7 +1570,8 @@ mod tests {
let _g = test_util::http_server();
let temp_dir = TempDir::new().expect("could not create tmp");
let location = temp_dir.path().join("registries");
let mut module_registry = ModuleRegistry::new(&location);
let mut module_registry =
ModuleRegistry::new(&location, ModuleRegistryOptions::default()).unwrap();
module_registry
.enable_custom("http://localhost:4545/lsp/registries/deno-import-intellisense-complex.json")
.await
@ -1601,7 +1621,8 @@ mod tests {
let _g = test_util::http_server();
let temp_dir = TempDir::new().expect("could not create tmp");
let location = temp_dir.path().join("registries");
let module_registry = ModuleRegistry::new(&location);
let module_registry =
ModuleRegistry::new(&location, ModuleRegistryOptions::default()).unwrap();
let result = module_registry.check_origin("http://localhost:4545").await;
assert!(result.is_ok());
}
@ -1611,7 +1632,8 @@ mod tests {
let _g = test_util::http_server();
let temp_dir = TempDir::new().expect("could not create tmp");
let location = temp_dir.path().join("registries");
let module_registry = ModuleRegistry::new(&location);
let module_registry =
ModuleRegistry::new(&location, ModuleRegistryOptions::default()).unwrap();
let result = module_registry.check_origin("https://deno.com").await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();

View file

@ -277,11 +277,14 @@ pub fn get_repl_workspace_settings() -> WorkspaceSettings {
WorkspaceSettings {
enable: true,
config: None,
certificate_stores: None,
cache: None,
import_map: None,
code_lens: Default::default(),
internal_debug: false,
lint: false,
tls_certificate: None,
unsafely_ignore_certificate_errors: None,
unstable: false,
suggest: CompletionSettings {
complete_function_calls: false,

View file

@ -8,6 +8,7 @@ use crate::config_file::ConfigFile;
use crate::config_file::MaybeImportsResult;
use crate::deno_dir;
use crate::emit;
use crate::file_fetcher::get_root_cert_store;
use crate::file_fetcher::CacheSetting;
use crate::file_fetcher::FileFetcher;
use crate::flags;
@ -42,11 +43,7 @@ use deno_graph::source::LoadFuture;
use deno_graph::source::Loader;
use deno_graph::MediaType;
use deno_runtime::deno_broadcast_channel::InMemoryBroadcastChannel;
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::inspector_server::InspectorServer;
use deno_runtime::permissions::Permissions;
@ -54,8 +51,6 @@ use import_map::ImportMap;
use log::warn;
use std::collections::HashSet;
use std::env;
use std::fs::File;
use std::io::BufReader;
use std::ops::Deref;
use std::sync::Arc;
@ -101,67 +96,11 @@ impl ProcState {
let deps_cache_location = dir.root.join("deps");
let http_cache = http_cache::HttpCache::new(&deps_cache_location);
let mut root_cert_store = RootCertStore::empty();
let ca_stores: Vec<String> = flags
.ca_stores
.clone()
.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 = flags.ca_file.clone().or_else(|| env::var("DENO_CERT").ok());
if let Some(ca_file) = ca_file {
let certfile = 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
));
}
}
}
let root_cert_store = get_root_cert_store(
None,
flags.ca_stores.clone(),
flags.ca_file.clone(),
)?;
if let Some(insecure_allowlist) =
flags.unsafely_ignore_certificate_errors.as_ref()

View file

@ -3216,6 +3216,124 @@ fn lsp_cache_location() {
shutdown(&mut client);
}
/// Sets the TLS root certificate on startup, which allows the LSP to connect to
/// the custom signed test server and be able to retrieve the registry config
/// and cache files.
#[test]
fn lsp_tls_cert() {
let _g = http_server();
let mut params: lsp::InitializeParams =
serde_json::from_value(load_fixture("initialize_params_tls_cert.json"))
.unwrap();
params.root_uri = Some(Url::from_file_path(testdata_path()).unwrap());
let deno_exe = deno_exe_path();
let mut client = LspClient::new(&deno_exe).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
client.write_notification("initialized", json!({})).unwrap();
did_open(
&mut client,
json!({
"textDocument": {
"uri": "file:///a/file_01.ts",
"languageId": "typescript",
"version": 1,
"text": "export const a = \"a\";\n",
}
}),
);
let diagnostics =
did_open(&mut client, load_fixture("did_open_params_tls_cert.json"));
let diagnostics = diagnostics.into_iter().flat_map(|x| x.diagnostics);
assert_eq!(diagnostics.count(), 14);
let (maybe_res, maybe_err) = client
.write_request::<_, _, Value>(
"deno/cache",
json!({
"referrer": {
"uri": "file:///a/file.ts",
},
"uris": [],
}),
)
.unwrap();
assert!(maybe_err.is_none());
assert!(maybe_res.is_some());
let (maybe_res, maybe_err) = client
.write_request(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
},
"position": {
"line": 0,
"character": 28
}
}),
)
.unwrap();
assert!(maybe_err.is_none());
assert_eq!(
maybe_res,
Some(json!({
"contents": {
"kind": "markdown",
"value": "**Resolved Dependency**\n\n**Code**: https&#8203;://localhost:5545/xTypeScriptTypes.js\n"
},
"range": {
"start": {
"line": 0,
"character": 19
},
"end":{
"line": 0,
"character": 63
}
}
}))
);
let (maybe_res, maybe_err) = client
.write_request::<_, _, Value>(
"textDocument/hover",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
},
"position": {
"line": 7,
"character": 28
}
}),
)
.unwrap();
assert!(maybe_err.is_none());
assert_eq!(
maybe_res,
Some(json!({
"contents": {
"kind": "markdown",
"value": "**Resolved Dependency**\n\n**Code**: http&#8203;://localhost:4545/x/a/mod.ts\n\n\n---\n\n**a**\n\nmod.ts"
},
"range": {
"start": {
"line": 7,
"character": 19
},
"end": {
"line": 7,
"character": 53
}
}
}))
);
shutdown(&mut client);
}
#[test]
fn lsp_diagnostics_warn() {
let _g = http_server();

View file

@ -0,0 +1,8 @@
{
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "import * as a from \"https://localhost:5545/xTypeScriptTypes.js\";\n// @deno-types=\"https://localhost:5545/type_definitions/foo.d.ts\"\nimport * as b from \"https://localhost:5545/type_definitions/foo.js\";\nimport * as c from \"https://localhost:5545/subdir/type_reference.js\";\nimport * as d from \"https://localhost:5545/subdir/mod1.ts\";\nimport * as e from \"data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=\";\nimport * as f from \"./file_01.ts\";\nimport * as g from \"http://localhost:4545/x/a/mod.ts\";\n\nconsole.log(a, b, c, d, e, f, g);\n"
}
}

View file

@ -8,6 +8,7 @@
"initializationOptions": {
"enable": true,
"cache": null,
"certificateStores": null,
"codeLens": {
"implementations": true,
"references": true,
@ -25,6 +26,8 @@
"hosts": {}
}
},
"tlsCertificate": null,
"unsafelyIgnoreCertificateErrors": null,
"unstable": false
},
"capabilities": {

View file

@ -0,0 +1,71 @@
{
"processId": 0,
"clientInfo": {
"name": "test-harness",
"version": "1.0.0"
},
"rootUri": null,
"initializationOptions": {
"enable": true,
"cache": null,
"certificateStores": null,
"codeLens": {
"implementations": true,
"references": true,
"test": true
},
"config": "",
"importMap": null,
"lint": true,
"suggest": {
"autoImports": true,
"completeFunctionCalls": false,
"names": true,
"paths": true,
"imports": {
"hosts": {
"https://localhost:5545": true,
"http://localhost:4545": true
}
}
},
"tlsCertificate": "tls/RootCA.pem",
"unsafelyIgnoreCertificateErrors": null,
"unstable": false
},
"capabilities": {
"textDocument": {
"codeAction": {
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"quickfix",
"refactor"
]
}
},
"isPreferredSupport": true,
"dataSupport": true,
"disabledSupport": true,
"resolveSupport": {
"properties": [
"edit"
]
}
},
"foldingRange": {
"lineFoldingOnly": true
},
"synchronization": {
"dynamicRegistration": true,
"willSave": true,
"willSaveWaitUntil": true,
"didSave": true
}
},
"workspace": {
"configuration": true,
"workspaceFolders": true
}
}
}