Skip to main content

featherbit/plugins/
mod.rs

1//! Two-tier plugin system: native Rust plugins ([`native`]) and scripted
2//! plugins ([`script`]). Defines the [`Plugin`] trait — the contract every
3//! graph node implements — and [`create_plugin`], the single factory that
4//! maps node-type strings from YAML config to plugin instances.
5
6pub mod native;
7pub mod resources;
8pub mod script;
9pub mod util;
10
11use async_trait::async_trait;
12use std::collections::HashMap;
13use std::sync::Arc;
14
15use crate::context::{Context, GatewayError};
16use resources::PluginResources;
17
18/// The result of a successful plugin execution.
19#[derive(Debug)]
20pub struct PluginOutput {
21    /// The (possibly mutated) context, passed on to the next node in the graph.
22    pub context: Context,
23    /// Values published under names that downstream nodes can consume as
24    /// `named_inputs`; most plugins leave this empty.
25    // Part of the plugin contract: every plugin populates it, but the engine
26    // does not yet wire named inputs between nodes.
27    #[allow(dead_code)]
28    pub named_outputs: HashMap<String, serde_json::Value>,
29}
30
31/// The result of a plugin execution: either success or an error with the context preserved.
32pub type PluginResult = Result<PluginOutput, PluginExecutionError>;
33
34/// An error that occurs during plugin execution. The context is preserved so the
35/// graph engine can route it through the error port.
36#[derive(Debug)]
37pub struct PluginExecutionError {
38    /// The context as it stood when the error occurred; execution continues
39    /// with it along the node's error edge.
40    pub context: Context,
41    /// The error details, appended to `Context.errors` by the graph engine.
42    pub error: GatewayError,
43}
44
45/// Every plugin (native or scripted) implements this trait.
46///
47/// A plugin is a node in a compiled policy graph. The engine drives each node
48/// through [`execute`](Plugin::execute) and follows the node's `success` or
49/// `error` port depending on the result.
50#[async_trait]
51pub trait Plugin: Send + Sync {
52    /// Unique identifier for the plugin type (e.g., "proxy-rewrite", "upstream").
53    fn plugin_type(&self) -> &str;
54
55    /// Executes the plugin logic against the request/response context.
56    ///
57    /// Contract:
58    /// - `ctx` is taken **by value**: the plugin owns the [`Context`] for the
59    ///   duration of the call and must hand it back in either outcome — inside
60    ///   [`PluginOutput`] on success, or inside [`PluginExecutionError`] on
61    ///   failure. The context is never lost.
62    /// - `named_inputs` carries values that upstream nodes published as
63    ///   `named_outputs`, keyed by name; most plugins ignore it.
64    /// - On `Ok`, the graph engine routes the returned context through the
65    ///   node's `success` port. On `Err`, the [`PluginExecutionError`] carries
66    ///   both the context and a [`GatewayError`], letting the engine record
67    ///   the error and continue through the node's `error` port (typically
68    ///   toward an `error-handler` node) instead of aborting the request.
69    async fn execute(
70        &self,
71        ctx: Context,
72        named_inputs: &HashMap<String, serde_json::Value>,
73    ) -> PluginResult;
74}
75
76/// Creates a plugin instance from a node type string and its YAML-derived config.
77///
78/// This is the single factory for all node types: the graph compiler calls it
79/// for every node in a policy, so adding a new node type means adding exactly
80/// one match arm here. `resources` hands plugins process-wide services
81/// ([`PluginResources`]); constructors that need them take the handle in
82/// `from_config`. Returns an error string for unknown node types or when
83/// a plugin's `from_config` rejects its configuration (surfaced at config load,
84/// not at request time).
85pub fn create_plugin(
86    node_type: &str,
87    config: &HashMap<String, serde_json::Value>,
88    resources: &Arc<PluginResources>,
89) -> Result<Box<dyn Plugin>, String> {
90    match node_type {
91        "proxy-rewrite" => Ok(Box::new(
92            native::proxy_rewrite::ProxyRewritePlugin::from_config(config)?,
93        )),
94        "upstream" => Ok(Box::new(native::upstream::UpstreamPlugin::from_config(
95            config, resources,
96        )?)),
97        "aws-lambda" => Ok(Box::new(native::aws_lambda::AwsLambdaPlugin::from_config(
98            config, resources,
99        )?)),
100        "azure-functions" => Ok(Box::new(
101            native::azure_functions::AzureFunctionsPlugin::from_config(config, resources)?,
102        )),
103        "openwhisk" => Ok(Box::new(native::openwhisk::OpenWhiskPlugin::from_config(
104            config, resources,
105        )?)),
106        "openfunction" => Ok(Box::new(
107            native::openfunction::OpenFunctionPlugin::from_config(config, resources)?,
108        )),
109        "error-handler" => Ok(Box::new(
110            native::error_handler::ErrorHandlerPlugin::from_config(config)?,
111        )),
112        "listener" => Ok(Box::new(native::listener::ListenerPlugin)),
113        "client" => Ok(Box::new(native::client::ClientPlugin)),
114        "cors" => Ok(Box::new(native::cors::CorsPlugin::from_config(config)?)),
115        "rate-limit" => Ok(Box::new(native::rate_limit::RateLimitPlugin::from_config(
116            config,
117        )?)),
118        "limit-conn" => Ok(Box::new(native::limit_conn::LimitConnPlugin::from_config(
119            config, resources,
120        )?)),
121        "api-breaker" => Ok(Box::new(
122            native::api_breaker::ApiBreakerPlugin::from_config(config, resources)?,
123        )),
124        "proxy-cache" => Ok(Box::new(
125            native::proxy_cache::ProxyCachePlugin::from_config(config, resources)?,
126        )),
127        "limit-count" => Ok(Box::new(
128            native::limit_count::LimitCountPlugin::from_config(config, resources)?,
129        )),
130        "proxy-mirror" => Ok(Box::new(
131            native::proxy_mirror::ProxyMirrorPlugin::from_config(config, resources)?,
132        )),
133        "ip-restriction" => Ok(Box::new(
134            native::ip_restriction::IpRestrictionPlugin::from_config(config)?,
135        )),
136        "consumer-restriction" => Ok(Box::new(
137            native::consumer_restriction::ConsumerRestrictionPlugin::from_config(config)?,
138        )),
139        "acl" => Ok(Box::new(native::acl::AclPlugin::from_config(config)?)),
140        "attach-consumer-label" => Ok(Box::new(
141            native::attach_consumer_label::AttachConsumerLabelPlugin::from_config(config)?,
142        )),
143        "ua-restriction" => Ok(Box::new(
144            native::ua_restriction::UaRestrictionPlugin::from_config(config)?,
145        )),
146        "referer-restriction" => Ok(Box::new(
147            native::referer_restriction::RefererRestrictionPlugin::from_config(config)?,
148        )),
149        "uri-blocker" => Ok(Box::new(
150            native::uri_blocker::UriBlockerPlugin::from_config(config)?,
151        )),
152        "csrf" => Ok(Box::new(native::csrf::CsrfPlugin::from_config(config)?)),
153        "request-size-limit" => Ok(Box::new(
154            native::request_size_limit::RequestSizeLimitPlugin::from_config(config)?,
155        )),
156        "key-auth" => Ok(Box::new(native::key_auth::KeyAuthPlugin::from_config(
157            config, resources,
158        )?)),
159        "basic-auth" => Ok(Box::new(native::basic_auth::BasicAuthPlugin::from_config(
160            config, resources,
161        )?)),
162        "jwt-auth" => Ok(Box::new(native::jwt_auth::JwtAuthPlugin::from_config(
163            config, resources,
164        )?)),
165        "hmac-auth" => Ok(Box::new(native::hmac_auth::HmacAuthPlugin::from_config(
166            config, resources,
167        )?)),
168        "jwe-decrypt" => Ok(Box::new(
169            native::jwe_decrypt::JweDecryptPlugin::from_config(config, resources)?,
170        )),
171        "multi-auth" => Ok(Box::new(native::multi_auth::MultiAuthPlugin::from_config(
172            config, resources,
173        )?)),
174        "forward-auth" => Ok(Box::new(
175            native::forward_auth::ForwardAuthPlugin::from_config(config, resources)?,
176        )),
177        "opa" => Ok(Box::new(native::opa::OpaPlugin::from_config(
178            config, resources,
179        )?)),
180        "opentelemetry" => Ok(Box::new(
181            native::opentelemetry::OpenTelemetryPlugin::from_config(config, resources)?,
182        )),
183        "zipkin" => Ok(Box::new(native::zipkin::ZipkinPlugin::from_config(
184            config, resources,
185        )?)),
186        "skywalking" => Ok(Box::new(native::skywalking::SkywalkingPlugin::from_config(
187            config, resources,
188        )?)),
189        "prometheus" => Ok(Box::new(native::prometheus::PrometheusPlugin::from_config(
190            config, resources,
191        )?)),
192        "ldap-auth" => Ok(Box::new(native::ldap_auth::LdapAuthPlugin::from_config(
193            config, resources,
194        )?)),
195        "wolf-rbac" => Ok(Box::new(native::wolf_rbac::WolfRbacPlugin::from_config(
196            config, resources,
197        )?)),
198        "cas-auth" => Ok(Box::new(native::cas_auth::CasAuthPlugin::from_config(
199            config, resources,
200        )?)),
201        "authz-casbin" => Ok(Box::new(
202            native::authz_casbin::AuthzCasbinPlugin::from_config(config, resources)?,
203        )),
204        "authz-keycloak" => Ok(Box::new(
205            native::authz_keycloak::AuthzKeycloakPlugin::from_config(config, resources)?,
206        )),
207        "authz-casdoor" => Ok(Box::new(
208            native::authz_casdoor::AuthzCasdoorPlugin::from_config(config, resources)?,
209        )),
210        "openid-connect" => Ok(Box::new(
211            native::openid_connect::OpenidConnectPlugin::from_config(config, resources)?,
212        )),
213        "dingtalk-auth" => Ok(Box::new(
214            native::dingtalk_auth::DingtalkAuthPlugin::from_config(config, resources)?,
215        )),
216        "feishu-auth" => Ok(Box::new(
217            native::feishu_auth::FeishuAuthPlugin::from_config(config, resources)?,
218        )),
219        "logging" => Ok(Box::new(native::logging::LoggingPlugin::from_config(
220            config,
221        )?)),
222        "http-logger" => Ok(Box::new(
223            native::http_logger::HttpLoggerPlugin::from_config(config, resources)?,
224        )),
225        "loki-logger" => Ok(Box::new(
226            native::loki_logger::LokiLoggerPlugin::from_config(config, resources)?,
227        )),
228        "splunk-hec-logging" => Ok(Box::new(
229            native::splunk_hec_logging::SplunkHecLoggingPlugin::from_config(config, resources)?,
230        )),
231        "datadog" => Ok(Box::new(native::datadog::DatadogPlugin::from_config(
232            config, resources,
233        )?)),
234        "loggly" => Ok(Box::new(native::loggly::LogglyPlugin::from_config(
235            config, resources,
236        )?)),
237        "tcp-logger" => Ok(Box::new(native::tcp_logger::TcpLoggerPlugin::from_config(
238            config,
239        )?)),
240        "udp-logger" => Ok(Box::new(native::udp_logger::UdpLoggerPlugin::from_config(
241            config,
242        )?)),
243        "syslog" => Ok(Box::new(native::syslog::SyslogPlugin::from_config(config)?)),
244        "file-logger" => Ok(Box::new(
245            native::file_logger::FileLoggerPlugin::from_config(config)?,
246        )),
247        "error-log-logger" => Ok(Box::new(
248            native::error_log_logger::ErrorLogLoggerPlugin::from_config(config)?,
249        )),
250        "google-cloud-logging" => Ok(Box::new(
251            native::google_cloud_logging::GoogleCloudLoggingPlugin::from_config(config, resources)?,
252        )),
253        "skywalking-logger" => Ok(Box::new(
254            native::skywalking_logger::SkywalkingLoggerPlugin::from_config(config, resources)?,
255        )),
256        "elasticsearch-logger" => Ok(Box::new(
257            native::elasticsearch_logger::ElasticsearchLoggerPlugin::from_config(
258                config, resources,
259            )?,
260        )),
261        "clickhouse-logger" => Ok(Box::new(
262            native::clickhouse_logger::ClickhouseLoggerPlugin::from_config(config, resources)?,
263        )),
264        "sls-logger" => Ok(Box::new(native::sls_logger::SlsLoggerPlugin::from_config(
265            config, resources,
266        )?)),
267        "tencent-cloud-cls" => Ok(Box::new(
268            native::tencent_cloud_cls::TencentCloudClsPlugin::from_config(config, resources)?,
269        )),
270        "lago" => Ok(Box::new(native::lago::LagoPlugin::from_config(
271            config, resources,
272        )?)),
273        "request-id" => Ok(Box::new(native::request_id::RequestIdPlugin::from_config(
274            config,
275        )?)),
276        "real-ip" => Ok(Box::new(native::real_ip::RealIpPlugin::from_config(
277            config,
278        )?)),
279        "redirect" => Ok(Box::new(native::redirect::RedirectPlugin::from_config(
280            config,
281        )?)),
282        "echo" => Ok(Box::new(native::echo::EchoPlugin::from_config(config)?)),
283        "fault-injection" => Ok(Box::new(
284            native::fault_injection::FaultInjectionPlugin::from_config(config)?,
285        )),
286        "workflow" => Ok(Box::new(native::workflow::WorkflowPlugin::from_config(
287            config, resources,
288        )?)),
289        "traffic-label" => Ok(Box::new(
290            native::traffic_label::TrafficLabelPlugin::from_config(config)?,
291        )),
292        "traffic-split" => Ok(Box::new(
293            native::traffic_split::TrafficSplitPlugin::from_config(config, resources)?,
294        )),
295        "mocking" => Ok(Box::new(native::mocking::MockingPlugin::from_config(
296            config,
297        )?)),
298        "response-rewrite" => Ok(Box::new(
299            native::response_rewrite::ResponseRewritePlugin::from_config(config)?,
300        )),
301        "gzip" => Ok(Box::new(native::gzip::GzipPlugin::from_config(config)?)),
302        "brotli" => Ok(Box::new(native::brotli::BrotliPlugin::from_config(config)?)),
303        "error-page" => Ok(Box::new(native::error_page::ErrorPagePlugin::from_config(
304            config,
305        )?)),
306        "exit-transformer" => Ok(Box::new(
307            native::exit_transformer::ExitTransformerPlugin::from_config(config)?,
308        )),
309        "data-mask" => Ok(Box::new(native::data_mask::DataMaskPlugin::from_config(
310            config,
311        )?)),
312        "request-validation" => Ok(Box::new(
313            native::request_validation::RequestValidationPlugin::from_config(config)?,
314        )),
315        "body-transformer" => Ok(Box::new(
316            native::body_transformer::BodyTransformerPlugin::from_config(config)?,
317        )),
318        "degraphql" => Ok(Box::new(native::degraphql::DegraphqlPlugin::from_config(
319            config,
320        )?)),
321        "oas-validator" => Ok(Box::new(
322            native::oas_validator::OasValidatorPlugin::from_config(config)?,
323        )),
324        "serverless-pre-function" => Ok(Box::new(
325            native::serverless_pre_function::ServerlessPreFunctionPlugin::from_config(config)?,
326        )),
327        "serverless-post-function" => Ok(Box::new(
328            native::serverless_post_function::ServerlessPostFunctionPlugin::from_config(config)?,
329        )),
330        "script" => Ok(Box::new(script::ScriptPlugin::from_config(config)?)),
331        _ => Err(format!("Unknown plugin type: {}", node_type)),
332    }
333}