diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index 18d78c514b83af..e69756b20ab962 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -112,7 +112,8 @@ impl SourceFileFetcher { cache_blacklist: Vec, no_remote: bool, cached_only: bool, - ) -> std::io::Result { + ca_file: Option, + ) -> Result { let file_fetcher = Self { deps_cache, progress, @@ -121,7 +122,7 @@ impl SourceFileFetcher { use_disk_cache, no_remote, cached_only, - http_client: create_http_client(), + http_client: create_http_client(ca_file)?, }; Ok(file_fetcher) @@ -862,6 +863,7 @@ mod tests { vec![], false, false, + None, ) .expect("setup fail") } diff --git a/cli/flags.rs b/cli/flags.rs index 445a08c0b7b899..82cd59ca764f58 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -102,6 +102,7 @@ pub struct DenoFlags { pub lock: Option, pub lock_write: bool, + pub ca_file: Option, } fn join_paths(whitelist: &[PathBuf], d: &str) -> String { @@ -313,6 +314,7 @@ fn fmt_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { fn install_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { permission_args_parse(flags, matches); + ca_file_arg_parse(flags, matches); let dir = if matches.is_present("dir") { let install_dir = matches.value_of("dir").unwrap(); @@ -343,6 +345,8 @@ fn install_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { } fn bundle_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { + ca_file_arg_parse(flags, matches); + let source_file = matches.value_of("source_file").unwrap().to_string(); let out_file = if let Some(out_file) = matches.value_of("out_file") { @@ -375,6 +379,7 @@ fn completions_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { fn repl_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { v8_flags_arg_parse(flags, matches); + ca_file_arg_parse(flags, matches); flags.subcommand = DenoSubcommand::Repl; flags.allow_net = true; flags.allow_env = true; @@ -387,6 +392,7 @@ fn repl_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { fn eval_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { v8_flags_arg_parse(flags, matches); + ca_file_arg_parse(flags, matches); flags.allow_net = true; flags.allow_env = true; flags.allow_run = true; @@ -399,6 +405,8 @@ fn eval_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { } fn info_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { + ca_file_arg_parse(flags, matches); + flags.subcommand = DenoSubcommand::Info { file: matches.value_of("file").map(|f| f.to_string()), }; @@ -410,6 +418,7 @@ fn fetch_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { importmap_arg_parse(flags, matches); config_arg_parse(flags, matches); no_remote_arg_parse(flags, matches); + ca_file_arg_parse(flags, matches); let files = matches .values_of("file") .unwrap() @@ -444,6 +453,7 @@ fn run_test_args_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { v8_flags_arg_parse(flags, matches); no_remote_arg_parse(flags, matches); permission_args_parse(flags, matches); + ca_file_arg_parse(flags, matches); if matches.is_present("cached-only") { flags.cached_only = true; @@ -558,6 +568,7 @@ fn repl_subcommand<'a, 'b>() -> App<'a, 'b> { SubCommand::with_name("repl") .about("Read Eval Print Loop") .arg(v8_flags_arg()) + .arg(ca_file_arg()) } fn install_subcommand<'a, 'b>() -> App<'a, 'b> { @@ -586,6 +597,7 @@ fn install_subcommand<'a, 'b>() -> App<'a, 'b> { .multiple(true) .allow_hyphen_values(true) ) + .arg(ca_file_arg()) .about("Install script as executable") .long_about( "Installs a script as executable. The default installation directory is @@ -608,6 +620,7 @@ fn bundle_subcommand<'a, 'b>() -> App<'a, 'b> { .required(true), ) .arg(Arg::with_name("out_file").takes_value(true).required(false)) + .arg(ca_file_arg()) .about("Bundle module and dependencies into single file") .long_about( "Output a single JavaScript file with all dependencies. @@ -642,6 +655,7 @@ Example: fn eval_subcommand<'a, 'b>() -> App<'a, 'b> { SubCommand::with_name("eval") + .arg(ca_file_arg()) .about("Eval script") .long_about( "Evaluate JavaScript from command-line @@ -677,6 +691,7 @@ Remote modules cache: directory containing remote modules TypeScript compiler cache: directory containing TS compiler output", ) .arg(Arg::with_name("file").takes_value(true).required(false)) + .arg(ca_file_arg()) } fn fetch_subcommand<'a, 'b>() -> App<'a, 'b> { @@ -693,6 +708,7 @@ fn fetch_subcommand<'a, 'b>() -> App<'a, 'b> { .required(true) .min_values(1), ) + .arg(ca_file_arg()) .about("Fetch the dependencies") .long_about( "Fetch and compile remote dependencies recursively. @@ -777,6 +793,7 @@ fn run_test_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .arg(lock_write_arg()) .arg(no_remote_arg()) .arg(v8_flags_arg()) + .arg(ca_file_arg()) .arg( Arg::with_name("cached-only") .long("cached-only") @@ -896,6 +913,17 @@ fn config_arg_parse(flags: &mut DenoFlags, matches: &ArgMatches) { flags.config_path = matches.value_of("config").map(ToOwned::to_owned); } +fn ca_file_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("cert") + .long("cert") + .value_name("FILE") + .help("Load certificate authority from PEM encoded file") + .takes_value(true) +} +fn ca_file_arg_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { + flags.ca_file = matches.value_of("cert").map(ToOwned::to_owned); +} + fn reload_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name("reload") .short("r") @@ -2045,3 +2073,163 @@ mod tests { ); } } + +#[test] +fn run_with_cafile() { + let r = flags_from_vec_safe(svec![ + "deno", + "run", + "--cert", + "example.crt", + "script.ts" + ]); + assert_eq!( + r.unwrap(), + DenoFlags { + subcommand: DenoSubcommand::Run { + script: "script.ts".to_string(), + }, + ca_file: Some("example.crt".to_owned()), + ..DenoFlags::default() + } + ); +} + +#[test] +fn bundle_with_cafile() { + let r = flags_from_vec_safe(svec![ + "deno", + "bundle", + "--cert", + "example.crt", + "source.ts" + ]); + assert_eq!( + r.unwrap(), + DenoFlags { + subcommand: DenoSubcommand::Bundle { + source_file: "source.ts".to_string(), + out_file: None, + }, + ca_file: Some("example.crt".to_owned()), + ..DenoFlags::default() + } + ); +} + +#[test] +fn eval_with_cafile() { + let r = flags_from_vec_safe(svec![ + "deno", + "eval", + "--cert", + "example.crt", + "console.log('hello world')" + ]); + assert_eq!( + r.unwrap(), + DenoFlags { + subcommand: DenoSubcommand::Eval { + code: "console.log('hello world')".to_string(), + }, + ca_file: Some("example.crt".to_owned()), + allow_net: true, + allow_env: true, + allow_run: true, + allow_read: true, + allow_write: true, + allow_plugin: true, + allow_hrtime: true, + ..DenoFlags::default() + } + ); +} + +#[test] +fn fetch_with_cafile() { + let r = flags_from_vec_safe(svec![ + "deno", + "fetch", + "--cert", + "example.crt", + "script.ts", + "script_two.ts" + ]); + assert_eq!( + r.unwrap(), + DenoFlags { + subcommand: DenoSubcommand::Fetch { + files: svec!["script.ts", "script_two.ts"], + }, + ca_file: Some("example.crt".to_owned()), + ..DenoFlags::default() + } + ); +} + +#[test] +fn info_with_cafile() { + let r = flags_from_vec_safe(svec![ + "deno", + "info", + "--cert", + "example.crt", + "https://example.com" + ]); + assert_eq!( + r.unwrap(), + DenoFlags { + subcommand: DenoSubcommand::Info { + file: Some("https://example.com".to_string()), + }, + ca_file: Some("example.crt".to_owned()), + ..DenoFlags::default() + } + ); +} + +#[test] +fn install_with_cafile() { + let r = flags_from_vec_safe(svec![ + "deno", + "install", + "--cert", + "example.crt", + "deno_colors", + "https://deno.land/std/examples/colors.ts" + ]); + assert_eq!( + r.unwrap(), + DenoFlags { + subcommand: DenoSubcommand::Install { + dir: None, + exe_name: "deno_colors".to_string(), + module_url: "https://deno.land/std/examples/colors.ts".to_string(), + args: vec![], + force: false, + }, + ca_file: Some("example.crt".to_owned()), + ..DenoFlags::default() + } + ); +} + +#[test] +fn repl_with_cafile() { + let r = flags_from_vec_safe(svec!["deno", "repl", "--cert", "example.crt"]); + assert_eq!( + r.unwrap(), + DenoFlags { + subcommand: DenoSubcommand::Repl {}, + ca_file: Some("example.crt".to_owned()), + allow_read: true, + allow_write: true, + allow_net: true, + allow_env: true, + allow_run: true, + allow_plugin: true, + allow_hrtime: true, + ..DenoFlags::default() + } + ); +} diff --git a/cli/global_state.rs b/cli/global_state.rs index 0bbd213aa416a6..a11900218a2e2f 100644 --- a/cli/global_state.rs +++ b/cli/global_state.rs @@ -81,6 +81,7 @@ impl GlobalState { flags.cache_blacklist.clone(), flags.no_remote, flags.cached_only, + flags.ca_file.clone(), )?; let ts_compiler = TsCompiler::new( diff --git a/cli/http_util.rs b/cli/http_util.rs index 6bff0f8bbd8eaf..0140d014a36d01 100644 --- a/cli/http_util.rs +++ b/cli/http_util.rs @@ -1,6 +1,7 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use crate::deno_error; use crate::deno_error::DenoError; +use crate::deno_error::ErrorKind; use crate::version; use brotli2::read::BrotliDecoder; use bytes::Bytes; @@ -21,6 +22,7 @@ use reqwest::Client; use reqwest::Response; use reqwest::StatusCode; use std::cmp::min; +use std::fs::File; use std::future::Future; use std::io; use std::io::Read; @@ -32,20 +34,31 @@ use url::Url; /// Create new instance of async reqwest::Client. This client supports /// proxies and doesn't follow redirects. -pub fn create_http_client() -> Client { +pub fn create_http_client(ca_file: Option) -> Result { let mut headers = HeaderMap::new(); headers.insert( USER_AGENT, format!("Deno/{}", version::DENO).parse().unwrap(), ); - Client::builder() + let mut builder = Client::builder() .redirect(Policy::none()) .default_headers(headers) - .use_rustls_tls() - .build() - .unwrap() -} + .use_rustls_tls(); + + if let Some(ca_file) = ca_file { + let mut buf = Vec::new(); + File::open(ca_file)?.read_to_end(&mut buf)?; + let cert = reqwest::Certificate::from_pem(&buf)?; + builder = builder.add_root_certificate(cert); + } + builder.build().map_err(|_| { + ErrBox::from(DenoError::new( + ErrorKind::Other, + "Unable to build http client".to_string(), + )) + }) +} /// Construct the next uri based on base uri and location header fragment /// See fn resolve_url_from_location(base_url: &Url, location: &str) -> Url { @@ -276,7 +289,7 @@ mod tests { // Relies on external http server. See tools/http_server.py let url = Url::parse("http://127.0.0.1:4545/cli/tests/fixture.json").unwrap(); - let client = create_http_client(); + let client = create_http_client(None).unwrap(); let result = fetch_once(client, &url, None).await; if let Ok(FetchOnceResult::Code(payload)) = result { assert!(!payload.body.is_empty()); @@ -297,7 +310,7 @@ mod tests { "http://127.0.0.1:4545/cli/tests/053_import_compression/gziped", ) .unwrap(); - let client = create_http_client(); + let client = create_http_client(None).unwrap(); let result = fetch_once(client, &url, None).await; if let Ok(FetchOnceResult::Code(payload)) = result { assert_eq!( @@ -320,7 +333,7 @@ mod tests { async fn test_fetch_with_etag() { let http_server_guard = crate::test_util::http_server(); let url = Url::parse("http://127.0.0.1:4545/etag_script.ts").unwrap(); - let client = create_http_client(); + let client = create_http_client(None).unwrap(); let result = fetch_once(client.clone(), &url, None).await; if let Ok(FetchOnceResult::Code(ResultPayload { body, @@ -353,7 +366,7 @@ mod tests { "http://127.0.0.1:4545/cli/tests/053_import_compression/brotli", ) .unwrap(); - let client = create_http_client(); + let client = create_http_client(None).unwrap(); let result = fetch_once(client, &url, None).await; if let Ok(FetchOnceResult::Code(payload)) = result { assert!(!payload.body.is_empty()); @@ -382,7 +395,7 @@ mod tests { // Dns resolver substitutes `127.0.0.1` with `localhost` let target_url = Url::parse("http://localhost:4545/cli/tests/fixture.json").unwrap(); - let client = create_http_client(); + let client = create_http_client(None).unwrap(); let result = fetch_once(client, &url, None).await; if let Ok(FetchOnceResult::Redirect(url)) = result { assert_eq!(url, target_url); @@ -429,4 +442,133 @@ mod tests { assert_eq!(new_uri.host_str().unwrap(), "deno.land"); assert_eq!(new_uri.path(), "/z"); } + + #[tokio::test] + async fn test_fetch_with_cafile_sync_string() { + let http_server_guard = crate::test_util::http_server(); + // Relies on external http server. See tools/http_server.py + let url = + Url::parse("https://localhost:5545/cli/tests/fixture.json").unwrap(); + + let client = create_http_client(Some(String::from( + crate::test_util::root_path() + .join("std/http/testdata/tls/RootCA.pem") + .to_str() + .unwrap(), + ))) + .unwrap(); + let result = fetch_once(client, &url, None).await; + + if let Ok(FetchOnceResult::Code(payload)) = result { + assert!(!payload.body.is_empty()); + assert_eq!(payload.content_type, Some("application/json".to_string())); + assert_eq!(payload.etag, None); + assert_eq!(payload.x_typescript_types, None); + } else { + panic!(); + } + drop(http_server_guard); + } + + #[tokio::test] + async fn test_fetch_with_cafile_gzip() { + let http_server_guard = crate::test_util::http_server(); + // Relies on external http server. See tools/http_server.py + let url = Url::parse( + "https://localhost:5545/cli/tests/053_import_compression/gziped", + ) + .unwrap(); + let client = create_http_client(Some(String::from( + crate::test_util::root_path() + .join("std/http/testdata/tls/RootCA.pem") + .to_str() + .unwrap(), + ))) + .unwrap(); + let result = fetch_once(client, &url, None).await; + if let Ok(FetchOnceResult::Code(payload)) = result { + assert_eq!( + String::from_utf8(payload.body).unwrap(), + "console.log('gzip')" + ); + assert_eq!( + payload.content_type, + Some("application/javascript".to_string()) + ); + assert_eq!(payload.etag, None); + assert_eq!(payload.x_typescript_types, None); + } else { + panic!(); + } + drop(http_server_guard); + } + + #[tokio::test] + async fn test_fetch_with_cafile_with_etag() { + let http_server_guard = crate::test_util::http_server(); + let url = Url::parse("https://localhost:5545/etag_script.ts").unwrap(); + let client = create_http_client(Some(String::from( + crate::test_util::root_path() + .join("std/http/testdata/tls/RootCA.pem") + .to_str() + .unwrap(), + ))) + .unwrap(); + let result = fetch_once(client.clone(), &url, None).await; + if let Ok(FetchOnceResult::Code(ResultPayload { + body, + content_type, + etag, + x_typescript_types, + })) = result + { + assert!(!body.is_empty()); + assert_eq!(String::from_utf8(body).unwrap(), "console.log('etag')"); + assert_eq!(content_type, Some("application/typescript".to_string())); + assert_eq!(etag, Some("33a64df551425fcc55e".to_string())); + assert_eq!(x_typescript_types, None); + } else { + panic!(); + } + + let res = + fetch_once(client, &url, Some("33a64df551425fcc55e".to_string())).await; + assert_eq!(res.unwrap(), FetchOnceResult::NotModified); + + drop(http_server_guard); + } + + #[tokio::test] + async fn test_fetch_with_cafile_brotli() { + let http_server_guard = crate::test_util::http_server(); + // Relies on external http server. See tools/http_server.py + let url = Url::parse( + "https://localhost:5545/cli/tests/053_import_compression/brotli", + ) + .unwrap(); + let client = create_http_client(Some(String::from( + crate::test_util::root_path() + .join("std/http/testdata/tls/RootCA.pem") + .to_str() + .unwrap(), + ))) + .unwrap(); + let result = fetch_once(client, &url, None).await; + if let Ok(FetchOnceResult::Code(payload)) = result { + assert!(!payload.body.is_empty()); + assert_eq!( + String::from_utf8(payload.body).unwrap(), + "console.log('brotli');" + ); + assert_eq!( + payload.content_type, + Some("application/javascript".to_string()) + ); + assert_eq!(payload.etag, None); + assert_eq!(payload.x_typescript_types, None); + } else { + panic!(); + } + drop(http_server_guard); + } } diff --git a/cli/installer.rs b/cli/installer.rs index eeae35c442a0d9..d2d263447c1795 100644 --- a/cli/installer.rs +++ b/cli/installer.rs @@ -155,6 +155,10 @@ pub fn install( let mut executable_args = vec!["run".to_string()]; executable_args.extend_from_slice(&flags.to_permission_args()); + if let Some(ca_file) = flags.ca_file { + executable_args.push("--cert".to_string()); + executable_args.push(ca_file) + } executable_args.push(module_url.to_string()); executable_args.extend_from_slice(&args); diff --git a/cli/ops/fetch.rs b/cli/ops/fetch.rs index f43133d7fed0b4..580fd993ae61ac 100644 --- a/cli/ops/fetch.rs +++ b/cli/ops/fetch.rs @@ -31,7 +31,8 @@ pub fn op_fetch( let args: FetchArgs = serde_json::from_value(args)?; let url = args.url; - let client = create_http_client(); + let client = + create_http_client(state.borrow().global_state.flags.ca_file.clone())?; let method = match args.method { Some(method_str) => Method::from_bytes(method_str.as_bytes())?, diff --git a/cli/test_util.rs b/cli/test_util.rs index 9c0307096028a1..d2a49d05fefbf0 100644 --- a/cli/test_util.rs +++ b/cli/test_util.rs @@ -74,7 +74,20 @@ pub fn http_server() -> HttpServerGuard { println!("tools/http_server.py starting..."); let mut child = Command::new("python") .current_dir(root_path()) - .args(&["-u", "tools/http_server.py"]) + .args(&[ + "-u", + "tools/http_server.py", + "--certfile", + root_path() + .join("std/http/testdata/tls/localhost.crt") + .to_str() + .unwrap(), + "--keyfile", + root_path() + .join("std/http/testdata/tls/localhost.key") + .to_str() + .unwrap(), + ]) .stdout(Stdio::piped()) .spawn() .expect("failed to execute child"); diff --git a/cli/tests/cafile_info.ts b/cli/tests/cafile_info.ts new file mode 100644 index 00000000000000..e11d106e651ad2 --- /dev/null +++ b/cli/tests/cafile_info.ts @@ -0,0 +1,24 @@ +// When run against the test HTTP server, it will serve different media types +// based on the URL containing `.t#.` strings, which exercises the different +// mapping of media types end to end. + +import { loaded as loadedTs1 } from "https://localhost:5545/cli/tests/subdir/mt_text_typescript.t1.ts"; +import { loaded as loadedTs2 } from "https://localhost:5545/cli/tests/subdir/mt_video_vdn.t2.ts"; +import { loaded as loadedTs3 } from "https://localhost:5545/cli/tests/subdir/mt_video_mp2t.t3.ts"; +import { loaded as loadedTs4 } from "https://localhost:5545/cli/tests/subdir/mt_application_x_typescript.t4.ts"; +import { loaded as loadedJs1 } from "https://localhost:5545/cli/tests/subdir/mt_text_javascript.j1.js"; +import { loaded as loadedJs2 } from "https://localhost:5545/cli/tests/subdir/mt_application_ecmascript.j2.js"; +import { loaded as loadedJs3 } from "https://localhost:5545/cli/tests/subdir/mt_text_ecmascript.j3.js"; +import { loaded as loadedJs4 } from "https://localhost:5545/cli/tests/subdir/mt_application_x_javascript.j4.js"; + +console.log( + "success", + loadedTs1, + loadedTs2, + loadedTs3, + loadedTs4, + loadedJs1, + loadedJs2, + loadedJs3, + loadedJs4 +); diff --git a/cli/tests/cafile_info.ts.out b/cli/tests/cafile_info.ts.out new file mode 100644 index 00000000000000..443b92eea66483 --- /dev/null +++ b/cli/tests/cafile_info.ts.out @@ -0,0 +1,14 @@ +local: [WILDCARD]cafile_info.ts +type: TypeScript +compiled: [WILDCARD].js +map: [WILDCARD].js.map +deps: +https://localhost:5545/cli/tests/cafile_info.ts + ├── https://localhost:5545/cli/tests/subdir/mt_text_typescript.t1.ts + ├── https://localhost:5545/cli/tests/subdir/mt_video_vdn.t2.ts + ├── https://localhost:5545/cli/tests/subdir/mt_video_mp2t.t3.ts + ├── https://localhost:5545/cli/tests/subdir/mt_application_x_typescript.t4.ts + ├── https://localhost:5545/cli/tests/subdir/mt_text_javascript.j1.js + ├── https://localhost:5545/cli/tests/subdir/mt_application_ecmascript.j2.js + ├── https://localhost:5545/cli/tests/subdir/mt_text_ecmascript.j3.js + └── https://localhost:5545/cli/tests/subdir/mt_application_x_javascript.j4.js diff --git a/cli/tests/cafile_ts_fetch.ts b/cli/tests/cafile_ts_fetch.ts new file mode 100644 index 00000000000000..be158bf70a888f --- /dev/null +++ b/cli/tests/cafile_ts_fetch.ts @@ -0,0 +1,3 @@ +fetch("https://localhost:5545/cli/tests/cafile_ts_fetch.ts.out") + .then(r => r.text()) + .then(t => console.log(t.trimEnd())); diff --git a/cli/tests/cafile_ts_fetch.ts.out b/cli/tests/cafile_ts_fetch.ts.out new file mode 100644 index 00000000000000..e965047ad7c578 --- /dev/null +++ b/cli/tests/cafile_ts_fetch.ts.out @@ -0,0 +1 @@ +Hello diff --git a/cli/tests/cafile_url_imports.ts b/cli/tests/cafile_url_imports.ts new file mode 100644 index 00000000000000..f781f32f50eeb7 --- /dev/null +++ b/cli/tests/cafile_url_imports.ts @@ -0,0 +1,3 @@ +import { printHello } from "https://localhost:5545/cli/tests/subdir/mod2.ts"; +printHello(); +console.log("success"); diff --git a/cli/tests/cafile_url_imports.ts.out b/cli/tests/cafile_url_imports.ts.out new file mode 100644 index 00000000000000..989ce33e9316dd --- /dev/null +++ b/cli/tests/cafile_url_imports.ts.out @@ -0,0 +1,2 @@ +Hello +success diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 35127c155a7e39..600e58041ff1ad 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -923,6 +923,174 @@ itest!(import_wasm_via_network { http_server: true, }); +itest!(cafile_url_imports { + args: "run --reload --cert tls/RootCA.pem cafile_url_imports.ts", + output: "cafile_url_imports.ts.out", + http_server: true, +}); + +itest!(cafile_ts_fetch { + args: "run --reload --allow-net --cert tls/RootCA.pem cafile_ts_fetch.ts", + output: "cafile_ts_fetch.ts.out", + http_server: true, +}); + +itest!(cafile_eval { + args: "eval --cert tls/RootCA.pem fetch('https://localhost:5545/cli/tests/cafile_ts_fetch.ts.out').then(r=>r.text()).then(t=>console.log(t.trimEnd()))", + output: "cafile_ts_fetch.ts.out", + http_server: true, +}); + +itest!(cafile_info { + args: + "info --cert tls/RootCA.pem https://localhost:5545/cli/tests/cafile_info.ts", + output: "cafile_info.ts.out", + http_server: true, +}); + +#[test] +fn cafile_fetch() { + pub use deno::test_util::*; + use std::process::Command; + use tempfile::TempDir; + + let g = util::http_server(); + + let deno_dir = TempDir::new().expect("tempdir fail"); + let t = util::root_path().join("cli/tests/cafile_url_imports.ts"); + let cafile = util::root_path().join("cli/tests/tls/RootCA.pem"); + let output = Command::new(deno_exe_path()) + .env("DENO_DIR", deno_dir.path()) + .current_dir(util::root_path()) + .arg("fetch") + .arg("--cert") + .arg(cafile) + .arg(t) + .output() + .expect("Failed to spawn script"); + + let code = output.status.code(); + let out = std::str::from_utf8(&output.stdout).unwrap(); + + assert_eq!(Some(0), code); + assert_eq!(out, ""); + + let expected_path = deno_dir + .path() + .join("deps/https/localhost_PORT5545/cli/tests/subdir/mod2.ts"); + assert_eq!(expected_path.exists(), true); + + drop(g); +} + +#[test] +fn cafile_install_remote_module() { + pub use deno::test_util::*; + use std::env; + use std::path::PathBuf; + use std::process::Command; + use tempfile::TempDir; + + let g = util::http_server(); + let temp_dir = TempDir::new().expect("tempdir fail"); + let deno_dir = TempDir::new().expect("tempdir fail"); + let cafile = util::root_path().join("cli/tests/tls/RootCA.pem"); + + let install_output = Command::new(deno_exe_path()) + .env("DENO_DIR", deno_dir.path()) + .current_dir(util::root_path()) + .arg("install") + .arg("--cert") + .arg(cafile) + .arg("--dir") + .arg(temp_dir.path()) + .arg("echo_test") + .arg("https://localhost:5545/cli/tests/echo.ts") + .output() + .expect("Failed to spawn script"); + + let code = install_output.status.code(); + assert_eq!(Some(0), code); + + let mut file_path = temp_dir.path().join("echo_test"); + if cfg!(windows) { + file_path = file_path.with_extension(".cmd"); + } + assert!(file_path.exists()); + + let path_var_name = if cfg!(windows) { "Path" } else { "PATH" }; + let paths_var = env::var_os(path_var_name).expect("PATH not set"); + let mut paths: Vec = env::split_paths(&paths_var).collect(); + paths.push(temp_dir.path().to_owned()); + paths.push(util::target_dir()); + let path_var_value = env::join_paths(paths).expect("Can't create PATH"); + + let output = Command::new(file_path) + .current_dir(temp_dir.path()) + .arg("foo") + .env(path_var_name, path_var_value) + .output() + .expect("failed to spawn script"); + assert!(std::str::from_utf8(&output.stdout) + .unwrap() + .trim() + .ends_with("foo")); + + drop(deno_dir); + drop(temp_dir); + drop(g) +} + +#[test] +fn cafile_bundle_remote_exports() { + use tempfile::TempDir; + + let g = util::http_server(); + + // First we have to generate a bundle of some remote module that has exports. + let mod1 = "https://localhost:5545/cli/tests/subdir/mod1.ts"; + let cafile = util::root_path().join("cli/tests/tls/RootCA.pem"); + let t = TempDir::new().expect("tempdir fail"); + let bundle = t.path().join("mod1.bundle.js"); + let mut deno = util::deno_cmd() + .current_dir(util::root_path()) + .arg("bundle") + .arg("--cert") + .arg(cafile) + .arg(mod1) + .arg(&bundle) + .spawn() + .expect("failed to spawn script"); + let status = deno.wait().expect("failed to wait for the child process"); + assert!(status.success()); + assert!(bundle.is_file()); + + // Now we try to use that bundle from another module. + let test = t.path().join("test.js"); + std::fs::write( + &test, + " + import { printHello3 } from \"./mod1.bundle.js\"; + printHello3(); ", + ) + .expect("error writing file"); + + let output = util::deno_cmd() + .current_dir(util::root_path()) + .arg("run") + .arg(&test) + .output() + .expect("failed to spawn script"); + // check the output of the test.ts program. + assert!(std::str::from_utf8(&output.stdout) + .unwrap() + .trim() + .ends_with("Hello")); + assert_eq!(output.stderr, b""); + + drop(g) +} + mod util { use deno::colors::strip_ansi_codes; pub use deno::test_util::*; diff --git a/cli/tests/tls/README.md b/cli/tests/tls/README.md index 14399ae828ba5e..34de47deadf812 100644 --- a/cli/tests/tls/README.md +++ b/cli/tests/tls/README.md @@ -5,7 +5,7 @@ https://gist.github.com/cecilemuller/9492b848eb8fe46d462abeb26656c4f8 ## Certificate authority (CA) -Generate RootCA.pem, RootCA.key & RootCA.crt: +Generate RootCA.pem, RootCA.key, RootCA.crt: ```shell openssl req -x509 -nodes -new -sha256 -days 36135 -newkey rsa:2048 -keyout RootCA.key -out RootCA.pem -subj "/C=US/CN=Example-Root-CA" diff --git a/tools/http_server.py b/tools/http_server.py index 871888a4e67eaa..2097c153d419ea 100755 --- a/tools/http_server.py +++ b/tools/http_server.py @@ -12,6 +12,9 @@ from time import sleep from threading import Thread from util import root_path +import ssl +import getopt +import argparse PORT = 4545 REDIRECT_PORT = 4546 @@ -19,7 +22,52 @@ DOUBLE_REDIRECTS_PORT = 4548 INF_REDIRECTS_PORT = 4549 -QUIET = '-v' not in sys.argv and '--verbose' not in sys.argv +HTTPS_PORT = 5545 + + +def create_http_arg_parser(): + parser = argparse.ArgumentParser() + parser.add_argument('--certfile') + parser.add_argument('--keyfile') + parser.add_argument('--verbose', '-v', action='store_true') + return parser + + +HttpArgParser = create_http_arg_parser() + +args, unknown = HttpArgParser.parse_known_args(sys.argv[1:]) +CERT_FILE = args.certfile +KEY_FILE = args.keyfile +QUIET = not args.verbose + + +class SSLTCPServer(SocketServer.TCPServer): + def __init__(self, + server_address, + request_handler, + certfile, + keyfile, + ssl_version=ssl.PROTOCOL_TLSv1_2, + bind_and_activate=True): + SocketServer.TCPServer.__init__(self, server_address, request_handler, + bind_and_activate) + self.certfile = certfile + self.keyfile = keyfile + self.ssl_version = ssl_version + + def get_request(self): + newsocket, fromaddr = self.socket.accept() + connstream = ssl.wrap_socket( + newsocket, + server_side=True, + certfile=self.certfile, + keyfile=self.keyfile, + ssl_version=self.ssl_version) + return connstream, fromaddr + + +class SSLThreadingTCPServer(SocketServer.ThreadingMixIn, SSLTCPServer): + pass class QuietSimpleHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): @@ -169,7 +217,7 @@ def guess_type(self, path): RunningServer = namedtuple("RunningServer", ["server", "thread"]) -def get_socket(port, handler): +def get_socket(port, handler, use_https): SocketServer.TCPServer.allow_reuse_address = True if os.name != "nt": # We use AF_INET6 to avoid flaky test issue, particularly with @@ -177,6 +225,9 @@ def get_socket(port, handler): # flaky tests, but it does appear to... # See https://github.com/denoland/deno/issues/3332 SocketServer.TCPServer.address_family = socket.AF_INET6 + + if use_https: + return SSLThreadingTCPServer(("", port), handler, CERT_FILE, KEY_FILE) return SocketServer.TCPServer(("", port), handler) @@ -190,7 +241,7 @@ def server(): ".jsx": "application/javascript", ".json": "application/json", }) - s = get_socket(PORT, Handler) + s = get_socket(PORT, Handler, False) if not QUIET: print "Deno test server http://localhost:%d/" % PORT return RunningServer(s, start(s)) @@ -207,7 +258,7 @@ def do_GET(self): target_host + extra_path_segment + self.path) self.end_headers() - s = get_socket(host_port, RedirectHandler) + s = get_socket(host_port, RedirectHandler, False) if not QUIET: print "redirect server http://localhost:%d/ -> http://localhost:%d/" % ( host_port, target_port) @@ -236,6 +287,22 @@ def inf_redirects_server(): return base_redirect_server(INF_REDIRECTS_PORT, INF_REDIRECTS_PORT) +def https_server(): + os.chdir(root_path) # Hopefully the main thread doesn't also chdir. + Handler = ContentTypeHandler + Handler.extensions_map.update({ + ".ts": "application/typescript", + ".js": "application/javascript", + ".tsx": "application/typescript", + ".jsx": "application/javascript", + ".json": "application/json", + }) + s = get_socket(HTTPS_PORT, Handler, True) + if not QUIET: + print "Deno https test server https://localhost:%d/" % HTTPS_PORT + return RunningServer(s, start(s)) + + def start(s): thread = Thread(target=s.serve_forever, kwargs={"poll_interval": 0.05}) thread.daemon = True @@ -246,7 +313,7 @@ def start(s): @contextmanager def spawn(): servers = (server(), redirect_server(), another_redirect_server(), - double_redirects_server()) + double_redirects_server(), https_server()) while any(not s.thread.is_alive() for s in servers): sleep(0.01) try: