blob: 2a6324ad325b6db2c4441a6a04f3dc4da90d6b62 [file] [log] [blame]
// Copyright 2022 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 std::collections::{BTreeSet, HashMap};
use anyhow::Context;
use config_lib::Config;
use fidl::endpoints;
use fidl_connector::Connect;
use fidl_fuchsia_element as felement;
use fuchsia_component::server;
use futures::StreamExt;
type Backend = fidl_connector::ServiceReconnector<felement::ManagerMarker>;
struct Router<'a> {
/// When a `element.Manager/ProposeElement` request comes in, if the URL
/// matches any of these `rules`, that request is proxied to to the
/// corresponding `element.Manager` backend.
rules: Vec<(Matcher, &'a Backend)>,
/// If the URL doesn't match any of the given `rules`, or the request is
/// somehow malformed, it gets proxied to the `default` backend.
default: &'a Backend,
}
impl Router<'_> {
async fn propose_element(
&self,
spec: felement::Spec,
controller: Option<endpoints::ServerEnd<felement::ControllerMarker>>,
) -> anyhow::Result<Result<(), felement::ProposeElementError>> {
let backend = self
.select_backend(spec.component_url.as_ref())
.connect()
.context("acquiring channel")?;
backend.propose_element(spec, controller).await.context("calling propose_element")
}
fn select_backend(&self, url: Option<&String>) -> &Backend {
if let Some(url) = url {
for (matcher, backend) in self.rules.iter() {
if matcher.matches(url) {
return backend;
}
}
}
self.default
}
async fn serve_element_manager_request_stream(&self, stream: felement::ManagerRequestStream) {
stream
.for_each_concurrent(None, |request: fidl::Result<felement::ManagerRequest>| async {
match request {
Err(err) => {
tracing::error!("FIDL error receiving element.Manager request: {}", err);
return;
}
Ok(felement::ManagerRequest::ProposeElement {
spec,
controller,
responder,
}) => {
let mut response = match self.propose_element(spec, controller).await {
Ok(response) => response,
Err(err) => {
tracing::error!(
"FIDL error proxying ProposeElement request.
Dropping responder without replying: {}",
err
);
return;
}
};
if let Err(err) = responder.send(&mut response) {
tracing::error!("FIDL error proxying ProposeElement response: {}", err)
}
}
}
})
.await;
}
}
/// A Matcher is a predicate over component_urls.
enum Matcher {
/// An Exact matcher matches a component_url if it exactly matches the given
/// string.
Exact(String),
/// A Scheme matcher matches a component_url if the component_url correctly
/// parses as a URL, and has a matching scheme.
Scheme(String),
}
impl Matcher {
fn matches(&self, component_url: &str) -> bool {
match self {
Matcher::Exact(matcher_url) => matcher_url == component_url,
Matcher::Scheme(matcher_scheme) => {
// NOTE: Parsing the URL inside `matches` means we may be
// parsing the same URL multiple times, but it really doesn't
// matter from a performance perspective, and the logic is
// easier to understand this way without having to worry about
// memoization.
match url::Url::parse(component_url) {
Ok(parsed_url) => matcher_scheme == parsed_url.scheme(),
Err(err) => {
tracing::info!(
"URL for ProposeElement ({:?}) request does not parse: {:?}",
component_url,
err
);
false
}
}
}
}
}
}
enum IncomingService {
ElementManager(felement::ManagerRequestStream),
}
#[fuchsia::main(logging = true)]
async fn main() -> anyhow::Result<()> {
let config = Config::take_from_startup_handle();
tracing::info!("element_router config: {:?}", config);
let exact_rules = config.url_to_backend.into_iter().map(|rule: String| -> (Matcher, String) {
let split: Vec<&str> = rule.split("|").collect();
assert_eq!(split.len(), 2, "malformed rule: {:?}", rule);
(Matcher::Exact(split[0].to_owned()), split[1].to_owned())
});
let scheme_rules =
config.scheme_to_backend.into_iter().map(|rule: String| -> (Matcher, String) {
let split: Vec<&str> = rule.split("|").collect();
assert_eq!(split.len(), 2, "malformed rule: {:?}", rule);
(Matcher::Scheme(split[0].to_owned()), split[1].to_owned())
});
let all_rules: Vec<(Matcher, String)> = exact_rules.chain(scheme_rules).collect();
let all_backend_names: BTreeSet<String> = all_rules
.iter()
.map(|(_, backend)| backend.to_owned())
.chain(std::iter::once(config.default_backend.to_owned()))
.collect();
let all_backends: HashMap<String, Backend> = all_backend_names
.into_iter()
.map(|backend_name| {
let backend = Backend::with_service_at_path("/svc/".to_owned() + &backend_name);
(backend_name, backend)
})
.collect();
let router = Router {
rules: all_rules
.into_iter()
.map(|(matcher, backend_name)| {
let backend = all_backends
.get(&backend_name)
.expect("somehow we didn't provision this backend?");
(matcher, backend)
})
.collect(),
default: all_backends
.get(&config.default_backend)
.expect("somehow we didn't provision this backend?"),
};
let mut fs = server::ServiceFs::new();
fs.dir("svc").add_fidl_service(IncomingService::ElementManager);
fs.take_and_serve_directory_handle()?;
fs.for_each_concurrent(None, |connection_request| async {
match connection_request {
IncomingService::ElementManager(stream) => {
router.serve_element_manager_request_stream(stream).await;
}
}
})
.await;
Ok(())
}