Merge branch 'master' into slog

master
Nicolas Favre-Felix 14 years ago
commit 72a2cbf73f

@ -17,17 +17,15 @@ curl -d "GET/hello" http://127.0.0.1:7379/
# Features
* GET and POST are supported.
* JSON output by default, optional JSONP parameter.
* Raw Redis 2.0 protocol output with `?format=raw`
* JSON output by default, optional JSONP parameter (`?jsonp=myFunction`).
* Raw Redis 2.0 protocol output with `.raw` suffix
* HTTP 1.1 pipelining (50,000 http requests per second on a desktop Linux machine.)
* Connects to Redis using a TCP or UNIX socket.
* Restricted commands by IP range (CIDR subnet + mask) or HTTP Basic Auth, returning 403 errors.
* Possible Redis authentication in the config file.
* Pub/Sub using `Transfer-Encoding: chunked`, works with JSONP as well. Webdis can be used as a Comet server.
* Drop privileges on startup.
* For `GET` commands:
* MIME type in a second key with `/GET/k?typeKey=type-k`. This will transform the `GET` request into `MGET` and fetch both `k` and `type-k`. If `type-k` is a string, it will be used as Content-Type in the response. If the key doesn't exist or isn't a string, `binary/octet-stream` is used instead.
* Custom MIME type with `?type=text/plain` (or any other MIME type).
* Custom Content-Type using a pre-defined file extension, or with `?type=some/thing`.
* URL-encoded parameters for binary data or slashes. For instance, `%2f` is decoded as `/` but not used as a command separator.
# Ideas, TODO...
@ -38,8 +36,10 @@ curl -d "GET/hello" http://127.0.0.1:7379/
* Enrich config file:
* Provide timeout (this needs to be added to hiredis first.)
* Multi-server support, using consistent hashing.
* Add WebSocket support (with which protocol?)
* Allow cross-origin XHR.
* Allow file upload with PUT? Saving a file in Redis using the `SET` command should be easy to do with cURL.
* Send your ideas using the github tracker, on twitter [@yowgi](http://twitter.com/yowgi) or by mail to n.favrefelix@gmail.com.
* Add WebSocket support, allow cross-origin XHR.
# HTTP error codes
* Unknown HTTP verb: 405 Method Not Allowed
@ -51,11 +51,15 @@ curl -d "GET/hello" http://127.0.0.1:7379/
* 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:
The URI `/COMMAND/arg0/arg1/.../argN.ext` executes the command on Redis and returns the response to the client. GET and POST are supported:
* `GET /COMMAND/arg0/.../argN`
* `GET /COMMAND/arg0/.../argN.ext`
* `POST /` with `COMMAND/arg0/.../argN` in the HTTP body.
`.ext` is an optional extension; it is not read as part of the last argument but only represents the output format. Several formats are available (see below).
Special characters: `/` and `.` have special meanings, `/` separates arguments and `.` changes the Content-Type. They can be replaced by `%2f` and `%2e`, respectively.
# ACL
Access control is configured in `webdis.json`. Each configuration tries to match a client profile according to two criterias:
@ -68,6 +72,7 @@ Examples:
{
"disabled": ["DEBUG", "FLUSHDB", "FLUSHALL"],
},
{
"http_basic_auth": "user:password",
"disabled": ["DEBUG", "FLUSHDB", "FLUSHALL"],
@ -115,24 +120,23 @@ $ curl http://127.0.0.1:7379/MAKE-ME-COFFEE
// JSONP callback:
$ curl "http://127.0.0.1:7379/TYPE/y?jsonp=myCustomFunction"
myCustomFunction({"TYPE":[true,"string"]})
</pre>
# RAW output
This is the raw output of Redis; enable it with `?format=raw`.
This is the raw output of Redis; enable it with the `.raw` suffix.
<pre>
// string
$ curl http://127.0.0.1:7379/GET/z?format=raw
$ curl http://127.0.0.1:7379/GET/z.raw
$5
hello
// number
curl http://127.0.0.1:7379/INCR/a?format=raw
curl http://127.0.0.1:7379/INCR/a.raw
:2
// list
$ curl http://127.0.0.1:7379/LRANGE/x/0/-1?format=raw
$ curl http://127.0.0.1:7379/LRANGE/x/0/-1.raw
*2
$3
abc
@ -140,22 +144,28 @@ $3
def
// status
$ curl http://127.0.0.1:7379/TYPE/y?format=raw
$ curl http://127.0.0.1:7379/TYPE/y.raw
+zset
// error, which is basically a status
$ curl http://127.0.0.1:7379/MAKE-ME-COFFEE?format=raw
$ curl http://127.0.0.1:7379/MAKE-ME-COFFEE.raw
-ERR unknown command 'MAKE-ME-COFFEE'
</pre>
# Custom content-type
Webdis can serve `GET` requests with a custom content-type. There are two ways of doing this; the content-type can be in a key that is fetched with the content, or given as a query string parameter.
Several content-types are available:
**Content-Type in parameter:**
* `.json` for `application/json` (this is the default Content-Type).
* `.txt` for `text/plain`
* `.html` for `text/html`
* `xhtml` for `application/xhtml+xml`
* `xml` for `text/xml`
* `.png` for `image/png`
* `jpg` or `jpeg` for `image/jpeg`
* Any other with the `?type=anything/youwant` query string.
<pre>
curl -v "http://127.0.0.1:7379/GET/hello.html?type=text/html"
curl -v "http://127.0.0.1:7379/GET/hello.html"
[...]
&lt; HTTP/1.1 200 OK
&lt; Content-Type: text/html
@ -164,28 +174,22 @@ curl -v "http://127.0.0.1:7379/GET/hello.html?type=text/html"
&lt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
...
[...]
&lt;/html&gt;
</pre>
**Content-Type in a separate key:**
<pre>
curl "http://127.0.0.1:7379/SET/hello.type/text%2fhtml"
{"SET":[true,"OK"]}
curl "http://127.0.0.1:7379/GET/hello.type"
{"GET":"text/html"}
curl -v "http://127.0.0.1:7379/GET/hello.html?typeKey=hello.type"
curl -v "http://127.0.0.1:7379/GET/hello.txt"
[...]
&lt; HTTP/1.1 200 OK
&lt; Content-Type: text/html
&lt; Date: Mon, 03 Jan 2011 20:56:43 GMT
&lt; Content-Type: text/plain
&lt; Date: Mon, 03 Jan 2011 20:43:36 GMT
&lt; Content-Length: 137
&lt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
...
&lt;/html&gt;
[...]
curl -v "http://127.0.0.1:7379/GET/big-file?type=application/pdf"
[...]
&lt; HTTP/1.1 200 OK
&lt; Content-Type: application/pdf
&lt; Date: Mon, 03 Jan 2011 20:45:12 GMT
[...]
</pre>

102
cmd.c

@ -33,8 +33,7 @@ cmd_free(struct cmd *c) {
free(c->argv);
free(c->argv_len);
free(c->mime);
free(c->mimeKey);
if(c->mime_free) free(c->mime);
free(c);
}
@ -100,7 +99,6 @@ cmd_run(struct server *s, struct evhttp_request *rq,
struct cmd *cmd;
formatting_fun f_format;
transform_fun f_transform = NULL;
/* count arguments */
if(qmark) {
@ -112,6 +110,13 @@ cmd_run(struct server *s, struct evhttp_request *rq,
cmd = cmd_new(rq, param_count);
/* parse URI parameters */
evhttp_parse_query(uri, &cmd->uri_params);
/* get output formatting function */
uri_len = cmd_select_format(cmd, uri, uri_len, &f_format);
/* check if we only have one command or more. */
slash = memchr(uri, '/', uri_len);
if(slash) {
cmd_len = slash - uri;
@ -119,12 +124,6 @@ cmd_run(struct server *s, struct evhttp_request *rq,
cmd_len = uri_len;
}
/* parse URI parameters */
evhttp_parse_query(uri, &cmd->uri_params);
/* get output formatting function */
cmd_read_params(cmd, &f_format, &f_transform);
/* there is always a first parameter, it's the command name */
cmd->argv[0] = uri;
cmd->argv_len[0] = cmd_len;
@ -138,9 +137,9 @@ cmd_run(struct server *s, struct evhttp_request *rq,
/* check if we have to split the connection */
if(cmd_is_subscribe(cmd)) {
struct pubsub_client *ps;
ps = calloc(1, sizeof(struct pubsub_client));
ps->s = s = server_copy(s);
ps->rq = rq;
evhttp_connection_set_closecb(rq->evcon, on_http_disconnect, ps);
@ -170,50 +169,85 @@ cmd_run(struct server *s, struct evhttp_request *rq,
cur_param++;
}
/* transform command if we need to. */
if(f_transform) f_transform(cmd);
/* push command to Redis. */
redisAsyncCommandArgv(s->ac, f_format, cmd, cmd->count, cmd->argv, cmd->argv_len);
for(i = 1; i < cmd->count; ++i) {
for(i = 1; i < cur_param; ++i) {
free((char*)cmd->argv[i]);
}
return 0;
}
/**
* Return 2 functions, one to format the reply and
* one to transform the command before processing it.
* Select Content-Type and processing function.
*/
void
cmd_read_params(struct cmd *cmd, formatting_fun *f_format, transform_fun *f_transform) {
int
cmd_select_format(struct cmd *cmd, const char *uri, size_t uri_len, formatting_fun *f_format) {
struct evkeyval *kv;
/* defaults */
const char *ext;
int ext_len = -1;
unsigned int i;
/* those are the available reply formats */
struct reply_format {
const char *s;
size_t sz;
formatting_fun f;
const char *ct;
};
struct reply_format funs[] = {
{.s = "json", .sz = 4, .f = json_reply, .ct = "application/json"},
{.s = "raw", .sz = 3, .f = raw_reply, .ct = "binary/octet-stream"},
{.s = "txt", .sz = 3, .f = custom_type_reply, .ct = "text/plain"},
{.s = "html", .sz = 4, .f = custom_type_reply, .ct = "text/html"},
{.s = "xhtml", .sz = 5, .f = custom_type_reply, .ct = "application/xhtml+xml"},
{.s = "xml", .sz = 3, .f = custom_type_reply, .ct = "text/xml"},
{.s = "png", .sz = 3, .f = custom_type_reply, .ct = "image/png"},
{.s = "jpg", .sz = 3, .f = custom_type_reply, .ct = "image/jpeg"},
{.s = "jpeg", .sz = 4, .f = custom_type_reply, .ct = "image/jpeg"},
};
/* default */
*f_format = json_reply;
*f_transform = NULL;
/* loop over the query string */
TAILQ_FOREACH(kv, &cmd->uri_params, next) {
if(strcmp(kv->key, "format") == 0) { /* output format */
if(strcmp(kv->value, "raw") == 0) {
*f_format = raw_reply;
} else if(strcmp(kv->value, "json") == 0) {
*f_format = json_reply;
}
/* find extension */
for(ext = uri + uri_len - 1; ext != uri && *ext != '/'; --ext) {
if(*ext == '.') {
ext++;
ext_len = uri + uri_len - ext;
break;
} else if(strcmp(kv->key, "typeKey") == 0) { /* MIME type in a key. */
cmd->mimeKey = strdup(kv->value);
*f_transform = custom_type_process_cmd;
}
}
if(!ext_len) return uri_len; /* nothing found */
/* find function for the given extension */
for(i = 0; i < sizeof(funs)/sizeof(funs[0]); ++i) {
if(ext_len == (int)funs[i].sz && strncmp(ext, funs[i].s, ext_len) == 0) {
if(cmd->mime_free) free(cmd->mime);
cmd->mime = (char*)funs[i].ct;
cmd->mime_free = 0;
*f_format = funs[i].f;
}
}
/* the user can force it with ?type=some/thing */
TAILQ_FOREACH(kv, &cmd->uri_params, next) {
if(strcmp(kv->key, "type") == 0) {
*f_format = custom_type_reply;
} else if(strcmp(kv->key, "type") == 0) { /* MIME type directly in parameter */
cmd->mime = strdup(kv->value);
*f_format = custom_type_reply;
cmd->mime_free = 1;
break;
}
}
return uri_len - ext_len - 1;
}
int

@ -12,7 +12,6 @@ struct server;
struct cmd;
typedef void (*formatting_fun)(redisAsyncContext *, void *, void *);
typedef void (*transform_fun)(struct cmd *);
struct cmd {
@ -27,7 +26,7 @@ struct cmd {
/* HTTP data */
char *mime;
char *mimeKey;
int mime_free;
char *if_none_match;
};
@ -47,8 +46,8 @@ int
cmd_run(struct server *s, struct evhttp_request *rq,
const char *uri, size_t uri_len);
void
cmd_read_params(struct cmd *cmd, formatting_fun *f_format, transform_fun *f_transform);
int
cmd_select_format(struct cmd *cmd, const char *uri, size_t uri_len, formatting_fun *f_format);
int
cmd_is_subscribe(struct cmd *cmd);

@ -5,6 +5,7 @@
#include <evhttp.h>
#include <string.h>
/* TODO: replace this with a faster hash function */
char *etag_new(const char *p, size_t sz) {
md5_byte_t buf[16];

@ -6,71 +6,33 @@
#include <hiredis/hiredis.h>
#include <hiredis/async.h>
static void
custom_400(struct cmd *cmd) {
evhttp_send_reply(cmd->rq, 400, "Bad request", NULL);
cmd_free(cmd);
}
void
custom_type_reply(redisAsyncContext *c, void *r, void *privdata) {
redisReply *reply = r;
struct cmd *cmd = privdata;
char *ct;
(void)c;
evhttp_clear_headers(&cmd->uri_params);
if (reply == NULL) {
if(reply == NULL) {
evhttp_send_reply(cmd->rq, 404, "Not Found", NULL);
return;
}
if(cmd->mime) { /* use the given content-type */
if(reply->type != REDIS_REPLY_STRING) {
custom_400(cmd);
return;
}
format_send_reply(cmd, reply->str, reply->len, cmd->mime);
return;
}
if(cmd->mime) { /* use the given content-type, but only for strings */
switch(reply->type) {
/* we expect array(string, string) */
if(!cmd->mimeKey || reply->type != REDIS_REPLY_ARRAY || reply->elements != 2 || reply->element[0]->type != REDIS_REPLY_STRING) {
custom_400(cmd);
return;
}
case REDIS_REPLY_NIL: /* or nil values */
format_send_reply(cmd, "", 0, cmd->mime);
return;
/* case of MGET, we need to have a string for content-type in element[1] */
if(reply->element[1]->type == REDIS_REPLY_STRING) {
ct = reply->element[1]->str;
} else {
ct = "binary/octet-stream";
case REDIS_REPLY_STRING:
format_send_reply(cmd, reply->str, reply->len, cmd->mime);
return;
}
}
/* send reply */
format_send_reply(cmd, reply->element[0]->str, reply->element[0]->len, ct);
return;
/* couldn't make sense of what the client wanted. */
evhttp_send_reply(cmd->rq, 400, "Bad request", NULL);
cmd_free(cmd);
}
/* This will change a GET command into MGET if a key is provided to get the response MIME-type from. */
void
custom_type_process_cmd(struct cmd *cmd) {
/* MGET if mode is “custom” */
if(cmd->count == 2 && cmd->argv_len[0] == 3 &&
strncasecmp(cmd->argv[0], "GET", 3) == 0 && cmd->mimeKey) {
cmd->count++; /* space for content-type key */
cmd->argv = realloc(cmd->argv, cmd->count * sizeof(char*));
cmd->argv_len = realloc(cmd->argv_len, cmd->count * sizeof(size_t));
/* replace command with MGET */
cmd->argv[0] = "MGET";
cmd->argv_len[0] = 4;
/* add mime key after the key. */
cmd->argv[2] = strdup(cmd->mimeKey);
cmd->argv_len[2] = strlen(cmd->mimeKey);
}
}

@ -9,7 +9,4 @@ struct cmd;
void
custom_type_reply(redisAsyncContext *c, void *r, void *privdata);
void
custom_type_process_cmd(struct cmd *cmd);
#endif

@ -18,8 +18,6 @@ raw_reply(redisAsyncContext *c, void *r, void *privdata) {
size_t sz;
(void)c;
evhttp_clear_headers(&cmd->uri_params);
if (reply == NULL) {
evhttp_send_reply(cmd->rq, 404, "Not Found", NULL);
return;

@ -122,7 +122,6 @@ on_request(struct evhttp_request *rq, void *ctx) {
int ret;
if(!s->ac) { /* redis is unavailable */
printf("503\n");
evhttp_send_reply(rq, 503, "Service Unavailable", NULL);
return;
}

Loading…
Cancel
Save