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}