module_utils/
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//! # Module helpers
16//!
17//! This crate contains some helpers that are useful when using `static-files-module` or
18//! `virtual-hosts-module` crates for example.
19
20use async_trait::async_trait;
21use log::trace;
22use pingora_core::{Error, ErrorType};
23use pingora_proxy::Session;
24use serde::de::DeserializeOwned;
25use std::fmt::Debug;
26use std::fs::File;
27use std::io::BufReader;
28use std::path::Path;
29
30pub use module_utils_macros::{merge_conf, merge_opt, RequestFilter};
31
32/// Request filter result indicating how the current request should be processed further
33#[derive(Debug, Copy, Clone, PartialEq, Default)]
34pub enum RequestFilterResult {
35    /// Response has been sent, no further processing should happen. Other Pingora phases should
36    /// not be triggered.
37    ResponseSent,
38
39    /// Request has been handled and further request filters should not run. Response hasn’t been
40    /// sent however, next Pingora phase should deal with that.
41    Handled,
42
43    /// Request filter could not handle this request, next request filter should run if it exists.
44    #[default]
45    Unhandled,
46}
47
48/// Trait to be implemented by request filters.
49#[async_trait]
50pub trait RequestFilter {
51    /// Configuration type of this handler.
52    type Conf;
53
54    /// Creates a new instance of the handler from its configuration.
55    fn new(conf: Self::Conf) -> Result<Self, Box<Error>>
56    where
57        Self: Sized,
58        Self::Conf: TryInto<Self, Error = Box<Error>>,
59    {
60        conf.try_into()
61    }
62
63    /// Handles the current request.
64    ///
65    /// This is essentially identical to the `request_filter` method but is supposed to be called
66    /// when there is only a single handler. Consequently, its result can be returned directly.
67    async fn handle(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result<bool, Box<Error>>
68    where
69        Self::CTX: Send,
70    {
71        let result = self.request_filter(session, ctx).await?;
72        Ok(result == RequestFilterResult::ResponseSent)
73    }
74
75    /// Per-request state of this handler, see [`pingora_proxy::ProxyHttp::CTX`]
76    type CTX;
77
78    /// Creates a new sate object, see [`pingora_proxy::ProxyHttp::new_ctx`]
79    fn new_ctx() -> Self::CTX;
80
81    /// Handler to run during Pingora’s `request_filter` state, see
82    /// [`pingora_proxy::ProxyHttp::request_filter`]. This uses a different return type to account
83    /// for the existence of multiple request filters.
84    async fn request_filter(
85        &self,
86        session: &mut Session,
87        ctx: &mut Self::CTX,
88    ) -> Result<RequestFilterResult, Box<Error>>;
89}
90
91/// Trait for configuration structures that can be loaded from YAML files. This trait has a blanket
92/// implementation for any structure implementing [`serde::Deserialize`].
93pub trait FromYaml {
94    /// Loads configuration from a YAML file.
95    fn load_from_yaml<P>(path: P) -> Result<Self, Box<Error>>
96    where
97        P: AsRef<Path>,
98        Self: Sized;
99}
100
101impl<D> FromYaml for D
102where
103    D: DeserializeOwned + Debug + ?Sized,
104{
105    fn load_from_yaml<P: AsRef<Path>>(path: P) -> Result<Self, Box<Error>> {
106        let file = File::open(path.as_ref()).map_err(|err| {
107            Error::because(
108                ErrorType::FileOpenError,
109                "failed opening configuration file",
110                err,
111            )
112        })?;
113        let reader = BufReader::new(file);
114
115        let conf = serde_yaml::from_reader(reader).map_err(|err| {
116            Error::because(
117                ErrorType::FileReadError,
118                "failed reading configuration file",
119                err,
120            )
121        })?;
122        trace!("Loaded configuration file: {conf:#?}");
123
124        Ok(conf)
125    }
126}