use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use axum::extract::Request;
use axum::middleware::Next;
use axum::response::IntoResponse;
use axum::response::Response;
use axum_extra::TypedHeader;
use axum_extra::headers::Authorization;
use axum_extra::headers::authorization::Basic;
use bon::Builder;
use planetary_db::Database;
use planetary_server::DEFAULT_ADDRESS;
use planetary_server::DEFAULT_PORT;
use planetary_server::Error;
use planetary_server::templating::is_username_valid;
use reqwest::Client;
use secrecy::SecretString;
use tes::v1::types::responses::ServiceInfo;
use tokio_retry2::strategy::ExponentialFactorBackoff;
use tokio_retry2::strategy::MaxInterval;
use tracing::warn;
use url::Url;
mod info;
mod tasks;
fn retry_durations() -> impl Iterator<Item = Duration> {
const INITIAL_DELAY_MILLIS: u64 = 1000;
const BASE_FACTOR: f64 = 2.0;
const MAX_DURATION: Duration = Duration::from_secs(60);
const RETRIES: usize = 5;
ExponentialFactorBackoff::from_millis(INITIAL_DELAY_MILLIS, BASE_FACTOR)
.max_duration(MAX_DURATION)
.take(RETRIES)
}
fn notify_retry(e: &reqwest::Error, duration: Duration) {
warn!(
"network operation failed: {e} (retrying after {duration} seconds)",
duration = duration.as_secs()
);
}
#[derive(Clone)]
struct Username(String);
async fn auth(
allow_authorization_fallback: bool,
authorization: Option<TypedHeader<Authorization<Basic>>>,
mut request: Request,
next: Next,
) -> Response {
let username = match (
request
.headers()
.get("X-Forwarded-User")
.map(|v| v.to_str()),
&authorization,
) {
(Some(Ok(username)), _) if !username.is_empty() => username,
(None, Some(auth)) if allow_authorization_fallback => auth.username(),
_ => return Error::forbidden().into_response(),
}
.to_string();
if username.is_empty() || !is_username_valid(&username) {
return Error::bad_request("username is invalid").into_response();
}
request.extensions_mut().insert(Username(username));
next.run(request).await
}
struct OrchestratorServiceInfo {
url: Url,
api_key: SecretString,
}
#[derive(Clone)]
struct State {
client: Arc<Client>,
info: Arc<ServiceInfo>,
database: Arc<dyn Database>,
orchestrator: Arc<OrchestratorServiceInfo>,
allow_file_urls: bool,
}
impl State {
pub fn new(
info: ServiceInfo,
database: Arc<dyn Database>,
orchestrator_url: Url,
orchestrator_api_key: SecretString,
allow_file_urls: bool,
) -> Self {
Self {
client: Arc::new(Client::new()),
info: Arc::new(info),
database: database.clone(),
orchestrator: Arc::new(OrchestratorServiceInfo {
url: orchestrator_url,
api_key: orchestrator_api_key,
}),
allow_file_urls,
}
}
}
#[derive(Clone, Builder)]
pub struct Server {
#[builder(into, default = DEFAULT_ADDRESS)]
address: String,
#[builder(into, default = DEFAULT_PORT)]
port: u16,
#[builder(into)]
info: ServiceInfo,
#[builder(name = "shared_database")]
database: Arc<dyn Database>,
#[builder(into)]
orchestrator_url: Url,
#[builder(into)]
orchestrator_api_key: SecretString,
allow_authorization_fallback: bool,
allow_file_urls: bool,
}
impl<S: server_builder::State> ServerBuilder<S> {
pub fn database(
self,
database: impl Database + 'static,
) -> ServerBuilder<server_builder::SetSharedDatabase<S>>
where
S::SharedDatabase: server_builder::IsUnset,
{
self.shared_database(Arc::new(database))
}
}
impl Server {
pub async fn run<F>(self, shutdown: F) -> anyhow::Result<()>
where
F: Future<Output = ()> + Send + 'static,
{
let server = planetary_server::Server::builder()
.address(self.address)
.port(self.port)
.routers(bon::vec![
info::router(),
tasks::router(self.allow_authorization_fallback)
])
.build();
let state = State::new(
self.info,
self.database,
self.orchestrator_url,
self.orchestrator_api_key,
self.allow_file_urls,
);
server.run(state, shutdown).await?;
Ok(())
}
}