Merge pull request #5944 from yossigo/command-filtering
Command Filtering API
This commit is contained in:
commit
5e8caca036
265
src/module.c
265
src/module.c
@ -49,6 +49,8 @@ struct RedisModule {
|
||||
list *types; /* Module data types. */
|
||||
list *usedby; /* List of modules using APIs from this one. */
|
||||
list *using; /* List of modules we use some APIs of. */
|
||||
list *filters; /* List of filters the module has registered. */
|
||||
int in_call; /* RM_Call() nesting level */
|
||||
};
|
||||
typedef struct RedisModule RedisModule;
|
||||
|
||||
@ -270,6 +272,25 @@ typedef struct RedisModuleDictIter {
|
||||
raxIterator ri;
|
||||
} RedisModuleDictIter;
|
||||
|
||||
typedef struct RedisModuleCommandFilterCtx {
|
||||
RedisModuleString **argv;
|
||||
int argc;
|
||||
} RedisModuleCommandFilterCtx;
|
||||
|
||||
typedef void (*RedisModuleCommandFilterFunc) (RedisModuleCommandFilterCtx *filter);
|
||||
|
||||
typedef struct RedisModuleCommandFilter {
|
||||
/* The module that registered the filter */
|
||||
RedisModule *module;
|
||||
/* Filter callback function */
|
||||
RedisModuleCommandFilterFunc callback;
|
||||
/* REDISMODULE_CMDFILTER_* flags */
|
||||
int flags;
|
||||
} RedisModuleCommandFilter;
|
||||
|
||||
/* Registered filters */
|
||||
static list *moduleCommandFilters;
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* Prototypes
|
||||
* -------------------------------------------------------------------------- */
|
||||
@ -731,6 +752,7 @@ void RM_SetModuleAttribs(RedisModuleCtx *ctx, const char *name, int ver, int api
|
||||
module->types = listCreate();
|
||||
module->usedby = listCreate();
|
||||
module->using = listCreate();
|
||||
module->filters = listCreate();
|
||||
ctx->module = module;
|
||||
}
|
||||
|
||||
@ -2724,12 +2746,6 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
|
||||
RedisModuleCallReply *reply = NULL;
|
||||
int replicate = 0; /* Replicate this command? */
|
||||
|
||||
cmd = lookupCommandByCString((char*)cmdname);
|
||||
if (!cmd) {
|
||||
errno = EINVAL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Create the client and dispatch the command. */
|
||||
va_start(ap, fmt);
|
||||
c = createClient(-1);
|
||||
@ -2743,11 +2759,25 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
|
||||
c->db = ctx->client->db;
|
||||
c->argv = argv;
|
||||
c->argc = argc;
|
||||
c->cmd = c->lastcmd = cmd;
|
||||
if (ctx->module) ctx->module->in_call++;
|
||||
|
||||
/* We handle the above format error only when the client is setup so that
|
||||
* we can free it normally. */
|
||||
if (argv == NULL) goto cleanup;
|
||||
|
||||
/* Call command filters */
|
||||
moduleCallCommandFilters(c);
|
||||
|
||||
/* Lookup command now, after filters had a chance to make modifications
|
||||
* if necessary.
|
||||
*/
|
||||
cmd = lookupCommand(c->argv[0]->ptr);
|
||||
if (!cmd) {
|
||||
errno = EINVAL;
|
||||
goto cleanup;
|
||||
}
|
||||
c->cmd = c->lastcmd = cmd;
|
||||
|
||||
/* Basic arity checks. */
|
||||
if ((cmd->arity > 0 && cmd->arity != argc) || (argc < -cmd->arity)) {
|
||||
errno = EINVAL;
|
||||
@ -2797,6 +2827,7 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
|
||||
autoMemoryAdd(ctx,REDISMODULE_AM_REPLY,reply);
|
||||
|
||||
cleanup:
|
||||
if (ctx->module) ctx->module->in_call--;
|
||||
freeClient(c);
|
||||
return reply;
|
||||
}
|
||||
@ -4778,6 +4809,214 @@ int moduleUnregisterUsedAPI(RedisModule *module) {
|
||||
return count;
|
||||
}
|
||||
|
||||
/* Unregister all filters registered by a module.
|
||||
* This is called when a module is being unloaded.
|
||||
*
|
||||
* Returns the number of filters unregistered. */
|
||||
int moduleUnregisterFilters(RedisModule *module) {
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
int count = 0;
|
||||
|
||||
listRewind(module->filters,&li);
|
||||
while((ln = listNext(&li))) {
|
||||
RedisModuleCommandFilter *filter = ln->value;
|
||||
listNode *ln = listSearchKey(moduleCommandFilters,filter);
|
||||
if (ln) {
|
||||
listDelNode(moduleCommandFilters,ln);
|
||||
count++;
|
||||
}
|
||||
zfree(filter);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* Module Command Filter API
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
/* Register a new command filter function.
|
||||
*
|
||||
* Command filtering makes it possible for modules to extend Redis by plugging
|
||||
* into the execution flow of all commands.
|
||||
*
|
||||
* A registered filter gets called before Redis executes *any* command. This
|
||||
* includes both core Redis commands and commands registered by any module. The
|
||||
* filter applies in all execution paths including:
|
||||
*
|
||||
* 1. Invocation by a client.
|
||||
* 2. Invocation through `RedisModule_Call()` by any module.
|
||||
* 3. Invocation through Lua 'redis.call()`.
|
||||
* 4. Replication of a command from a master.
|
||||
*
|
||||
* The filter executes in a special filter context, which is different and more
|
||||
* limited than a RedisModuleCtx. Because the filter affects any command, it
|
||||
* must be implemented in a very efficient way to reduce the performance impact
|
||||
* on Redis. All Redis Module API calls that require a valid context (such as
|
||||
* `RedisModule_Call()`, `RedisModule_OpenKey()`, etc.) are not supported in a
|
||||
* filter context.
|
||||
*
|
||||
* The `RedisModuleCommandFilterCtx` can be used to inspect or modify the
|
||||
* executed command and its arguments. As the filter executes before Redis
|
||||
* begins processing the command, any change will affect the way the command is
|
||||
* processed. For example, a module can override Redis commands this way:
|
||||
*
|
||||
* 1. Register a `MODULE.SET` command which implements an extended version of
|
||||
* the Redis `SET` command.
|
||||
* 2. Register a command filter which detects invocation of `SET` on a specific
|
||||
* pattern of keys. Once detected, the filter will replace the first
|
||||
* argument from `SET` to `MODULE.SET`.
|
||||
* 3. When filter execution is complete, Redis considers the new command name
|
||||
* and therefore executes the module's own command.
|
||||
*
|
||||
* Note that in the above use case, if `MODULE.SET` itself uses
|
||||
* `RedisModule_Call()` the filter will be applied on that call as well. If
|
||||
* that is not desired, the `REDISMODULE_CMDFILTER_NOSELF` flag can be set when
|
||||
* registering the filter.
|
||||
*
|
||||
* The `REDISMODULE_CMDFILTER_NOSELF` flag prevents execution flows that
|
||||
* originate from the module's own `RM_Call()` from reaching the filter. This
|
||||
* flag is effective for all execution flows, including nested ones, as long as
|
||||
* the execution begins from the module's command context or a thread-safe
|
||||
* context that is associated with a blocking command.
|
||||
*
|
||||
* Detached thread-safe contexts are *not* associated with the module and cannot
|
||||
* be protected by this flag.
|
||||
*
|
||||
* If multiple filters are registered (by the same or different modules), they
|
||||
* are executed in the order of registration.
|
||||
*/
|
||||
|
||||
RedisModuleCommandFilter *RM_RegisterCommandFilter(RedisModuleCtx *ctx, RedisModuleCommandFilterFunc callback, int flags) {
|
||||
RedisModuleCommandFilter *filter = zmalloc(sizeof(*filter));
|
||||
filter->module = ctx->module;
|
||||
filter->callback = callback;
|
||||
filter->flags = flags;
|
||||
|
||||
listAddNodeTail(moduleCommandFilters, filter);
|
||||
listAddNodeTail(ctx->module->filters, filter);
|
||||
return filter;
|
||||
}
|
||||
|
||||
/* Unregister a command filter.
|
||||
*/
|
||||
int RM_UnregisterCommandFilter(RedisModuleCtx *ctx, RedisModuleCommandFilter *filter) {
|
||||
listNode *ln;
|
||||
|
||||
/* A module can only remove its own filters */
|
||||
if (filter->module != ctx->module) return REDISMODULE_ERR;
|
||||
|
||||
ln = listSearchKey(moduleCommandFilters,filter);
|
||||
if (!ln) return REDISMODULE_ERR;
|
||||
listDelNode(moduleCommandFilters,ln);
|
||||
|
||||
ln = listSearchKey(ctx->module->filters,filter);
|
||||
if (!ln) return REDISMODULE_ERR; /* Shouldn't happen */
|
||||
listDelNode(ctx->module->filters,ln);
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
void moduleCallCommandFilters(client *c) {
|
||||
if (listLength(moduleCommandFilters) == 0) return;
|
||||
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
listRewind(moduleCommandFilters,&li);
|
||||
|
||||
RedisModuleCommandFilterCtx filter = {
|
||||
.argv = c->argv,
|
||||
.argc = c->argc
|
||||
};
|
||||
|
||||
while((ln = listNext(&li))) {
|
||||
RedisModuleCommandFilter *f = ln->value;
|
||||
|
||||
/* Skip filter if REDISMODULE_CMDFILTER_NOSELF is set and module is
|
||||
* currently processing a command.
|
||||
*/
|
||||
if ((f->flags & REDISMODULE_CMDFILTER_NOSELF) && f->module->in_call) continue;
|
||||
|
||||
/* Call filter */
|
||||
f->callback(&filter);
|
||||
}
|
||||
|
||||
c->argv = filter.argv;
|
||||
c->argc = filter.argc;
|
||||
}
|
||||
|
||||
/* Return the number of arguments a filtered command has. The number of
|
||||
* arguments include the command itself.
|
||||
*/
|
||||
int RM_CommandFilterArgsCount(RedisModuleCommandFilterCtx *fctx)
|
||||
{
|
||||
return fctx->argc;
|
||||
}
|
||||
|
||||
/* Return the specified command argument. The first argument (position 0) is
|
||||
* the command itself, and the rest are user-provided args.
|
||||
*/
|
||||
const RedisModuleString *RM_CommandFilterArgGet(RedisModuleCommandFilterCtx *fctx, int pos)
|
||||
{
|
||||
if (pos < 0 || pos >= fctx->argc) return NULL;
|
||||
return fctx->argv[pos];
|
||||
}
|
||||
|
||||
/* Modify the filtered command by inserting a new argument at the specified
|
||||
* position. The specified RedisModuleString argument may be used by Redis
|
||||
* after the filter context is destroyed, so it must not be auto-memory
|
||||
* allocated, freed or used elsewhere.
|
||||
*/
|
||||
|
||||
int RM_CommandFilterArgInsert(RedisModuleCommandFilterCtx *fctx, int pos, RedisModuleString *arg)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (pos < 0 || pos > fctx->argc) return REDISMODULE_ERR;
|
||||
|
||||
fctx->argv = zrealloc(fctx->argv, (fctx->argc+1)*sizeof(RedisModuleString *));
|
||||
for (i = fctx->argc; i > pos; i--) {
|
||||
fctx->argv[i] = fctx->argv[i-1];
|
||||
}
|
||||
fctx->argv[pos] = arg;
|
||||
fctx->argc++;
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
/* Modify the filtered command by replacing an existing argument with a new one.
|
||||
* The specified RedisModuleString argument may be used by Redis after the
|
||||
* filter context is destroyed, so it must not be auto-memory allocated, freed
|
||||
* or used elsewhere.
|
||||
*/
|
||||
|
||||
int RM_CommandFilterArgReplace(RedisModuleCommandFilterCtx *fctx, int pos, RedisModuleString *arg)
|
||||
{
|
||||
if (pos < 0 || pos >= fctx->argc) return REDISMODULE_ERR;
|
||||
|
||||
decrRefCount(fctx->argv[pos]);
|
||||
fctx->argv[pos] = arg;
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
/* Modify the filtered command by deleting an argument at the specified
|
||||
* position.
|
||||
*/
|
||||
int RM_CommandFilterArgDelete(RedisModuleCommandFilterCtx *fctx, int pos)
|
||||
{
|
||||
int i;
|
||||
if (pos < 0 || pos >= fctx->argc) return REDISMODULE_ERR;
|
||||
|
||||
decrRefCount(fctx->argv[pos]);
|
||||
for (i = pos; i < fctx->argc-1; i++) {
|
||||
fctx->argv[i] = fctx->argv[i+1];
|
||||
}
|
||||
fctx->argc--;
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* Modules API internals
|
||||
* -------------------------------------------------------------------------- */
|
||||
@ -4824,6 +5063,9 @@ void moduleInitModulesSystem(void) {
|
||||
moduleFreeContextReusedClient->flags |= CLIENT_MODULE;
|
||||
moduleFreeContextReusedClient->user = NULL; /* root user. */
|
||||
|
||||
/* Set up filter list */
|
||||
moduleCommandFilters = listCreate();
|
||||
|
||||
moduleRegisterCoreAPI();
|
||||
if (pipe(server.module_blocked_pipe) == -1) {
|
||||
serverLog(LL_WARNING,
|
||||
@ -4873,6 +5115,7 @@ void moduleLoadFromQueue(void) {
|
||||
|
||||
void moduleFreeModuleStructure(struct RedisModule *module) {
|
||||
listRelease(module->types);
|
||||
listRelease(module->filters);
|
||||
sdsfree(module->name);
|
||||
zfree(module);
|
||||
}
|
||||
@ -4964,6 +5207,7 @@ int moduleUnload(sds name) {
|
||||
moduleUnregisterCommands(module);
|
||||
moduleUnregisterSharedAPI(module);
|
||||
moduleUnregisterUsedAPI(module);
|
||||
moduleUnregisterFilters(module);
|
||||
|
||||
/* Remove any notification subscribers this module might have */
|
||||
moduleUnsubscribeNotifications(module);
|
||||
@ -5228,4 +5472,11 @@ void moduleRegisterCoreAPI(void) {
|
||||
REGISTER_API(DictCompare);
|
||||
REGISTER_API(ExportSharedAPI);
|
||||
REGISTER_API(GetSharedAPI);
|
||||
REGISTER_API(RegisterCommandFilter);
|
||||
REGISTER_API(UnregisterCommandFilter);
|
||||
REGISTER_API(CommandFilterArgsCount);
|
||||
REGISTER_API(CommandFilterArgGet);
|
||||
REGISTER_API(CommandFilterArgInsert);
|
||||
REGISTER_API(CommandFilterArgReplace);
|
||||
REGISTER_API(CommandFilterArgDelete);
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ endif
|
||||
|
||||
.SUFFIXES: .c .so .xo .o
|
||||
|
||||
all: helloworld.so hellotype.so helloblock.so testmodule.so hellocluster.so hellotimer.so hellodict.so
|
||||
all: helloworld.so hellotype.so helloblock.so testmodule.so hellocluster.so hellotimer.so hellodict.so hellofilter.so
|
||||
|
||||
.c.xo:
|
||||
$(CC) -I. $(CFLAGS) $(SHOBJ_CFLAGS) -fPIC -c $< -o $@
|
||||
@ -46,6 +46,10 @@ hellotimer.so: hellotimer.xo
|
||||
hellodict.xo: ../redismodule.h
|
||||
|
||||
hellodict.so: hellodict.xo
|
||||
|
||||
hellofilter.xo: ../redismodule.h
|
||||
|
||||
hellofilter.so: hellofilter.xo
|
||||
$(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lc
|
||||
|
||||
testmodule.xo: ../redismodule.h
|
||||
|
149
src/modules/hellofilter.c
Normal file
149
src/modules/hellofilter.c
Normal file
@ -0,0 +1,149 @@
|
||||
#define REDISMODULE_EXPERIMENTAL_API
|
||||
#include "../redismodule.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static RedisModuleString *log_key_name;
|
||||
|
||||
static const char log_command_name[] = "hellofilter.log";
|
||||
static const char ping_command_name[] = "hellofilter.ping";
|
||||
static const char unregister_command_name[] = "hellofilter.unregister";
|
||||
static int in_log_command = 0;
|
||||
|
||||
static RedisModuleCommandFilter *filter = NULL;
|
||||
|
||||
int HelloFilter_UnregisterCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
|
||||
{
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
RedisModule_ReplyWithLongLong(ctx,
|
||||
RedisModule_UnregisterCommandFilter(ctx, filter));
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
int HelloFilter_PingCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
|
||||
{
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
RedisModuleCallReply *reply = RedisModule_Call(ctx, "ping", "c", "@log");
|
||||
if (reply) {
|
||||
RedisModule_ReplyWithCallReply(ctx, reply);
|
||||
RedisModule_FreeCallReply(reply);
|
||||
} else {
|
||||
RedisModule_ReplyWithSimpleString(ctx, "Unknown command or invalid arguments");
|
||||
}
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
int HelloFilter_LogCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
|
||||
{
|
||||
RedisModuleString *s = RedisModule_CreateString(ctx, "", 0);
|
||||
|
||||
int i;
|
||||
for (i = 1; i < argc; i++) {
|
||||
size_t arglen;
|
||||
const char *arg = RedisModule_StringPtrLen(argv[i], &arglen);
|
||||
|
||||
if (i > 1) RedisModule_StringAppendBuffer(ctx, s, " ", 1);
|
||||
RedisModule_StringAppendBuffer(ctx, s, arg, arglen);
|
||||
}
|
||||
|
||||
RedisModuleKey *log = RedisModule_OpenKey(ctx, log_key_name, REDISMODULE_WRITE|REDISMODULE_READ);
|
||||
RedisModule_ListPush(log, REDISMODULE_LIST_HEAD, s);
|
||||
RedisModule_CloseKey(log);
|
||||
RedisModule_FreeString(ctx, s);
|
||||
|
||||
in_log_command = 1;
|
||||
|
||||
size_t cmdlen;
|
||||
const char *cmdname = RedisModule_StringPtrLen(argv[1], &cmdlen);
|
||||
RedisModuleCallReply *reply = RedisModule_Call(ctx, cmdname, "v", &argv[2], argc - 2);
|
||||
if (reply) {
|
||||
RedisModule_ReplyWithCallReply(ctx, reply);
|
||||
RedisModule_FreeCallReply(reply);
|
||||
} else {
|
||||
RedisModule_ReplyWithSimpleString(ctx, "Unknown command or invalid arguments");
|
||||
}
|
||||
|
||||
in_log_command = 0;
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
void HelloFilter_CommandFilter(RedisModuleCommandFilterCtx *filter)
|
||||
{
|
||||
if (in_log_command) return; /* don't process our own RM_Call() from HelloFilter_LogCommand() */
|
||||
|
||||
/* Fun manipulations:
|
||||
* - Remove @delme
|
||||
* - Replace @replaceme
|
||||
* - Append @insertbefore or @insertafter
|
||||
* - Prefix with Log command if @log encounterd
|
||||
*/
|
||||
int log = 0;
|
||||
int pos = 0;
|
||||
while (pos < RedisModule_CommandFilterArgsCount(filter)) {
|
||||
const RedisModuleString *arg = RedisModule_CommandFilterArgGet(filter, pos);
|
||||
size_t arg_len;
|
||||
const char *arg_str = RedisModule_StringPtrLen(arg, &arg_len);
|
||||
|
||||
if (arg_len == 6 && !memcmp(arg_str, "@delme", 6)) {
|
||||
RedisModule_CommandFilterArgDelete(filter, pos);
|
||||
continue;
|
||||
}
|
||||
if (arg_len == 10 && !memcmp(arg_str, "@replaceme", 10)) {
|
||||
RedisModule_CommandFilterArgReplace(filter, pos,
|
||||
RedisModule_CreateString(NULL, "--replaced--", 12));
|
||||
} else if (arg_len == 13 && !memcmp(arg_str, "@insertbefore", 13)) {
|
||||
RedisModule_CommandFilterArgInsert(filter, pos,
|
||||
RedisModule_CreateString(NULL, "--inserted-before--", 19));
|
||||
pos++;
|
||||
} else if (arg_len == 12 && !memcmp(arg_str, "@insertafter", 12)) {
|
||||
RedisModule_CommandFilterArgInsert(filter, pos + 1,
|
||||
RedisModule_CreateString(NULL, "--inserted-after--", 18));
|
||||
pos++;
|
||||
} else if (arg_len == 4 && !memcmp(arg_str, "@log", 4)) {
|
||||
log = 1;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (log) RedisModule_CommandFilterArgInsert(filter, 0,
|
||||
RedisModule_CreateString(NULL, log_command_name, sizeof(log_command_name)-1));
|
||||
}
|
||||
|
||||
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
if (RedisModule_Init(ctx,"hellofilter",1,REDISMODULE_APIVER_1)
|
||||
== REDISMODULE_ERR) return REDISMODULE_ERR;
|
||||
|
||||
if (argc != 2) {
|
||||
RedisModule_Log(ctx, "warning", "Log key name not specified");
|
||||
return REDISMODULE_ERR;
|
||||
}
|
||||
|
||||
long long noself = 0;
|
||||
log_key_name = RedisModule_CreateStringFromString(ctx, argv[0]);
|
||||
RedisModule_StringToLongLong(argv[1], &noself);
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,log_command_name,
|
||||
HelloFilter_LogCommand,"write deny-oom",1,1,1) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,ping_command_name,
|
||||
HelloFilter_PingCommand,"deny-oom",1,1,1) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,unregister_command_name,
|
||||
HelloFilter_UnregisterCommand,"write deny-oom",1,1,1) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if ((filter = RedisModule_RegisterCommandFilter(ctx, HelloFilter_CommandFilter,
|
||||
noself ? REDISMODULE_CMDFILTER_NOSELF : 0))
|
||||
== NULL) return REDISMODULE_ERR;
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
@ -133,6 +133,11 @@
|
||||
* of timers that are going to expire, sorted by expire time. */
|
||||
typedef uint64_t RedisModuleTimerID;
|
||||
|
||||
/* CommandFilter Flags */
|
||||
|
||||
/* Do filter RedisModule_Call() commands initiated by module itself. */
|
||||
#define REDISMODULE_CMDFILTER_NOSELF (1<<0)
|
||||
|
||||
/* ------------------------- End of common defines ------------------------ */
|
||||
|
||||
#ifndef REDISMODULE_CORE
|
||||
@ -151,6 +156,8 @@ typedef struct RedisModuleBlockedClient RedisModuleBlockedClient;
|
||||
typedef struct RedisModuleClusterInfo RedisModuleClusterInfo;
|
||||
typedef struct RedisModuleDict RedisModuleDict;
|
||||
typedef struct RedisModuleDictIter RedisModuleDictIter;
|
||||
typedef struct RedisModuleCommandFilterCtx RedisModuleCommandFilterCtx;
|
||||
typedef struct RedisModuleCommandFilter RedisModuleCommandFilter;
|
||||
|
||||
typedef int (*RedisModuleCmdFunc)(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
|
||||
typedef void (*RedisModuleDisconnectFunc)(RedisModuleCtx *ctx, RedisModuleBlockedClient *bc);
|
||||
@ -163,6 +170,7 @@ typedef void (*RedisModuleTypeDigestFunc)(RedisModuleDigest *digest, void *value
|
||||
typedef void (*RedisModuleTypeFreeFunc)(void *value);
|
||||
typedef void (*RedisModuleClusterMessageReceiver)(RedisModuleCtx *ctx, const char *sender_id, uint8_t type, const unsigned char *payload, uint32_t len);
|
||||
typedef void (*RedisModuleTimerProc)(RedisModuleCtx *ctx, void *data);
|
||||
typedef void (*RedisModuleCommandFilterFunc) (RedisModuleCommandFilterCtx *filter);
|
||||
|
||||
#define REDISMODULE_TYPE_METHOD_VERSION 1
|
||||
typedef struct RedisModuleTypeMethods {
|
||||
@ -339,6 +347,13 @@ void REDISMODULE_API_FUNC(RedisModule_SetDisconnectCallback)(RedisModuleBlockedC
|
||||
void REDISMODULE_API_FUNC(RedisModule_SetClusterFlags)(RedisModuleCtx *ctx, uint64_t flags);
|
||||
int REDISMODULE_API_FUNC(RedisModule_ExportSharedAPI)(RedisModuleCtx *ctx, const char *apiname, void *func);
|
||||
void *REDISMODULE_API_FUNC(RedisModule_GetSharedAPI)(RedisModuleCtx *ctx, const char *apiname);
|
||||
RedisModuleCommandFilter *REDISMODULE_API_FUNC(RedisModule_RegisterCommandFilter)(RedisModuleCtx *ctx, RedisModuleCommandFilterFunc cb, int flags);
|
||||
int REDISMODULE_API_FUNC(RedisModule_UnregisterCommandFilter)(RedisModuleCtx *ctx, RedisModuleCommandFilter *filter);
|
||||
int REDISMODULE_API_FUNC(RedisModule_CommandFilterArgsCount)(RedisModuleCommandFilterCtx *fctx);
|
||||
const RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CommandFilterArgGet)(RedisModuleCommandFilterCtx *fctx, int pos);
|
||||
int REDISMODULE_API_FUNC(RedisModule_CommandFilterArgInsert)(RedisModuleCommandFilterCtx *fctx, int pos, RedisModuleString *arg);
|
||||
int REDISMODULE_API_FUNC(RedisModule_CommandFilterArgReplace)(RedisModuleCommandFilterCtx *fctx, int pos, RedisModuleString *arg);
|
||||
int REDISMODULE_API_FUNC(RedisModule_CommandFilterArgDelete)(RedisModuleCommandFilterCtx *fctx, int pos);
|
||||
#endif
|
||||
|
||||
/* This is included inline inside each Redis module. */
|
||||
@ -502,6 +517,13 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
|
||||
REDISMODULE_GET_API(SetClusterFlags);
|
||||
REDISMODULE_GET_API(ExportSharedAPI);
|
||||
REDISMODULE_GET_API(GetSharedAPI);
|
||||
REDISMODULE_GET_API(RegisterCommandFilter);
|
||||
REDISMODULE_GET_API(UnregisterCommandFilter);
|
||||
REDISMODULE_GET_API(CommandFilterArgsCount);
|
||||
REDISMODULE_GET_API(CommandFilterArgGet);
|
||||
REDISMODULE_GET_API(CommandFilterArgInsert);
|
||||
REDISMODULE_GET_API(CommandFilterArgReplace);
|
||||
REDISMODULE_GET_API(CommandFilterArgDelete);
|
||||
#endif
|
||||
|
||||
if (RedisModule_IsModuleNameBusy && RedisModule_IsModuleNameBusy(name)) return REDISMODULE_ERR;
|
||||
|
@ -462,6 +462,11 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) {
|
||||
c->argc = argc;
|
||||
c->user = server.lua_caller->user;
|
||||
|
||||
/* Process module hooks */
|
||||
moduleCallCommandFilters(c);
|
||||
argv = c->argv;
|
||||
argc = c->argc;
|
||||
|
||||
/* Log the command if debugging is active. */
|
||||
if (ldb.active && ldb.step) {
|
||||
sds cmdlog = sdsnew("<redis>");
|
||||
|
@ -3268,6 +3268,8 @@ void call(client *c, int flags) {
|
||||
* other operations can be performed by the caller. Otherwise
|
||||
* if C_ERR is returned the client was destroyed (i.e. after QUIT). */
|
||||
int processCommand(client *c) {
|
||||
moduleCallCommandFilters(c);
|
||||
|
||||
/* The QUIT command is handled separately. Normal command procs will
|
||||
* go through checking for replication and QUIT will cause trouble
|
||||
* when FORCE_REPLICATION is enabled and would be implemented in
|
||||
|
@ -1492,7 +1492,7 @@ size_t moduleCount(void);
|
||||
void moduleAcquireGIL(void);
|
||||
void moduleReleaseGIL(void);
|
||||
void moduleNotifyKeyspaceEvent(int type, const char *event, robj *key, int dbid);
|
||||
|
||||
void moduleCallCommandFilters(client *c);
|
||||
|
||||
/* Utils */
|
||||
long long ustime(void);
|
||||
|
84
tests/modules/commandfilter.tcl
Normal file
84
tests/modules/commandfilter.tcl
Normal file
@ -0,0 +1,84 @@
|
||||
set testmodule [file normalize src/modules/hellofilter.so]
|
||||
|
||||
start_server {tags {"modules"}} {
|
||||
r module load $testmodule log-key 0
|
||||
|
||||
test {Command Filter handles redirected commands} {
|
||||
r set mykey @log
|
||||
r lrange log-key 0 -1
|
||||
} "{set mykey @log}"
|
||||
|
||||
test {Command Filter can call RedisModule_CommandFilterArgDelete} {
|
||||
r rpush mylist elem1 @delme elem2
|
||||
r lrange mylist 0 -1
|
||||
} {elem1 elem2}
|
||||
|
||||
test {Command Filter can call RedisModule_CommandFilterArgInsert} {
|
||||
r del mylist
|
||||
r rpush mylist elem1 @insertbefore elem2 @insertafter elem3
|
||||
r lrange mylist 0 -1
|
||||
} {elem1 --inserted-before-- @insertbefore elem2 @insertafter --inserted-after-- elem3}
|
||||
|
||||
test {Command Filter can call RedisModule_CommandFilterArgReplace} {
|
||||
r del mylist
|
||||
r rpush mylist elem1 @replaceme elem2
|
||||
r lrange mylist 0 -1
|
||||
} {elem1 --replaced-- elem2}
|
||||
|
||||
test {Command Filter applies on RM_Call() commands} {
|
||||
r del log-key
|
||||
r hellofilter.ping
|
||||
r lrange log-key 0 -1
|
||||
} "{ping @log}"
|
||||
|
||||
test {Command Filter applies on Lua redis.call()} {
|
||||
r del log-key
|
||||
r eval "redis.call('ping', '@log')" 0
|
||||
r lrange log-key 0 -1
|
||||
} "{ping @log}"
|
||||
|
||||
test {Command Filter applies on Lua redis.call() that calls a module} {
|
||||
r del log-key
|
||||
r eval "redis.call('hellofilter.ping')" 0
|
||||
r lrange log-key 0 -1
|
||||
} "{ping @log}"
|
||||
|
||||
test {Command Filter is unregistered implicitly on module unload} {
|
||||
r del log-key
|
||||
r module unload hellofilter
|
||||
r set mykey @log
|
||||
r lrange log-key 0 -1
|
||||
} {}
|
||||
|
||||
r module load $testmodule log-key 0
|
||||
|
||||
test {Command Filter unregister works as expected} {
|
||||
# Validate reloading succeeded
|
||||
r del log-key
|
||||
r set mykey @log
|
||||
assert_equal "{set mykey @log}" [r lrange log-key 0 -1]
|
||||
|
||||
# Unregister
|
||||
r hellofilter.unregister
|
||||
r del log-key
|
||||
|
||||
r set mykey @log
|
||||
r lrange log-key 0 -1
|
||||
} {}
|
||||
|
||||
r module unload hellofilter
|
||||
r module load $testmodule log-key 1
|
||||
|
||||
test {Command Filter REDISMODULE_CMDFILTER_NOSELF works as expected} {
|
||||
r set mykey @log
|
||||
assert_equal "{set mykey @log}" [r lrange log-key 0 -1]
|
||||
|
||||
r del log-key
|
||||
r hellofilter.ping
|
||||
assert_equal {} [r lrange log-key 0 -1]
|
||||
|
||||
r eval "redis.call('hellofilter.ping')" 0
|
||||
assert_equal {} [r lrange log-key 0 -1]
|
||||
}
|
||||
|
||||
}
|
@ -63,6 +63,7 @@ set ::all_tests {
|
||||
unit/lazyfree
|
||||
unit/wait
|
||||
unit/pendingquerybuf
|
||||
modules/commandfilter
|
||||
}
|
||||
# Index to the next test to run in the ::all_tests list.
|
||||
set ::next_test 0
|
||||
|
Loading…
x
Reference in New Issue
Block a user