diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index 11c05d88700..419ee311d7f 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -209,6 +209,7 @@ mod from_script { Self::MoveTo(..) => target_variant!("MoveTo"), Self::ResizeTo(..) => target_variant!("ResizeTo"), Self::Prompt(..) => target_variant!("Prompt"), + Self::RequestAuthentication(..) => target_variant!("RequestAuthentication"), Self::ShowContextMenu(..) => target_variant!("ShowContextMenu"), Self::AllowNavigationRequest(..) => target_variant!("AllowNavigationRequest"), Self::AllowOpeningWebView(..) => target_variant!("AllowOpeningWebView"), diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs index 71470100f0d..e75175c8f34 100644 --- a/components/net/http_loader.rs +++ b/components/net/http_loader.rs @@ -9,15 +9,13 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use async_recursion::async_recursion; use base::cross_process_instant::CrossProcessInstant; -use base::id::{HistoryStateId, PipelineId, WebViewId}; +use base::id::{HistoryStateId, PipelineId}; use crossbeam_channel::Sender; use devtools_traits::{ ChromeToDevtoolsControlMsg, DevtoolsControlMsg, HttpRequest as DevtoolsHttpRequest, HttpResponse as DevtoolsHttpResponse, NetworkEvent, }; -use embedder_traits::{ - EmbedderMsg, EmbedderProxy, PromptCredentialsInput, PromptDefinition, PromptOrigin, -}; +use embedder_traits::{AuthenticationResponse, EmbedderMsg, EmbedderProxy}; use futures::{future, TryFutureExt, TryStreamExt}; use headers::authorization::Basic; use headers::{ @@ -107,6 +105,28 @@ pub struct HttpState { pub embedder_proxy: Mutex, } +impl HttpState { + fn request_authentication( + &self, + request: &Request, + response: &Response, + ) -> Option { + // We do not make an authentication request for non-WebView associated HTTP requests. + let webview_id = request.target_webview_id?; + let for_proxy = response.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED; + + let embedder_proxy = self.embedder_proxy.lock().unwrap(); + let (ipc_sender, ipc_receiver) = ipc::channel().unwrap(); + embedder_proxy.send(EmbedderMsg::RequestAuthentication( + webview_id, + request.url(), + for_proxy, + ipc_sender, + )); + ipc_receiver.recv().ok()? + } +} + /// Step 13 of . pub(crate) fn set_default_accept(request: &mut Request) { if request.headers.contains_key(header::ACCEPT) { @@ -1595,27 +1615,22 @@ 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 !request.use_url_credentials || authentication_fetch_flag { - let Some(webview_id) = request.target_webview_id else { - return response; - }; - let Some(credentials) = - prompt_user_for_credentials(&context.state.embedder_proxy, webview_id) - else { - return response; - }; - let Some(username) = credentials.username else { - return response; - }; - let Some(password) = credentials.password else { + let Some(credentials) = context.state.request_authentication(request, &response) else { return response; }; - if let Err(err) = request.current_url_mut().set_username(&username) { + if let Err(err) = request + .current_url_mut() + .set_username(&credentials.username) + { error!("error setting username for url: {:?}", err); return response; }; - if let Err(err) = request.current_url_mut().set_password(Some(&password)) { + if let Err(err) = request + .current_url_mut() + .set_password(Some(&credentials.password)) + { error!("error setting password for url: {:?}", err); return response; }; @@ -1654,25 +1669,14 @@ async fn http_network_or_cache_fetch( // 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(webview_id) = request.target_webview_id else { - return response; - }; - let Some(credentials) = - prompt_user_for_credentials(&context.state.embedder_proxy, webview_id) - else { - return response; - }; - let Some(user_name) = credentials.username else { - return response; - }; - let Some(password) = credentials.password else { + let Some(credentials) = context.state.request_authentication(request, &response) else { return response; }; - // store the credentials as a proxy-authentication entry. + // Store the credentials as a proxy-authentication entry. let entry = AuthCacheEntry { - user_name, - password, + user_name: credentials.username, + password: credentials.password, }; { let mut auth_cache = context.state.auth_cache.write().unwrap(); @@ -1794,28 +1798,6 @@ impl Drop for ResponseEndTimer { } } -fn prompt_user_for_credentials( - embedder_proxy: &Mutex, - webview_id: WebViewId, -) -> Option { - let proxy = embedder_proxy.lock().unwrap(); - - let (ipc_sender, ipc_receiver) = ipc::channel().unwrap(); - - proxy.send(EmbedderMsg::Prompt( - webview_id, - 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( fetch_params: &mut FetchParams, diff --git a/components/net/tests/http_loader.rs b/components/net/tests/http_loader.rs index 793a73826dd..e2dfa3938fb 100644 --- a/components/net/tests/http_loader.rs +++ b/components/net/tests/http_loader.rs @@ -17,6 +17,7 @@ use devtools_traits::{ ChromeToDevtoolsControlMsg, DevtoolsControlMsg, HttpRequest as DevtoolsHttpRequest, HttpResponse as DevtoolsHttpResponse, NetworkEvent, }; +use embedder_traits::AuthenticationResponse; use flate2::write::{GzEncoder, ZlibEncoder}; use flate2::Compression; use headers::authorization::Basic; @@ -1577,8 +1578,10 @@ fn test_user_credentials_prompt_when_proxy_authentication_is_required() { 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()), + Some(AuthenticationResponse { + username: "username".into(), + password: "test".into(), + }), ); let mut context = new_fetch_context(None, Some(embedder_proxy), None); @@ -1625,8 +1628,10 @@ fn test_prompt_credentials_when_client_receives_unauthorized_response() { 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()), + Some(AuthenticationResponse { + username: "username".into(), + password: "test".into(), + }), ); let mut context = new_fetch_context(None, Some(embedder_proxy), None); @@ -1670,7 +1675,7 @@ fn test_prompt_credentials_user_cancels_dialog_input() { .build(); let (embedder_proxy, embedder_receiver) = create_embedder_proxy_and_receiver(); - let _ = receive_credential_prompt_msgs(embedder_receiver, None, None); + let _ = receive_credential_prompt_msgs(embedder_receiver, None); let mut context = new_fetch_context(None, Some(embedder_proxy), None); let response = fetch_with_context(request, &mut context); @@ -1715,8 +1720,10 @@ fn test_prompt_credentials_user_input_incorrect_credentials() { 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()), + Some(AuthenticationResponse { + username: "test".into(), + password: "test".into(), + }), ); let mut context = new_fetch_context(None, Some(embedder_proxy), None); diff --git a/components/net/tests/main.rs b/components/net/tests/main.rs index 0bbbde9cf59..99bb918091a 100644 --- a/components/net/tests/main.rs +++ b/components/net/tests/main.rs @@ -28,7 +28,7 @@ use std::sync::{Arc, LazyLock, Mutex, RwLock, Weak}; use crossbeam_channel::{unbounded, Receiver, Sender}; use devtools_traits::DevtoolsControlMsg; -use embedder_traits::{EmbedderMsg, EmbedderProxy, EventLoopWaker}; +use embedder_traits::{AuthenticationResponse, EmbedderMsg, EmbedderProxy, EventLoopWaker}; use futures::future::ready; use http_body_util::combinators::BoxBody; use http_body_util::{BodyExt, Empty, Full}; @@ -125,21 +125,13 @@ fn create_embedder_proxy_and_receiver() -> (EmbedderProxy, Receiver fn receive_credential_prompt_msgs( embedder_receiver: Receiver, - username: Option, - password: Option, + response: Option, ) -> std::thread::JoinHandle<()> { std::thread::spawn(move || loop { let embedder_msg = embedder_receiver.recv().unwrap(); 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!(), - } + embedder_traits::EmbedderMsg::RequestAuthentication(_, _, _, response_sender) => { + let _ = response_sender.send(response); break; }, embedder_traits::EmbedderMsg::WebResourceRequested(..) => {}, diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 6902c8d6b6a..d533c392fbd 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -119,7 +119,8 @@ use crate::proxies::ConstellationProxy; pub use crate::servo_delegate::{ServoDelegate, ServoError}; pub use crate::webview::WebView; pub use crate::webview_delegate::{ - AllowOrDenyRequest, NavigationRequest, PermissionRequest, WebViewDelegate, + AllowOrDenyRequest, AuthenticationRequest, NavigationRequest, PermissionRequest, + WebViewDelegate, }; #[cfg(feature = "webdriver")] @@ -916,6 +917,19 @@ impl Servo { ); } }, + EmbedderMsg::RequestAuthentication(webview_id, url, for_proxy, response_sender) => { + let authentication_request = AuthenticationRequest { + url: url.into_url(), + for_proxy, + response_sender, + response_sent: false, + }; + if let Some(webview) = self.get_webview_handle(webview_id) { + webview + .delegate() + .request_authentication(webview, authentication_request); + } + }, EmbedderMsg::PromptPermission(webview_id, requested_feature, response_sender) => { if let Some(webview) = self.get_webview_handle(webview_id) { let permission_request = PermissionRequest { diff --git a/components/servo/webview_delegate.rs b/components/servo/webview_delegate.rs index 6eb71ed7230..8499231d8db 100644 --- a/components/servo/webview_delegate.rs +++ b/components/servo/webview_delegate.rs @@ -7,9 +7,9 @@ use std::path::PathBuf; use base::id::PipelineId; use compositing_traits::ConstellationMsg; use embedder_traits::{ - AllowOrDeny, CompositorEventVariant, ContextMenuResult, Cursor, FilterPattern, - GamepadHapticEffectType, InputMethodType, LoadStatus, MediaSessionEvent, PermissionFeature, - PromptDefinition, PromptOrigin, WebResourceRequest, WebResourceResponseMsg, + AllowOrDeny, AuthenticationResponse, CompositorEventVariant, ContextMenuResult, Cursor, + FilterPattern, GamepadHapticEffectType, InputMethodType, LoadStatus, MediaSessionEvent, + PermissionFeature, PromptDefinition, PromptOrigin, WebResourceRequest, WebResourceResponseMsg, }; use ipc_channel::ipc::IpcSender; use keyboard_types::KeyboardEvent; @@ -107,6 +107,42 @@ impl Drop for AllowOrDenyRequest { } } +/// A request to authenticate a [`WebView`] navigation. Embedders may choose to prompt +/// the user to enter credentials or simply ignore this request (in which case credentials +/// will not be used). +pub struct AuthenticationRequest { + pub(crate) url: Url, + pub(crate) for_proxy: bool, + pub(crate) response_sender: IpcSender>, + pub(crate) response_sent: bool, +} + +impl AuthenticationRequest { + /// The URL of the request that triggered this authentication. + pub fn url(&self) -> &Url { + &self.url + } + /// Whether or not this authentication request is associated with a proxy server authentication. + pub fn for_proxy(&self) -> bool { + self.for_proxy + } + /// Respond to the [`AuthenticationRequest`] with the given username and password. + pub fn authenticate(mut self, username: String, password: String) { + let _ = self + .response_sender + .send(Some(AuthenticationResponse { username, password })); + self.response_sent = true; + } +} + +impl Drop for AuthenticationRequest { + fn drop(&mut self) { + if !self.response_sent { + let _ = self.response_sender.send(None); + } + } +} + pub trait WebViewDelegate { /// The URL of the currently loaded page in this [`WebView`] has changed. The new /// URL can accessed via [`WebView::url`]. @@ -176,6 +212,13 @@ pub trait WebViewDelegate { /// reading a cached value or querying the user for permission via the user interface. fn request_permission(&self, _webview: WebView, _: PermissionRequest) {} + fn request_authentication( + &self, + _webview: WebView, + _authentication_request: AuthenticationRequest, + ) { + } + /// Show dialog to user /// TODO: This API needs to be reworked to match the new model of how responses are sent. fn show_prompt(&self, _webview: WebView, prompt: PromptDefinition, _: PromptOrigin) { @@ -185,9 +228,6 @@ pub trait WebViewDelegate { response_sender.send(embedder_traits::PromptResult::Dismissed) }, PromptDefinition::Input(_, _, response_sender) => response_sender.send(None), - PromptDefinition::Credentials(response_sender) => { - response_sender.send(Default::default()) - }, }; } /// Show a context menu to the user @@ -203,6 +243,7 @@ pub trait WebViewDelegate { /// Enter or exit fullscreen fn request_fullscreen_state_change(&self, _webview: WebView, _: bool) {} + /// Open dialog to select bluetooth device. /// TODO: This API needs to be reworked to match the new model of how responses are sent. fn show_bluetooth_device_dialog( diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index 8dc71ed02d8..fbb2534e97d 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -116,16 +116,14 @@ pub enum PromptDefinition { OkCancel(String, IpcSender), /// Ask the user to enter text. Input(String, String, IpcSender>), - /// Ask user to enter their username and password - Credentials(IpcSender), } #[derive(Debug, Default, Deserialize, Serialize)] -pub struct PromptCredentialsInput { +pub struct AuthenticationResponse { /// Username for http request authentication - pub username: Option, + pub username: String, /// Password for http request authentication - pub password: Option, + pub password: String, } #[derive(Deserialize, PartialEq, Serialize)] @@ -166,6 +164,13 @@ pub enum EmbedderMsg { ResizeTo(WebViewId, DeviceIntSize), /// Show dialog to user Prompt(WebViewId, PromptDefinition, PromptOrigin), + /// Request authentication for a load or navigation from the embedder. + RequestAuthentication( + WebViewId, + ServoUrl, + bool, /* for proxy */ + IpcSender>, + ), /// Show a context menu to the user ShowContextMenu( WebViewId, @@ -278,6 +283,7 @@ impl Debug for EmbedderMsg { EmbedderMsg::MoveTo(..) => write!(f, "MoveTo"), EmbedderMsg::ResizeTo(..) => write!(f, "ResizeTo"), EmbedderMsg::Prompt(..) => write!(f, "Prompt"), + EmbedderMsg::RequestAuthentication(..) => write!(f, "RequestAuthentication"), EmbedderMsg::AllowUnload(..) => write!(f, "AllowUnload"), EmbedderMsg::AllowNavigationRequest(..) => write!(f, "AllowNavigationRequest"), EmbedderMsg::Keyboard(..) => write!(f, "Keyboard"), diff --git a/ports/servoshell/desktop/app_state.rs b/ports/servoshell/desktop/app_state.rs index f0e6d60932c..1fee84cd34d 100644 --- a/ports/servoshell/desktop/app_state.rs +++ b/ports/servoshell/desktop/app_state.rs @@ -17,9 +17,9 @@ use servo::ipc_channel::ipc::IpcSender; use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize}; use servo::webrender_api::ScrollLocation; use servo::{ - AllowOrDenyRequest, CompositorEventVariant, FilterPattern, GamepadHapticEffectType, LoadStatus, - PermissionRequest, PromptCredentialsInput, PromptDefinition, PromptOrigin, PromptResult, Servo, - ServoDelegate, ServoError, TouchEventType, WebView, WebViewDelegate, + AllowOrDenyRequest, AuthenticationRequest, CompositorEventVariant, FilterPattern, + GamepadHapticEffectType, LoadStatus, PermissionRequest, PromptDefinition, PromptOrigin, + PromptResult, Servo, ServoDelegate, ServoError, TouchEventType, WebView, WebViewDelegate, }; use tinyfiledialogs::{self, MessageBoxIcon, OkCancel}; use url::Url; @@ -354,10 +354,6 @@ impl WebViewDelegate for RunningAppState { 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() @@ -397,12 +393,6 @@ impl WebViewDelegate for RunningAppState { 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() @@ -414,6 +404,23 @@ impl WebViewDelegate for RunningAppState { } } + fn request_authentication( + &self, + _webview: WebView, + authentication_request: AuthenticationRequest, + ) { + if self.inner().headless { + return; + } + + if let (Some(username), Some(password)) = ( + tinyfiledialogs::input_box("", "username", ""), + tinyfiledialogs::input_box("", "password", ""), + ) { + authentication_request.authenticate(username, password); + } + } + fn request_open_auxiliary_webview( &self, parent_webview: servo::WebView, diff --git a/ports/servoshell/egl/app_state.rs b/ports/servoshell/egl/app_state.rs index 5dd9520b899..7cec82951a5 100644 --- a/ports/servoshell/egl/app_state.rs +++ b/ports/servoshell/egl/app_state.rs @@ -255,10 +255,6 @@ impl WebViewDelegate for RunningAppState { PromptDefinition::Input(message, default, response_sender) => { response_sender.send(cb.prompt_input(message, default, trusted)) }, - PromptDefinition::Credentials(response_sender) => { - warn!("implement credentials prompt for OpenHarmony OS and Android"); - response_sender.send(Default::default()) - }, }; }