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 *types; /* Module data types. */
|
||||||
list *usedby; /* List of modules using APIs from this one. */
|
list *usedby; /* List of modules using APIs from this one. */
|
||||||
list *using; /* List of modules we use some APIs of. */
|
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;
|
typedef struct RedisModule RedisModule;
|
||||||
|
|
||||||
@ -270,6 +272,25 @@ typedef struct RedisModuleDictIter {
|
|||||||
raxIterator ri;
|
raxIterator ri;
|
||||||
} RedisModuleDictIter;
|
} 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
|
* Prototypes
|
||||||
* -------------------------------------------------------------------------- */
|
* -------------------------------------------------------------------------- */
|
||||||
@ -731,6 +752,7 @@ void RM_SetModuleAttribs(RedisModuleCtx *ctx, const char *name, int ver, int api
|
|||||||
module->types = listCreate();
|
module->types = listCreate();
|
||||||
module->usedby = listCreate();
|
module->usedby = listCreate();
|
||||||
module->using = listCreate();
|
module->using = listCreate();
|
||||||
|
module->filters = listCreate();
|
||||||
ctx->module = module;
|
ctx->module = module;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2724,12 +2746,6 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
|
|||||||
RedisModuleCallReply *reply = NULL;
|
RedisModuleCallReply *reply = NULL;
|
||||||
int replicate = 0; /* Replicate this command? */
|
int replicate = 0; /* Replicate this command? */
|
||||||
|
|
||||||
cmd = lookupCommandByCString((char*)cmdname);
|
|
||||||
if (!cmd) {
|
|
||||||
errno = EINVAL;
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Create the client and dispatch the command. */
|
/* Create the client and dispatch the command. */
|
||||||
va_start(ap, fmt);
|
va_start(ap, fmt);
|
||||||
c = createClient(-1);
|
c = createClient(-1);
|
||||||
@ -2743,11 +2759,25 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
|
|||||||
c->db = ctx->client->db;
|
c->db = ctx->client->db;
|
||||||
c->argv = argv;
|
c->argv = argv;
|
||||||
c->argc = argc;
|
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 handle the above format error only when the client is setup so that
|
||||||
* we can free it normally. */
|
* we can free it normally. */
|
||||||
if (argv == NULL) goto cleanup;
|
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. */
|
/* Basic arity checks. */
|
||||||
if ((cmd->arity > 0 && cmd->arity != argc) || (argc < -cmd->arity)) {
|
if ((cmd->arity > 0 && cmd->arity != argc) || (argc < -cmd->arity)) {
|
||||||
errno = EINVAL;
|
errno = EINVAL;
|
||||||
@ -2797,6 +2827,7 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
|
|||||||
autoMemoryAdd(ctx,REDISMODULE_AM_REPLY,reply);
|
autoMemoryAdd(ctx,REDISMODULE_AM_REPLY,reply);
|
||||||
|
|
||||||
cleanup:
|
cleanup:
|
||||||
|
if (ctx->module) ctx->module->in_call--;
|
||||||
freeClient(c);
|
freeClient(c);
|
||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
@ -4778,6 +4809,214 @@ int moduleUnregisterUsedAPI(RedisModule *module) {
|
|||||||
return count;
|
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
|
* Modules API internals
|
||||||
* -------------------------------------------------------------------------- */
|
* -------------------------------------------------------------------------- */
|
||||||
@ -4824,6 +5063,9 @@ void moduleInitModulesSystem(void) {
|
|||||||
moduleFreeContextReusedClient->flags |= CLIENT_MODULE;
|
moduleFreeContextReusedClient->flags |= CLIENT_MODULE;
|
||||||
moduleFreeContextReusedClient->user = NULL; /* root user. */
|
moduleFreeContextReusedClient->user = NULL; /* root user. */
|
||||||
|
|
||||||
|
/* Set up filter list */
|
||||||
|
moduleCommandFilters = listCreate();
|
||||||
|
|
||||||
moduleRegisterCoreAPI();
|
moduleRegisterCoreAPI();
|
||||||
if (pipe(server.module_blocked_pipe) == -1) {
|
if (pipe(server.module_blocked_pipe) == -1) {
|
||||||
serverLog(LL_WARNING,
|
serverLog(LL_WARNING,
|
||||||
@ -4873,6 +5115,7 @@ void moduleLoadFromQueue(void) {
|
|||||||
|
|
||||||
void moduleFreeModuleStructure(struct RedisModule *module) {
|
void moduleFreeModuleStructure(struct RedisModule *module) {
|
||||||
listRelease(module->types);
|
listRelease(module->types);
|
||||||
|
listRelease(module->filters);
|
||||||
sdsfree(module->name);
|
sdsfree(module->name);
|
||||||
zfree(module);
|
zfree(module);
|
||||||
}
|
}
|
||||||
@ -4964,6 +5207,7 @@ int moduleUnload(sds name) {
|
|||||||
moduleUnregisterCommands(module);
|
moduleUnregisterCommands(module);
|
||||||
moduleUnregisterSharedAPI(module);
|
moduleUnregisterSharedAPI(module);
|
||||||
moduleUnregisterUsedAPI(module);
|
moduleUnregisterUsedAPI(module);
|
||||||
|
moduleUnregisterFilters(module);
|
||||||
|
|
||||||
/* Remove any notification subscribers this module might have */
|
/* Remove any notification subscribers this module might have */
|
||||||
moduleUnsubscribeNotifications(module);
|
moduleUnsubscribeNotifications(module);
|
||||||
@ -5228,4 +5472,11 @@ void moduleRegisterCoreAPI(void) {
|
|||||||
REGISTER_API(DictCompare);
|
REGISTER_API(DictCompare);
|
||||||
REGISTER_API(ExportSharedAPI);
|
REGISTER_API(ExportSharedAPI);
|
||||||
REGISTER_API(GetSharedAPI);
|
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
|
.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:
|
.c.xo:
|
||||||
$(CC) -I. $(CFLAGS) $(SHOBJ_CFLAGS) -fPIC -c $< -o $@
|
$(CC) -I. $(CFLAGS) $(SHOBJ_CFLAGS) -fPIC -c $< -o $@
|
||||||
@ -46,6 +46,10 @@ hellotimer.so: hellotimer.xo
|
|||||||
hellodict.xo: ../redismodule.h
|
hellodict.xo: ../redismodule.h
|
||||||
|
|
||||||
hellodict.so: hellodict.xo
|
hellodict.so: hellodict.xo
|
||||||
|
|
||||||
|
hellofilter.xo: ../redismodule.h
|
||||||
|
|
||||||
|
hellofilter.so: hellofilter.xo
|
||||||
$(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lc
|
$(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lc
|
||||||
|
|
||||||
testmodule.xo: ../redismodule.h
|
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. */
|
* of timers that are going to expire, sorted by expire time. */
|
||||||
typedef uint64_t RedisModuleTimerID;
|
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 ------------------------ */
|
/* ------------------------- End of common defines ------------------------ */
|
||||||
|
|
||||||
#ifndef REDISMODULE_CORE
|
#ifndef REDISMODULE_CORE
|
||||||
@ -151,6 +156,8 @@ typedef struct RedisModuleBlockedClient RedisModuleBlockedClient;
|
|||||||
typedef struct RedisModuleClusterInfo RedisModuleClusterInfo;
|
typedef struct RedisModuleClusterInfo RedisModuleClusterInfo;
|
||||||
typedef struct RedisModuleDict RedisModuleDict;
|
typedef struct RedisModuleDict RedisModuleDict;
|
||||||
typedef struct RedisModuleDictIter RedisModuleDictIter;
|
typedef struct RedisModuleDictIter RedisModuleDictIter;
|
||||||
|
typedef struct RedisModuleCommandFilterCtx RedisModuleCommandFilterCtx;
|
||||||
|
typedef struct RedisModuleCommandFilter RedisModuleCommandFilter;
|
||||||
|
|
||||||
typedef int (*RedisModuleCmdFunc)(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
|
typedef int (*RedisModuleCmdFunc)(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
|
||||||
typedef void (*RedisModuleDisconnectFunc)(RedisModuleCtx *ctx, RedisModuleBlockedClient *bc);
|
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 (*RedisModuleTypeFreeFunc)(void *value);
|
||||||
typedef void (*RedisModuleClusterMessageReceiver)(RedisModuleCtx *ctx, const char *sender_id, uint8_t type, const unsigned char *payload, uint32_t len);
|
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 (*RedisModuleTimerProc)(RedisModuleCtx *ctx, void *data);
|
||||||
|
typedef void (*RedisModuleCommandFilterFunc) (RedisModuleCommandFilterCtx *filter);
|
||||||
|
|
||||||
#define REDISMODULE_TYPE_METHOD_VERSION 1
|
#define REDISMODULE_TYPE_METHOD_VERSION 1
|
||||||
typedef struct RedisModuleTypeMethods {
|
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);
|
void REDISMODULE_API_FUNC(RedisModule_SetClusterFlags)(RedisModuleCtx *ctx, uint64_t flags);
|
||||||
int REDISMODULE_API_FUNC(RedisModule_ExportSharedAPI)(RedisModuleCtx *ctx, const char *apiname, void *func);
|
int REDISMODULE_API_FUNC(RedisModule_ExportSharedAPI)(RedisModuleCtx *ctx, const char *apiname, void *func);
|
||||||
void *REDISMODULE_API_FUNC(RedisModule_GetSharedAPI)(RedisModuleCtx *ctx, const char *apiname);
|
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
|
#endif
|
||||||
|
|
||||||
/* This is included inline inside each Redis module. */
|
/* 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(SetClusterFlags);
|
||||||
REDISMODULE_GET_API(ExportSharedAPI);
|
REDISMODULE_GET_API(ExportSharedAPI);
|
||||||
REDISMODULE_GET_API(GetSharedAPI);
|
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
|
#endif
|
||||||
|
|
||||||
if (RedisModule_IsModuleNameBusy && RedisModule_IsModuleNameBusy(name)) return REDISMODULE_ERR;
|
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->argc = argc;
|
||||||
c->user = server.lua_caller->user;
|
c->user = server.lua_caller->user;
|
||||||
|
|
||||||
|
/* Process module hooks */
|
||||||
|
moduleCallCommandFilters(c);
|
||||||
|
argv = c->argv;
|
||||||
|
argc = c->argc;
|
||||||
|
|
||||||
/* Log the command if debugging is active. */
|
/* Log the command if debugging is active. */
|
||||||
if (ldb.active && ldb.step) {
|
if (ldb.active && ldb.step) {
|
||||||
sds cmdlog = sdsnew("<redis>");
|
sds cmdlog = sdsnew("<redis>");
|
||||||
|
@ -3268,6 +3268,8 @@ void call(client *c, int flags) {
|
|||||||
* other operations can be performed by the caller. Otherwise
|
* other operations can be performed by the caller. Otherwise
|
||||||
* if C_ERR is returned the client was destroyed (i.e. after QUIT). */
|
* if C_ERR is returned the client was destroyed (i.e. after QUIT). */
|
||||||
int processCommand(client *c) {
|
int processCommand(client *c) {
|
||||||
|
moduleCallCommandFilters(c);
|
||||||
|
|
||||||
/* The QUIT command is handled separately. Normal command procs will
|
/* The QUIT command is handled separately. Normal command procs will
|
||||||
* go through checking for replication and QUIT will cause trouble
|
* go through checking for replication and QUIT will cause trouble
|
||||||
* when FORCE_REPLICATION is enabled and would be implemented in
|
* when FORCE_REPLICATION is enabled and would be implemented in
|
||||||
|
@ -1492,7 +1492,7 @@ size_t moduleCount(void);
|
|||||||
void moduleAcquireGIL(void);
|
void moduleAcquireGIL(void);
|
||||||
void moduleReleaseGIL(void);
|
void moduleReleaseGIL(void);
|
||||||
void moduleNotifyKeyspaceEvent(int type, const char *event, robj *key, int dbid);
|
void moduleNotifyKeyspaceEvent(int type, const char *event, robj *key, int dbid);
|
||||||
|
void moduleCallCommandFilters(client *c);
|
||||||
|
|
||||||
/* Utils */
|
/* Utils */
|
||||||
long long ustime(void);
|
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/lazyfree
|
||||||
unit/wait
|
unit/wait
|
||||||
unit/pendingquerybuf
|
unit/pendingquerybuf
|
||||||
|
modules/commandfilter
|
||||||
}
|
}
|
||||||
# Index to the next test to run in the ::all_tests list.
|
# Index to the next test to run in the ::all_tests list.
|
||||||
set ::next_test 0
|
set ::next_test 0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user