Skip to main content

actix_web/
config.rs

1use std::{net::SocketAddr, rc::Rc};
2
3use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _};
4
5use crate::{
6    data::Data,
7    dev::{Extensions, ResourceDef},
8    error::Error,
9    guard::Guard,
10    resource::Resource,
11    rmap::ResourceMap,
12    route::Route,
13    service::{
14        AppServiceFactory, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper,
15        ServiceRequest, ServiceResponse,
16    },
17};
18
19type Guards = Vec<Box<dyn Guard>>;
20
21/// Application configuration
22pub struct AppService {
23    config: AppConfig,
24    root: bool,
25    default: Rc<BoxedHttpServiceFactory>,
26    #[allow(clippy::type_complexity)]
27    services: Vec<(
28        ResourceDef,
29        BoxedHttpServiceFactory,
30        Option<Guards>,
31        Option<Rc<ResourceMap>>,
32    )>,
33    #[cfg(feature = "experimental-introspection")]
34    pub current_prefix: String,
35    #[cfg(feature = "experimental-introspection")]
36    pub(crate) introspector:
37        std::rc::Rc<std::cell::RefCell<crate::introspection::IntrospectionCollector>>,
38    #[cfg(feature = "experimental-introspection")]
39    pub(crate) scope_id_stack: Vec<usize>,
40    #[cfg(feature = "experimental-introspection")]
41    pending_scope_id: Option<usize>,
42}
43
44impl AppService {
45    /// Crate server settings instance.
46    pub(crate) fn new(config: AppConfig, default: Rc<BoxedHttpServiceFactory>) -> Self {
47        AppService {
48            config,
49            default,
50            root: true,
51            services: Vec::new(),
52            #[cfg(feature = "experimental-introspection")]
53            current_prefix: "".to_string(),
54            #[cfg(feature = "experimental-introspection")]
55            introspector: std::rc::Rc::new(std::cell::RefCell::new(
56                crate::introspection::IntrospectionCollector::new(),
57            )),
58            #[cfg(feature = "experimental-introspection")]
59            scope_id_stack: Vec::new(),
60            #[cfg(feature = "experimental-introspection")]
61            pending_scope_id: None,
62        }
63    }
64
65    /// Check if root is being configured
66    pub fn is_root(&self) -> bool {
67        self.root
68    }
69
70    #[allow(clippy::type_complexity)]
71    #[cfg(feature = "experimental-introspection")]
72    pub(crate) fn into_services(
73        self,
74    ) -> (
75        AppConfig,
76        Vec<(
77            ResourceDef,
78            BoxedHttpServiceFactory,
79            Option<Guards>,
80            Option<Rc<ResourceMap>>,
81        )>,
82        std::rc::Rc<std::cell::RefCell<crate::introspection::IntrospectionCollector>>,
83    ) {
84        (self.config, self.services, self.introspector)
85    }
86
87    #[allow(clippy::type_complexity)]
88    #[cfg(not(feature = "experimental-introspection"))]
89    pub(crate) fn into_services(
90        self,
91    ) -> (
92        AppConfig,
93        Vec<(
94            ResourceDef,
95            BoxedHttpServiceFactory,
96            Option<Guards>,
97            Option<Rc<ResourceMap>>,
98        )>,
99    ) {
100        (self.config, self.services)
101    }
102
103    /// Clones inner config and default service, returning new `AppService` with empty service list
104    /// marked as non-root.
105    pub(crate) fn clone_config(&self) -> Self {
106        AppService {
107            config: self.config.clone(),
108            default: Rc::clone(&self.default),
109            services: Vec::new(),
110            root: false,
111            #[cfg(feature = "experimental-introspection")]
112            current_prefix: self.current_prefix.clone(),
113            #[cfg(feature = "experimental-introspection")]
114            introspector: std::rc::Rc::clone(&self.introspector),
115            #[cfg(feature = "experimental-introspection")]
116            scope_id_stack: self.scope_id_stack.clone(),
117            #[cfg(feature = "experimental-introspection")]
118            pending_scope_id: None,
119        }
120    }
121
122    /// Returns reference to configuration.
123    pub fn config(&self) -> &AppConfig {
124        &self.config
125    }
126
127    /// Returns default handler factory.
128    pub fn default_service(&self) -> Rc<BoxedHttpServiceFactory> {
129        Rc::clone(&self.default)
130    }
131
132    /// Register HTTP service.
133    pub fn register_service<F, S>(
134        &mut self,
135        rdef: ResourceDef,
136        guards: Option<Vec<Box<dyn Guard>>>,
137        factory: F,
138        nested: Option<Rc<ResourceMap>>,
139    ) where
140        F: IntoServiceFactory<S, ServiceRequest>,
141        S: ServiceFactory<
142                ServiceRequest,
143                Response = ServiceResponse,
144                Error = Error,
145                Config = (),
146                InitError = (),
147            > + 'static,
148    {
149        #[cfg(feature = "experimental-introspection")]
150        {
151            use std::borrow::Borrow;
152
153            // Extract methods and guards for introspection
154            let guard_list: &[Box<dyn Guard>] = guards.borrow().as_ref().map_or(&[], |v| &v[..]);
155            let methods = guard_list
156                .iter()
157                .flat_map(|g| g.details().unwrap_or_default())
158                .flat_map(|d| {
159                    if let crate::guard::GuardDetail::HttpMethods(v) = d {
160                        v.into_iter()
161                            .filter_map(|s| s.parse().ok())
162                            .collect::<Vec<_>>()
163                    } else {
164                        Vec::new()
165                    }
166                })
167                .collect::<Vec<_>>();
168            let guard_names = guard_list.iter().map(|g| g.name()).collect::<Vec<_>>();
169            let guard_details = crate::introspection::guard_reports_from_iter(guard_list.iter());
170
171            let is_resource = nested.is_none();
172            let full_paths = crate::introspection::expand_patterns(&self.current_prefix, &rdef);
173            let patterns = rdef
174                .pattern_iter()
175                .map(|pattern| pattern.to_string())
176                .collect::<Vec<_>>();
177            let resource_name = rdef.name().map(|name| name.to_string());
178            let is_prefix = rdef.is_prefix();
179            let scope_id = if nested.is_some() {
180                self.pending_scope_id.take()
181            } else {
182                None
183            };
184            let parent_scope_id = self.scope_id_stack.last().copied();
185
186            for full_path in full_paths {
187                let info = crate::introspection::RouteInfo::new(
188                    full_path,
189                    methods.clone(),
190                    guard_names.clone(),
191                    guard_details.clone(),
192                    patterns.clone(),
193                    resource_name.clone(),
194                );
195                self.introspector.borrow_mut().register_service(
196                    info,
197                    is_resource,
198                    is_prefix,
199                    scope_id,
200                    parent_scope_id,
201                );
202            }
203        }
204
205        self.services
206            .push((rdef, boxed::factory(factory.into_factory()), guards, nested));
207    }
208
209    /// Update the current path prefix.
210    #[cfg(feature = "experimental-introspection")]
211    pub(crate) fn update_prefix(&mut self, prefix: &str) {
212        let next = ResourceDef::root_prefix(prefix);
213
214        if self.current_prefix.is_empty() {
215            self.current_prefix = next.pattern().unwrap_or("").to_string();
216            return;
217        }
218
219        let current = ResourceDef::root_prefix(&self.current_prefix);
220        let joined = current.join(&next);
221        self.current_prefix = joined.pattern().unwrap_or("").to_string();
222    }
223
224    #[cfg(feature = "experimental-introspection")]
225    pub(crate) fn prepare_scope_id(&mut self) -> usize {
226        let scope_id = self.introspector.borrow_mut().next_scope_id();
227        self.pending_scope_id = Some(scope_id);
228        scope_id
229    }
230}
231
232/// Application connection config.
233#[derive(Debug, Clone)]
234pub struct AppConfig {
235    secure: bool,
236    host: String,
237    addr: SocketAddr,
238}
239
240impl AppConfig {
241    pub(crate) fn new(secure: bool, host: String, addr: SocketAddr) -> Self {
242        AppConfig { secure, host, addr }
243    }
244
245    /// Needed in actix-test crate. Semver exempt.
246    #[doc(hidden)]
247    pub fn __priv_test_new(secure: bool, host: String, addr: SocketAddr) -> Self {
248        AppConfig::new(secure, host, addr)
249    }
250
251    /// Server host name.
252    ///
253    /// Host name is used by application router as a hostname for URL generation.
254    /// Check [ConnectionInfo](super::dev::ConnectionInfo::host())
255    /// documentation for more information.
256    ///
257    /// By default host name is set to a "localhost" value.
258    pub fn host(&self) -> &str {
259        &self.host
260    }
261
262    /// Returns true if connection is secure (i.e., running over `https:`).
263    pub fn secure(&self) -> bool {
264        self.secure
265    }
266
267    /// Returns the socket address of the local half of this TCP connection.
268    pub fn local_addr(&self) -> SocketAddr {
269        self.addr
270    }
271
272    #[cfg(test)]
273    pub(crate) fn set_host(&mut self, host: &str) {
274        host.clone_into(&mut self.host);
275    }
276}
277
278impl Default for AppConfig {
279    /// Returns the default AppConfig.
280    /// Note: The included socket address is "127.0.0.1".
281    ///
282    /// 127.0.0.1: non-routable meta address that denotes an unknown, invalid or non-applicable target.
283    /// If you need a service only accessed by itself, use a loopback address.
284    /// A loopback address for IPv4 is any loopback address that begins with "127".
285    /// Loopback addresses should be only used to test your application locally.
286    /// The default configuration provides a loopback address.
287    ///
288    /// 0.0.0.0: if configured to use this special address, the application will listen to any IP address configured on the machine.
289    fn default() -> Self {
290        AppConfig::new(
291            false,
292            "localhost:8080".to_owned(),
293            "127.0.0.1:8080".parse().unwrap(),
294        )
295    }
296}
297
298/// Enables parts of app configuration to be declared separately from the app itself. Helpful for
299/// modularizing large applications.
300///
301/// Merge a `ServiceConfig` into an app using [`App::configure`](crate::App::configure). Scope and
302/// resources services have similar methods.
303///
304/// ```
305/// use actix_web::{web, App, HttpResponse};
306///
307/// // this function could be located in different module
308/// fn config(cfg: &mut web::ServiceConfig) {
309///     cfg.service(web::resource("/test")
310///         .route(web::get().to(|| HttpResponse::Ok()))
311///         .route(web::head().to(|| HttpResponse::MethodNotAllowed()))
312///     );
313/// }
314///
315/// // merge `/test` routes from config function to App
316/// App::new().configure(config);
317/// ```
318pub struct ServiceConfig {
319    pub(crate) services: Vec<Box<dyn AppServiceFactory>>,
320    pub(crate) external: Vec<ResourceDef>,
321    pub(crate) app_data: Extensions,
322    pub(crate) default: Option<Rc<BoxedHttpServiceFactory>>,
323}
324
325impl ServiceConfig {
326    pub(crate) fn new() -> Self {
327        Self {
328            services: Vec::new(),
329            external: Vec::new(),
330            app_data: Extensions::new(),
331            default: None,
332        }
333    }
334
335    /// Add shared app data item.
336    ///
337    /// Counterpart to [`App::data()`](crate::App::data).
338    #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")]
339    pub fn data<U: 'static>(&mut self, data: U) -> &mut Self {
340        self.app_data(Data::new(data));
341        self
342    }
343
344    /// Add arbitrary app data item.
345    ///
346    /// Counterpart to [`App::app_data()`](crate::App::app_data).
347    pub fn app_data<U: 'static>(&mut self, ext: U) -> &mut Self {
348        self.app_data.insert(ext);
349        self
350    }
351
352    /// Default service to be used if no matching resource could be found.
353    ///
354    /// Counterpart to [`App::default_service()`](crate::App::default_service).
355    pub fn default_service<F, U>(&mut self, f: F) -> &mut Self
356    where
357        F: IntoServiceFactory<U, ServiceRequest>,
358        U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error>
359            + 'static,
360        U::InitError: std::fmt::Debug,
361    {
362        let svc = f
363            .into_factory()
364            .map_init_err(|err| log::error!("Can not construct default service: {:?}", err));
365
366        self.default = Some(Rc::new(boxed::factory(svc)));
367
368        self
369    }
370
371    /// Run external configuration as part of the application building process
372    ///
373    /// Counterpart to [`App::configure()`](crate::App::configure) that allows for easy nesting.
374    pub fn configure<F>(&mut self, f: F) -> &mut Self
375    where
376        F: FnOnce(&mut ServiceConfig),
377    {
378        f(self);
379        self
380    }
381
382    /// Configure route for a specific path.
383    ///
384    /// Counterpart to [`App::route()`](crate::App::route).
385    pub fn route(&mut self, path: &str, mut route: Route) -> &mut Self {
386        self.service(
387            Resource::new(path)
388                .add_guards(route.take_guards())
389                .route(route),
390        )
391    }
392
393    /// Register HTTP service factory.
394    ///
395    /// Counterpart to [`App::service()`](crate::App::service).
396    pub fn service<F>(&mut self, factory: F) -> &mut Self
397    where
398        F: HttpServiceFactory + 'static,
399    {
400        self.services
401            .push(Box::new(ServiceFactoryWrapper::new(factory)));
402        self
403    }
404
405    /// Register an external resource.
406    ///
407    /// External resources are useful for URL generation purposes only and are never considered for
408    /// matching at request time. Calls to [`HttpRequest::url_for()`](crate::HttpRequest::url_for)
409    /// will work as expected.
410    ///
411    /// Counterpart to [`App::external_resource()`](crate::App::external_resource).
412    pub fn external_resource<N, U>(&mut self, name: N, url: U) -> &mut Self
413    where
414        N: AsRef<str>,
415        U: AsRef<str>,
416    {
417        let mut rdef = ResourceDef::new(url.as_ref());
418        rdef.set_name(name.as_ref());
419        self.external.push(rdef);
420        self
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use actix_service::Service;
427    use bytes::Bytes;
428
429    use super::*;
430    use crate::{
431        http::{Method, StatusCode},
432        test::{assert_body_eq, call_service, init_service, read_body, TestRequest},
433        web, App, HttpRequest, HttpResponse,
434    };
435
436    // allow deprecated `ServiceConfig::data`
437    #[allow(deprecated)]
438    #[actix_rt::test]
439    async fn test_data() {
440        let cfg = |cfg: &mut ServiceConfig| {
441            cfg.data(10usize);
442            cfg.app_data(15u8);
443        };
444
445        let srv = init_service(App::new().configure(cfg).service(web::resource("/").to(
446            |_: web::Data<usize>, req: HttpRequest| {
447                assert_eq!(*req.app_data::<u8>().unwrap(), 15u8);
448                HttpResponse::Ok()
449            },
450        )))
451        .await;
452        let req = TestRequest::default().to_request();
453        let resp = srv.call(req).await.unwrap();
454        assert_eq!(resp.status(), StatusCode::OK);
455    }
456
457    #[actix_rt::test]
458    async fn test_external_resource() {
459        let srv = init_service(
460            App::new()
461                .configure(|cfg| {
462                    cfg.external_resource("youtube", "https://youtube.com/watch/{video_id}");
463                })
464                .route(
465                    "/test",
466                    web::get().to(|req: HttpRequest| {
467                        HttpResponse::Ok()
468                            .body(req.url_for("youtube", ["12345"]).unwrap().to_string())
469                    }),
470                ),
471        )
472        .await;
473        let req = TestRequest::with_uri("/test").to_request();
474        let resp = call_service(&srv, req).await;
475        assert_eq!(resp.status(), StatusCode::OK);
476        let body = read_body(resp).await;
477        assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345"));
478    }
479
480    #[actix_rt::test]
481    async fn registers_default_service() {
482        let srv = init_service(
483            App::new()
484                .configure(|cfg| {
485                    cfg.default_service(
486                        web::get().to(|| HttpResponse::NotFound().body("four oh four")),
487                    );
488                })
489                .service(web::scope("/scoped").configure(|cfg| {
490                    cfg.default_service(
491                        web::get().to(|| HttpResponse::NotFound().body("scoped four oh four")),
492                    );
493                })),
494        )
495        .await;
496
497        // app registers default service
498        let req = TestRequest::with_uri("/path/i/did/not-configure").to_request();
499        let resp = call_service(&srv, req).await;
500        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
501        let body = read_body(resp).await;
502        assert_eq!(body, Bytes::from_static(b"four oh four"));
503
504        // scope registers default service
505        let req = TestRequest::with_uri("/scoped/path/i/did/not-configure").to_request();
506        let resp = call_service(&srv, req).await;
507        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
508        let body = read_body(resp).await;
509        assert_eq!(body, Bytes::from_static(b"scoped four oh four"));
510    }
511
512    #[actix_rt::test]
513    async fn test_service() {
514        let srv = init_service(App::new().configure(|cfg| {
515            cfg.service(web::resource("/test").route(web::get().to(HttpResponse::Created)))
516                .route("/index.html", web::get().to(HttpResponse::Ok));
517        }))
518        .await;
519
520        let req = TestRequest::with_uri("/test")
521            .method(Method::GET)
522            .to_request();
523        let resp = call_service(&srv, req).await;
524        assert_eq!(resp.status(), StatusCode::CREATED);
525
526        let req = TestRequest::with_uri("/index.html")
527            .method(Method::GET)
528            .to_request();
529        let resp = call_service(&srv, req).await;
530        assert_eq!(resp.status(), StatusCode::OK);
531    }
532
533    #[actix_rt::test]
534    async fn nested_service_configure() {
535        fn cfg_root(cfg: &mut ServiceConfig) {
536            cfg.configure(cfg_sub);
537        }
538
539        fn cfg_sub(cfg: &mut ServiceConfig) {
540            cfg.route("/", web::get().to(|| async { "hello world" }));
541        }
542
543        let srv = init_service(App::new().configure(cfg_root)).await;
544
545        let req = TestRequest::with_uri("/").to_request();
546        let res = call_service(&srv, req).await;
547        assert_eq!(res.status(), StatusCode::OK);
548        assert_body_eq!(res, b"hello world");
549    }
550}