blob: 6af17ce710e77d0c1f95658416f035628b1f46d4 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! This module provides methods for controlling and responding to events from
//! a web frame.
use crate::error::{AuthProviderError, ResultExt};
use fidl::endpoints::{create_proxy, create_request_stream};
use fidl_fuchsia_auth::AuthProviderStatus;
use fidl_fuchsia_ui_views::ViewToken;
use fidl_fuchsia_web::{
ContextProxy, FrameProxy, LoadUrlParams, NavigationControllerMarker,
NavigationEventListenerMarker, NavigationEventListenerRequest,
NavigationEventListenerRequestStream, PageType,
};
use futures::future::FutureObj;
use futures::prelude::*;
use log::warn;
use url::Url;
type AuthProviderResult<T> = Result<T, AuthProviderError>;
/// A trait for representations of a web frame that is the only frame in a web context.
pub trait StandaloneWebFrame {
/// Creates a new scenic view using the given |view_token| and loads the
/// given |url| as a webpage in the view. The view must be attached to
/// the global Scenic graph using the ViewHolderToken paired with
/// |view_token|. This method should be called prior to attaching to the
/// Scenic graph to ensure that loading is successful prior to displaying
/// the page to the user.
fn display_url<'a>(
&'a mut self,
view_token: ViewToken,
url: Url,
) -> FutureObj<'a, AuthProviderResult<()>>;
/// Waits until the frame redirects to a URL matching the scheme,
/// domain, and path of |redirect_target|. Returns the matching URL,
/// including any query parameters.
fn wait_for_redirect<'a>(
&'a mut self,
redirect_target: Url,
) -> FutureObj<'a, AuthProviderResult<Url>>;
}
/// A `StandaloneWebFrame` implementation that uses the default fuchsia.web
/// implementation to display a web frame.
pub struct DefaultStandaloneWebFrame {
/// Connection to the web context. Needs to be kept in scope to
/// keep context alive.
_context: ContextProxy,
/// Connection to the web frame within the context.
frame: FrameProxy,
}
impl StandaloneWebFrame for DefaultStandaloneWebFrame {
fn display_url<'a>(
&'a mut self,
view_token: ViewToken,
url: Url,
) -> FutureObj<'a, AuthProviderResult<()>> {
FutureObj::new(Box::new(
async move { Self::display_url_inner(self, view_token, url).await },
))
}
fn wait_for_redirect<'a>(
&'a mut self,
redirect_target: Url,
) -> FutureObj<'a, AuthProviderResult<Url>> {
FutureObj::new(Box::new(async move {
Self::wait_for_redirect_inner(self, redirect_target).await
}))
}
}
// TODO(satsukiu): return resource errors instead of UnknownError once a
// distinct errortype exists
impl DefaultStandaloneWebFrame {
/// Create a new `StandaloneWebFrame`. The context and frame passed
/// in should not be reused.
pub fn new(context: ContextProxy, frame: FrameProxy) -> Self {
DefaultStandaloneWebFrame { _context: context, frame }
}
async fn display_url_inner(
&mut self,
mut view_token: ViewToken,
url: Url,
) -> AuthProviderResult<()> {
let (navigation_controller_proxy, navigation_controller_server_end) =
create_proxy::<NavigationControllerMarker>()
.auth_provider_status(AuthProviderStatus::UnknownError)?;
self.frame
.get_navigation_controller(navigation_controller_server_end)
.auth_provider_status(AuthProviderStatus::UnknownError)?;
navigation_controller_proxy
.load_url(
url.as_str(),
LoadUrlParams {
type_: None,
referrer_url: None,
was_user_activated: None,
headers: None,
},
)
.await
.auth_provider_status(AuthProviderStatus::UnknownError)??;
self.frame
.create_view(&mut view_token)
.auth_provider_status(AuthProviderStatus::UnknownError)?;
let navigation_event_stream = self.get_navigation_event_stream()?;
Self::poll_until_loaded(navigation_event_stream).await
}
async fn wait_for_redirect_inner(&mut self, redirect_target: Url) -> AuthProviderResult<Url> {
let navigation_event_stream = self.get_navigation_event_stream()?;
// pull redirect URL out from events.
Self::poll_for_url_navigation_event(navigation_event_stream, |url| {
(url.scheme(), url.domain(), url.path())
== (redirect_target.scheme(), redirect_target.domain(), redirect_target.path())
})
.await
}
/// Registers a navigation listener with the Chrome frame and returns the created event
/// stream.
fn get_navigation_event_stream(
&self,
) -> AuthProviderResult<NavigationEventListenerRequestStream> {
let (navigation_event_client, navigation_event_stream) =
create_request_stream::<NavigationEventListenerMarker>()
.auth_provider_status(AuthProviderStatus::UnknownError)?;
self.frame
.set_navigation_event_listener(Some(navigation_event_client))
.auth_provider_status(AuthProviderStatus::UnknownError)?;
Ok(navigation_event_stream)
}
/// Polls for events on the given request stream until a event with a
/// matching url is found.
async fn poll_for_url_navigation_event<F>(
mut request_stream: NavigationEventListenerRequestStream,
url_match_fn: F,
) -> AuthProviderResult<Url>
where
F: Fn(&Url) -> bool,
{
// Any errors encountered with the stream here may be a result of the
// overlay being canceled externally.
while let Some(request) = request_stream
.try_next()
.await
.auth_provider_status(AuthProviderStatus::UnknownError)?
{
let NavigationEventListenerRequest::OnNavigationStateChanged { change, responder } =
request;
responder.send().auth_provider_status(AuthProviderStatus::UnknownError)?;
match change.url.map(|raw_url| Url::parse(raw_url.as_str())) {
Some(Ok(url)) => {
if url_match_fn(&url) {
return Ok(url);
}
}
Some(Err(err)) => {
warn!("Browser redirected to malformed URL: {:?}", &err);
return Err(
AuthProviderError::new(AuthProviderStatus::UnknownError).with_cause(err)
);
}
None => (),
}
}
Err(AuthProviderError::new(AuthProviderStatus::UnknownError))
}
/// Completes when the frame has finished loading or some error has occurred.
async fn poll_until_loaded(
mut request_stream: NavigationEventListenerRequestStream,
) -> AuthProviderResult<()> {
// Verify that the page has loaded and is not an error page. Since
// this information may be delivered through two different events,
// we need to keep track of the known state and search through events
// until both points are found.
let mut known_page_type: Option<PageType> = None;
let mut main_document_loaded = false;
while let Some(request) = request_stream
.try_next()
.await
.auth_provider_status(AuthProviderStatus::UnknownError)?
{
// update known state.
let NavigationEventListenerRequest::OnNavigationStateChanged { change, responder } =
request;
responder.send().auth_provider_status(AuthProviderStatus::UnknownError)?;
if let Some(is_main_document_loaded) = change.is_main_document_loaded {
main_document_loaded = is_main_document_loaded;
}
if let Some(page_type) = change.page_type {
known_page_type.replace(page_type);
}
// check if state is terminal
match (known_page_type, main_document_loaded) {
(Some(PageType::Normal), true) => return Ok(()),
(Some(PageType::Normal), false) => (),
(Some(PageType::Error), _) => {
return Err(AuthProviderError::new(AuthProviderStatus::NetworkError))
}
(None, _) => (),
}
}
Err(AuthProviderError::new(AuthProviderStatus::UnknownError))
}
}
#[cfg(test)]
mod test {
use super::*;
use failure::Error;
use fidl::endpoints::ClientEnd;
use fidl_fuchsia_web::{
ContextMarker, FrameMarker, FrameRequest, FrameRequestStream, NavigationState,
};
use fuchsia_async as fasync;
use log::error;
fn create_frame_with_events(
events: Vec<NavigationState>,
) -> Result<DefaultStandaloneWebFrame, Error> {
let (context, _) = create_proxy::<ContextMarker>()?;
let (frame, frame_server_end) = create_request_stream::<FrameMarker>()?;
fasync::spawn(async move {
handle_frame_stream(frame_server_end, events)
.await
.unwrap_or_else(|e| error!("Error running frame stream: {:?}", e));
});
Ok(DefaultStandaloneWebFrame::new(context, frame.into_proxy()?))
}
async fn handle_frame_stream(
mut stream: FrameRequestStream,
events: Vec<NavigationState>,
) -> Result<(), Error> {
if let Some(request) = stream.try_next().await? {
match request {
FrameRequest::SetNavigationEventListener { listener, .. } => {
fasync::spawn(async move {
feed_event_requests(listener.unwrap(), events)
.await
.unwrap_or_else(|e| error!("Error in event sender: {:?}", e));
});
}
req => panic!("Unimplemented method {:?} called in test stub", req),
}
}
Ok(())
}
async fn feed_event_requests(
client_end: ClientEnd<NavigationEventListenerMarker>,
events: Vec<NavigationState>,
) -> Result<(), Error> {
let client_end = client_end.into_proxy()?;
for event in events.into_iter() {
client_end.on_navigation_state_changed(event).await?;
}
Ok(())
}
fn create_navigate_to_url_event(url: Option<&str>) -> NavigationState {
let parsed_url = url.map(|url| String::from(url));
NavigationState {
url: parsed_url,
title: None,
page_type: None,
can_go_forward: None,
can_go_back: None,
is_main_document_loaded: None,
}
}
fn create_navigate_to_page_event(
page_type: Option<PageType>,
is_main_document_loaded: Option<bool>,
) -> NavigationState {
NavigationState {
url: None,
title: None,
page_type,
can_go_forward: None,
can_go_back: None,
is_main_document_loaded,
}
}
#[fasync::run_until_stalled(test)]
async fn test_wait_for_redirect() -> Result<(), Error> {
let events = vec![
create_navigate_to_url_event(None),
create_navigate_to_url_event(None),
create_navigate_to_url_event(Some("http://test/path/")),
create_navigate_to_url_event(Some("http://test/?key=val")),
create_navigate_to_url_event(None),
];
let mut web_frame = create_frame_with_events(events)?;
let target_url = Url::parse("http://test/")?;
let matched_url = web_frame.wait_for_redirect(target_url).await?;
assert_eq!(matched_url, Url::parse("http://test/?key=val").unwrap());
Ok(())
}
#[fasync::run_until_stalled(test)]
async fn test_no_matching_redirect_found() -> Result<(), Error> {
let events = vec![
create_navigate_to_url_event(None),
create_navigate_to_url_event(Some("http://domain/")),
];
let mut web_frame = create_frame_with_events(events)?;
let target_url = Url::parse("http://test/")?;
let result = web_frame.wait_for_redirect(target_url).await;
assert!(result.is_err());
Ok(())
}
#[fasync::run_until_stalled(test)]
async fn test_poll_until_loaded() -> Result<(), Error> {
let events = vec![
create_navigate_to_page_event(None, None),
create_navigate_to_page_event(Some(PageType::Normal), Some(true)),
];
let web_frame = create_frame_with_events(events)?;
let stream = web_frame.get_navigation_event_stream()?;
assert!(DefaultStandaloneWebFrame::poll_until_loaded(stream).await.is_ok());
// Verify functionality when pagetype and document_loaded events sent
// separately
let events = vec![
create_navigate_to_page_event(None, None),
create_navigate_to_page_event(Some(PageType::Normal), None),
create_navigate_to_page_event(None, Some(true)),
];
let web_frame = create_frame_with_events(events)?;
let stream = web_frame.get_navigation_event_stream()?;
assert!(DefaultStandaloneWebFrame::poll_until_loaded(stream).await.is_ok());
Ok(())
}
#[fasync::run_until_stalled(test)]
async fn test_poll_until_loaded_network_error() -> Result<(), Error> {
let events = vec![
create_navigate_to_page_event(None, None),
create_navigate_to_page_event(None, Some(true)),
create_navigate_to_page_event(Some(PageType::Error), None),
];
let web_frame = create_frame_with_events(events)?;
let stream = web_frame.get_navigation_event_stream()?;
assert_eq!(
DefaultStandaloneWebFrame::poll_until_loaded(stream).await.unwrap_err().status,
AuthProviderStatus::NetworkError
);
Ok(())
}
#[fasync::run_until_stalled(test)]
async fn test_poll_until_loaded_stream_closed() -> Result<(), Error> {
let events = vec![
create_navigate_to_page_event(None, None),
create_navigate_to_page_event(Some(PageType::Normal), None),
];
let web_frame = create_frame_with_events(events)?;
let stream = web_frame.get_navigation_event_stream()?;
assert_eq!(
DefaultStandaloneWebFrame::poll_until_loaded(stream).await.unwrap_err().status,
AuthProviderStatus::UnknownError
);
Ok(())
}
}