diff --git a/README.markdown b/README.markdown index d523137..68b8e4c 100644 --- a/README.markdown +++ b/README.markdown @@ -21,6 +21,7 @@ curl -d "GET/hello" http://127.0.0.1:7379/ * Raw Redis 2.0 protocol output with `?format=raw` * HTTP 1.1 pipelining (45 kqps on a desktop Linux machine.) * Connects to Redis using a TCP or UNIX socket. +* Restricted commands by IP range (CIDR subnet + mask), returning 403 errors. # Ideas, TODO... * Add meta-data info per key (MIME type in a second key, for instance). @@ -30,9 +31,7 @@ curl -d "GET/hello" http://127.0.0.1:7379/ * Add logging. * Enrich config file: * Provide timeout (this needs to be added to hiredis first.) - * Restrict commands by IP range * Get config file path from command line. -* Change config file to JSON format? That would be convenient. * Send your ideas using the github tracker or on twitter [@yowgi](http://twitter.com/yowgi). # HTTP error codes @@ -41,6 +40,7 @@ curl -d "GET/hello" http://127.0.0.1:7379/ * Could also be used: * Timeout on the redis side: 503 Service Unavailable * Missing key: 404 Not Found + * Unauthorized command (disabled in config file): 403 Forbidden # Command format The URI `/COMMAND/arg0/arg1/.../argN` executes the command on Redis and returns the response to the client. GET and POST are supported: diff --git a/cmd.c b/cmd.c index 0c3053a..b9e09b0 100644 --- a/cmd.c +++ b/cmd.c @@ -1,5 +1,6 @@ #include "cmd.h" #include "server.h" +#include "conf.h" #include "formats/json.h" #include "formats/raw.h" @@ -7,6 +8,8 @@ #include #include #include +#include +#include struct cmd * cmd_new(struct evhttp_request *rq, int count) { @@ -32,7 +35,39 @@ cmd_free(struct cmd *c) { free(c); } -void +int +cmd_authorized(struct conf *cfg, struct evhttp_request *rq, const char *verb, size_t verb_len) { + + struct disabled_command *dc; + unsigned int i; + + char *client_ip; + u_short client_port; + in_addr_t client_addr; + + /* find client's address */ + evhttp_connection_get_peer(rq->evcon, &client_ip, &client_port); + client_addr = ntohl(inet_addr(client_ip)); + + for(dc = cfg->disabled; dc; dc = dc->next) { + /* CIDR test */ + + if((client_addr & dc->mask) != (dc->subnet & dc->mask)) { + continue; + } + + /* matched an ip */ + for(i = 0; i < dc->count; ++i) { + if(strncasecmp(dc->commands[i], verb, verb_len) == 0) { + return 0; + } + } + } + + return 1; +} + +int cmd_run(struct server *s, struct evhttp_request *rq, const char *uri, size_t uri_len) { @@ -71,9 +106,14 @@ cmd_run(struct server *s, struct evhttp_request *rq, cmd->argv[0] = uri; cmd->argv_len[0] = cmd_len; + /* check that the client is able to run this command */ + if(!cmd_authorized(s->cfg, rq, cmd->argv[0], cmd->argv_len[0])) { + return -1; + } + if(!slash) { redisAsyncCommandArgv(s->ac, fun, cmd, 1, cmd->argv, cmd->argv_len); - return; + return 0; } p = slash + 1; while(p < uri + uri_len) { @@ -97,6 +137,7 @@ cmd_run(struct server *s, struct evhttp_request *rq, } redisAsyncCommandArgv(s->ac, fun, cmd, param_count, cmd->argv, cmd->argv_len); + return 0; } diff --git a/cmd.h b/cmd.h index 74af65c..bc586fd 100644 --- a/cmd.h +++ b/cmd.h @@ -28,7 +28,7 @@ cmd_new(struct evhttp_request *rq, int count); void cmd_free(struct cmd *c); -void +int cmd_run(struct server *s, struct evhttp_request *rq, const char *uri, size_t uri_len); diff --git a/conf.c b/conf.c index dfaabfe..bd8dde8 100644 --- a/conf.c +++ b/conf.c @@ -66,7 +66,7 @@ conf_disable_commands(json_t *jtab) { unsigned int i, cur, n; char *p, *ip; const char *s; - in_addr_t mask_ip; + in_addr_t mask, subnet; short mask_bits = 0; struct disabled_command *dc; @@ -78,7 +78,7 @@ conf_disable_commands(json_t *jtab) { /* parse key in format "ip/mask" */ s = json_object_iter_key(kv); - p = strchr(s, ':'); + p = strchr(s, '/'); if(!p) { ip = strdup(s); } else { @@ -86,8 +86,8 @@ conf_disable_commands(json_t *jtab) { memcpy(ip, s, p - s); mask_bits = atoi(p+1); } - mask_ip = inet_addr(ip); - + mask = (mask_bits == 0 ? 0 : (0xffffffff << (32 - mask_bits))); + subnet = ntohl(inet_addr(ip)) & mask; /* count strings in the array */ n = 0; @@ -101,8 +101,9 @@ conf_disable_commands(json_t *jtab) { /* allocate block */ dc = calloc(1, sizeof(struct disabled_command)); dc->commands = calloc(n, sizeof(char*)); - dc->mask_ip = mask_ip; - dc->mask_bits = mask_bits; + dc->subnet = subnet; + dc->mask = mask; + dc->count = n; dc->next = root; root = dc; diff --git a/conf.h b/conf.h index b38c6c1..cde415f 100644 --- a/conf.h +++ b/conf.h @@ -6,9 +6,10 @@ struct disabled_command { - in_addr_t mask_ip; - short mask_bits; + in_addr_t subnet; + in_addr_t mask; + unsigned int count; char **commands; struct disabled_command *next; diff --git a/turnip.c b/turnip.c index adf967a..097c81c 100644 --- a/turnip.c +++ b/turnip.c @@ -18,19 +18,22 @@ on_request(struct evhttp_request *rq, void *ctx) { const char *uri = evhttp_request_uri(rq); struct server *s = ctx; + int ret; if(!s->ac) { /* redis is unavailable */ evhttp_send_reply(rq, 503, "Service Unavailable", NULL); return; } + /* check that the command can be executed */ + switch(rq->type) { case EVHTTP_REQ_GET: - cmd_run(s, rq, 1+uri, strlen(uri)-1); + ret = cmd_run(s, rq, 1+uri, strlen(uri)-1); break; case EVHTTP_REQ_POST: - cmd_run(s, rq, + ret = cmd_run(s, rq, (const char*)EVBUFFER_DATA(rq->input_buffer), EVBUFFER_LENGTH(rq->input_buffer)); break; @@ -39,6 +42,10 @@ on_request(struct evhttp_request *rq, void *ctx) { evhttp_send_reply(rq, 405, "Method Not Allowed", NULL); return; } + + if(ret < 0) { + evhttp_send_reply(rq, 403, "Forbidden", NULL); + } } int diff --git a/turnip.conf b/turnip.conf deleted file mode 100644 index b2d37cb..0000000 --- a/turnip.conf +++ /dev/null @@ -1,5 +0,0 @@ -redis_host 127.0.0.1 -redis_port 6379 - -http_host 0.0.0.0 -http_port 7379 diff --git a/turnip.json b/turnip.json index 1227414..5df0381 100644 --- a/turnip.json +++ b/turnip.json @@ -6,6 +6,6 @@ "http_port": 7379, "disable": { - "255.255.255.255/32": ["DEBUG", "FLUSHDB", "FLUSHALL"] + "0.0.0.0/0": ["DEBUG", "FLUSHDB", "FLUSHALL"] } }