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
23pub 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
31pub 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
63async 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
71pub 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#[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}