diff --git a/Cargo.lock b/Cargo.lock index 34cbbf275e..09eb674c01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3838,6 +3838,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-stream", + "base64 0.13.0", "bytes", "futures", "hyper", diff --git a/cli/auth_tokens.rs b/cli/auth_tokens.rs index 83c97e641e..c81c296f37 100644 --- a/cli/auth_tokens.rs +++ b/cli/auth_tokens.rs @@ -5,15 +5,27 @@ use log::debug; use log::error; use std::fmt; +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AuthTokenData { + Bearer(String), + Basic { username: String, password: String }, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct AuthToken { host: String, - token: String, + token: AuthTokenData, } impl fmt::Display for AuthToken { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Bearer {}", self.token) + match &self.token { + AuthTokenData::Bearer(token) => write!(f, "Bearer {}", token), + AuthTokenData::Basic { username, password } => { + let credentials = format!("{}:{}", username, password); + write!(f, "Basic {}", base64::encode(credentials)) + } + } } } @@ -34,9 +46,22 @@ impl AuthTokens { for token_str in tokens_str.split(';') { if token_str.contains('@') { let pair: Vec<&str> = token_str.rsplitn(2, '@').collect(); - let token = pair[1].to_string(); + let token = pair[1]; let host = pair[0].to_lowercase(); - tokens.push(AuthToken { host, token }); + if token.contains(':') { + let pair: Vec<&str> = token.rsplitn(2, ':').collect(); + let username = pair[1].to_string(); + let password = pair[0].to_string(); + tokens.push(AuthToken { + host, + token: AuthTokenData::Basic { username, password }, + }) + } else { + tokens.push(AuthToken { + host, + token: AuthTokenData::Bearer(token.to_string()), + }); + } } else { error!("Badly formed auth token discarded."); } @@ -133,4 +158,26 @@ mod tests { "Bearer abc@123".to_string() ); } + + #[test] + fn test_auth_token_basic() { + let auth_tokens = AuthTokens::new(Some("abc:123@deno.land".to_string())); + let fixture = resolve_url("https://deno.land/x/mod.ts").unwrap(); + assert_eq!( + auth_tokens.get(&fixture).unwrap().to_string(), + "Basic YWJjOjEyMw==" + ); + let fixture = resolve_url("https://www.deno.land/x/mod.ts").unwrap(); + assert_eq!( + auth_tokens.get(&fixture).unwrap().to_string(), + "Basic YWJjOjEyMw==".to_string() + ); + let fixture = resolve_url("http://127.0.0.1:8080/x/mod.ts").unwrap(); + assert_eq!(auth_tokens.get(&fixture), None); + let fixture = + resolve_url("https://deno.land.example.com/x/mod.ts").unwrap(); + assert_eq!(auth_tokens.get(&fixture), None); + let fixture = resolve_url("https://deno.land:8080/x/mod.ts").unwrap(); + assert_eq!(auth_tokens.get(&fixture), None); + } } diff --git a/cli/tests/integration/mod.rs b/cli/tests/integration/mod.rs index 7d13c78316..4ada4ef54e 100644 --- a/cli/tests/integration/mod.rs +++ b/cli/tests/integration/mod.rs @@ -1077,6 +1077,54 @@ fn js_unit_tests() { assert!(status.success()); } +#[test] +fn basic_auth_tokens() { + let _g = util::http_server(); + + let output = util::deno_cmd() + .current_dir(util::root_path()) + .arg("run") + .arg("http://127.0.0.1:4554/001_hello.js") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + + assert!(!output.status.success()); + + let stdout_str = std::str::from_utf8(&output.stdout).unwrap().trim(); + assert!(stdout_str.is_empty()); + + let stderr_str = std::str::from_utf8(&output.stderr).unwrap().trim(); + eprintln!("{}", stderr_str); + + assert!(stderr_str.contains( + "Import 'http://127.0.0.1:4554/001_hello.js' failed: 404 Not Found" + )); + + let output = util::deno_cmd() + .current_dir(util::root_path()) + .arg("run") + .arg("http://127.0.0.1:4554/001_hello.js") + .env("DENO_AUTH_TOKENS", "testuser123:testpassabc@127.0.0.1:4554") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + + let stderr_str = std::str::from_utf8(&output.stderr).unwrap().trim(); + eprintln!("{}", stderr_str); + + assert!(output.status.success()); + + let stdout_str = std::str::from_utf8(&output.stdout).unwrap().trim(); + assert_eq!(util::strip_ansi_codes(stdout_str), "Hello World"); +} + #[tokio::test] async fn listen_tls_alpn() { // TLS streams require the presence of an ambient local task set to gracefully diff --git a/test_util/Cargo.toml b/test_util/Cargo.toml index 5e2a41ed8f..b33f389a68 100644 --- a/test_util/Cargo.toml +++ b/test_util/Cargo.toml @@ -14,6 +14,7 @@ path = "src/test_server.rs" [dependencies] anyhow = "1.0.43" async-stream = "0.3.2" +base64 = "0.13.0" bytes = "1.1.0" futures = "0.3.16" hyper = { version = "0.14.12", features = ["server", "http1", "runtime"] } diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs index 46679b98d1..3d9913576a 100644 --- a/test_util/src/lib.rs +++ b/test_util/src/lib.rs @@ -51,6 +51,8 @@ pub mod lsp; const PORT: u16 = 4545; const TEST_AUTH_TOKEN: &str = "abcdef123456789"; +const TEST_BASIC_AUTH_USERNAME: &str = "testuser123"; +const TEST_BASIC_AUTH_PASSWORD: &str = "testpassabc"; const REDIRECT_PORT: u16 = 4546; const ANOTHER_REDIRECT_PORT: u16 = 4547; const DOUBLE_REDIRECTS_PORT: u16 = 4548; @@ -58,6 +60,7 @@ const INF_REDIRECTS_PORT: u16 = 4549; const REDIRECT_ABSOLUTE_PORT: u16 = 4550; const AUTH_REDIRECT_PORT: u16 = 4551; const TLS_CLIENT_AUTH_PORT: u16 = 4552; +const BASIC_AUTH_REDIRECT_PORT: u16 = 4554; const HTTPS_PORT: u16 = 5545; const HTTPS_CLIENT_AUTH_PORT: u16 = 5552; const WS_PORT: u16 = 4242; @@ -229,6 +232,29 @@ async fn auth_redirect(req: Request) -> hyper::Result> { Ok(resp) } +async fn basic_auth_redirect( + req: Request, +) -> hyper::Result> { + if let Some(auth) = req + .headers() + .get("authorization") + .map(|v| v.to_str().unwrap()) + { + let credentials = + format!("{}:{}", TEST_BASIC_AUTH_USERNAME, TEST_BASIC_AUTH_PASSWORD); + if auth == format!("Basic {}", base64::encode(credentials)) { + let p = req.uri().path(); + assert_eq!(&p[0..1], "/"); + let url = format!("http://localhost:{}{}", PORT, p); + return Ok(redirect_resp(url)); + } + } + + let mut resp = Response::new(Body::empty()); + *resp.status_mut() = StatusCode::NOT_FOUND; + Ok(resp) +} + async fn run_ws_server(addr: &SocketAddr) { let listener = TcpListener::bind(addr).await.unwrap(); println!("ready: ws"); // Eye catcher for HttpServerCount @@ -837,6 +863,19 @@ async fn wrap_auth_redirect_server() { } } +async fn wrap_basic_auth_redirect_server() { + let basic_auth_redirect_svc = make_service_fn(|_| async { + Ok::<_, Infallible>(service_fn(basic_auth_redirect)) + }); + let basic_auth_redirect_addr = + SocketAddr::from(([127, 0, 0, 1], BASIC_AUTH_REDIRECT_PORT)); + let basic_auth_redirect_server = + Server::bind(&basic_auth_redirect_addr).serve(basic_auth_redirect_svc); + if let Err(e) = basic_auth_redirect_server.await { + eprintln!("Basic auth redirect error: {:?}", e); + } +} + async fn wrap_abs_redirect_server() { let abs_redirect_svc = make_service_fn(|_| async { Ok::<_, Infallible>(service_fn(absolute_redirect)) @@ -969,6 +1008,7 @@ pub async fn run_all_servers() { let inf_redirects_server_fut = wrap_inf_redirect_server(); let another_redirect_server_fut = wrap_another_redirect_server(); let auth_redirect_server_fut = wrap_auth_redirect_server(); + let basic_auth_redirect_server_fut = wrap_basic_auth_redirect_server(); let abs_redirect_server_fut = wrap_abs_redirect_server(); let ws_addr = SocketAddr::from(([127, 0, 0, 1], WS_PORT)); @@ -992,6 +1032,7 @@ pub async fn run_all_servers() { ws_close_server_fut, another_redirect_server_fut, auth_redirect_server_fut, + basic_auth_redirect_server_fut, inf_redirects_server_fut, double_redirects_server_fut, abs_redirect_server_fut,