Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ crate-type = ["cdylib"]
name = "expire"
crate-type = ["cdylib"]

[[example]]
name = "command_filter"
crate-type = ["cdylib"]

[dependencies]
bitflags = "2"
libc = "0.2"
Expand Down
29 changes: 29 additions & 0 deletions examples/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,34 @@ fn call_blocking_from_detach_ctx(ctx: &Context, _: Vec<RedisString>) -> RedisRes
Ok(RedisValue::NoReply)
}

fn call_dump_test(ctx: &Context, _: Vec<RedisString>) -> RedisResult {
// Set a key with a value
ctx.call("SET", &["test_dump_key", "test_value"])?;

// Call DUMP which returns binary data (may not be valid UTF-8)
let dump_result = ctx.call("DUMP", &["test_dump_key"])?;

// Verify we got a result (should be StringBuffer for binary data)
match dump_result {
RedisValue::StringBuffer(data) => {
if data.is_empty() {
return Err(RedisError::Str("DUMP returned empty data"));
}
}
RedisValue::SimpleString(_) => {
// Also acceptable if the binary data happens to be valid UTF-8
}
_ => {
return Err(RedisError::Str("DUMP returned unexpected type"));
}
}

// Clean up
ctx.call("DEL", &["test_dump_key"])?;

Ok("pass".into())
}

//////////////////////////////////////////////////////

redis_module! {
Expand All @@ -167,5 +195,6 @@ redis_module! {
["call.test", call_test, "", 0, 0, 0, ""],
["call.blocking", call_blocking, "", 0, 0, 0, ""],
["call.blocking_from_detached_ctx", call_blocking_from_detach_ctx, "", 0, 0, 0, ""],
["call.dump_test", call_dump_test, "", 0, 0, 0, ""],
],
}
123 changes: 123 additions & 0 deletions examples/command_filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use redis_module::{
raw, redis_module, CommandFilter, CommandFilterContext, Context, NextArg, RedisError,
RedisResult, RedisString, RedisValue,
};
use std::sync::atomic::{AtomicPtr, Ordering};

static COMMAND_FILTER: AtomicPtr<raw::RedisModuleCommandFilter> =
AtomicPtr::new(std::ptr::null_mut());

extern "C" fn command_filter_callback(fctx: *mut raw::RedisModuleCommandFilterCtx) {
let filter_ctx = CommandFilterContext::new(fctx);
command_filter_impl(&filter_ctx);
}

fn command_filter_impl(fctx: &CommandFilterContext) {
// Get the command name
if let Ok(cmd_str) = fctx.cmd_get_try_as_str() {
// Example: Log all SET commands
if cmd_str.eq_ignore_ascii_case("set") {
// You can inspect or modify arguments here
// For example, you could replace sensitive data

// Get all arguments (excluding command)
let args = fctx.get_all_args_wo_cmd();
let _num_args = args.len();

// Note: In a real implementation, you would use the Context
// to log, but we don't have access to it in the filter callback

#[cfg(any(
feature = "min-redis-compatibility-version-7-4",
feature = "min-redis-compatibility-version-7-2"
))]
{
let _client_id = fctx.get_client_id();
}
}
}
}

fn filter_register(ctx: &Context, _args: Vec<RedisString>) -> RedisResult {
let current = COMMAND_FILTER.load(Ordering::Acquire);

if !current.is_null() {
return Err(RedisError::String("Filter already registered".to_string()));
}

let filter = ctx.register_command_filter(command_filter_callback, 0);
COMMAND_FILTER.store(filter.as_ptr(), Ordering::Release);

Ok(RedisValue::SimpleStringStatic("OK"))
}

fn filter_unregister(ctx: &Context, _args: Vec<RedisString>) -> RedisResult {
let filter_ptr = COMMAND_FILTER.swap(std::ptr::null_mut(), Ordering::AcqRel);

if !filter_ptr.is_null() {
let filter = CommandFilter::new(filter_ptr);
ctx.unregister_command_filter(&filter);
Ok(RedisValue::SimpleStringStatic("OK"))
} else {
Err(RedisError::String("No filter registered".to_string()))
}
}

fn filter_test_args(ctx: &Context, args: Vec<RedisString>) -> RedisResult {
let mut args_iter = args.into_iter().skip(1);
let key = args_iter.next_arg()?;
let value = args_iter.next_arg()?;

// This SET command will be intercepted by the filter if it's registered
ctx.call("SET", &[&key, &value])?;

Ok(RedisValue::SimpleStringStatic("OK"))
}

fn filter_modify_example(ctx: &Context, args: Vec<RedisString>) -> RedisResult {
// This example demonstrates how to modify command arguments in a filter
// In this case, we'll register a temporary filter that adds a prefix to SET keys

extern "C" fn modify_filter(fctx: *mut raw::RedisModuleCommandFilterCtx) {
let filter_ctx = CommandFilterContext::new(fctx);

// Check if this is a SET command
if let Ok(cmd) = filter_ctx.cmd_get_try_as_str() {
if cmd.eq_ignore_ascii_case("set") && filter_ctx.args_count() >= 2 {
// Get the current key
if let Ok(key) = filter_ctx.arg_get_try_as_str(1) {
// Replace it with a prefixed version
let new_key = format!("filtered:{}", key);
filter_ctx.arg_replace(1, &new_key);
}
}
}
}

let filter = ctx.register_command_filter(modify_filter, 0);

// Execute a SET command which will be modified by the filter
if args.len() > 2 {
let _ = ctx.call("SET", &[&args[1], &args[2]]);
}

// Unregister the filter
ctx.unregister_command_filter(&filter);

Ok(RedisValue::SimpleStringStatic("OK"))
}

//////////////////////////////////////////////////////

redis_module! {
name: "command_filter",
version: 1,
allocator: (redis_module::alloc::RedisAlloc, redis_module::alloc::RedisAlloc),
data_types: [],
commands: [
["filter.register", filter_register, "", 0, 0, 0, ""],
["filter.unregister", filter_unregister, "", 0, 0, 0, ""],
["filter.test_args", filter_test_args, "", 0, 0, 0, ""],
["filter.modify_example", filter_modify_example, "", 0, 0, 0, ""],
],
}
23 changes: 22 additions & 1 deletion examples/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use std::sync::atomic::{AtomicI64, Ordering};

static NUM_KEY_MISSES: AtomicI64 = AtomicI64::new(0);
static NUM_KEYS: AtomicI64 = AtomicI64::new(0);
static NUM_OVERWRITTEN: AtomicI64 = AtomicI64::new(0);
static NUM_TYPE_CHANGED: AtomicI64 = AtomicI64::new(0);

fn on_event(ctx: &Context, event_type: NotifyEvent, event: &str, key: &[u8]) {
if key == b"num_sets" {
Expand Down Expand Up @@ -60,7 +62,22 @@ fn on_new_key(_ctx: &Context, _event_type: NotifyEvent, _event: &str, _key: &[u8
fn num_keys(_ctx: &Context, _args: Vec<RedisString>) -> RedisResult {
Ok(RedisValue::Integer(NUM_KEYS.load(Ordering::SeqCst)))
}
//////////////////////////////////////////////////////

fn on_key_overwritten(_ctx: &Context, _event_type: NotifyEvent, _event: &str, _key: &[u8]) {
NUM_OVERWRITTEN.fetch_add(1, Ordering::SeqCst);
}

fn num_overwritten(_ctx: &Context, _args: Vec<RedisString>) -> RedisResult {
Ok(RedisValue::Integer(NUM_OVERWRITTEN.load(Ordering::SeqCst)))
}

fn on_key_changed(_ctx: &Context, _event_type: NotifyEvent, _event: &str, _key: &[u8]) {
NUM_TYPE_CHANGED.fetch_add(1, Ordering::SeqCst);
}

fn num_type_changed(_ctx: &Context, _args: Vec<RedisString>) -> RedisResult {
Ok(RedisValue::Integer(NUM_TYPE_CHANGED.load(Ordering::SeqCst)))
}

redis_module! {
name: "events",
Expand All @@ -71,11 +88,15 @@ redis_module! {
["events.send", event_send, "", 0, 0, 0, ""],
["events.num_key_miss", num_key_miss, "", 0, 0, 0, ""],
["events.num_keys", num_keys, "", 0, 0, 0, ""],
["events.num_overwritten", num_overwritten, "", 0, 0, 0, ""],
["events.num_type_changed", num_type_changed, "", 0, 0, 0, ""],
],
event_handlers: [
[@STRING: on_event],
[@STREAM: on_stream],
[@MISSED: on_key_miss],
[@NEW: on_new_key],
[@OVERWRITTEN: on_key_overwritten],
[@TYPE_CHANGED: on_key_changed],
],
}
Loading