Skip to main content

pingap_core/
plugin.rs

1// Copyright 2024-2025 Tree xie.
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
15use super::{Ctx, HttpResponse};
16use ahash::AHashMap;
17use async_trait::async_trait;
18use pingora::http::ResponseHeader;
19use pingora::proxy::Session;
20use std::borrow::Cow;
21use std::sync::Arc;
22use strum::EnumString;
23
24#[derive(
25    PartialEq, Debug, Default, Clone, Copy, EnumString, strum::Display,
26)]
27#[strum(serialize_all = "snake_case")]
28pub enum PluginStep {
29    EarlyRequest,
30    #[default]
31    Request,
32    ProxyUpstream,
33    UpstreamResponse,
34    Response,
35}
36
37/// A more expressive return type for `handle_request`.
38/// It clearly states the plugin's decision.
39#[derive(Debug)]
40pub enum RequestPluginResult {
41    /// The plugin did not run or took no action.
42    Skipped,
43    /// The plugin ran and modified the request; processing should continue.
44    Continue,
45    /// The plugin has decided to terminate the request and send an immediate response.
46    Respond(HttpResponse),
47}
48
49/// Represents the action a plugin takes on a response.
50#[derive(Debug, PartialEq, Eq)]
51pub enum ResponsePluginResult {
52    /// The plugin did not change the response.
53    Unchanged,
54    /// The plugin modified the response (e.g., headers or body).
55    Modified,
56    // TODO
57    // FullyReplaced(HttpResponse),
58}
59
60// Represents the action a plugin task on a response
61#[derive(Debug, PartialEq, Eq)]
62pub enum ResponseBodyPluginResult {
63    /// The plugin did not modify the response body.
64    Unchanged,
65    /// The plugin partially replaced the response body.
66    PartialReplaced,
67    /// The plugin fully replaced the response body.
68    FullyReplaced,
69}
70
71// Manually implement the PartialEq trait for RequestPluginResult
72impl PartialEq for RequestPluginResult {
73    fn eq(&self, other: &Self) -> bool {
74        match (self, other) {
75            // Two Skipped variants are always equal.
76            (RequestPluginResult::Skipped, RequestPluginResult::Skipped) => {
77                true
78            },
79
80            // Two Continue variants are always equal.
81            (RequestPluginResult::Continue, RequestPluginResult::Continue) => {
82                true
83            },
84
85            // Any other combination is not equal.
86            _ => false,
87        }
88    }
89}
90
91/// Core trait that defines the interface all plugins must implement.
92///
93/// Plugins can handle both requests and responses at different processing steps.
94/// The default implementations do nothing and return Ok.
95#[async_trait]
96pub trait Plugin: Sync + Send {
97    /// Returns a unique key that identifies this specific plugin instance.
98    ///
99    /// # Purpose
100    /// - Can be used for caching plugin results
101    /// - Helps differentiate between multiple instances of the same plugin type
102    /// - Useful for tracking and debugging
103    ///
104    /// # Default
105    /// Returns an empty string by default, which means no specific instance identification.
106    fn config_key(&self) -> Cow<'_, str> {
107        Cow::Borrowed("")
108    }
109
110    /// Processes an HTTP request at a specified lifecycle step.
111    ///
112    /// # Parameters
113    /// * `_step` - Current processing step in the request lifecycle (e.g., pre-routing, post-routing)
114    /// * `_session` - Mutable reference to the HTTP session containing request data
115    /// * `_ctx` - Mutable reference to the request context for storing state
116    ///
117    /// # Returns
118    /// * `Ok(result)` where:
119    ///   * `result` - The result of the plugin's action on the request
120    ///     - `Skipped`: Plugin did not run or took no action
121    ///     - `Continue`: Plugin ran and modified the request; processing should continue
122    ///     - `Respond(response)`: Plugin has decided to terminate the request and send an immediate response
123    ///   * `response` - Optional HTTP response:
124    ///     - `Some(response)`: Terminates request processing and returns this response to client
125    ///     - `None`: Allows request to continue to next plugin or upstream
126    /// * `Err` - Returns error if plugin processing failed
127    #[inline]
128    async fn handle_request(
129        &self,
130        _step: PluginStep,
131        _session: &mut Session,
132        _ctx: &mut Ctx,
133    ) -> pingora::Result<RequestPluginResult> {
134        Ok(RequestPluginResult::Skipped)
135    }
136
137    /// Processes an HTTP response at a specified lifecycle step.
138    ///
139    /// # Parameters
140    /// * `_session` - Mutable reference to the HTTP session
141    /// * `_ctx` - Mutable reference to the request context
142    /// * `_upstream_response` - Mutable reference to the upstream response header
143    ///
144    /// # Returns
145    /// * `Ok(result)` - The result of the plugin's action on the response
146    ///   - `Unchanged`: Plugin did not modify the response
147    ///   - `Modified`: Plugin modified the response in some way
148    /// * `Err` - Returns error if plugin processing failed
149    #[inline]
150    async fn handle_response(
151        &self,
152        _session: &mut Session,
153        _ctx: &mut Ctx,
154        _upstream_response: &mut ResponseHeader,
155    ) -> pingora::Result<ResponsePluginResult> {
156        Ok(ResponsePluginResult::Unchanged)
157    }
158
159    /// Processes an HTTP response body at a specified lifecycle step.
160    ///
161    /// # Parameters
162    /// * `_session` - Mutable reference to the HTTP session
163    /// * `_ctx` - Mutable reference to the request context
164    /// * `_body` - Mutable reference to the response body
165    /// * `_end_of_stream` - Boolean flag:
166    ///   - `true`: The end of the response body has been reached
167    ///   - `false`: The response body is still being received
168    ///
169    /// # Returns
170    /// * `Ok(result)` - The result of the plugin's action on the response body
171    ///   - `Unchanged`: Plugin did not modify the response body
172    ///   - `PartialReplaced(new_body)`: Plugin replaced a part of the response body
173    ///   - `FullyReplaced(new_body)`: Plugin replaced the response body with a new one
174    /// * `Err` - Returns error if plugin processing failed
175    #[inline]
176    fn handle_response_body(
177        &self,
178        _session: &mut Session,
179        _ctx: &mut Ctx,
180        _body: &mut Option<bytes::Bytes>,
181        _end_of_stream: bool,
182    ) -> pingora::Result<ResponseBodyPluginResult> {
183        Ok(ResponseBodyPluginResult::Unchanged)
184    }
185
186    /// Processes an upstream response at a specified lifecycle step.
187    ///
188    /// # Parameters
189    /// * `_session` - Mutable reference to the HTTP session
190    /// * `_ctx` - Mutable reference to the request context
191    /// * `_upstream_response` - Mutable reference to the upstream response header
192    ///
193    /// # Returns
194    /// * `Ok(result)` - The result of the plugin's action on the response
195    ///   - `Unchanged`: Plugin did not modify the response
196    ///   - `Modified`: Plugin modified the response in some way
197    /// * `Err` - Returns error if plugin processing failed
198    #[inline]
199    fn handle_upstream_response(
200        &self,
201        _session: &mut Session,
202        _ctx: &mut Ctx,
203        _upstream_response: &mut ResponseHeader,
204    ) -> pingora::Result<ResponsePluginResult> {
205        Ok(ResponsePluginResult::Unchanged)
206    }
207
208    /// Processes an upstream response body at a specified lifecycle step.
209    ///
210    /// # Parameters
211    /// * `_session` - Mutable reference to the HTTP session
212    /// * `_ctx` - Mutable reference to the request context
213    /// * `_body` - Mutable reference to the upstream response body
214    /// * `_end_of_stream` - Boolean flag:
215    ///   - `true`: The end of the upstream response body has been reached
216    ///   - `false`: The upstream response body is still being received
217    ///
218    /// # Returns
219    /// * `Ok(result)` - The result of the plugin's action on the response body
220    ///   - `Unchanged`: Plugin did not modify the response body
221    ///   - `PartialReplaced(new_body)`: Plugin replaced a part of the response body
222    ///   - `FullyReplaced(new_body)`: Plugin replaced the response body with a new one
223    /// * `Err` - Returns error if plugin processing failed
224    #[inline]
225    fn handle_upstream_response_body(
226        &self,
227        _session: &mut Session,
228        _ctx: &mut Ctx,
229        _body: &mut Option<bytes::Bytes>,
230        _end_of_stream: bool,
231    ) -> pingora::Result<ResponseBodyPluginResult> {
232        Ok(ResponseBodyPluginResult::Unchanged)
233    }
234}
235
236/// Plugin provider trait
237pub trait PluginProvider: Send + Sync {
238    /// Get a plugin by name
239    ///
240    /// # Arguments
241    /// * `name` - The name of the plugin to get
242    ///
243    /// # Returns
244    /// * `Option<Arc<dyn Plugin>>` - The plugin if found, None otherwise
245    fn get(&self, name: &str) -> Option<Arc<dyn Plugin>>;
246
247    /// A number that changes whenever `get` could start returning different
248    /// plugins - after a plugin reload. Locations cache the plugins they
249    /// resolved through `get` and rebuild the list when this moves, so a
250    /// provider whose plugins can change must implement it.
251    fn version(&self) -> u64 {
252        0
253    }
254}
255
256pub type Plugins = AHashMap<String, Arc<dyn Plugin>>;
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use pretty_assertions::assert_eq;
262
263    #[test]
264    fn test_plugin_step() {
265        let step = "early_request".parse::<PluginStep>().unwrap();
266        assert_eq!(step, PluginStep::EarlyRequest);
267        assert_eq!(step.to_string(), "early_request");
268
269        let step = "request".parse::<PluginStep>().unwrap();
270        assert_eq!(step, PluginStep::Request);
271        assert_eq!(step.to_string(), "request");
272
273        let step = "proxy_upstream".parse::<PluginStep>().unwrap();
274        assert_eq!(step, PluginStep::ProxyUpstream);
275        assert_eq!(step.to_string(), "proxy_upstream");
276
277        let step = "response".parse::<PluginStep>().unwrap();
278        assert_eq!(step, PluginStep::Response);
279        assert_eq!(step.to_string(), "response");
280    }
281
282    #[test]
283    fn test_request_plugin_result() {
284        let skip1 = RequestPluginResult::Skipped;
285        let skip2 = RequestPluginResult::Skipped;
286        assert_eq!(true, skip1 == skip2);
287
288        let continue1 = RequestPluginResult::Continue;
289        let continue2 = RequestPluginResult::Continue;
290        assert_eq!(true, continue1 == continue2);
291
292        let respond1 = RequestPluginResult::Respond(HttpResponse::no_content());
293        let respond2 = RequestPluginResult::Respond(HttpResponse::no_content());
294        assert_eq!(false, respond1 == respond2);
295    }
296}