blob: a0fe055346b046e9ce0a502abab2187e35ebf925 [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.
use {
crate::rewrite_manager::{CommitError, RewriteManager},
anyhow::{anyhow, Error},
async_lock::RwLock,
fidl_fuchsia_pkg_rewrite::{
EditTransactionRequest, EditTransactionRequestStream, EngineRequest, EngineRequestStream,
RuleIteratorRequest, RuleIteratorRequestStream,
},
fidl_fuchsia_pkg_rewrite_ext::Rule,
fuchsia_async as fasync,
fuchsia_syslog::{fx_log_err, fx_log_info, fx_log_warn},
fuchsia_zircon::Status,
futures::prelude::*,
std::{convert::TryInto, sync::Arc},
};
const LIST_CHUNK_SIZE: usize = 100;
#[derive(Debug, Clone)]
pub struct RewriteService {
state: Arc<RwLock<RewriteManager>>,
}
impl RewriteService {
pub fn new(state: Arc<RwLock<RewriteManager>>) -> Self {
RewriteService { state }
}
pub async fn handle_client(&mut self, mut stream: EngineRequestStream) -> Result<(), Error> {
while let Some(request) = stream.try_next().await? {
match request {
EngineRequest::List { iterator, control_handle: _control_handle } => {
let iterator = iterator.into_stream()?;
self.serve_list(iterator).await;
}
EngineRequest::ListStatic { iterator, control_handle: _control_handle } => {
let iterator = iterator.into_stream()?;
self.serve_list_static(iterator).await;
}
EngineRequest::StartEditTransaction {
transaction,
control_handle: _control_handle,
} => {
let transaction = transaction.into_stream()?;
self.serve_edit_transaction(transaction).await;
}
EngineRequest::TestApply { url, responder } => {
responder.send(
&mut self
.handle_test_apply(url.as_str())
.await
.map(|url| url.to_string())
.map_err(|e| e.into_raw()),
)?;
}
}
}
Ok(())
}
pub(self) async fn handle_test_apply(
&self,
url: &str,
) -> Result<fuchsia_url::AbsolutePackageUrl, Status> {
let url = url.parse::<fuchsia_url::AbsolutePackageUrl>().map_err(|e| {
fx_log_err!("client provided invalid URL ({:?}): {:#}", url, anyhow!(e));
Status::INVALID_ARGS
})?;
let rewritten = self.state.read().await.rewrite(&url);
Ok(rewritten)
}
pub(self) async fn serve_list(&mut self, stream: RuleIteratorRequestStream) {
let rules = self.state.read().await.list().cloned().collect();
Self::serve_rule_iterator(rules, stream);
}
pub(self) async fn serve_list_static(&mut self, stream: RuleIteratorRequestStream) {
let rules = self.state.read().await.list_static().cloned().collect();
Self::serve_rule_iterator(rules, stream);
}
pub(self) async fn serve_edit_transaction(&mut self, mut stream: EditTransactionRequestStream) {
fx_log_info!("opening rewrite transaction");
let state = self.state.clone();
let mut transaction = state.read().await.transaction();
fasync::Task::spawn(
async move {
while let Some(request) = stream.try_next().await? {
match request {
EditTransactionRequest::ListDynamic {
iterator,
control_handle: _control_handle,
} => {
let rules = transaction.list_dynamic().cloned().collect();
let iterator = iterator.into_stream()?;
Self::serve_rule_iterator(rules, iterator);
}
EditTransactionRequest::ResetAll { control_handle: _control_handle } => {
transaction.reset_all();
}
EditTransactionRequest::Add { rule, responder } => {
let mut response = match rule.try_into() {
Ok(rule) => {
transaction.add(rule);
Ok(())
}
Err(_) => Err(Status::INVALID_ARGS.into_raw()),
};
responder.send(&mut response)?;
}
EditTransactionRequest::Commit { responder } => {
let mut response = match state.write().await.apply(transaction).await {
Ok(()) => {
fx_log_info!("rewrite transaction committed");
Ok(())
}
Err(CommitError::TooLate) => {
fx_log_warn!("rewrite transaction out of date");
Err(Status::UNAVAILABLE.into_raw())
}
Err(CommitError::DynamicConfigurationDisabled) => {
fx_log_err!("rewrite transaction failed, dynamic configuration is disabled");
Err(Status::ACCESS_DENIED.into_raw())
}
};
responder.send(&mut response)?;
return Ok(());
}
}
}
fx_log_info!("rewrite transaction dropped");
Ok(())
}
.unwrap_or_else(|e: Error| {
fx_log_err!("while serving rewrite rule edit transaction: {:#}", anyhow!(e))
}),
).detach()
}
fn serve_rule_iterator(rules: Vec<Rule>, mut stream: RuleIteratorRequestStream) {
let mut rules = rules.into_iter().map(|rule| rule.into()).collect::<Vec<_>>();
fasync::Task::spawn(
async move {
let mut iter = rules.iter_mut().peekable();
while let Some(request) = stream.try_next().await? {
let RuleIteratorRequest::Next { responder } = request;
match iter.peek() {
Some(_) => {
responder.send(&mut iter.by_ref().take(LIST_CHUNK_SIZE))?;
}
None => {
responder.send(&mut vec![].into_iter())?;
return Ok(());
}
}
}
Ok(())
}
.unwrap_or_else(|e: fidl::Error| {
fx_log_err!("while serving rewrite rule iterator: {:#}", anyhow!(e))
}),
)
.detach();
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::rewrite_manager::{
tests::{make_rule_config, temp_path_into_proxy_and_path},
RewriteManagerBuilder, Transaction,
},
fidl::endpoints::{create_proxy, create_proxy_and_stream},
fidl_fuchsia_pkg_rewrite::{
EditTransactionMarker, EditTransactionProxy, RuleIteratorMarker,
},
};
macro_rules! rule {
($host_match:expr => $host_replacement:expr,
$path_prefix_match:expr => $path_prefix_replacement:expr) => {
fidl_fuchsia_pkg_rewrite_ext::Rule::new(
$host_match.to_owned(),
$host_replacement.to_owned(),
$path_prefix_match.to_owned(),
$path_prefix_replacement.to_owned(),
)
.unwrap()
};
}
/// Given the future of a FIDL API call, wait for it to complete, and assert that it
/// successfully returns the given Result<(),[`fuchsia_zircon::Status`]>.
macro_rules! assert_yields_result {
($expr:expr, $result:expr) => {
assert_eq!($expr.await.unwrap().map_err(Status::from_raw), $result);
};
}
async fn collect_iterator<F, I, O>(mut next: impl FnMut() -> F) -> Vec<O>
where
F: Future<Output = Result<Vec<I>, fidl::Error>>,
I: TryInto<O>,
<I as TryInto<O>>::Error: std::fmt::Debug,
{
let mut res = Vec::new();
loop {
let more = next().await.unwrap();
if more.is_empty() {
break;
}
res.extend(more.into_iter().map(|item| item.try_into().unwrap()));
}
res
}
async fn list_rules(state: Arc<RwLock<RewriteManager>>) -> Vec<Rule> {
let (iterator, request_stream) = create_proxy_and_stream::<RuleIteratorMarker>().unwrap();
RewriteService::new(state).serve_list(request_stream).await;
collect_iterator(move || iterator.next()).await
}
async fn list_static_rules(state: Arc<RwLock<RewriteManager>>) -> Vec<Rule> {
let (iterator, request_stream) = create_proxy_and_stream::<RuleIteratorMarker>().unwrap();
RewriteService::new(state).serve_list_static(request_stream).await;
collect_iterator(move || iterator.next()).await
}
fn transaction_list_dynamic_rules(
client: &EditTransactionProxy,
) -> impl Future<Output = Vec<Rule>> {
let (iterator, request_stream) = create_proxy::<RuleIteratorMarker>().unwrap();
client.list_dynamic(request_stream).unwrap();
collect_iterator(move || iterator.next())
}
#[fasync::run_singlethreaded(test)]
async fn test_list() {
let dynamic_rules = vec![
rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice"),
rule!("fuchsia.com" => "fuchsia.com", "/rolldice/" => "/rolldice/"),
];
let path = make_rule_config(dynamic_rules.clone());
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let static_rules = vec![
rule!("fuchsia.com" => "static.fuchsia.com", "/3" => "/3"),
rule!("fuchsia.com" => "static.fuchsia.com", "/4" => "/4"),
];
let state = Arc::new(RwLock::new(
RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.static_rules(static_rules.clone())
.build(),
));
let mut expected = dynamic_rules;
expected.extend(static_rules);
assert_eq!(list_rules(state.clone()).await, expected);
}
#[fasync::run_singlethreaded(test)]
async fn test_list_static() {
let path = make_rule_config(vec![
rule!("fuchsia.com" => "dynamic.fuchsia.com", "/1" => "/1"),
rule!("fuchsia.com" => "dynamic.fuchsia.com", "/2" => "/2"),
]);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let static_rules = vec![
rule!("fuchsia.com" => "static.fuchsia.com", "/3" => "/3"),
rule!("fuchsia.com" => "static.fuchsia.com", "/4" => "/4"),
];
let state = Arc::new(RwLock::new(
RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.static_rules(static_rules.clone())
.build(),
));
assert_eq!(list_static_rules(state.clone()).await, static_rules);
}
#[fasync::run_singlethreaded(test)]
async fn test_reset_all() {
let rules = vec![
rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice"),
rule!("fuchsia.com" => "fuchsia.com", "/rolldice/" => "/rolldice/"),
];
let path = make_rule_config(rules.clone());
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let state = Arc::new(RwLock::new(
RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build(),
));
let mut service = RewriteService::new(state.clone());
let (client, request_stream) = create_proxy_and_stream::<EditTransactionMarker>().unwrap();
service.serve_edit_transaction(request_stream).await;
client.reset_all().unwrap();
assert_yields_result!(client.commit(), Ok(()));
assert_eq!(state.read().await.transaction(), Transaction::new(vec![], 1));
}
#[fasync::run_singlethreaded(test)]
async fn test_transaction_list_dynamic() {
let dynamic_rules = vec![
rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice"),
rule!("fuchsia.com" => "fuchsia.com", "/rolldice/" => "/rolldice/"),
];
let path = make_rule_config(dynamic_rules.clone());
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let static_rules = vec![rule!("fuchsia.com" => "static.fuchsia.com", "/" => "/")];
let state = Arc::new(RwLock::new(
RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.static_rules(static_rules)
.build(),
));
let mut service = RewriteService::new(state.clone());
let (client, request_stream) = create_proxy_and_stream::<EditTransactionMarker>().unwrap();
service.serve_edit_transaction(request_stream).await;
// The transaction should list all dynamic rules.
assert_eq!(transaction_list_dynamic_rules(&client).await, dynamic_rules.clone());
// Start a list call, but don't drain it yet.
let pending_list = transaction_list_dynamic_rules(&client);
// Remove all dynamic rules and ensure the transaction lists no rules.
client.reset_all().unwrap();
assert_eq!(transaction_list_dynamic_rules(&client).await, vec![]);
assert_yields_result!(client.commit(), Ok(()));
// Ensure the list call from earlier lists the dynamic rules available at the time the
// iterator was created.
assert_eq!(pending_list.await, dynamic_rules);
}
#[fasync::run_singlethreaded(test)]
async fn test_concurrent_edit() {
let rules = vec![
rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice"),
rule!("fuchsia.com" => "fuchsia.com", "/rolldice/" => "/rolldice/"),
];
let path = make_rule_config(rules.clone());
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let state = Arc::new(RwLock::new(
RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build(),
));
let mut service = RewriteService::new(state.clone());
let client1 = {
let (client, request_stream) =
create_proxy_and_stream::<EditTransactionMarker>().unwrap();
service.serve_edit_transaction(request_stream).await;
client
};
let client2 = {
let (client, request_stream) =
create_proxy_and_stream::<EditTransactionMarker>().unwrap();
service.serve_edit_transaction(request_stream).await;
client
};
client1.reset_all().unwrap();
client2.reset_all().unwrap();
let rule = rule!("fuchsia.com" => "fuchsia.com", "/foo" => "/foo");
assert_yields_result!(client1.add(&mut rule.clone().into()), Ok(()));
assert_yields_result!(client1.commit(), Ok(()));
assert_yields_result!(client2.commit(), Err(Status::UNAVAILABLE));
assert_eq!(state.read().await.transaction(), Transaction::new(vec![rule], 1),);
let client2 = {
let (client, request_stream) =
create_proxy_and_stream::<EditTransactionMarker>().unwrap();
service.serve_edit_transaction(request_stream).await;
client
};
client2.reset_all().unwrap();
assert_yields_result!(client2.commit(), Ok(()));
assert_eq!(state.read().await.transaction(), Transaction::new(vec![], 2));
}
#[fasync::run_singlethreaded(test)]
async fn test_concurrent_list_and_edit() {
let path = make_rule_config(vec![]);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let state = Arc::new(RwLock::new(
RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build(),
));
let mut service = RewriteService::new(state.clone());
assert_eq!(list_rules(state.clone()).await, vec![]);
let edit_client = {
let (client, request_stream) =
create_proxy_and_stream::<EditTransactionMarker>().unwrap();
service.serve_edit_transaction(request_stream).await;
client
};
let rule = rule!("fuchsia.com" => "devhost.fuchsia.com", "/" => "/");
assert_yields_result!(edit_client.add(&mut rule.clone().into()), Ok(()));
assert_eq!(list_rules(state.clone()).await, vec![]);
let long_list_call = {
let (client, request_stream) = create_proxy_and_stream::<RuleIteratorMarker>().unwrap();
service.serve_list(request_stream).await;
client
};
assert_yields_result!(edit_client.commit(), Ok(()));
assert_eq!(long_list_call.next().await.unwrap(), vec![]);
assert_eq!(list_rules(state.clone()).await, vec![rule]);
}
#[fasync::run_singlethreaded(test)]
async fn test_rewrite() {
let path = make_rule_config(vec![]);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let static_rules = vec![
rule!("fuchsia.com" => "fuchsia.com", "/old/" => "/new/"),
rule!("fuchsia.com" => "devhost", "/rolldice" => "/rolldice"),
rule!("fuchsia.com" => "fuchsia.com", "/identity" => "/identity"),
];
let state = Arc::new(RwLock::new(
RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.static_rules(static_rules.clone())
.build(),
));
let service = RewriteService::new(state.clone());
for (url, rewritten) in &[
("fuchsia-pkg://fuchsia.com/old/a", "fuchsia-pkg://fuchsia.com/new/a"),
("fuchsia-pkg://fuchsia.com/rolldice", "fuchsia-pkg://devhost/rolldice"),
("fuchsia-pkg://fuchsia.com/identity", "fuchsia-pkg://fuchsia.com/identity"),
] {
assert_eq!(service.handle_test_apply(url).await, Ok(rewritten.parse().unwrap()));
}
for url in &[
"fuchsia-pkg://subdomain.fuchsia.com/unmatcheddomain",
"fuchsia-pkg://devhost/unmatcheddomain",
"fuchsia-pkg://fuchsia.com/new/unmatcheddir",
] {
assert_eq!(service.handle_test_apply(url).await, Ok(url.parse().unwrap()));
}
}
#[fasync::run_singlethreaded(test)]
async fn test_rewrite_rejects_invalid_inputs() {
let path = make_rule_config(vec![]);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let state = Arc::new(RwLock::new(
RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build(),
));
let service = RewriteService::new(state.clone());
for url in &["not-fuchsia-pkg://fuchsia.com/test", "fuchsia-pkg://fuchsia.com/a*"] {
assert_eq!(service.handle_test_apply(url).await, Err(Status::INVALID_ARGS));
}
}
#[fasync::run_singlethreaded(test)]
async fn test_concurrent_rewrite_and_edit() {
let path = make_rule_config(vec![rule!("fuchsia.com" => "fuchsia.com", "/a" => "/b")]);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let state = Arc::new(RwLock::new(
RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build(),
));
let mut service = RewriteService::new(state.clone());
let (edit_client, request_stream) =
create_proxy_and_stream::<EditTransactionMarker>().unwrap();
service.serve_edit_transaction(request_stream).await;
let replacement_rule = rule!("fuchsia.com" => "fuchsia.com", "/a" => "/c");
edit_client.reset_all().unwrap();
assert_yields_result!(edit_client.add(&mut replacement_rule.into()), Ok(()));
// Pending transaction does not affect apply call.
assert_eq!(
service.handle_test_apply("fuchsia-pkg://fuchsia.com/a").await,
Ok("fuchsia-pkg://fuchsia.com/b".parse().unwrap())
);
assert_yields_result!(edit_client.commit(), Ok(()));
// New rule set now applies.
assert_eq!(
service.handle_test_apply("fuchsia-pkg://fuchsia.com/a").await,
Ok("fuchsia-pkg://fuchsia.com/c".parse().unwrap())
);
}
#[fasync::run_singlethreaded(test)]
async fn test_enables_amber_source() {
let path = make_rule_config(vec![]);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let state = Arc::new(RwLock::new(
RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build(),
));
let mut service = RewriteService::new(state.clone());
let (client, request_stream) = create_proxy_and_stream::<EditTransactionMarker>().unwrap();
service.serve_edit_transaction(request_stream).await;
let rules = vec![
rule!("example.com" => "wrong-host.fuchsia.com", "/" => "/"),
rule!("fuchsia.com" => "correct.fuchsia.com", "/" => "/"),
rule!("fuchsia.com" => "wrong-priority.fuchsia.com", "/" => "/"),
rule!("fuchsia.com" => "wrong-match.fuchsia.com", "/foo/" => "/"),
rule!("fuchsia.com" => "wrong-replacement.fuchsia.com", "/" => "/bar/"),
];
for rule in rules.into_iter().rev() {
assert_yields_result!(client.add(&mut rule.into()), Ok(()));
}
assert_yields_result!(client.commit(), Ok(()));
// Adding a duplicate of the currently enabled source is a no-op.
let (client, request_stream) = create_proxy_and_stream::<EditTransactionMarker>().unwrap();
service.serve_edit_transaction(request_stream).await;
let rule = rule!("fuchsia.com" => "correct.fuchsia.com", "/" => "/");
assert_yields_result!(client.add(&mut rule.into()), Ok(()));
assert_yields_result!(client.commit(), Ok(()));
// Adding a different entry with higher priority enables the new source.
let (client, request_stream) = create_proxy_and_stream::<EditTransactionMarker>().unwrap();
service.serve_edit_transaction(request_stream).await;
let rule = rule!("fuchsia.com" => "correcter.fuchsia.com", "/" => "/");
assert_yields_result!(client.add(&mut rule.into()), Ok(()));
assert_yields_result!(client.commit(), Ok(()));
}
#[fasync::run_singlethreaded(test)]
async fn test_disables_amber_sources() {
let rules = vec![rule!("fuchsia.com" => "enabled.fuchsia.com", "/" => "/")];
let path = make_rule_config(rules);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let state = Arc::new(RwLock::new(
RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build(),
));
let mut service = RewriteService::new(state.clone());
let (client, request_stream) = create_proxy_and_stream::<EditTransactionMarker>().unwrap();
service.serve_edit_transaction(request_stream).await;
client.reset_all().unwrap();
assert_yields_result!(client.commit(), Ok(()));
// Edits that don't change the enabled source are a no-op.
let (client, request_stream) = create_proxy_and_stream::<EditTransactionMarker>().unwrap();
service.serve_edit_transaction(request_stream).await;
client.reset_all().unwrap();
assert_yields_result!(client.commit(), Ok(()));
}
#[fasync::run_singlethreaded(test)]
async fn test_transaction_commit_fails_if_no_dynamic_rules_path() {
let state = Arc::new(RwLock::new(
RewriteManagerBuilder::new(None, Option::<&str>::None).await.unwrap().build(),
));
let mut service = RewriteService::new(state);
let (client, request_stream) = create_proxy_and_stream::<EditTransactionMarker>().unwrap();
service.serve_edit_transaction(request_stream).await;
let status = Status::from_raw(client.commit().await.unwrap().unwrap_err());
assert_eq!(status, Status::ACCESS_DENIED);
}
}