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