aiway_plugin/
lib.rs

1//! # 插件
2//! ## 基本准则
3//! - 插件是网关实现业务逻辑的核心组件,应提供良好的设计,且不应该频繁变更定义。
4//! - 插件与网关之间通过可序列化的数据通信。
5//! - 插件模块仅提供插件定义、加载、及执行,不应提供插件注册、卸载等管理功能,这些功能交给网关自身实现。
6//!
7//! ## 插件分类
8//! ### 全局插件
9//! 全局插件对整个网关的所有请求生效(不含控制台请求,因为控制台是独立的)。
10//!
11//! 全局插件分两个阶段执行:
12//! - 请求阶段:在请求到达API处理端点前执行,可对请求改写、安全验证、限流、缓存等。
13//! - 响应阶段:在API处理完成,响应客户端前执行,可修改响应、记录日志等。该阶段的插件可通过参数控制当前一个插件中断时,是否仍然执行。
14//!
15//! 各阶段的插件按配置的顺序依次执行,可被中断。
16//!
17//! 中断处理:
18//! - 请求阶段中断:转发到特殊的API端点,执行响应,此时,后置阶段的插件仍会执行
19//! - 响应阶段中断:返回错误响应。由于日志拦截是在最后一步执行,所以,返回错误响应后,日志仍然能被记录。
20//!
21//! ### 路由插件
22//! 对特定路由生效。
23//!
24//! 路由插件和全局插件实现方式相同,仅执行时机不同。
25//!
26//! ## 使用方式
27//! ```rust
28//! // 示例插件
29//! use plugin::plugin_version;
30//!
31//! pub struct DemoPlugin;
32//!
33//! impl DemoPlugin {
34//!     pub fn new() -> Self {
35//!         Self {}
36//!     }
37//! }
38//! #[async_trait]
39//! impl Plugin for DemoPlugin {
40//!     fn name(&self) -> &'static str {
41//!         "demo"
42//!     }
43//!
44//!     fn info(&self) -> PluginInfo {
45//!         PluginInfo {
46//!             version: plugin_version!(),
47//!             default_config: Default::default(),
48//!             description: "Demo Plugin".to_string(),
49//!         }
50//!     }
51//!
52//!     // 实现插件逻辑
53//!     async fn execute(&self, context: &HttpContext, config: &Value) -> Result<(), PluginError> {
54//!         println!("run demo plugin, context: {:?}", context);
55//!         Ok(())
56//!     }
57//! }
58//!
59//! // 导出插件
60//! export!(DemoPlugin);
61//! ```
62//!
63
64mod macros;
65mod manager;
66mod network;
67
68use crate::network::NETWORK;
69pub use async_trait::async_trait;
70use libloading::Symbol;
71pub use manager::PluginManager;
72pub use aiway_protocol as protocol;
73use protocol::gateway::HttpContext;
74pub use semver::Version;
75use serde::{Deserialize, Serialize};
76pub use serde_json;
77use serde_json::Value;
78use std::env::temp_dir;
79use std::fs;
80use std::fs::File;
81use std::io::Write;
82use std::path::PathBuf;
83#[derive(Debug)]
84pub enum PluginError {
85    /// 执行插件业务逻辑时的错误
86    ExecuteError(String),
87    /// 插件不存在
88    NotFound(String),
89    /// 从磁盘或网络加载插件时错误
90    LoadError(String),
91}
92
93impl std::fmt::Display for PluginError {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        match self {
96            PluginError::ExecuteError(msg) => write!(f, "{}", msg),
97            PluginError::NotFound(msg) => write!(f, "{}", msg),
98            PluginError::LoadError(msg) => write!(f, "{}", msg),
99        }
100    }
101}
102
103/// 插件定义
104///
105/// - name
106///
107/// 插件的名称,原则上不要重复。在`PluginManager`中,如果重复了,后添加的将被覆盖。
108///
109/// - execute
110///
111/// `execute`接收HttpContext参数,该HttpContext是可变的(内部可变性),可在插件逻辑内部修改请求和响应。
112/// 注意:当多个插件修改HttpContext的同一个属性时,后执行的插件会覆盖前一个插件的修改。
113/// 插件实现方应该自行决定插件运行阶段(请求阶段或者响应阶段),从而获取或修改request或response的数据。
114///
115/// - 返回值
116/// 返回[serde_json:Value]
117///
118#[async_trait]
119pub trait Plugin: Send + Sync {
120    /// 插件名称
121    fn name(&self) -> &str;
122    /// 插件信息
123    fn info(&self) -> PluginInfo;
124    /// 执行插件
125    async fn execute(&self, context: &HttpContext, config: &Value) -> Result<Value, PluginError>;
126}
127
128/// 插件信息
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct PluginInfo {
131    /// 插件版本
132    pub version: Version,
133    /// 默认配置
134    pub default_config: Value,
135    /// 描述
136    pub description: String,
137}
138
139impl TryFrom<PathBuf> for Box<dyn Plugin> {
140    type Error = PluginError;
141
142    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
143        unsafe {
144            let lib = libloading::Library::new(&value)
145                .map_err(|e| PluginError::LoadError(e.to_string()))?;
146
147            let create_plugin: Symbol<unsafe extern "C" fn() -> *mut dyn Plugin> = lib
148                .get(b"create_plugin")
149                .map_err(|e| PluginError::LoadError(e.to_string()))?;
150
151            let plugin_ptr = create_plugin();
152
153            if plugin_ptr.is_null() {
154                return Err(PluginError::LoadError(
155                    "Failed to create plugin: ptr is null".to_string(),
156                ));
157            }
158
159            let plugin = Box::from_raw(plugin_ptr);
160
161            // 包装一层,保持对lib的引用
162            let wrapped_plugin = Box::new(LibraryPluginWrapper { plugin, _lib: lib });
163
164            Ok(wrapped_plugin)
165        }
166    }
167}
168
169struct LibraryPluginWrapper {
170    plugin: Box<dyn Plugin>,
171    _lib: libloading::Library,
172}
173
174#[async_trait]
175impl Plugin for LibraryPluginWrapper {
176    fn name(&self) -> &str {
177        self.plugin.name()
178    }
179
180    fn info(&self) -> PluginInfo {
181        self.plugin.info()
182    }
183
184    async fn execute(&self, context: &HttpContext, config: &Value) -> Result<Value, PluginError> {
185        self.plugin.execute(context, config).await
186    }
187}
188
189impl Drop for LibraryPluginWrapper {
190    fn drop(&mut self) {
191        unsafe {
192            let destructor: Symbol<unsafe extern "C" fn(*mut dyn Plugin)> = self
193                ._lib
194                .get(b"destroy_plugin")
195                .expect("Failed to get destructor function");
196
197            destructor(self.plugin.as_mut());
198        }
199    }
200}
201
202/// 从指定的URL加载插件
203pub struct NetworkPlugin(pub String);
204
205#[async_trait]
206pub trait AsyncTryInto<T>: Sized {
207    type Error;
208
209    async fn async_try_into(self) -> Result<T, Self::Error>;
210}
211
212#[async_trait]
213impl AsyncTryInto<Box<dyn Plugin>> for NetworkPlugin {
214    type Error = PluginError;
215
216    async fn async_try_into(self) -> Result<Box<dyn Plugin>, Self::Error> {
217        let response = NETWORK
218            .client
219            .get(&self.0)
220            .send()
221            .await
222            .map_err(|e| PluginError::LoadError(e.to_string()))?
223            .error_for_status()
224            .map_err(|e| PluginError::LoadError(e.to_string()))?;
225
226        let bytes = response
227            .bytes()
228            .await
229            .map_err(|e| PluginError::LoadError(e.to_string()))?;
230
231        let tpf = temp_dir().join(uuid::Uuid::new_v4().to_string());
232
233        let plugin = {
234            let tpf = tpf.clone();
235            let mut file = File::create(&tpf).map_err(|e| PluginError::LoadError(e.to_string()))?;
236
237            file.write_all(&bytes)
238                .map_err(|e| PluginError::LoadError(e.to_string()))?;
239
240            drop(file);
241
242            tpf.try_into()
243        };
244
245        fs::remove_file(tpf).map_err(|e| PluginError::LoadError(e.to_string()))?;
246
247        plugin
248    }
249}
250
251impl TryFrom<Vec<u8>> for Box<dyn Plugin> {
252    type Error = PluginError;
253
254    fn try_from(from: Vec<u8>) -> Result<Box<dyn Plugin>, Self::Error> {
255        let temp = temp_dir().join(format!("{}.so", uuid::Uuid::new_v4()));
256        fs::write(&temp, from).map_err(|e| PluginError::LoadError(e.to_string()))?;
257        temp.try_into()
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::manager::PluginManager;
265    use std::io::Read;
266    #[tokio::test]
267    async fn test_network_plugin() {
268        let p = NetworkPlugin(
269            "http://192.168.1.242:10000/aiway/test/plugins/libdemo_plugin.so".to_string(),
270        );
271        let plugin: Box<dyn Plugin> = p.async_try_into().await.unwrap();
272        plugin
273            .execute(&HttpContext::default(), &Value::Null)
274            .await
275            .unwrap();
276    }
277    #[tokio::test]
278    async fn test_plugin_manager() {
279        let p = NetworkPlugin(
280            "http://192.168.1.242:10000/aiway/test/plugins/libdemo_plugin.so".to_string(),
281        );
282        let plugin: Box<dyn Plugin> = p.async_try_into().await.unwrap();
283        let mut manager = PluginManager::new();
284        manager.register(plugin);
285        manager
286            .run("demo", &HttpContext::default(), &Value::Null)
287            .await
288            .unwrap();
289    }
290
291    #[tokio::test]
292    async fn test_plugin_from_bytes() {
293        let file =
294            File::open("../../target/release/libaha_model_request_wrapper_plugin.so").unwrap();
295        // 获取file的bytes
296        let bytes = file.bytes().collect::<Result<Vec<_>, _>>().unwrap();
297        let plugin: Box<dyn Plugin> = bytes.try_into().unwrap();
298        println!("{:?}", plugin.info());
299    }
300}