Skip to main content

htsget_actix/
lib.rs

1extern crate core;
2
3use crate::handlers::{
4  HttpVersionCompat, get, handle_response, post, reads_service_info, variants_service_info,
5};
6use actix_cors::Cors;
7use actix_web::dev::Server;
8use actix_web::{App, HttpRequest, HttpServer, Responder, web};
9use htsget_config::config::advanced::cors::CorsConfig;
10use htsget_config::config::service_info::{PackageInfo, ServiceInfo};
11use htsget_config::config::ticket_server::TicketServerConfig;
12pub use htsget_config::config::{Config, USAGE};
13use htsget_http::error::HtsGetError;
14use htsget_http::middleware::auth::{Auth, AuthBuilder};
15use htsget_search::HtsGet;
16use std::io;
17use tracing::info;
18use tracing::instrument;
19use tracing_actix_web::TracingLogger;
20
21pub mod handlers;
22
23/// Represents the actix app state.
24pub struct AppState<H: HtsGet> {
25  pub htsget: H,
26  pub config_service_info: ServiceInfo,
27  pub auth: Option<Auth>,
28  pub package_info: Option<PackageInfo>,
29}
30
31/// Configure the query server.
32pub fn configure_server<H: HtsGet + Clone + Send + Sync + 'static>(
33  service_config: &mut web::ServiceConfig,
34  htsget: H,
35  config_service_info: ServiceInfo,
36  auth: Option<Auth>,
37  package_info: Option<PackageInfo>,
38) {
39  service_config
40    .app_data(web::Data::new(AppState {
41      htsget,
42      config_service_info,
43      auth,
44      package_info,
45    }))
46    .service(
47      web::scope("/reads")
48        .route("/service-info", web::get().to(reads_service_info::<H>))
49        .route("/service-info", web::post().to(reads_service_info::<H>))
50        .route("/{id:.+}", web::get().to(get::reads::<H>))
51        .route("/{id:.+}", web::post().to(post::reads::<H>)),
52    )
53    .service(
54      web::scope("/variants")
55        .route("/service-info", web::get().to(variants_service_info::<H>))
56        .route("/service-info", web::post().to(variants_service_info::<H>))
57        .route("/{id:.+}", web::get().to(get::variants::<H>))
58        .route("/{id:.+}", web::post().to(post::variants::<H>)),
59    )
60    .default_service(web::to(fallback));
61}
62
63/// A handler for when a route is not found.
64async fn fallback(http_request: HttpRequest) -> impl Responder {
65  handle_response(Err(HtsGetError::NotFound(format!(
66    "No route for {}",
67    http_request.uri()
68  ))))
69}
70
71/// Configure cors, settings allowed methods, max age, allowed origins, and if credentials
72/// are supported.
73pub fn configure_cors(cors: CorsConfig) -> Cors {
74  let mut cors_layer = Cors::default();
75  cors_layer = cors.allow_origins().apply_any(
76    |cors_layer| cors_layer.allow_any_origin().send_wildcard(),
77    cors_layer,
78  );
79  cors_layer = cors
80    .allow_origins()
81    .apply_mirror(|cors_layer| cors_layer.allow_any_origin(), cors_layer);
82  cors_layer = cors.allow_origins().apply_list(
83    |mut cors_layer, origins| {
84      for origin in origins {
85        cors_layer = cors_layer.allowed_origin(&origin.to_string());
86      }
87      cors_layer
88    },
89    cors_layer,
90  );
91
92  cors_layer = cors
93    .allow_headers()
94    .apply_any(|cors_layer| cors_layer.allow_any_header(), cors_layer);
95  cors_layer = cors
96    .allow_headers()
97    .apply_mirror(|cors_layer| cors_layer.allow_any_header(), cors_layer);
98  cors_layer = cors.allow_headers().apply_list(
99    |cors_layer, headers| {
100      cors_layer.allowed_headers(HttpVersionCompat::header_names_1_to_0_2(headers.clone()))
101    },
102    cors_layer,
103  );
104
105  cors_layer = cors
106    .allow_methods()
107    .apply_any(|cors_layer| cors_layer.allow_any_method(), cors_layer);
108  cors_layer = cors
109    .allow_methods()
110    .apply_mirror(|cors_layer| cors_layer.allow_any_method(), cors_layer);
111  cors_layer = cors.allow_methods().apply_list(
112    |cors_layer, methods| {
113      cors_layer.allowed_methods(HttpVersionCompat::methods_0_2_to_1(methods.clone()))
114    },
115    cors_layer,
116  );
117
118  cors_layer = cors
119    .expose_headers()
120    .apply_any(|cors_layer| cors_layer.expose_any_header(), cors_layer);
121  cors_layer = cors.expose_headers().apply_list(
122    |cors_layer, headers| {
123      cors_layer.expose_headers(HttpVersionCompat::header_names_1_to_0_2(headers.clone()))
124    },
125    cors_layer,
126  );
127
128  if cors.allow_credentials() {
129    cors_layer = cors_layer.supports_credentials();
130  }
131
132  cors_layer.max_age(cors.max_age())
133}
134
135/// Run the server using a http-actix `HttpServer`.
136#[instrument(skip_all)]
137pub fn run_server<H: HtsGet + Clone + Send + Sync + 'static>(
138  htsget: H,
139  config: TicketServerConfig,
140  service_info: ServiceInfo,
141  package_info: PackageInfo,
142) -> io::Result<Server> {
143  let app = |htsget: H,
144             config: TicketServerConfig,
145             service_info: ServiceInfo,
146             auth: Option<Auth>,
147             package_info: PackageInfo| {
148    App::new()
149      .configure(|service_config: &mut web::ServiceConfig| {
150        configure_server(
151          service_config,
152          htsget,
153          service_info,
154          auth,
155          Some(package_info),
156        );
157      })
158      .wrap(configure_cors(config.cors().clone()))
159      .wrap(TracingLogger::default())
160  };
161
162  let auth = config
163    .auth()
164    .cloned()
165    .map(|auth| AuthBuilder::default().with_config(auth).build())
166    .transpose()
167    .map_err(io::Error::other)?;
168  let addr = config.addr();
169  let config_copy = config.clone();
170  let server = HttpServer::new(move || {
171    app(
172      htsget.clone(),
173      config_copy.clone(),
174      service_info.clone(),
175      auth.clone(),
176      package_info.clone(),
177    )
178  });
179
180  let server = match config.into_tls() {
181    None => {
182      info!("using non-TLS ticket server");
183      server.bind(addr)?
184    }
185    Some(tls) => {
186      info!("using TLS ticket server");
187      server.bind_rustls_0_23(addr, tls.into_inner())?
188    }
189  };
190
191  info!(addresses = ?server.addrs(), "htsget query server addresses bound");
192  Ok(server.run())
193}
194
195#[cfg(test)]
196mod tests {
197  use std::path::Path;
198
199  use actix_web::body::BoxBody;
200  use actix_web::dev::ServiceResponse;
201  use actix_web::{App, test, web};
202  use async_trait::async_trait;
203  use htsget_test::http::auth::{
204    create_test_auth_config, mock_id_test, mock_prefix_test, mock_regex_test,
205  };
206  use rustls::crypto::aws_lc_rs;
207  use serde_json::Value;
208  use tempfile::TempDir;
209
210  use crate::Config;
211  use htsget_axum::server::BindServer;
212  use htsget_config::package_info;
213  use htsget_config::storage::file::default_path;
214  use htsget_config::types::JsonResponse;
215  use htsget_http::middleware::auth::AuthBuilder;
216  use htsget_test::http::auth::MockAuthServer;
217  use htsget_test::http::server::expected_url_path;
218  use htsget_test::http::{
219    Header as TestHeader, Response as TestResponse, TestRequest, TestServer,
220  };
221  use htsget_test::http::{auth, config_with_tls, default_test_config};
222  use htsget_test::http::{cors, server};
223  use htsget_test::util::generate_key_pair;
224
225  use super::*;
226
227  struct ActixTestServer {
228    config: Config,
229    auth: Option<Auth>,
230  }
231
232  struct ActixTestRequest<T>(T);
233
234  impl TestRequest for ActixTestRequest<test::TestRequest> {
235    fn insert_header(
236      self,
237      header: TestHeader<impl Into<http_1::HeaderName>, impl Into<http_1::HeaderValue>>,
238    ) -> Self {
239      let (name, value) = header.into_tuple();
240      Self(
241        self
242          .0
243          .insert_header((name.to_string(), value.to_str().unwrap())),
244      )
245    }
246
247    fn set_payload(self, payload: impl Into<String>) -> Self {
248      Self(self.0.set_payload(payload.into()))
249    }
250
251    fn uri(self, uri: impl Into<String>) -> Self {
252      Self(self.0.uri(&uri.into()))
253    }
254
255    fn method(self, method: impl Into<http_1::Method>) -> Self {
256      Self(
257        self.0.method(
258          method
259            .into()
260            .to_string()
261            .parse()
262            .expect("expected valid method"),
263        ),
264      )
265    }
266  }
267
268  impl Default for ActixTestServer {
269    fn default() -> Self {
270      Self {
271        config: default_test_config(None),
272        auth: None,
273      }
274    }
275  }
276
277  #[async_trait(?Send)]
278  impl TestServer<ActixTestRequest<test::TestRequest>> for ActixTestServer {
279    async fn get_expected_path(&self) -> String {
280      let data_server = self
281        .get_config()
282        .data_server()
283        .as_data_server_config()
284        .unwrap();
285
286      let path = data_server
287        .local_path()
288        .unwrap_or_else(|| default_path().as_ref())
289        .to_path_buf();
290      let mut bind_data_server = BindServer::from(data_server.clone());
291      let server = bind_data_server.bind_data_server().await.unwrap();
292      let addr = server.local_addr();
293
294      tokio::spawn(async move { server.serve(path).await.unwrap() });
295
296      expected_url_path(self.get_config(), addr.unwrap())
297    }
298
299    fn get_config(&self) -> &Config {
300      &self.config
301    }
302
303    fn request(&self) -> ActixTestRequest<test::TestRequest> {
304      ActixTestRequest(test::TestRequest::default())
305    }
306
307    async fn test_server(
308      &self,
309      request: ActixTestRequest<test::TestRequest>,
310      expected_path: String,
311    ) -> TestResponse {
312      let response = self.get_response(request.0).await;
313
314      let status: u16 = response.status().into();
315      let mut headers = response.headers().clone();
316      let bytes = test::read_body(response).await.to_vec();
317
318      TestResponse::new(
319        status,
320        HttpVersionCompat::header_map_0_2_to_1(
321          headers
322            .drain()
323            .map(|(name, value)| (name.unwrap(), value))
324            .collect(),
325        ),
326        bytes,
327        expected_path,
328      )
329    }
330  }
331
332  impl ActixTestServer {
333    fn new_with_tls<P: AsRef<Path>>(path: P) -> Self {
334      let _ = aws_lc_rs::default_provider().install_default();
335
336      Self {
337        config: config_with_tls(path),
338        auth: None,
339      }
340    }
341
342    async fn new_with_auth(public_key: Vec<u8>, suppressed: bool, mock_location: Value) -> Self {
343      let mock_server = MockAuthServer::new(mock_location).await;
344      let auth_config = create_test_auth_config(&mock_server, public_key, suppressed);
345      let auth = AuthBuilder::default()
346        .with_config(auth_config.clone())
347        .build()
348        .unwrap();
349
350      Self {
351        config: default_test_config(Some(auth_config)),
352        auth: Some(auth),
353      }
354    }
355
356    async fn get_response(&self, request: test::TestRequest) -> ServiceResponse<BoxBody> {
357      let app = App::new()
358        .configure(|service_config: &mut web::ServiceConfig| {
359          configure_server(
360            service_config,
361            self.config.clone().into_locations(),
362            self.config.service_info().clone(),
363            self.auth.clone(),
364            Some(package_info!()),
365          );
366        })
367        .wrap(configure_cors(self.config.ticket_server().cors().clone()));
368
369      let app = test::init_service(app).await;
370      request.send_request(&app).await.map_into_boxed_body()
371    }
372  }
373
374  #[actix_web::test]
375  async fn get_http_tickets() {
376    server::test_get::<JsonResponse, _>(&ActixTestServer::default()).await;
377  }
378
379  #[actix_web::test]
380  async fn post_http_tickets() {
381    server::test_post::<JsonResponse, _>(&ActixTestServer::default()).await;
382  }
383
384  #[actix_web::test]
385  async fn parameterized_get_http_tickets() {
386    server::test_parameterized_get::<JsonResponse, _>(&ActixTestServer::default()).await;
387  }
388
389  #[actix_web::test]
390  async fn parameterized_post_http_tickets() {
391    server::test_parameterized_post::<JsonResponse, _>(&ActixTestServer::default()).await;
392  }
393
394  #[actix_web::test]
395  async fn parameterized_post_class_header_http_tickets() {
396    server::test_parameterized_post_class_header::<JsonResponse, _>(&ActixTestServer::default())
397      .await;
398  }
399
400  #[actix_web::test]
401  async fn service_info() {
402    server::test_service_info(&ActixTestServer::default()).await;
403  }
404
405  #[actix_web::test]
406  async fn get_https_tickets() {
407    let base_path = TempDir::new().unwrap();
408    server::test_get::<JsonResponse, _>(&ActixTestServer::new_with_tls(base_path.path())).await;
409  }
410
411  #[actix_web::test]
412  async fn post_https_tickets() {
413    let base_path = TempDir::new().unwrap();
414    server::test_post::<JsonResponse, _>(&ActixTestServer::new_with_tls(base_path.path())).await;
415  }
416
417  #[actix_web::test]
418  async fn parameterized_get_https_tickets() {
419    let base_path = TempDir::new().unwrap();
420    server::test_parameterized_get::<JsonResponse, _>(&ActixTestServer::new_with_tls(
421      base_path.path(),
422    ))
423    .await;
424  }
425
426  #[actix_web::test]
427  async fn parameterized_post_https_tickets() {
428    let base_path = TempDir::new().unwrap();
429    server::test_parameterized_post::<JsonResponse, _>(&ActixTestServer::new_with_tls(
430      base_path.path(),
431    ))
432    .await;
433  }
434
435  #[actix_web::test]
436  async fn parameterized_post_class_header_https_tickets() {
437    let base_path = TempDir::new().unwrap();
438    server::test_parameterized_post_class_header::<JsonResponse, _>(
439      &ActixTestServer::new_with_tls(base_path.path()),
440    )
441    .await;
442  }
443
444  #[actix_web::test]
445  async fn cors_simple_request() {
446    cors::test_cors_simple_request(&ActixTestServer::default()).await;
447  }
448
449  #[actix_web::test]
450  async fn cors_preflight_request() {
451    cors::test_cors_preflight_request(&ActixTestServer::default(), "x-requested-with", "POST")
452      .await;
453  }
454
455  #[actix_web::test]
456  async fn test_auth_insufficient_permissions() {
457    let (private_key, public_key) = generate_key_pair();
458
459    let server = ActixTestServer::new_with_auth(public_key, false, mock_id_test()).await;
460    auth::test_auth_insufficient_permissions::<JsonResponse, _>(&server, private_key).await;
461  }
462
463  #[actix_web::test]
464  async fn test_auth_succeeds_id() {
465    let (private_key, public_key) = generate_key_pair();
466
467    auth::test_auth_succeeds::<JsonResponse, _>(
468      &ActixTestServer::new_with_auth(public_key, false, mock_id_test()).await,
469      private_key,
470    )
471    .await;
472  }
473
474  #[actix_web::test]
475  async fn test_auth_succeeds_prefix() {
476    let (private_key, public_key) = generate_key_pair();
477
478    auth::test_auth_succeeds::<JsonResponse, _>(
479      &ActixTestServer::new_with_auth(public_key, false, mock_prefix_test()).await,
480      private_key,
481    )
482    .await;
483  }
484
485  #[actix_web::test]
486  async fn test_auth_succeeds_regex() {
487    let (private_key, public_key) = generate_key_pair();
488
489    auth::test_auth_succeeds::<JsonResponse, _>(
490      &ActixTestServer::new_with_auth(public_key, false, mock_regex_test()).await,
491      private_key,
492    )
493    .await;
494  }
495
496  #[cfg(feature = "experimental")]
497  #[actix_web::test]
498  async fn test_auth_insufficient_permissions_suppressed() {
499    let (private_key, public_key) = generate_key_pair();
500
501    let server = ActixTestServer::new_with_auth(public_key, true, mock_id_test()).await;
502    auth::test_auth_insufficient_permissions::<JsonResponse, _>(&server, private_key).await;
503  }
504
505  #[cfg(feature = "experimental")]
506  #[actix_web::test]
507  async fn test_auth_succeeds_suppressed() {
508    let (private_key, public_key) = generate_key_pair();
509
510    auth::test_auth_succeeds::<JsonResponse, _>(
511      &ActixTestServer::new_with_auth(public_key, true, mock_id_test()).await,
512      private_key,
513    )
514    .await;
515  }
516}