Example Plugin: String Reversal
A simple plugin that reverses strings, demonstrating the core plugin interface. Location:plugins/example-plugin/src/lib.rs
Cargo.toml
Copy
[package]
name = "reqflow-example-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Plugin Implementation
1. Define ABI Structures
Copy
use std::collections::HashMap;
use std::ffi::{c_char, CStr, CString};
use std::sync::OnceLock;
#[repr(C)]
pub struct PluginInfo {
pub name: *const c_char,
pub version: *const c_char,
pub author: *const c_char,
pub description: *const c_char,
pub block_count: u32,
}
#[repr(C)]
pub struct BlockInfo {
pub block_type_name: *const c_char,
pub label: *const c_char,
pub category: *const c_char,
pub color: *const c_char,
pub icon: *const c_char,
pub settings_schema_json: *const c_char,
pub default_settings_json: *const c_char,
}
#[repr(C)]
pub struct ExecuteResult {
pub success: bool,
pub updated_variables_json: *const c_char,
pub log_message: *const c_char,
pub error_message: *const c_char,
}
2. Create Static Metadata
Copy
fn leak_cstring(s: &str) -> *const c_char {
CString::new(s).unwrap().into_raw() as *const c_char
}
static PLUGIN_INFO: OnceLock<PluginInfo> = OnceLock::new();
static BLOCK_INFO: OnceLock<BlockInfo> = OnceLock::new();
fn get_plugin_info() -> &'static PluginInfo {
PLUGIN_INFO.get_or_init(|| PluginInfo {
name: leak_cstring("ExamplePlugin"),
version: leak_cstring("0.1.0"),
author: leak_cstring("reqflow"),
description: leak_cstring("Example plugin that reverses strings"),
block_count: 1,
})
}
fn get_block_info() -> &'static BlockInfo {
BLOCK_INFO.get_or_init(|| BlockInfo {
block_type_name: leak_cstring("ExamplePlugin.ReverseString"),
label: leak_cstring("Reverse String"),
category: leak_cstring("Utilities"),
color: leak_cstring("#9b59b6"),
icon: leak_cstring("repeat"),
settings_schema_json: leak_cstring(
r#"{"type":"object","properties":{"input_var":{"type":"string","title":"Input Variable","default":"data.SOURCE"}}}"#
),
default_settings_json: leak_cstring(r#"{"input_var":"data.SOURCE"}"#),
})
}
3. Export Plugin Info Functions
Copy
#[no_mangle]
pub extern "C" fn plugin_info() -> *const PluginInfo {
get_plugin_info() as *const PluginInfo
}
#[no_mangle]
pub extern "C" fn plugin_block_info(index: u32) -> *const BlockInfo {
if index == 0 {
get_block_info() as *const BlockInfo
} else {
std::ptr::null()
}
}
4. Implement Execution Logic
Copy
#[no_mangle]
pub extern "C" fn plugin_execute(
_block_index: u32,
settings_json: *const c_char,
variables_json: *const c_char,
) -> *const ExecuteResult {
// Parse settings JSON
let settings_str = if settings_json.is_null() {
"{}".to_string()
} else {
unsafe { CStr::from_ptr(settings_json).to_string_lossy().to_string() }
};
// Parse variables JSON
let vars_str = if variables_json.is_null() {
"{}".to_string()
} else {
unsafe { CStr::from_ptr(variables_json).to_string_lossy().to_string() }
};
let settings: HashMap<String, String> =
serde_json::from_str(&settings_str).unwrap_or_default();
let input_var = settings
.get("input_var")
.cloned()
.unwrap_or_else(|| "data.SOURCE".to_string());
let vars: HashMap<String, String> =
serde_json::from_str(&vars_str).unwrap_or_default();
let input_value = vars.get(&input_var).cloned().unwrap_or_default();
// Reverse the string
let reversed: String = input_value.chars().rev().collect();
// Build updated variables
let mut updated = vars.clone();
updated.insert("PLUGIN_RESULT".to_string(), reversed.clone());
let updated_json = serde_json::to_string(&updated)
.unwrap_or_else(|_| "{}".to_string());
let log_msg = format!("Reversed '{}' → '{}'", input_value, reversed);
let result = Box::new(ExecuteResult {
success: true,
updated_variables_json: CString::new(updated_json).unwrap().into_raw(),
log_message: CString::new(log_msg).unwrap().into_raw(),
error_message: std::ptr::null(),
});
Box::into_raw(result) as *const ExecuteResult
}
5. Implement Memory Cleanup
Copy
#[no_mangle]
pub extern "C" fn plugin_free_string(ptr: *const c_char) {
if !ptr.is_null() {
unsafe {
drop(CString::from_raw(ptr as *mut c_char));
}
}
}
Usage
Copy
{
"block_type": "ExamplePlugin.ReverseString",
"settings": {
"input_var": "data.SOURCE"
}
}
Copy
{
"data.SOURCE": "hello world"
}
Copy
{
"data.SOURCE": "hello world",
"PLUGIN_RESULT": "dlrow olleh"
}
CF Clearance Plugin
A real-world plugin that generates Cloudflare clearance cookies for bypass operations. Location:plugins/cf-clearance/src/lib.rs
Cargo.toml
Copy
[package]
name = "cf-clearance"
version = "1.0.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rand = "0.8"
Plugin Implementation
1. Define Static Metadata
Using byte strings for compile-time constants:Copy
static PLUGIN_NAME: &[u8] = b"CfClearance\0";
static PLUGIN_VERSION: &[u8] = b"1.0.0\0";
static PLUGIN_AUTHOR: &[u8] = b"paul\0";
static PLUGIN_DESC: &[u8] = b"CF clearance cookie generator for non-managed sites\0";
static BLOCK_TYPE: &[u8] = b"CfClearance.Generate\0";
static BLOCK_LABEL: &[u8] = b"CF Clearance\0";
static BLOCK_CATEGORY: &[u8] = b"Bypass\0";
static BLOCK_COLOR: &[u8] = b"#e5c07b\0";
static BLOCK_ICON: &[u8] = b"shield\0";
static SETTINGS_SCHEMA: &[u8] = b"[\
{\"key\":\"domain\",\"label\":\"Domain\",\"type\":\"string\",\"default\":\"\",\"placeholder\":\"example.com\"},\
{\"key\":\"output_clearance\",\"label\":\"Clearance Var\",\"type\":\"string\",\"default\":\"CF_CLEARANCE\"},\
{\"key\":\"output_bm\",\"label\":\"BM Var\",\"type\":\"string\",\"default\":\"CF_BM\"},\
{\"key\":\"capture\",\"label\":\"Capture\",\"type\":\"bool\",\"default\":false}\
]\0";
static DEFAULT_SETTINGS: &[u8] = b"{\"domain\":\"\",\"output_clearance\":\"CF_CLEARANCE\",\"output_bm\":\"CF_BM\",\"capture\":false}\0";
2. Create Static Plugin Info
Copy
static PLUGIN_INFO: PluginInfo = PluginInfo {
name: PLUGIN_NAME.as_ptr() as *const c_char,
version: PLUGIN_VERSION.as_ptr() as *const c_char,
author: PLUGIN_AUTHOR.as_ptr() as *const c_char,
description: PLUGIN_DESC.as_ptr() as *const c_char,
block_count: 1,
};
static BLOCK_INFO_0: BlockInfo = BlockInfo {
block_type_name: BLOCK_TYPE.as_ptr() as *const c_char,
label: BLOCK_LABEL.as_ptr() as *const c_char,
category: BLOCK_CATEGORY.as_ptr() as *const c_char,
color: BLOCK_COLOR.as_ptr() as *const c_char,
icon: BLOCK_ICON.as_ptr() as *const c_char,
settings_schema_json: SETTINGS_SCHEMA.as_ptr() as *const c_char,
default_settings_json: DEFAULT_SETTINGS.as_ptr() as *const c_char,
};
#[no_mangle]
pub extern "C" fn plugin_info() -> *const PluginInfo {
&PLUGIN_INFO
}
#[no_mangle]
pub extern "C" fn plugin_block_info(index: u32) -> *const BlockInfo {
match index {
0 => &BLOCK_INFO_0,
_ => std::ptr::null(),
}
}
3. Define Settings Structure
Copy
use serde::Deserialize;
#[derive(Deserialize)]
struct Settings {
#[serde(default)]
domain: String,
#[serde(default = "def_clearance_var")]
output_clearance: String,
#[serde(default = "def_bm_var")]
output_bm: String,
#[serde(default)]
capture: bool,
}
fn def_clearance_var() -> String { "CF_CLEARANCE".into() }
fn def_bm_var() -> String { "CF_BM".into() }
4. Implement Cookie Generation
Copy
use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
fn rng_str(len: usize) -> String {
let mut rng = rand::thread_rng();
(0..len)
.map(|_| CHARSET[rng.gen_range(0..CHARSET.len())] as char)
.collect()
}
fn gen_clearance() -> String {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
format!("{}-{}-1.0.1.1-{}-{}", rng_str(16), ts, rng_str(20), rng_str(80))
}
fn gen_bm() -> String {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
format!("{}-{}-1.0.1.1-{}", rng_str(20), ts, rng_str(40))
}
5. Implement Execute Function
Copy
#[no_mangle]
pub extern "C" fn plugin_execute(
block_index: u32,
settings_json: *const c_char,
variables_json: *const c_char,
) -> *const ExecuteResult {
if block_index != 0 {
return Box::into_raw(Box::new(ExecuteResult {
success: false,
updated_variables_json: empty_cstr(),
log_message: empty_cstr(),
error_message: to_cstring("Unknown block index"),
}));
}
let settings_str = unsafe {
if settings_json.is_null() { "{}" }
else { CStr::from_ptr(settings_json).to_str().unwrap_or("{}") }
};
let vars_str = unsafe {
if variables_json.is_null() { "{}" }
else { CStr::from_ptr(variables_json).to_str().unwrap_or("{}") }
};
let settings: Settings = serde_json::from_str(settings_str)
.unwrap_or(Settings {
domain: String::new(),
output_clearance: def_clearance_var(),
output_bm: def_bm_var(),
capture: false,
});
let mut vars: HashMap<String, String> =
serde_json::from_str(vars_str).unwrap_or_default();
let clearance = gen_clearance();
let bm = gen_bm();
// Interpolate domain from variables if needed
let domain = if settings.domain.contains('<') && settings.domain.contains('>') {
let key = settings.domain.trim_start_matches('<').trim_end_matches('>');
vars.get(key).cloned().unwrap_or(settings.domain.clone())
} else {
settings.domain.clone()
};
// Build cookie string for the domain
let cookie_str = format!("cf_clearance={}; __cf_bm={}", clearance, bm);
vars.insert(settings.output_clearance.clone(), clearance.clone());
vars.insert(settings.output_bm.clone(), bm.clone());
if !domain.is_empty() {
vars.insert("CF_COOKIE_STRING".into(), cookie_str.clone());
vars.insert("CF_DOMAIN".into(), domain);
}
let vars_json = serde_json::to_string(&vars).unwrap_or_default();
let log = format!("cf_clearance={} | __cf_bm={}", &clearance[..16], &bm[..16]);
Box::into_raw(Box::new(ExecuteResult {
success: true,
updated_variables_json: to_cstring(&vars_json),
log_message: to_cstring(&log),
error_message: empty_cstr(),
}))
}
6. Helper Functions
Copy
fn to_cstring(s: &str) -> *const c_char {
CString::new(s).unwrap_or_default().into_raw()
}
fn empty_cstr() -> *const c_char {
CString::new("").unwrap_or_default().into_raw()
}
#[no_mangle]
pub extern "C" fn plugin_free_string(ptr: *const c_char) {
if !ptr.is_null() {
unsafe { drop(CString::from_raw(ptr as *mut _)); }
}
}
Usage
Copy
{
"block_type": "CfClearance.Generate",
"settings": {
"domain": "example.com",
"output_clearance": "CF_CLEARANCE",
"output_bm": "CF_BM",
"capture": false
}
}
Copy
{
"CF_CLEARANCE": "AbCd1234EfGh5678-1234567890-1.0.1.1-IjKlMnOpQrStUvWxYz01-...",
"CF_BM": "XyZ9876543210AbCdEfG-1234567890-1.0.1.1-HiJkLmNoPqRsTuVwXyZ012345...",
"CF_COOKIE_STRING": "cf_clearance=...; __cf_bm=...",
"CF_DOMAIN": "example.com"
}
Variable Interpolation
The domain setting supports variable interpolation:Copy
{
"settings": {
"domain": "<TARGET_DOMAIN>"
},
"variables": {
"TARGET_DOMAIN": "api.example.com"
}
}
<...> and looks it up in the variables map.
Key Patterns
Static vs Dynamic Metadata
Example Plugin usesOnceLock for lazy initialization:
Copy
static PLUGIN_INFO: OnceLock<PluginInfo> = OnceLock::new();
Copy
static PLUGIN_INFO: PluginInfo = PluginInfo { /* ... */ };
Error Handling
Return errors viaExecuteResult:
Copy
if something_went_wrong {
return Box::into_raw(Box::new(ExecuteResult {
success: false,
updated_variables_json: empty_cstr(),
log_message: empty_cstr(),
error_message: to_cstring("Error description"),
}));
}
Memory Safety
Always allocate strings withCString::new().into_raw() and implement plugin_free_string to properly deallocate them.
Thread Safety
Mark shared static data asSync:
Copy
unsafe impl Sync for PluginInfo {}
unsafe impl Sync for BlockInfo {}
Building Plugins
Copy
cd plugins/example-plugin
cargo build --release
# Output: target/release/reqflow_example_plugin.dll
.dll file in the plugins directory scanned by IronBullet.