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}