1use std::future::Future;
4use std::sync::Arc;
5use std::time::Duration;
6
7use axum::extract::Request;
8use axum::middleware::Next;
9use axum::response::IntoResponse;
10use axum::response::Response;
11use axum_extra::TypedHeader;
12use axum_extra::headers::Authorization;
13use axum_extra::headers::authorization::Basic;
14use bon::Builder;
15use planetary_db::Database;
16use planetary_server::DEFAULT_ADDRESS;
17use planetary_server::DEFAULT_PORT;
18use planetary_server::Error;
19use planetary_server::templating::is_username_valid;
20use reqwest::Client;
21use secrecy::SecretString;
22use tes::v1::types::responses::ServiceInfo;
23use tokio_retry2::strategy::ExponentialFactorBackoff;
24use tokio_retry2::strategy::MaxInterval;
25use tracing::warn;
26use url::Url;
27
28mod info;
29mod tasks;
30
31fn retry_durations() -> impl Iterator<Item = Duration> {
36 const INITIAL_DELAY_MILLIS: u64 = 1000;
37 const BASE_FACTOR: f64 = 2.0;
38 const MAX_DURATION: Duration = Duration::from_secs(60);
39 const RETRIES: usize = 5;
40
41 ExponentialFactorBackoff::from_millis(INITIAL_DELAY_MILLIS, BASE_FACTOR)
42 .max_duration(MAX_DURATION)
43 .take(RETRIES)
44}
45
46fn notify_retry(e: &reqwest::Error, duration: Duration) {
48 warn!(
49 "network operation failed: {e} (retrying after {duration} seconds)",
50 duration = duration.as_secs()
51 );
52}
53
54#[derive(Clone)]
57struct Username(String);
58
59async fn auth(
69 allow_authorization_fallback: bool,
70 authorization: Option<TypedHeader<Authorization<Basic>>>,
71 mut request: Request,
72 next: Next,
73) -> Response {
74 let username = match (
77 request
78 .headers()
79 .get("X-Forwarded-User")
80 .map(|v| v.to_str()),
81 &authorization,
82 ) {
83 (Some(Ok(username)), _) if !username.is_empty() => username,
86 (None, Some(auth)) if allow_authorization_fallback => auth.username(),
87 _ => return Error::forbidden().into_response(),
88 }
89 .to_string();
90
91 if username.is_empty() || !is_username_valid(&username) {
93 return Error::bad_request("username is invalid").into_response();
94 }
95
96 request.extensions_mut().insert(Username(username));
97 next.run(request).await
98}
99
100struct OrchestratorServiceInfo {
102 url: Url,
104 api_key: SecretString,
106}
107
108#[derive(Clone)]
110struct State {
111 client: Arc<Client>,
113 info: Arc<ServiceInfo>,
115 database: Arc<dyn Database>,
117 orchestrator: Arc<OrchestratorServiceInfo>,
119 allow_file_urls: bool,
121}
122
123impl State {
124 pub fn new(
127 info: ServiceInfo,
128 database: Arc<dyn Database>,
129 orchestrator_url: Url,
130 orchestrator_api_key: SecretString,
131 allow_file_urls: bool,
132 ) -> Self {
133 Self {
134 client: Arc::new(Client::new()),
135 info: Arc::new(info),
136 database: database.clone(),
137 orchestrator: Arc::new(OrchestratorServiceInfo {
138 url: orchestrator_url,
139 api_key: orchestrator_api_key,
140 }),
141 allow_file_urls,
142 }
143 }
144}
145
146#[derive(Clone, Builder)]
148pub struct Server {
149 #[builder(into, default = DEFAULT_ADDRESS)]
151 address: String,
152
153 #[builder(into, default = DEFAULT_PORT)]
155 port: u16,
156
157 #[builder(into)]
159 info: ServiceInfo,
160
161 #[builder(name = "shared_database")]
163 database: Arc<dyn Database>,
164
165 #[builder(into)]
167 orchestrator_url: Url,
168
169 #[builder(into)]
171 orchestrator_api_key: SecretString,
172
173 allow_authorization_fallback: bool,
175
176 allow_file_urls: bool,
178}
179
180impl<S: server_builder::State> ServerBuilder<S> {
181 pub fn database(
186 self,
187 database: impl Database + 'static,
188 ) -> ServerBuilder<server_builder::SetSharedDatabase<S>>
189 where
190 S::SharedDatabase: server_builder::IsUnset,
191 {
192 self.shared_database(Arc::new(database))
193 }
194}
195
196impl Server {
197 pub async fn run<F>(self, shutdown: F) -> anyhow::Result<()>
199 where
200 F: Future<Output = ()> + Send + 'static,
201 {
202 let server = planetary_server::Server::builder()
203 .address(self.address)
204 .port(self.port)
205 .routers(bon::vec![
206 info::router(),
207 tasks::router(self.allow_authorization_fallback)
208 ])
209 .build();
210
211 let state = State::new(
212 self.info,
213 self.database,
214 self.orchestrator_url,
215 self.orchestrator_api_key,
216 self.allow_file_urls,
217 );
218
219 server.run(state, shutdown).await?;
220 Ok(())
221 }
222}