diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs index d5541c99565..24ce335a84a 100644 --- a/components/net/http_loader.rs +++ b/components/net/http_loader.rs @@ -16,6 +16,9 @@ use devtools_traits::{ ChromeToDevtoolsControlMsg, DevtoolsControlMsg, HttpRequest as DevtoolsHttpRequest, HttpResponse as DevtoolsHttpResponse, NetworkEvent, }; +use embedder_traits::{ + EmbedderMsg, EmbedderProxy, PromptCredentialsInput, PromptDefinition, PromptOrigin, +}; use futures::{future, StreamExt, TryFutureExt, TryStreamExt}; use headers::authorization::Basic; use headers::{ @@ -60,10 +63,7 @@ use tokio::sync::mpsc::{ use tokio_stream::wrappers::ReceiverStream; use crate::async_runtime::HANDLE; -use crate::connector::{ - create_http_client, create_tls_config, CACertificates, CertificateErrorOverrideManager, - Connector, -}; +use crate::connector::{CertificateErrorOverrideManager, Connector}; use crate::cookie::ServoCookie; use crate::cookie_storage::CookieStorage; use crate::decoder::Decoder; @@ -72,7 +72,7 @@ use crate::fetch::headers::{SecFetchDest, SecFetchMode, SecFetchSite, SecFetchUs use crate::fetch::methods::{main_fetch, Data, DoneChannel, FetchContext, Target}; use crate::hsts::HstsList; use crate::http_cache::{CacheKey, HttpCache}; -use crate::resource_thread::AuthCache; +use crate::resource_thread::{AuthCache, AuthCacheEntry}; /// pub const DOCUMENT_ACCEPT_HEADER_VALUE: HeaderValue = @@ -103,26 +103,7 @@ pub struct HttpState { pub history_states: RwLock>>, pub client: Client, pub override_manager: CertificateErrorOverrideManager, -} - -impl Default for HttpState { - fn default() -> Self { - let override_manager = CertificateErrorOverrideManager::new(); - Self { - hsts_list: RwLock::new(HstsList::default()), - cookie_jar: RwLock::new(CookieStorage::new(150)), - auth_cache: RwLock::new(AuthCache::default()), - history_states: RwLock::new(HashMap::new()), - http_cache: RwLock::new(HttpCache::default()), - http_cache_state: Mutex::new(HashMap::new()), - client: create_http_client(create_tls_config( - CACertificates::Default, - false, /* ignore_certificate_errors */ - override_manager.clone(), - )), - override_manager, - } - } + pub embedder_proxy: Mutex, } /// Step 13 of . @@ -1590,12 +1571,26 @@ async fn http_network_or_cache_fetch( // Step 14.3 If request’s use-URL-credentials flag is unset or isAuthenticationFetch is true, then: if !http_request.use_url_credentials || authentication_fetch_flag { - // TODO(#33616, #27439): Prompt the user for username and password from the window + let Some(credentials) = prompt_user_for_credentials(&context.state.embedder_proxy) + else { + return response; + }; + let Some(username) = credentials.username else { + return response; + }; + let Some(password) = credentials.password else { + return response; + }; - // Wrong, but will have to do until we are able to prompt the user - // otherwise this creates an infinite loop - // We basically pretend that the user declined to enter credentials (#33616) - return response; + if let Err(err) = http_request.current_url_mut().set_username(&username) { + error!("error setting username for url: {:?}", err); + return response; + }; + + if let Err(err) = http_request.current_url_mut().set_password(Some(&password)) { + error!("error setting password for url: {:?}", err); + return response; + }; } // Make sure this is set to None, @@ -1627,15 +1622,43 @@ async fn http_network_or_cache_fetch( // TODO(#33616): Step 15.3 If fetchParams is canceled, then return // the appropriate network error for fetchParams. - // TODO(#33616): Step 15.4 Prompt the end user as appropriate in request’s window and store the - // result as a proxy-authentication entry. + + // Step 15.4 Prompt the end user as appropriate in request’s window + // window and store the result as a proxy-authentication entry. + let Some(credentials) = prompt_user_for_credentials(&context.state.embedder_proxy) else { + return response; + }; + let Some(user_name) = credentials.username else { + return response; + }; + let Some(password) = credentials.password else { + return response; + }; + + // store the credentials as a proxy-authentication entry. + let entry = AuthCacheEntry { + user_name, + password, + }; + { + let mut auth_cache = context.state.auth_cache.write().unwrap(); + let key = http_request.current_url().origin().ascii_serialization(); + auth_cache.entries.insert(key, entry); + } + + // Make sure this is set to None, + // since we're about to start a new `http_network_or_cache_fetch`. + *done_chan = None; // Step 15.5 Set response to the result of running HTTP-network-or-cache fetch given fetchParams. - - // Wrong, but will have to do until we are able to prompt the user - // otherwise this creates an infinite loop - // We basically pretend that the user declined to enter credentials (#33616) - return response; + response = http_network_or_cache_fetch( + http_request, + false, /* authentication flag */ + cors_flag, + done_chan, + context, + ) + .await; } // TODO(#33616): Step 16. If all of the following are true: @@ -1737,6 +1760,29 @@ impl Drop for ResponseEndTimer { } } +fn prompt_user_for_credentials( + embedder_proxy: &Mutex, +) -> Option { + let proxy = embedder_proxy.lock().unwrap(); + + let (ipc_sender, ipc_receiver) = ipc::channel().unwrap(); + + proxy.send(( + None, + EmbedderMsg::Prompt( + PromptDefinition::Credentials(ipc_sender), + PromptOrigin::Trusted, + ), + )); + + let Ok(credentials) = ipc_receiver.recv() else { + warn!("error getting user credentials"); + return None; + }; + + Some(credentials) +} + /// [HTTP network fetch](https://fetch.spec.whatwg.org/#http-network-fetch) async fn http_network_fetch( request: &mut Request, diff --git a/components/net/resource_thread.rs b/components/net/resource_thread.rs index dc27a05d195..97e2791f5bb 100644 --- a/components/net/resource_thread.rs +++ b/components/net/resource_thread.rs @@ -132,7 +132,7 @@ pub fn new_core_resource_thread( user_agent, devtools_sender, time_profiler_chan, - embedder_proxy, + embedder_proxy.clone(), ca_certificates.clone(), ignore_certificate_errors, ); @@ -151,6 +151,7 @@ pub fn new_core_resource_thread( private_setup_port, report_port, protocols, + embedder_proxy, ) }, String::from("network-cache-reporter"), @@ -173,6 +174,7 @@ fn create_http_states( config_dir: Option<&Path>, ca_certificates: CACertificates, ignore_certificate_errors: bool, + embedder_proxy: EmbedderProxy, ) -> (Arc, Arc) { let mut hsts_list = HstsList::from_servo_preload(); let mut auth_cache = AuthCache::default(); @@ -198,6 +200,7 @@ fn create_http_states( override_manager.clone(), )), override_manager, + embedder_proxy: Mutex::new(embedder_proxy.clone()), }; let override_manager = CertificateErrorOverrideManager::new(); @@ -214,6 +217,7 @@ fn create_http_states( override_manager.clone(), )), override_manager, + embedder_proxy: Mutex::new(embedder_proxy), }; (Arc::new(http_state), Arc::new(private_http_state)) @@ -227,11 +231,13 @@ impl ResourceChannelManager { private_receiver: IpcReceiver, memory_reporter: IpcReceiver, protocols: Arc, + embedder_proxy: EmbedderProxy, ) { let (public_http_state, private_http_state) = create_http_states( self.config_dir.as_deref(), self.ca_certificates.clone(), self.ignore_certificate_errors, + embedder_proxy, ); let mut rx_set = IpcReceiverSet::new().unwrap(); diff --git a/components/net/tests/fetch.rs b/components/net/tests/fetch.rs index 79020a7c998..0b1a7ac196c 100644 --- a/components/net/tests/fetch.rs +++ b/components/net/tests/fetch.rs @@ -29,7 +29,6 @@ use net::filemanager_thread::FileManager; use net::hsts::HstsEntry; use net::protocols::ProtocolRegistry; use net::resource_thread::CoreResourceThreadPool; -use net::test::HttpState; use net_traits::filemanager_thread::FileTokenCheck; use net_traits::http_status::HttpStatus; use net_traits::request::{ @@ -47,8 +46,8 @@ use uuid::Uuid; use crate::http_loader::{expect_devtools_http_request, expect_devtools_http_response}; use crate::{ - create_embedder_proxy, fetch, fetch_with_context, fetch_with_cors_cache, make_server, - make_ssl_server, new_fetch_context, DEFAULT_USER_AGENT, + create_embedder_proxy, create_http_state, fetch, fetch_with_context, fetch_with_cors_cache, + make_server, make_ssl_server, new_fetch_context, DEFAULT_USER_AGENT, }; // TODO write a struct that impls Handler for storing test values @@ -669,7 +668,7 @@ fn test_fetch_with_hsts() { let (server, url) = make_ssl_server(handler); let mut context = FetchContext { - state: Arc::new(HttpState::default()), + state: Arc::new(create_http_state(None)), user_agent: DEFAULT_USER_AGENT.into(), devtools_chan: None, filemanager: Arc::new(Mutex::new(FileManager::new( @@ -724,7 +723,7 @@ fn test_load_adds_host_to_hsts_list_when_url_is_https() { url.as_mut_url().set_scheme("https").unwrap(); let mut context = FetchContext { - state: Arc::new(HttpState::default()), + state: Arc::new(create_http_state(None)), user_agent: DEFAULT_USER_AGENT.into(), devtools_chan: None, filemanager: Arc::new(Mutex::new(FileManager::new( @@ -781,7 +780,7 @@ fn test_fetch_self_signed() { url.as_mut_url().set_scheme("https").unwrap(); let mut context = FetchContext { - state: Arc::new(HttpState::default()), + state: Arc::new(create_http_state(None)), user_agent: DEFAULT_USER_AGENT.into(), devtools_chan: None, filemanager: Arc::new(Mutex::new(FileManager::new( diff --git a/components/net/tests/http_loader.rs b/components/net/tests/http_loader.rs index 68c8868428a..c7478cb06da 100644 --- a/components/net/tests/http_loader.rs +++ b/components/net/tests/http_loader.rs @@ -6,7 +6,6 @@ use std::collections::HashMap; use std::io::Write; -use std::str; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; @@ -47,7 +46,10 @@ use servo_url::{ImmutableOrigin, ServoUrl}; use tokio_test::block_on; use url::Url; -use crate::{fetch, fetch_with_context, make_server, new_fetch_context}; +use crate::{ + create_embedder_proxy_and_receiver, fetch, fetch_with_context, make_server, new_fetch_context, + receive_credential_prompt_msgs, +}; fn mock_origin() -> ImmutableOrigin { ServoUrl::parse("http://servo.org").unwrap().origin() @@ -1479,3 +1481,180 @@ fn test_origin_serialization_compatability() { ensure_serialiations_match("data:,dataurltexta"); } + +#[test] +fn test_user_credentials_prompt_when_proxy_authentication_is_required() { + let handler = move |request: HyperRequest, response: &mut HyperResponse| { + let expected = Authorization::basic("username", "test"); + if let Some(credentials) = request.headers().typed_get::>() { + if credentials == expected { + *response.status_mut() = StatusCode::OK; + } else { + *response.status_mut() = StatusCode::UNAUTHORIZED; + } + } else { + *response.status_mut() = StatusCode::PROXY_AUTHENTICATION_REQUIRED; + } + }; + let (server, url) = make_server(handler); + + let mut request = RequestBuilder::new(url.clone(), Referrer::NoReferrer) + .method(Method::GET) + .body(None) + .destination(Destination::Document) + .origin(mock_origin()) + .pipeline_id(Some(TEST_PIPELINE_ID)) + .credentials_mode(CredentialsMode::Include) + .build(); + + let (embedder_proxy, embedder_receiver) = create_embedder_proxy_and_receiver(); + let _ = receive_credential_prompt_msgs( + embedder_receiver, + Some("username".to_string()), + Some("test".to_string()), + ); + + let mut context = new_fetch_context(None, Some(embedder_proxy), None); + + let response = fetch_with_context(&mut request, &mut context); + + let _ = server.close(); + + assert!(response + .internal_response + .unwrap() + .status + .code() + .is_success()); +} + +#[test] +fn test_prompt_credentials_when_client_receives_unauthorized_response() { + let handler = move |request: HyperRequest, response: &mut HyperResponse| { + let expected = Authorization::basic("username", "test"); + if let Some(credentials) = request.headers().typed_get::>() { + if credentials == expected { + *response.status_mut() = StatusCode::OK; + } else { + *response.status_mut() = StatusCode::UNAUTHORIZED; + } + } else { + *response.status_mut() = StatusCode::UNAUTHORIZED; + } + }; + let (server, url) = make_server(handler); + + let mut request = RequestBuilder::new(url.clone(), Referrer::NoReferrer) + .method(Method::GET) + .body(None) + .destination(Destination::Document) + .origin(mock_origin()) + .pipeline_id(Some(TEST_PIPELINE_ID)) + .credentials_mode(CredentialsMode::Include) + .build(); + + let (embedder_proxy, embedder_receiver) = create_embedder_proxy_and_receiver(); + let _ = receive_credential_prompt_msgs( + embedder_receiver, + Some("username".to_string()), + Some("test".to_string()), + ); + let mut context = new_fetch_context(None, Some(embedder_proxy), None); + + let response = fetch_with_context(&mut request, &mut context); + + server.close(); + + assert!(response + .internal_response + .unwrap() + .status + .code() + .is_success()); +} + +#[test] +fn test_prompt_credentials_user_cancels_dialog_input() { + let handler = move |request: HyperRequest, response: &mut HyperResponse| { + let expected = Authorization::basic("username", "test"); + if let Some(credentials) = request.headers().typed_get::>() { + if credentials == expected { + *response.status_mut() = StatusCode::OK; + } else { + *response.status_mut() = StatusCode::UNAUTHORIZED; + } + } else { + *response.status_mut() = StatusCode::UNAUTHORIZED; + } + }; + let (server, url) = make_server(handler); + + let mut request = RequestBuilder::new(url.clone(), Referrer::NoReferrer) + .method(Method::GET) + .body(None) + .destination(Destination::Document) + .origin(mock_origin()) + .pipeline_id(Some(TEST_PIPELINE_ID)) + .credentials_mode(CredentialsMode::Include) + .build(); + + let (embedder_proxy, embedder_receiver) = create_embedder_proxy_and_receiver(); + let _ = receive_credential_prompt_msgs(embedder_receiver, None, None); + let mut context = new_fetch_context(None, Some(embedder_proxy), None); + + let response = fetch_with_context(&mut request, &mut context); + + server.close(); + + assert!(response + .internal_response + .unwrap() + .status + .code() + .is_client_error()); +} + +#[test] +fn test_prompt_credentials_user_input_incorrect_credentials() { + let handler = move |request: HyperRequest, response: &mut HyperResponse| { + let expected = Authorization::basic("username", "test"); + if let Some(credentials) = request.headers().typed_get::>() { + if credentials == expected { + *response.status_mut() = StatusCode::OK; + } else { + *response.status_mut() = StatusCode::UNAUTHORIZED; + } + } else { + *response.status_mut() = StatusCode::UNAUTHORIZED; + } + }; + let (server, url) = make_server(handler); + + let mut request = RequestBuilder::new(url.clone(), Referrer::NoReferrer) + .method(Method::GET) + .body(None) + .destination(Destination::Document) + .origin(mock_origin()) + .pipeline_id(Some(TEST_PIPELINE_ID)) + .credentials_mode(CredentialsMode::Include) + .build(); + + let (embedder_proxy, embedder_receiver) = create_embedder_proxy_and_receiver(); + let _ = receive_credential_prompt_msgs( + embedder_receiver, + Some("test".to_string()), + Some("test".to_string()), + ); + let mut context = new_fetch_context(None, Some(embedder_proxy), None); + + let response = fetch_with_context(&mut request, &mut context); + + server.close(); + + assert!(response + .internal_response + .unwrap() + .status + .code() + .is_client_error()); +} diff --git a/components/net/tests/main.rs b/components/net/tests/main.rs index 010898c2f83..da62192644a 100644 --- a/components/net/tests/main.rs +++ b/components/net/tests/main.rs @@ -19,21 +19,23 @@ mod resource_thread; mod subresource_integrity; use core::convert::Infallible; +use std::collections::HashMap; use std::fs::File; use std::io::{self, BufReader}; use std::net::TcpListener as StdTcpListener; use std::path::{Path, PathBuf}; -use std::sync::{Arc, LazyLock, Mutex, Weak}; +use std::sync::{Arc, LazyLock, Mutex, RwLock, Weak}; use crossbeam_channel::{unbounded, Sender}; use devtools_traits::DevtoolsControlMsg; -use embedder_traits::{EmbedderProxy, EventLoopWaker}; +use embedder_traits::{EmbedderProxy, EmbedderReceiver, EventLoopWaker}; use futures::future::ready; use futures::StreamExt; use hyper::server::conn::Http; use hyper::server::Server as HyperServer; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request as HyperRequest, Response as HyperResponse}; +use net::connector::{create_http_client, create_tls_config}; use net::fetch::cors_cache::CorsCache; use net::fetch::methods::{self, CancellationListener, FetchContext}; use net::filemanager_thread::FileManager; @@ -95,6 +97,76 @@ fn create_embedder_proxy() -> EmbedderProxy { } } +fn create_embedder_proxy_and_receiver() -> (EmbedderProxy, EmbedderReceiver) { + let (sender, receiver) = unbounded(); + let event_loop_waker = || { + struct DummyEventLoopWaker {} + impl DummyEventLoopWaker { + fn new() -> DummyEventLoopWaker { + DummyEventLoopWaker {} + } + } + impl embedder_traits::EventLoopWaker for DummyEventLoopWaker { + fn wake(&self) {} + fn clone_box(&self) -> Box { + Box::new(DummyEventLoopWaker {}) + } + } + + Box::new(DummyEventLoopWaker::new()) + }; + + let embedder_proxy = embedder_traits::EmbedderProxy { + sender: sender.clone(), + event_loop_waker: event_loop_waker(), + }; + + let embedder_receiver = EmbedderReceiver { receiver }; + (embedder_proxy, embedder_receiver) +} + +fn receive_credential_prompt_msgs( + mut embedder_receiver: EmbedderReceiver, + username: Option, + password: Option, +) -> std::thread::JoinHandle<()> { + std::thread::spawn(move || { + let (_browser_context_id, embedder_msg) = embedder_receiver.recv_embedder_msg(); + match embedder_msg { + embedder_traits::EmbedderMsg::Prompt(prompt_definition, _prompt_origin) => { + match prompt_definition { + embedder_traits::PromptDefinition::Credentials(ipc_sender) => { + ipc_sender + .send(embedder_traits::PromptCredentialsInput { username, password }) + .unwrap(); + }, + _ => unreachable!(), + } + }, + _ => unreachable!(), + } + }) +} + +fn create_http_state(fc: Option) -> HttpState { + let override_manager = net::connector::CertificateErrorOverrideManager::new(); + HttpState { + hsts_list: RwLock::new(net::hsts::HstsList::default()), + cookie_jar: RwLock::new(net::cookie_storage::CookieStorage::new(150)), + auth_cache: RwLock::new(net::resource_thread::AuthCache::default()), + history_states: RwLock::new(HashMap::new()), + http_cache: RwLock::new(net::http_cache::HttpCache::default()), + http_cache_state: Mutex::new(HashMap::new()), + client: create_http_client(create_tls_config( + net::connector::CACertificates::Default, + false, /* ignore_certificate_errors */ + override_manager.clone(), + )), + override_manager, + embedder_proxy: Mutex::new(fc.unwrap_or_else(|| create_embedder_proxy())), + } +} + fn new_fetch_context( dc: Option>, fc: Option, @@ -103,7 +175,7 @@ fn new_fetch_context( let sender = fc.unwrap_or_else(|| create_embedder_proxy()); FetchContext { - state: Arc::new(HttpState::default()), + state: Arc::new(create_http_state(Some(sender.clone()))), user_agent: DEFAULT_USER_AGENT.into(), devtools_chan: dc.map(|dc| Arc::new(Mutex::new(dc))), filemanager: Arc::new(Mutex::new(FileManager::new( diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index f21f3980641..2548cad70f3 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -131,6 +131,16 @@ pub enum PromptDefinition { YesNo(String, IpcSender), /// Ask the user to enter text. Input(String, String, IpcSender>), + /// Ask user to enter their username and password + Credentials(IpcSender), +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct PromptCredentialsInput { + /// Username for http request authentication + pub username: Option, + /// Password for http request authentication + pub password: Option, } #[derive(Deserialize, PartialEq, Serialize)] diff --git a/ports/servoshell/desktop/webview.rs b/ports/servoshell/desktop/webview.rs index a88f35ef973..43e4007d77d 100644 --- a/ports/servoshell/desktop/webview.rs +++ b/ports/servoshell/desktop/webview.rs @@ -21,8 +21,8 @@ use servo::base::id::TopLevelBrowsingContextId as WebViewId; use servo::compositing::windowing::{EmbedderEvent, WebRenderDebugOption}; use servo::embedder_traits::{ CompositorEventVariant, ContextMenuResult, DualRumbleEffectParams, EmbedderMsg, FilterPattern, - GamepadHapticEffectType, PermissionPrompt, PermissionRequest, PromptDefinition, PromptOrigin, - PromptResult, + GamepadHapticEffectType, PermissionPrompt, PermissionRequest, PromptCredentialsInput, + PromptDefinition, PromptOrigin, PromptResult, }; use servo::ipc_channel::ipc::IpcSender; use servo::script_traits::{ @@ -697,6 +697,12 @@ where PromptDefinition::Input(_message, default, sender) => { sender.send(Some(default.to_owned())) }, + PromptDefinition::Credentials(sender) => { + sender.send(PromptCredentialsInput { + username: None, + password: None, + }) + }, } } else { thread::Builder::new() @@ -751,6 +757,12 @@ where let result = tinyfiledialogs::input_box("", &message, &default); sender.send(result) }, + PromptDefinition::Credentials(sender) => { + // TODO: figure out how to make the message a localized string + let username = tinyfiledialogs::input_box("", "username", ""); + let password = tinyfiledialogs::input_box("", "password", ""); + sender.send(PromptCredentialsInput { username, password }) + }, }) .unwrap() .join() diff --git a/ports/servoshell/egl/servo_glue.rs b/ports/servoshell/egl/servo_glue.rs index 5bba384e420..381e72c4dae 100644 --- a/ports/servoshell/egl/servo_glue.rs +++ b/ports/servoshell/egl/servo_glue.rs @@ -512,6 +512,10 @@ impl ServoGlue { PromptDefinition::Input(message, default, sender) => { sender.send(cb.prompt_input(message, default, trusted)) }, + PromptDefinition::Credentials(_) => { + warn!("implement credentials prompt for OpenHarmony OS and Android"); + Ok(()) + }, }; if let Err(e) = res { let reason = format!("Failed to send Prompt response: {}", e);