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