virtual_hosts_module/lib.rs
1// Copyright 2024 Wladimir Palant
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! # Virtual Hosts Module for Pingora
16//!
17//! This module simplifies dealing with virtual hosts. It wraps any handler implementing
18//! [`module_utils::RequestFilter`] and its configuration, allowing to supply a different
19//! configuration for that handler for each virtual host and subdirectories of that host. For
20//! example, if Static Files Module is the wrapped handler, the configuration file might look like this:
21//!
22//! ```yaml
23//! vhosts:
24//! localhost:8000:
25//! aliases:
26//! - 127.0.0.1:8000
27//! - "[::1]:8000"
28//! root: ./local-debug-root
29//! example.com:
30//! aliases:
31//! - www.example.com
32//! default: true
33//! root: ./production-root
34//! subdirs:
35//! /metrics
36//! root: ./metrics
37//! /test:
38//! strip_prefix: true
39//! root: ./local-debug-root
40//! redirect_prefix: /test
41//! ```
42//!
43//! A virtual host configuration adds three configuration settings to the configuration of the
44//! wrapped handler:
45//!
46//! * `aliases` lists additional host names that should share the same configuration.
47//! * `default` can be set to `true` to indicate that this configuration should apply to all host
48//! names not listed explicitly.
49//! * `subdirs` maps subdirectories to their respective configuration. The configuration is that of
50//! the wrapped handler with the added `strip_prefix` setting. If `true`, this setting will
51//! remove the subdirectory path from the URI before the request is passed on to the handler.
52//!
53//! If no default host entry is present and a request is made for an unknown host name, this
54//! handler will leave the request unhandled. Otherwise the handling is delegated to the wrapped
55//! handler.
56//!
57//! When selecting a subdirectory configuration, longer matching paths are preferred. Matching
58//! always happens against full file names, meaning that URI `/test/abc` matches the subdirectory
59//! `/test` whereas the URI `/test_abc` doesn’t. If no matching path is found, the host
60//! configuration will be used.
61//!
62//! *Note*: When the `strip_prefix` option is used, the subsequent handlers will receive a URI
63//! which doesn’t match the actual URI of the request. This might result in wrong links or
64//! redirects. When using Static Files Module you can set `redirect_prefix` setting like in the
65//! example above to compensate. Upstream responses might have to be corrected via Pingora’s
66//! `upstream_response_filter`.
67
68//! ## Code example
69//!
70//! Usually, the virtual hosts configuration will be read from a configuration file and used to
71//! instantiate the corresponding handler. This is how it would be done:
72//!
73//! ```rust
74//! use pingora_core::server::configuration::{Opt, ServerConf};
75//! use module_utils::{FromYaml, merge_conf};
76//! use static_files_module::{StaticFilesConf, StaticFilesHandler};
77//! use structopt::StructOpt;
78//! use virtual_hosts_module::{VirtualHostsConf, VirtualHostsHandler};
79//!
80//! // Combine Pingora server configuration with virtual hosts wrapping static files configuration.
81//! #[merge_conf]
82//! struct Conf {
83//! server: ServerConf,
84//! virtual_hosts: VirtualHostsConf<StaticFilesConf>,
85//! }
86//!
87//! // Read command line options and configuration file.
88//! let opt = Opt::from_args();
89//! let conf = opt
90//! .conf
91//! .as_ref()
92//! .and_then(|path| Conf::load_from_yaml(path).ok())
93//! .unwrap_or_else(Conf::default);
94//!
95//! // Create handler from configuration
96//! let handler: VirtualHostsHandler<StaticFilesHandler> = conf.virtual_hosts.try_into().unwrap();
97//! ```
98//!
99//! You can then use that handler in your server implementation:
100//!
101//! ```rust
102//! use async_trait::async_trait;
103//! use pingora_core::upstreams::peer::HttpPeer;
104//! use pingora_core::Error;
105//! use pingora_proxy::{ProxyHttp, Session};
106//! use module_utils::RequestFilter;
107//! use static_files_module::StaticFilesHandler;
108//! use virtual_hosts_module::VirtualHostsHandler;
109//!
110//! pub struct MyServer {
111//! handler: VirtualHostsHandler<StaticFilesHandler>,
112//! }
113//!
114//! #[async_trait]
115//! impl ProxyHttp for MyServer {
116//! type CTX = <VirtualHostsHandler<StaticFilesHandler> as RequestFilter>::CTX;
117//! fn new_ctx(&self) -> Self::CTX {
118//! VirtualHostsHandler::<StaticFilesHandler>::new_ctx()
119//! }
120//!
121//! async fn request_filter(
122//! &self,
123//! session: &mut Session,
124//! ctx: &mut Self::CTX,
125//! ) -> Result<bool, Box<Error>> {
126//! self.handler.handle(session, ctx).await
127//! }
128//!
129//! async fn upstream_peer(
130//! &self,
131//! _session: &mut Session,
132//! _ctx: &mut Self::CTX,
133//! ) -> Result<Box<HttpPeer>, Box<Error>> {
134//! // Virtual hosts handler didn't handle the request, meaning no matching virtual host in
135//! // configuration. Delegate to upstream peer.
136//! Ok(Box::new(HttpPeer::new(
137//! "example.com:443",
138//! true,
139//! "example.com".to_owned(),
140//! )))
141//! }
142//! }
143//! ```
144//!
145//! For complete and more comprehensive code see `virtual-hosts` example in the repository.
146
147mod configuration;
148mod handler;
149
150pub use configuration::{
151 SubDirCombined, SubDirConf, VirtualHostCombined, VirtualHostConf, VirtualHostsConf,
152};
153pub use handler::VirtualHostsHandler;