| // 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 { |
| anyhow::{anyhow, Context as _}, |
| fidl_fuchsia_io as fio, |
| fidl_fuchsia_pkg_rewrite_ext::{Rule, RuleConfig}, |
| fuchsia_inspect::{self as inspect, Property}, |
| fuchsia_syslog::fx_log_err, |
| fuchsia_url::AbsolutePackageUrl, |
| std::collections::VecDeque, |
| thiserror::Error, |
| }; |
| |
| /// [RewriteManager] controls access to all static and dynamic rewrite rules used by the package |
| /// resolver. |
| /// |
| /// No two instances of [RewriteManager] should be configured to use the same `dynamic_rules_path`, |
| /// or concurrent saves could corrupt the config file or lose edits. Instead, use the provided |
| /// [RewriteManager::transaction] API to safely manage concurrent edits. |
| #[derive(Debug)] |
| pub struct RewriteManager { |
| enable_dynamic_configuration: bool, |
| static_rules: Vec<Rule>, |
| dynamic_rules: Vec<Rule>, |
| generation: u32, |
| data_proxy: Option<fio::DirectoryProxy>, |
| dynamic_rules_path: Option<String>, |
| inspect: RewriteManagerInspectState, |
| } |
| |
| #[derive(Debug)] |
| struct RewriteManagerInspectState { |
| static_rules_node: inspect::Node, |
| static_rules_states: Vec<RuleInspectState>, |
| dynamic_rules_node: inspect::Node, |
| dynamic_rules_states: Vec<RuleInspectState>, |
| generation_property: fuchsia_inspect::UintProperty, |
| // TODO(fxbug.dev/84729) |
| #[allow(unused)] |
| dynamic_rules_path_property: fuchsia_inspect::StringProperty, |
| // TODO(fxbug.dev/84729) |
| #[allow(unused)] |
| node: inspect::Node, |
| } |
| |
| #[derive(Debug, Error)] |
| pub enum CommitError { |
| #[error("the provided rule set is based on an older generation")] |
| TooLate, |
| #[error("editing rewrite rules is permanently disabled")] |
| DynamicConfigurationDisabled, |
| } |
| |
| impl RewriteManager { |
| /// Rewrite the given [AbsolutePackageUrl] using the first dynamic or static rewrite rule that |
| /// matches and produces a valid [AbsolutePackageUrl]. If no rewrite rules match or all that do |
| /// produce invalid [AbsolutePackageUrl]s, return the original, unmodified [AbsolutePackageUrl]. |
| pub fn rewrite(&self, url: &AbsolutePackageUrl) -> AbsolutePackageUrl { |
| for rule in self.list() { |
| match rule.apply(&url) { |
| Some(Ok(res)) => { |
| return res; |
| } |
| Some(Err(err)) => { |
| fx_log_err!( |
| "ignoring rewrite rule {:?} that produced an invalid URL: {:#}", |
| rule, |
| anyhow!(err) |
| ); |
| } |
| _ => {} |
| } |
| } |
| url.clone() |
| } |
| |
| async fn save( |
| dynamic_rules: &mut Vec<Rule>, |
| data_proxy: &fio::DirectoryProxy, |
| dynamic_rules_path: &str, |
| ) -> Result<(), anyhow::Error> { |
| let config = RuleConfig::Version1(std::mem::replace(dynamic_rules, vec![])); |
| |
| let result = async { |
| // TODO(fxbug.dev/83342): We need to reopen because `resolve_succeeds_with_broken_minfs` |
| // expects it, this should be removed once the test is fixed. |
| let data_proxy = fuchsia_fs::directory::open_directory( |
| &data_proxy, |
| ".", |
| fio::OpenFlags::RIGHT_WRITABLE, |
| ) |
| .await |
| .context("opening data-proxy directory")?; |
| |
| let temp_filename = &format!("{dynamic_rules_path}.new"); |
| |
| let data = serde_json::to_vec(&config).context("encoding config")?; |
| |
| crate::util::do_with_atomic_file( |
| &data_proxy, |
| temp_filename, |
| &dynamic_rules_path, |
| |proxy| async move { |
| fuchsia_fs::file::write(&proxy, &data) |
| .await |
| .with_context(|| format!("writing file: {}", temp_filename)) |
| }, |
| ) |
| .await |
| } |
| .await; |
| |
| let RuleConfig::Version1(rules) = config; |
| *dynamic_rules = rules; |
| |
| result |
| } |
| |
| /// Construct a new [Transaction] containing the dynamic config rules from this |
| /// [RewriteManager]. |
| pub fn transaction(&self) -> Transaction { |
| Transaction { |
| dynamic_rules: self.dynamic_rules.clone().into(), |
| generation: self.generation, |
| } |
| } |
| |
| /// Apply the given [Transaction] object to this [RewriteManager] if and only if: |
| /// * dynamic configuration is enabled, and |
| /// * no other [RewriteRuleStates] have been applied since `transaction` was |
| /// cloned from this [RewriteManager]. |
| pub async fn apply(&mut self, transaction: Transaction) -> Result<(), CommitError> { |
| if !self.enable_dynamic_configuration { |
| return Err(CommitError::DynamicConfigurationDisabled); |
| } |
| if self.generation != transaction.generation { |
| Err(CommitError::TooLate) |
| } else { |
| self.dynamic_rules = transaction.dynamic_rules.into(); |
| self.generation += 1; |
| if let (Some(ref data_proxy), Some(ref dynamic_rules_path)) = |
| (self.data_proxy.as_ref(), self.dynamic_rules_path.as_ref()) |
| { |
| if let Err(err) = |
| Self::save(&mut self.dynamic_rules, data_proxy, dynamic_rules_path).await |
| { |
| fx_log_err!("error while saving dynamic rewrite rules: {:#}", anyhow!(err)); |
| } |
| } |
| self.update_inspect_objects(); |
| Ok(()) |
| } |
| } |
| |
| /// Return an iterator through all rewrite rules in the order they should be applied to |
| /// incoming `fuchsia-pkg://` URLs. |
| pub fn list(&self) -> impl Iterator<Item = &Rule> { |
| self.dynamic_rules.iter().chain(self.list_static()) |
| } |
| |
| /// Return an iterator through all static rewrite rules in the order they should be applied to |
| /// incoming `fuchsia-pkg://` URLs, after all dynamic rules have been considered. |
| pub fn list_static(&self) -> impl Iterator<Item = &Rule> { |
| self.static_rules.iter() |
| } |
| |
| fn update_rule_inspect_states( |
| node: &inspect::Node, |
| rules: &[Rule], |
| states: &mut Vec<RuleInspectState>, |
| ) { |
| states.clear(); |
| for (i, rule) in rules.iter().enumerate() { |
| let rule_node = node.create_child(&i.to_string()); |
| states.push(create_rule_inspect_state(rule, rule_node)); |
| } |
| } |
| |
| fn update_inspect_objects(&mut self) { |
| self.inspect.generation_property.set(self.generation.into()); |
| RewriteManager::update_rule_inspect_states( |
| &self.inspect.dynamic_rules_node, |
| &self.dynamic_rules, |
| &mut self.inspect.dynamic_rules_states, |
| ); |
| } |
| } |
| |
| #[allow(missing_docs)] |
| #[derive(Debug, PartialEq, Eq)] |
| pub struct RuleInspectState { |
| _host_match_property: inspect::StringProperty, |
| _host_replacement_property: inspect::StringProperty, |
| _path_prefix_match_property: inspect::StringProperty, |
| _path_prefix_replacement_property: inspect::StringProperty, |
| _node: inspect::Node, |
| } |
| |
| fn create_rule_inspect_state(rule: &Rule, node: inspect::Node) -> RuleInspectState { |
| RuleInspectState { |
| _host_match_property: node.create_string("host_match", &rule.host_match()), |
| _host_replacement_property: node |
| .create_string("host_replacement", &rule.host_replacement()), |
| _path_prefix_match_property: node |
| .create_string("path_prefix_match", &rule.path_prefix_match()), |
| _path_prefix_replacement_property: node |
| .create_string("path_prefix_replacement", &rule.path_prefix_replacement()), |
| _node: node, |
| } |
| } |
| |
| /// [Transaction] tracks an edit transaction to a set of dynamic rewrite rules. |
| #[derive(Debug, PartialEq, Eq)] |
| pub struct Transaction { |
| dynamic_rules: VecDeque<Rule>, |
| generation: u32, |
| } |
| |
| impl Transaction { |
| #[cfg(test)] |
| pub fn new(dynamic_rules: Vec<Rule>, generation: u32) -> Self { |
| Self { dynamic_rules: dynamic_rules.into(), generation } |
| } |
| |
| /// Remove all dynamic rules from this [Transaction]. |
| pub fn reset_all(&mut self) { |
| self.dynamic_rules.clear(); |
| } |
| |
| /// Add the given [Rule] to this [Transaction] with the highest match priority. |
| pub fn add(&mut self, rule: Rule) { |
| self.dynamic_rules.push_front(rule); |
| } |
| |
| /// Return an iterator through all dynamic rewrite rules in the order they should be applied to |
| /// incoming `fuchsia-pkg://` URLs. |
| pub fn list_dynamic(&self) -> impl Iterator<Item = &Rule> { |
| self.dynamic_rules.iter() |
| } |
| } |
| |
| #[derive(Debug)] |
| pub struct UnsetInspectNode; |
| |
| /// [RewriteManagerBuilder] constructs a [RewriteManager], optionally initializing it with [Rule]s |
| /// passed in directly or loaded out of the filesystem. |
| #[derive(Debug)] |
| pub struct RewriteManagerBuilder<N> { |
| static_rules: Vec<Rule>, |
| dynamic_rules: Vec<Rule>, |
| data_proxy: Option<fio::DirectoryProxy>, |
| dynamic_rules_path: Option<String>, |
| enable_dynamic_configuration: bool, |
| inspect_node: N, |
| } |
| |
| impl RewriteManagerBuilder<UnsetInspectNode> { |
| /// Create a new [RewriteManagerBuilder] and initialize it with the dynamic [Rule]s from the |
| /// provided path. If the provided dynamic rule config file does not exist or is corrupt, this |
| /// method returns an [RewriteManagerBuilder] initialized with no rules and configured with the |
| /// given dynamic config path. |
| pub async fn new<P>( |
| data_proxy: Option<fio::DirectoryProxy>, |
| dynamic_rules_path: Option<P>, |
| ) -> Result<Self, (Self, LoadRulesError)> |
| where |
| P: Into<String>, |
| { |
| let enable_dynamic_configuration = dynamic_rules_path.is_some(); |
| let mut builder = RewriteManagerBuilder { |
| static_rules: vec![], |
| dynamic_rules: vec![], |
| data_proxy: data_proxy.clone(), |
| dynamic_rules_path: dynamic_rules_path.map(|p| p.into()), |
| inspect_node: UnsetInspectNode, |
| enable_dynamic_configuration, |
| }; |
| if let Some(ref dynamic_rules_path) = builder.dynamic_rules_path { |
| match Self::load_rules(&data_proxy, dynamic_rules_path).await { |
| Ok(rules) => { |
| builder.dynamic_rules = rules; |
| Ok(builder) |
| } |
| Err(err) => Err((builder, err)), |
| } |
| } else { |
| Ok(builder) |
| } |
| } |
| |
| /// Use the given inspect_node in the [RewriteManager]. |
| pub fn inspect_node(self, inspect_node: inspect::Node) -> RewriteManagerBuilder<inspect::Node> { |
| RewriteManagerBuilder { |
| static_rules: self.static_rules, |
| dynamic_rules: self.dynamic_rules, |
| data_proxy: self.data_proxy, |
| dynamic_rules_path: self.dynamic_rules_path, |
| enable_dynamic_configuration: self.enable_dynamic_configuration, |
| inspect_node, |
| } |
| } |
| } |
| |
| #[derive(Debug, Error)] |
| pub enum LoadRulesError { |
| #[error("directory open")] |
| DirOpen(#[source] anyhow::Error), |
| #[error("file open")] |
| FileOpen(#[from] fuchsia_fs::node::OpenError), |
| #[error("read file")] |
| ReadFile(#[source] anyhow::Error), |
| #[error("parse")] |
| Parse(#[from] serde_json::Error), |
| } |
| |
| impl<N> RewriteManagerBuilder<N> { |
| /// Load [Rule]s from the provided path and register them as static rewrite rules. On error, |
| /// return this [RewriteManagerBuilder] unmodified along with the encountered error. |
| pub async fn static_rules_path( |
| mut self, |
| config_proxy: Option<fio::DirectoryProxy>, |
| static_rules_path: &str, |
| ) -> Result<Self, (Self, LoadRulesError)> { |
| match Self::load_rules(&config_proxy, static_rules_path).await { |
| Ok(rules) => { |
| self.static_rules = rules; |
| Ok(self) |
| } |
| Err(err) => Err((self, err)), |
| } |
| } |
| |
| async fn load_rules( |
| dir_proxy: &Option<fio::DirectoryProxy>, |
| path: &str, |
| ) -> Result<Vec<Rule>, LoadRulesError> { |
| let dir_proxy = dir_proxy |
| .as_ref() |
| .ok_or_else(|| LoadRulesError::DirOpen(anyhow!("failed to open config directory")))?; |
| let file_proxy = |
| fuchsia_fs::directory::open_file(&dir_proxy, &path, fio::OpenFlags::RIGHT_READABLE) |
| .await?; |
| let contents = |
| fuchsia_fs::read_file(&file_proxy).await.map_err(LoadRulesError::ReadFile)?; |
| let RuleConfig::Version1(rules) = serde_json::from_str(&contents)?; |
| Ok(rules) |
| } |
| |
| /// Append the given [Rule]s to the static rewrite rules. |
| #[cfg(test)] |
| pub fn static_rules<T>(mut self, iter: T) -> Self |
| where |
| T: IntoIterator<Item = Rule>, |
| { |
| self.static_rules.extend(iter); |
| self |
| } |
| |
| /// Replace the dynamic rules with the given [Rule]s. |
| pub fn replace_dynamic_rules<T>(mut self, iter: T) -> Self |
| where |
| T: IntoIterator<Item = Rule>, |
| { |
| self.dynamic_rules.clear(); |
| self.dynamic_rules.extend(iter); |
| self |
| } |
| } |
| |
| #[cfg(test)] |
| impl RewriteManagerBuilder<UnsetInspectNode> { |
| /// In test configurations, allow building the [RewriteManager] without a configured inspect |
| /// node. |
| pub fn build(self) -> RewriteManager { |
| let node = inspect::Inspector::new().root().create_child("test"); |
| self.inspect_node(node).build() |
| } |
| } |
| |
| impl RewriteManagerBuilder<inspect::Node> { |
| /// Build the [RewriteManager]. |
| pub fn build(self) -> RewriteManager { |
| let inspect = RewriteManagerInspectState { |
| static_rules_node: self.inspect_node.create_child("static_rules"), |
| static_rules_states: vec![], |
| dynamic_rules_node: self.inspect_node.create_child("dynamic_rules"), |
| dynamic_rules_states: vec![], |
| generation_property: self.inspect_node.create_uint("generation", 0), |
| dynamic_rules_path_property: self |
| .inspect_node |
| .create_string("dynamic_rules_path", &format!("{:?}", self.dynamic_rules_path)), |
| node: self.inspect_node, |
| }; |
| |
| let mut rw = RewriteManager { |
| static_rules: self.static_rules, |
| dynamic_rules: self.dynamic_rules, |
| generation: 0, |
| data_proxy: self.data_proxy, |
| dynamic_rules_path: self.dynamic_rules_path, |
| enable_dynamic_configuration: self.enable_dynamic_configuration, |
| inspect, |
| }; |
| RewriteManager::update_rule_inspect_states( |
| &rw.inspect.static_rules_node, |
| &rw.static_rules, |
| &mut rw.inspect.static_rules_states, |
| ); |
| rw.update_inspect_objects(); |
| rw |
| } |
| } |
| |
| #[cfg(test)] |
| pub(crate) mod tests { |
| use { |
| super::*, |
| anyhow::Error, |
| assert_matches::assert_matches, |
| fuchsia_async as fasync, |
| fuchsia_inspect::assert_data_tree, |
| serde_json::json, |
| std::{io, path::Path}, |
| }; |
| |
| 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() |
| }; |
| } |
| |
| pub(crate) fn make_temp_file<CB, E>(writer: CB) -> tempfile::TempPath |
| where |
| CB: FnOnce(&mut dyn io::Write) -> Result<(), E>, |
| E: Into<Error>, |
| { |
| let mut f = tempfile::NamedTempFile::new().unwrap(); |
| writer(f.as_file_mut()).map_err(|err| err.into()).unwrap(); |
| f.into_temp_path() |
| } |
| |
| pub(crate) fn make_rule_config(rules: Vec<Rule>) -> tempfile::TempPath { |
| let config = RuleConfig::Version1(rules); |
| make_temp_file(|writer| serde_json::to_writer(writer, &config)) |
| } |
| |
| pub(crate) fn temp_path_into_proxy_and_path( |
| path: &tempfile::TempPath, |
| ) -> (Option<fio::DirectoryProxy>, Option<String>) { |
| let filename = Some(path.file_name().unwrap().to_str().unwrap().to_string()); |
| let dir = path.parent().unwrap().to_str().unwrap().to_string(); |
| let proxy = fuchsia_fs::directory::open_in_namespace( |
| &dir, |
| fuchsia_fs::OpenFlags::RIGHT_READABLE | fuchsia_fs::OpenFlags::RIGHT_WRITABLE, |
| ) |
| .ok(); |
| (proxy, filename) |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_empty_configs() { |
| let path = make_rule_config(vec![]); |
| let (config_dir, config_file) = temp_path_into_proxy_and_path(&path); |
| |
| let manager = RewriteManagerBuilder::new(config_dir.clone(), config_file.clone()) |
| .await |
| .unwrap() |
| .static_rules_path(config_dir, &config_file.unwrap()) |
| .await |
| .unwrap() |
| .build(); |
| |
| assert_eq!(manager.list_static().cloned().collect::<Vec<_>>(), vec![]); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_load_single_static_rule() { |
| let rules = vec![rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice")]; |
| |
| let dynamic_path = make_rule_config(vec![]); |
| let (dynamic_config_dir, dynamic_config_file) = |
| temp_path_into_proxy_and_path(&dynamic_path); |
| let path = make_rule_config(rules.clone()); |
| let (static_config_dir, static_config_file) = temp_path_into_proxy_and_path(&path); |
| let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file) |
| .await |
| .unwrap() |
| .static_rules_path(static_config_dir, &static_config_file.unwrap()) |
| .await |
| .unwrap() |
| .build(); |
| |
| assert_eq!(manager.list_static().cloned().collect::<Vec<_>>(), rules); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_load_single_dynamic_rule() { |
| let rules = vec![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 manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file) |
| .await |
| .unwrap() |
| .build(); |
| |
| assert_eq!(manager.list_static().cloned().collect::<Vec<_>>(), vec![]); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_rejects_invalid_static_config() { |
| let rules = vec![rule!("fuchsia.com" => "fuchsia.com", "/a" => "/b")]; |
| let dynamic_path = make_rule_config(rules.clone()); |
| let (dynamic_config_dir, dynamic_config_file) = |
| temp_path_into_proxy_and_path(&dynamic_path); |
| let path = make_temp_file(|writer| { |
| write!( |
| writer, |
| "{}", |
| json!({ |
| "version": "1", |
| "content": {} // should be an array |
| }) |
| ) |
| }); |
| let (static_config_dir, static_config_file) = temp_path_into_proxy_and_path(&path); |
| let (builder, _) = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file) |
| .await |
| .unwrap() |
| .static_rules_path(static_config_dir, &static_config_file.unwrap()) |
| .await |
| .unwrap_err(); |
| let manager = builder.build(); |
| |
| assert_eq!(manager.list_static().cloned().collect::<Vec<_>>(), vec![]); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_recovers_from_invalid_dynamic_config() { |
| let path = make_temp_file(|writer| write!(writer, "invalid")); |
| let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path); |
| let rule = rule!("test.com" => "test.com", "/a" => "/b"); |
| |
| { |
| let (builder, _) = |
| RewriteManagerBuilder::new(dynamic_config_dir.clone(), dynamic_config_file.clone()) |
| .await |
| .unwrap_err(); |
| let mut manager = builder.build(); |
| |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]); |
| |
| let mut transaction = manager.transaction(); |
| transaction.add(rule.clone()); |
| manager.apply(transaction).await.unwrap(); |
| |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule.clone()]); |
| } |
| |
| // Verify the dynamic config file is no longer corrupt and contains the newly added rule. |
| let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file) |
| .await |
| .unwrap() |
| .build(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule]); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_rewrite_identity_if_no_rules_match() { |
| let rules = vec![ |
| rule!("fuchsia.com" => "fuchsia.com", "/a" => "/aa"), |
| rule!("fuchsia.com" => "fuchsia.com", "/b" => "/bb"), |
| ]; |
| |
| let path = make_rule_config(rules); |
| let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path); |
| let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file) |
| .await |
| .unwrap() |
| .build(); |
| |
| let url: AbsolutePackageUrl = "fuchsia-pkg://fuchsia.com/c".parse().unwrap(); |
| assert_eq!(manager.rewrite(&url), url); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_rewrite_first_rule_wins() { |
| let rules = vec![ |
| rule!("fuchsia.com" => "fuchsia.com", "/package" => "/remapped"), |
| rule!("fuchsia.com" => "fuchsia.com", "/package" => "/incorrect"), |
| ]; |
| |
| let path = make_rule_config(rules); |
| let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path); |
| let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file) |
| .await |
| .unwrap() |
| .build(); |
| |
| let url = "fuchsia-pkg://fuchsia.com/package".parse().unwrap(); |
| assert_eq!(manager.rewrite(&url), "fuchsia-pkg://fuchsia.com/remapped".parse().unwrap()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_rewrite_dynamic_rules_override_static_rules() { |
| let dynamic_path = make_rule_config(vec![ |
| rule!("fuchsia.com" => "fuchsia.com", "/package" => "/remapped"), |
| ]); |
| let (dynamic_config_dir, dynamic_config_file) = |
| temp_path_into_proxy_and_path(&dynamic_path); |
| let path = make_rule_config(vec![ |
| rule!("fuchsia.com" => "fuchsia.com", "/package" => "/incorrect"), |
| ]); |
| let (static_config_dir, static_config_file) = temp_path_into_proxy_and_path(&path); |
| let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file) |
| .await |
| .unwrap() |
| .static_rules_path(static_config_dir, &static_config_file.unwrap()) |
| .await |
| .unwrap() |
| .build(); |
| |
| let url = "fuchsia-pkg://fuchsia.com/package".parse().unwrap(); |
| assert_eq!(manager.rewrite(&url), "fuchsia-pkg://fuchsia.com/remapped".parse().unwrap()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_rewrite_replace_dynamic_configs() { |
| let static_rules = vec![rule!("fuchsia.com" => "foo.com", "/" => "/")]; |
| let dynamic_rules = vec![rule!("fuchsia.com" => "bar.com", "/" => "/")]; |
| let new_dynamic_rules = vec![rule!("fuchsia.com" => "baz.com", "/" => "/")]; |
| |
| let static_path = make_rule_config(static_rules); |
| let (static_config_dir, static_config_file) = temp_path_into_proxy_and_path(&static_path); |
| let path = make_rule_config(dynamic_rules); |
| let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path); |
| |
| let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file) |
| .await |
| .unwrap() |
| .static_rules_path(static_config_dir, &static_config_file.unwrap()) |
| .await |
| .unwrap() |
| .replace_dynamic_rules(new_dynamic_rules.clone()) |
| .build(); |
| |
| let url = "fuchsia-pkg://fuchsia.com/package".parse().unwrap(); |
| assert_eq!(manager.rewrite(&url), "fuchsia-pkg://baz.com/package".parse().unwrap()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_rewrite_with_pending_transaction() { |
| let override_rule = rule!("fuchsia.com" => "fuchsia.com", "/a" => "/c"); |
| 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 mut manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file) |
| .await |
| .unwrap() |
| .build(); |
| |
| let mut transaction = manager.transaction(); |
| transaction.add(override_rule.clone()); |
| |
| // new rule is not yet committed and should not be used yet |
| let url: AbsolutePackageUrl = "fuchsia-pkg://fuchsia.com/a".parse().unwrap(); |
| assert_eq!(manager.rewrite(&url), "fuchsia-pkg://fuchsia.com/b".parse().unwrap()); |
| |
| manager.apply(transaction).await.unwrap(); |
| |
| let url = "fuchsia-pkg://fuchsia.com/a".parse().unwrap(); |
| assert_eq!(manager.rewrite(&url), "fuchsia-pkg://fuchsia.com/c".parse().unwrap()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_commit_additional_rule() { |
| let existing_rule = rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice"); |
| let new_rule = rule!("fuchsia.com" => "fuchsia.com", "/rolldice/" => "/rolldice/"); |
| |
| let rules = vec![existing_rule.clone()]; |
| let path = make_rule_config(rules.clone()); |
| let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path); |
| let mut manager = |
| RewriteManagerBuilder::new(dynamic_config_dir.clone(), dynamic_config_file.clone()) |
| .await |
| .unwrap() |
| .build(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules); |
| |
| // Fork the existing state, add a rule, and verify both instances are distinct |
| let new_rules = vec![new_rule.clone(), existing_rule.clone()]; |
| let mut transaction = manager.transaction(); |
| transaction.add(new_rule); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules); |
| assert_eq!(transaction.list_dynamic().cloned().collect::<Vec<_>>(), new_rules); |
| |
| // Commit the new rule set |
| let () = manager.apply(transaction).await.unwrap(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), new_rules); |
| |
| // Ensure new rules are persisted to the dynamic config file |
| let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file) |
| .await |
| .unwrap() |
| .build(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), new_rules); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_erase_all_dynamic_rules() { |
| 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 mut manager = |
| RewriteManagerBuilder::new(dynamic_config_dir.clone(), dynamic_config_file.clone()) |
| .await |
| .unwrap() |
| .build(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules); |
| |
| let mut transaction = manager.transaction(); |
| transaction.reset_all(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules); |
| assert_eq!(transaction.list_dynamic().cloned().collect::<Vec<_>>(), vec![]); |
| |
| let () = manager.apply(transaction).await.unwrap(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]); |
| |
| let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file) |
| .await |
| .unwrap() |
| .build(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_building_rewrite_manager_populates_inspect() { |
| let inspector = fuchsia_inspect::Inspector::new(); |
| let node = inspector.root().create_child("rewrite_manager"); |
| let dynamic_rules = vec![ |
| rule!("this.example.com" => "that.example.com", "/this_rolldice" => "/that_rolldice"), |
| ]; |
| let dynamic_path = make_rule_config(dynamic_rules.clone()); |
| let (dynamic_config_dir, dynamic_config_file) = |
| temp_path_into_proxy_and_path(&dynamic_path); |
| |
| let static_rules = |
| vec![rule!("example.com" => "example.org", "/this_throwdice" => "/that_throwdice")]; |
| let path = make_rule_config(static_rules.clone()); |
| let (static_config_dir, static_config_file) = temp_path_into_proxy_and_path(&path); |
| |
| let _manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file.clone()) |
| .await |
| .unwrap() |
| .static_rules_path(static_config_dir, &static_config_file.unwrap()) |
| .await |
| .unwrap() |
| .inspect_node(node) |
| .build(); |
| |
| assert_data_tree!( |
| inspector, |
| root: { |
| rewrite_manager: { |
| dynamic_rules: { |
| "0": { |
| host_match: "this.example.com", |
| host_replacement: "that.example.com", |
| path_prefix_match: "/this_rolldice", |
| path_prefix_replacement: "/that_rolldice", |
| }, |
| }, |
| dynamic_rules_path: format!("{:?}", dynamic_config_file), |
| static_rules: { |
| "0": { |
| host_match: "example.com", |
| host_replacement: "example.org", |
| path_prefix_match: "/this_throwdice", |
| path_prefix_replacement: "/that_throwdice", |
| }, |
| }, |
| generation: 0u64, |
| } |
| } |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_inspect_rewrite_manager_no_dynamic_rules_path() { |
| let inspector = fuchsia_inspect::Inspector::new(); |
| let node = inspector.root().create_child("rewrite_manager"); |
| |
| let _manager = RewriteManagerBuilder::new(None, Option::<&str>::None) |
| .await |
| .unwrap() |
| .inspect_node(node) |
| .build(); |
| |
| assert_data_tree!( |
| inspector, |
| root: { |
| rewrite_manager: { |
| dynamic_rules: {}, |
| dynamic_rules_path: format!("{:?}", Option::<&Path>::None), |
| static_rules: {}, |
| generation: 0u64, |
| } |
| } |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_transaction_updates_inspect() { |
| let inspector = fuchsia_inspect::Inspector::new(); |
| let node = inspector.root().create_child("rewrite_manager"); |
| let path = make_rule_config(vec![]); |
| let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path); |
| let mut manager = |
| RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file.clone()) |
| .await |
| .unwrap() |
| .inspect_node(node) |
| .build(); |
| assert_data_tree!( |
| inspector, |
| root: { |
| rewrite_manager: { |
| dynamic_rules: {}, |
| dynamic_rules_path: format!("{:?}", dynamic_config_file.clone()), |
| static_rules: {}, |
| generation: 0u64, |
| } |
| } |
| ); |
| |
| let mut transaction = manager.transaction(); |
| transaction |
| .add(rule!("example.com" => "example.org", "/this_rolldice/" => "/that_rolldice/")); |
| manager.apply(transaction).await.unwrap(); |
| |
| assert_data_tree!( |
| inspector, |
| root: { |
| rewrite_manager: { |
| dynamic_rules: { |
| "0": { |
| host_match: "example.com", |
| host_replacement: "example.org", |
| path_prefix_match: "/this_rolldice/", |
| path_prefix_replacement: "/that_rolldice/", |
| }, |
| }, |
| dynamic_rules_path: format!("{:?}", dynamic_config_file), |
| static_rules: {}, |
| generation: 1u64, |
| } |
| } |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_no_dynamic_rules_if_no_dynamic_rules_path() { |
| let manager = RewriteManagerBuilder::new(None, Option::<&str>::None).await.unwrap().build(); |
| |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_same_dynamic_rules_if_apply_fails() { |
| let path = make_rule_config(vec![]); |
| let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path); |
| let rule0 = rule!("test0.com" => "test0.com", "/a" => "/b"); |
| let rule1 = rule!("test1.com" => "test1.com", "/a" => "/b"); |
| let mut manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file) |
| .await |
| .unwrap() |
| .build(); |
| assert_eq!(manager.list().collect::<Vec<_>>(), Vec::<&Rule>::new()); |
| |
| // transaction0 adds a dynamic rule |
| let mut transaction0 = manager.transaction(); |
| let mut transaction1 = manager.transaction(); |
| transaction0.add(rule0.clone()); |
| manager.apply(transaction0).await.unwrap(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule0.clone()]); |
| |
| // transaction1 fails to apply b/c it was created before transaction0 was applied |
| // the dynamic rewrite rules should be unchanged |
| transaction1.add(rule1.clone()); |
| assert_matches!(manager.apply(transaction1).await, Err(CommitError::TooLate)); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule0.clone()]); |
| |
| // transaction2 applies the rule from transaction1 |
| let mut transaction2 = manager.transaction(); |
| transaction2.add(rule1.clone()); |
| manager.apply(transaction2).await.unwrap(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule1.clone(), rule0.clone()]); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_apply_fails_with_no_change_if_no_dynamic_rules_path() { |
| let mut manager = |
| RewriteManagerBuilder::new(None, Option::<&str>::None).await.unwrap().build(); |
| let mut transaction = manager.transaction(); |
| transaction.add(rule!("test0.com" => "test0.com", "/a" => "/b")); |
| |
| assert_matches!( |
| manager.apply(transaction).await, |
| Err(CommitError::DynamicConfigurationDisabled) |
| ); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_rewrite_works_when_data_inaccessible() { |
| let rule0 = rule!("test0.com" => "test0.com", "/a" => "/b"); |
| let (builder, _) = |
| RewriteManagerBuilder::new(None, Some("nonemptyfilename")).await.unwrap_err(); |
| let mut manager = builder.build(); |
| |
| // transaction0 adds a dynamic rule |
| let mut transaction0 = manager.transaction(); |
| transaction0.add(rule0.clone()); |
| manager.apply(transaction0).await.unwrap(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule0.clone()]); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_constructor_returns_not_found_if_file_missing() { |
| let path = make_rule_config(vec![]); |
| let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path); |
| // Delete the config file. |
| path.close().unwrap(); |
| |
| assert_matches!( |
| RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file).await, |
| Err(( |
| _, |
| LoadRulesError::FileOpen(fuchsia_fs::node::OpenError::OpenError( |
| fuchsia_zircon::Status::NOT_FOUND |
| )) |
| )) |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_static_rules_return_not_found_if_file_missing() { |
| let path = make_rule_config(vec![]); |
| let (config_dir, config_file) = temp_path_into_proxy_and_path(&path); |
| // Delete the config file. |
| path.close().unwrap(); |
| |
| let builder = RewriteManagerBuilder::new(None, Option::<&str>::None).await.unwrap(); |
| assert_matches!( |
| builder.static_rules_path(config_dir, &config_file.unwrap()).await, |
| Err(( |
| _, |
| LoadRulesError::FileOpen(fuchsia_fs::node::OpenError::OpenError( |
| fuchsia_zircon::Status::NOT_FOUND |
| )) |
| )) |
| ); |
| } |
| |
| #[test] |
| fn test_create_rule_inspect_state_passes_through_fields() { |
| let inspector = inspect::Inspector::new(); |
| let node = inspector.root().create_child("rule_node"); |
| |
| let rule = rule!("fuchsia.com" => "example.com", "/foo" => "/bar"); |
| let _state = create_rule_inspect_state(&rule, node); |
| |
| assert_data_tree!( |
| inspector, |
| root: { |
| rule_node: { |
| host_match: rule.host_match().to_string(), |
| host_replacement: rule.host_replacement().to_string(), |
| path_prefix_match: rule.path_prefix_match().to_string(), |
| path_prefix_replacement: rule.path_prefix_replacement().to_string(), |
| } |
| } |
| ); |
| } |
| } |