utoipa_swagger_ui/
actix.rs

1#![cfg(feature = "actix-web")]
2
3use std::future;
4
5use actix_web::{
6    dev::{HttpServiceFactory, Service, ServiceResponse},
7    guard::Get,
8    web,
9    web::Data,
10    HttpResponse, Resource, Responder as ActixResponder,
11};
12use base64::Engine;
13
14use crate::{ApiDoc, BasicAuth, Config, SwaggerUi};
15
16impl HttpServiceFactory for SwaggerUi {
17    fn register(self, config: &mut actix_web::dev::AppService) {
18        let mut urls = self
19            .urls
20            .into_iter()
21            .map(|(url, openapi)| {
22                register_api_doc_url_resource(url.url.as_ref(), ApiDoc::Utoipa(openapi), config);
23                url
24            })
25            .collect::<Vec<_>>();
26        let external_api_docs = self.external_urls.into_iter().map(|(url, api_doc)| {
27            register_api_doc_url_resource(url.url.as_ref(), ApiDoc::Value(api_doc), config);
28            url
29        });
30        urls.extend(external_api_docs);
31
32        let swagger_resource = Resource::new(self.path.as_ref())
33            .guard(Get())
34            .app_data(Data::new(if let Some(config) = self.config.clone() {
35                if config.url.is_some() || !config.urls.is_empty() {
36                    config
37                } else {
38                    config.configure_defaults(urls)
39                }
40            } else {
41                Config::new(urls)
42            }))
43            .wrap_fn(move |req, srv| {
44                if let Some(BasicAuth { username, password }) = self
45                    .config
46                    .as_ref()
47                    .and_then(|config| config.basic_auth.clone())
48                {
49                    let encoded_credentials = format!(
50                        "Basic {}",
51                        base64::prelude::BASE64_STANDARD.encode(format!("{username}:{password}"))
52                    );
53                    if let Some(auth_header) = req.headers().get("Authorization") {
54                        if auth_header.to_str().unwrap() == encoded_credentials {
55                            return srv.call(req);
56                        }
57                    }
58                    return Box::pin(future::ready(Ok(ServiceResponse::new(
59                        req.request().clone(),
60                        HttpResponse::Unauthorized()
61                            .insert_header(("WWW-Authenticate", "Basic realm=\":\""))
62                            .finish(),
63                    ))));
64                }
65                srv.call(req)
66            })
67            .to(serve_swagger_ui);
68
69        HttpServiceFactory::register(swagger_resource, config);
70    }
71}
72
73fn register_api_doc_url_resource(url: &str, api: ApiDoc, config: &mut actix_web::dev::AppService) {
74    async fn get_api_doc(api_doc: web::Data<ApiDoc>) -> impl ActixResponder {
75        HttpResponse::Ok().json(api_doc.as_ref())
76    }
77
78    let url_resource = Resource::new(url)
79        .guard(Get())
80        .app_data(Data::new(api))
81        .to(get_api_doc);
82    HttpServiceFactory::register(url_resource, config);
83}
84
85async fn serve_swagger_ui(path: web::Path<String>, data: web::Data<Config<'_>>) -> HttpResponse {
86    match super::serve(&path.into_inner(), data.into_inner()) {
87        Ok(swagger_file) => swagger_file
88            .map(|file| {
89                HttpResponse::Ok()
90                    .content_type(file.content_type)
91                    .body(file.bytes.to_vec())
92            })
93            .unwrap_or_else(|| HttpResponse::NotFound().finish()),
94        Err(error) => HttpResponse::InternalServerError().body(error.to_string()),
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use actix_web::{http::StatusCode, test, App};
101    use base64::prelude::BASE64_STANDARD;
102
103    use super::*;
104    #[actix_web::test]
105    async fn mount_onto_path_with_slash() {
106        let swagger_ui = SwaggerUi::new("/swagger-ui/{_:.*}");
107
108        let app = test::init_service(App::new().service(swagger_ui)).await;
109        let req = test::TestRequest::get().uri("/swagger-ui/").to_request();
110        let resp = test::call_service(&app, req).await;
111
112        assert!(resp.status().is_success());
113    }
114
115    #[actix_web::test]
116    async fn basic_auth() {
117        let swagger_ui =
118            SwaggerUi::new("/swagger-ui/{_:.*}").config(Config::default().basic_auth(BasicAuth {
119                username: "admin".to_string(),
120                password: "password".to_string(),
121            }));
122
123        let app = test::init_service(App::new().service(swagger_ui)).await;
124        let req = test::TestRequest::get().uri("/swagger-ui/").to_request();
125        let resp = test::call_service(&app, req).await;
126        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
127        let encoded_credentials = BASE64_STANDARD.encode("admin:password");
128        let req = test::TestRequest::get()
129            .uri("/swagger-ui/")
130            .insert_header(("Authorization", format!("Basic {}", encoded_credentials)))
131            .to_request();
132        let resp = test::call_service(&app, req).await;
133        assert!(resp.status().is_success());
134    }
135}