kvarn_extensions/
lib.rs

1//! # Kvarn extensions
2//! A *supporter-lib* for Kvarn to supply extensions to the web server.
3//!
4//! Use [`new()`] to get started quickly.
5//!
6//! ## An introduction to the *Kvarn extension system*
7//! On of the many things Kvarn extensions can to is bind to *extension declarations* and to *file extensions*.
8//! For example, if you mount the extensions [`download`], it binds the *extension declaration* `download`.
9//! If you then, in a file inside your `public/` directory, add `!> download` to the top, the client visiting the url pointing to the file will download it.
10
11#![cfg_attr(docsrs, feature(doc_auto_cfg))]
12#![deny(clippy::all)]
13
14use kvarn::{extensions::*, prelude::*};
15
16#[cfg(feature = "reverse-proxy")]
17#[path = "reverse-proxy.rs"]
18pub mod reverse_proxy;
19#[cfg(feature = "connection")]
20pub use connection::Connection;
21#[cfg(feature = "reverse-proxy")]
22pub use reverse_proxy::{localhost, static_connection, Manager as ReverseProxy};
23
24#[cfg(feature = "push")]
25pub mod push;
26#[cfg(feature = "push")]
27pub use push::{mount as mount_push, SmartPush};
28
29#[cfg(feature = "kvarn-fastcgi-client")]
30pub mod fastcgi;
31
32#[cfg(feature = "php")]
33pub mod php;
34#[cfg(feature = "php")]
35pub use php::mount_php as php;
36
37#[cfg(feature = "templates")]
38pub mod templates;
39#[cfg(feature = "templates")]
40pub use templates::templates as templates_ext;
41
42#[cfg(feature = "connection")]
43pub mod connection;
44
45#[cfg(feature = "certificate")]
46pub mod certificate;
47
48#[cfg(feature = "view-counter")]
49#[path = "view-counter.rs"]
50pub mod view_counter;
51
52/// Creates a new `Extensions` and adds all enabled `kvarn_extensions`.
53///
54/// See [`mount_all()`] for more information.
55pub fn new() -> Extensions {
56    let mut e = Extensions::new();
57    mount_all(&mut e);
58    e
59}
60
61/// Mounts all extensions specified in Cargo.toml dependency declaration.
62/// The extensions listed below will always get included in your server after calling this function.
63///
64/// The current defaults are:
65/// - [`download()`] (present name `download`)
66/// - [`cache()`] (present name `cache`)
67/// - [`hide()`] (present name `hide` & `private`)
68/// - [`ip_allow()`] (present name `allow-ips`)
69/// - [`templates_ext()`] if the feature `templates` is enabled (present name `tmpl`)
70/// - [`push::mount()`] if the feature `push` is enabled
71///
72/// > To add PHP, use [`php()`].
73///
74/// The push extension uses the [default](SmartPush::default) settings.
75///
76/// # Examples
77///
78/// ```no_run
79/// use kvarn::prelude::*;
80/// # #[tokio::main(flavor = "current_thread")]
81/// # async fn main() {
82/// let mut extensions = Extensions::new();
83/// kvarn_extensions::mount_all(&mut extensions);
84///
85/// let host = Host::unsecure("localhost", "web", Extensions::default(), host::Options::default());
86/// let data = HostCollection::builder().insert(host).build();
87/// let port_descriptor = PortDescriptor::new(8080, data);
88///
89/// let shutdown_manager = run_config![port_descriptor].execute().await;
90/// shutdown_manager.wait().await;
91/// # }
92pub fn mount_all(extensions: &mut Extensions) {
93    #[cfg(feature = "templates")]
94    let template_cache = templates::Cache::new();
95    extensions.add_present_internal("download", Box::new(download));
96    extensions.add_present_internal("cache", Box::new(cache));
97    extensions.add_present_internal(
98        "hide",
99        hide(
100            #[cfg(feature = "templates")]
101            template_cache.clone(),
102        ),
103    );
104    extensions.add_present_file(
105        "private",
106        hide(
107            #[cfg(feature = "templates")]
108            template_cache.clone(),
109        ),
110    );
111    extensions.add_present_internal("allow-ips", Box::new(ip_allow));
112    #[cfg(feature = "templates")]
113    extensions.add_present_internal("tmpl", templates_ext(template_cache));
114    #[cfg(feature = "push")]
115    push::mount(extensions, SmartPush::default());
116}
117
118// Ok, since it is used, just not by every extension, and #[CFG] would be too fragile for this.
119#[allow(dead_code)]
120pub mod parse {
121    use super::*;
122
123    pub fn format_file_name<P: AsRef<Path>>(path: &P) -> Option<&str> {
124        path.as_ref().file_name().and_then(std::ffi::OsStr::to_str)
125    }
126    pub fn format_file_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf, io::Error> {
127        let mut file_path = std::env::current_dir()?;
128        file_path.push(path);
129        Ok(file_path)
130    }
131}
132
133/// Makes the client download the file.
134pub fn download<'a>(data: &'a mut extensions::PresentData<'a>) -> RetFut<'a, ()> {
135    let headers = data.response.headers_mut();
136    headers.insert(
137        "content-type",
138        HeaderValue::from_static("application/octet-stream"),
139    );
140    ready(())
141}
142
143pub fn cache<'a>(data: &'a mut extensions::PresentData<'a>) -> RetFut<'a, ()> {
144    fn parse<'a, I: Iterator<Item = &'a str>>(
145        iter: I,
146    ) -> (
147        Option<comprash::ClientCachePreference>,
148        Option<comprash::ServerCachePreference>,
149    ) {
150        let mut c = None;
151        let mut s = None;
152        for arg in iter {
153            let mut parts = arg.split(':');
154            let domain = parts.next();
155            let cache = parts.next();
156            if let (Some(domain), Some(cache)) = (domain, cache) {
157                match domain {
158                    "client" => {
159                        if let Ok(preference) = cache.parse() {
160                            c = Some(preference)
161                        } else {
162                            warn!("Parsing of client cache argument failed: {cache}");
163                        }
164                    }
165                    "server" => {
166                        if let Ok(preference) = cache.parse() {
167                            s = Some(preference)
168                        } else {
169                            warn!("Parsing of client cache argument failed: {cache}");
170                        }
171                    }
172                    _ => {
173                        warn!(
174                            "client extension used incorrectly, format: !> cache \
175                            <client OR server>:<<integer>s OR ignore OR full \
176                            OR changing OR none OR <empty>>"
177                        );
178                    }
179                }
180            }
181        }
182        (c, s)
183    }
184    let preference = parse(data.args.iter());
185    if let Some(c) = preference.0 {
186        *data.client_cache_preference = c;
187    }
188    if let Some(s) = preference.1 {
189        *data.server_cache_preference = s;
190    }
191    ready(())
192}
193
194pub fn hide(
195    #[cfg(feature = "templates")] template_cache: Arc<templates::Cache>,
196) -> Box<dyn PresentCall> {
197    async fn inner(
198        data: &mut extensions::PresentData<'_>,
199        #[cfg(feature = "templates")] template_cache: &Arc<templates::Cache>,
200    ) {
201        #[allow(unused_mut)] // cfg
202        let mut error = default_error(StatusCode::NOT_FOUND, Some(data.host), None).await;
203        let arguments = utils::extensions::PresentExtensions::new(error.body().clone());
204        if let Some(arguments) = &arguments {
205            #[allow(unused_variables)] // cfg
206            for argument in arguments.iter_clone() {
207                #[cfg(feature = "templates")]
208                if argument.name() == "tmpl" {
209                    let mut error = error.map(|b| {
210                        let mut c = utils::BytesCow::from(b);
211                        c.replace(0..arguments.data_start(), b"");
212                        c
213                    });
214                    templates::handle_template(
215                        template_cache,
216                        &argument,
217                        error.body_mut(),
218                        data.host,
219                    )
220                    .await;
221                    *data.response = error;
222                    return;
223                }
224            }
225        }
226
227        *data.response = error.map(Into::into);
228    }
229    #[cfg(not(feature = "templates"))]
230    {
231        present!(data, {
232            inner(data).await;
233        })
234    }
235    #[cfg(feature = "templates")]
236    {
237        present!(data, move |template_cache: Arc<templates::Cache>| {
238            inner(data, template_cache).await;
239        })
240    }
241}
242
243pub fn ip_allow<'a>(data: &'a mut extensions::PresentData<'a>) -> RetFut<'a, ()> {
244    box_fut!({
245        let mut matched = false;
246        // Loop over denied ip in args
247        for denied in data.args.iter() {
248            // If parsed
249            if let Ok(ip) = denied.parse::<IpAddr>() {
250                // check it against the requests IP.
251                if data.address.ip() == ip {
252                    matched = true;
253                    // Then break out of loop
254                    break;
255                }
256            }
257        }
258        *data.server_cache_preference = comprash::ServerCachePreference::None;
259        *data.client_cache_preference = comprash::ClientCachePreference::Changing;
260
261        if !matched {
262            // If it does not match, set the response to 404
263            let error = default_error(StatusCode::NOT_FOUND, Some(data.host), None).await;
264            *data.response = error.map(Into::into);
265        }
266    })
267}
268
269/// Forces the responses matching `rules` to be cached according to their respective preference.
270/// Useful when you have compiled away cache, but still want images and fonts to be cached.
271///
272/// Rules can take three shapes.
273/// 1. Matching all file extensions. Here, the rule str have to start with a `.`
274/// 2. Path start with. Matches all responses which start with the rule. str has to start with `/`
275/// 3. Path contains rule. For example, `*target*` matches `/target/bin/kvarn`,
276///    `/a/really/long/path/with/some_target_name/in/it`, but not `/tar/get` or
277///    `/articles/rust_Target`.
278///
279/// The priority for the [`Package`] extension is `16`
280pub type ForceCacheRules = Vec<(String, comprash::ClientCachePreference)>;
281pub fn force_cache(extensions: &mut Extensions, rules: ForceCacheRules) {
282    let rules = Arc::new(rules);
283    let r1 = rules.clone();
284
285    fn resolve(
286        path: &str,
287        extension: Option<&str>,
288        rules: &ForceCacheRules,
289    ) -> Option<comprash::ClientCachePreference> {
290        if let Some(extension) = extension {
291            for (rule, preference) in &**rules {
292                let replace = (rule.starts_with('/') && path.starts_with(rule))
293                    || rule.strip_prefix('.').map_or(false, |ext| ext == extension)
294                    || rule
295                        .strip_prefix('*')
296                        .and_then(|rule| rule.strip_suffix('*'))
297                        .map_or(false, |rule| path.contains(rule));
298                if replace {
299                    return Some(*preference);
300                }
301            }
302        }
303        None
304    }
305
306    extensions.add_present_fn(
307        Box::new(move |req, _host| {
308            let rules = &r1;
309            let extension = req.uri().path().split('.').last();
310            let path = req.uri().path();
311            resolve(path, extension, rules).is_some()
312        }),
313        present!(data, move |rules: Arc<ForceCacheRules>| {
314            let req = data.request;
315            let extension = req.uri().path().split('.').last();
316            let path = req.uri().path();
317            if let Some(preference) = resolve(path, extension, rules) {
318                *data.client_cache_preference = preference;
319            } else {
320                error!("(internal bug) force cache rules inconsistent")
321            }
322        }),
323        extensions::Id::new(16, "force_cache: Adding cache-control header").no_override(),
324    );
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    #[tokio::test]
331    async fn all() {
332        let extensions = new();
333        let _server = kvarn_testing::ServerBuilder::from(extensions).run().await;
334    }
335}