diff --git a/Makefile b/Makefile index b481b5b..635ae37 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,13 @@ OUT=webdis HIREDIS_OBJ=hiredis/hiredis.o hiredis/sds.o hiredis/net.o hiredis/async.o hiredis/dict.o JANSSON_OBJ=jansson/src/dump.o jansson/src/error.o jansson/src/hashtable.o jansson/src/load.o jansson/src/strbuffer.o jansson/src/utf.o jansson/src/value.o jansson/src/variadic.o +<<<<<<< HEAD FORMAT_OBJS=formats/json.o formats/raw.o formats/common.o OBJS=webdis.o conf.o $(FORMAT_OBJS) cmd.o slog.o server.o $(HIREDIS_OBJ) $(JANSSON_OBJ) libb64/cencode.o acl.o +======= +FORMAT_OBJS=formats/json.o formats/raw.o formats/common.o formats/custom-type.o +OBJS=webdis.o conf.o $(FORMAT_OBJS) cmd.o server.o $(HIREDIS_OBJ) $(JANSSON_OBJ) libb64/cencode.o acl.o +>>>>>>> d8298c355662c701727ae270897923bdbfa58aac CFLAGS=-O3 -Wall -Wextra -I. -Ijansson/src LDFLAGS=-levent diff --git a/README.markdown b/README.markdown index 37cc4ec..2ec6eed 100644 --- a/README.markdown +++ b/README.markdown @@ -25,9 +25,12 @@ curl -d "GET/hello" http://127.0.0.1:7379/ * 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). +* URL-encoded parameters for binary data or slashes. For instance, `%2f` is decoded as `/` but not used as a command separator. # Ideas, TODO... -* Add meta-data info per key (MIME type in a second key, for instance). * Support PUT, DELETE, HEAD, OPTIONS? How? For which commands? * MULTI/EXEC/DISCARD/WATCH are disabled at the moment; find a way to use them. * Add logs. @@ -36,6 +39,7 @@ curl -d "GET/hello" http://127.0.0.1:7379/ * Provide timeout (this needs to be added to hiredis first.) * Multi-server support, using consistent hashing. * 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 @@ -143,3 +147,44 @@ $ curl http://127.0.0.1:7379/MAKE-ME-COFFEE?format=raw -ERR unknown command 'MAKE-ME-COFFEE' + +# 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. + +**Content-Type in parameter:** + +
+curl -v "http://127.0.0.1:7379/GET/hello.html?type=text/html"
+[...]
+< HTTP/1.1 200 OK
+< Content-Type: text/html
+< Date: Mon, 03 Jan 2011 20:43:36 GMT
+< Content-Length: 137
+<
+<!DOCTYPE html>
+<html>
+...
+</html>
+
+ +**Content-Type in a separate key:** + +
+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"
+[...]
+< HTTP/1.1 200 OK
+< Content-Type: text/html
+< Date: Mon, 03 Jan 2011 20:56:43 GMT
+< Content-Length: 137
+<
+<!DOCTYPE html>
+<html>
+...
+</html>
+
diff --git a/cmd.c b/cmd.c index 51c11e5..742cadd 100644 --- a/cmd.c +++ b/cmd.c @@ -5,10 +5,12 @@ #include "formats/json.h" #include "formats/raw.h" +#include "formats/custom-type.h" #include #include #include +#include struct cmd * cmd_new(struct evhttp_request *rq, int count) { @@ -31,6 +33,9 @@ cmd_free(struct cmd *c) { free(c->argv); free(c->argv_len); + free(c->mime); + free(c->mimeKey); + free(c); } @@ -53,18 +58,49 @@ void on_http_disconnect(struct evhttp_connection *evcon, void *ctx) { free(ps); } +/* taken from libevent */ +static char * +decode_uri(const char *uri, size_t length, size_t *out_len, int always_decode_plus) { + char c; + size_t i, j; + int in_query = always_decode_plus; + + char *ret = malloc(length); + + for (i = j = 0; i < length; i++) { + c = uri[i]; + if (c == '?') { + in_query = 1; + } else if (c == '+' && in_query) { + c = ' '; + } else if (c == '%' && isxdigit((unsigned char)uri[i+1]) && + isxdigit((unsigned char)uri[i+2])) { + char tmp[] = { uri[i+1], uri[i+2], '\0' }; + c = (char)strtol(tmp, NULL, 16); + i += 2; + } + ret[j++] = c; + } + *out_len = (size_t)j; + + return ret; +} + + int cmd_run(struct server *s, struct evhttp_request *rq, const char *uri, size_t uri_len) { char *qmark = strchr(uri, '?'); - char *slash = strchr(uri, '/'); + char *slash; const char *p; int cmd_len; - int param_count = 0, cur_param = 1; + int param_count = 0, cur_param = 1, i; struct cmd *cmd; - formatting_fun fun; + + formatting_fun f_format; + transform_fun f_transform = NULL; /* count arguments */ if(qmark) { @@ -76,6 +112,7 @@ cmd_run(struct server *s, struct evhttp_request *rq, cmd = cmd_new(rq, param_count); + slash = memchr(uri, '/', uri_len); if(slash) { cmd_len = slash - uri; } else { @@ -86,7 +123,7 @@ cmd_run(struct server *s, struct evhttp_request *rq, evhttp_parse_query(uri, &cmd->uri_params); /* get output formatting function */ - fun = get_formatting_function(&cmd->uri_params); + get_functions(cmd, &f_format, &f_transform); /* there is always a first parameter, it's the command name */ cmd->argv[0] = uri; @@ -109,8 +146,9 @@ cmd_run(struct server *s, struct evhttp_request *rq, evhttp_connection_set_closecb(rq->evcon, on_http_disconnect, ps); } + /* no args (e.g. INFO command) */ if(!slash) { - redisAsyncCommandArgv(s->ac, fun, cmd, 1, cmd->argv, cmd->argv_len); + redisAsyncCommandArgv(s->ac, f_format, cmd, 1, cmd->argv, cmd->argv_len); return 0; } p = slash + 1; @@ -119,45 +157,63 @@ cmd_run(struct server *s, struct evhttp_request *rq, const char *arg = p; int arg_len; char *next = strchr(arg, '/'); - if(next) { /* found a slash */ + if(!next || next > uri + uri_len) { /* last argument */ + p = uri + uri_len; + arg_len = p - arg; + } else { /* found a slash */ arg_len = next - arg; p = next + 1; - } else { /* last argument */ - arg_len = uri + uri_len - arg; - p = uri + uri_len; } /* record argument */ - cmd->argv[cur_param] = arg; - cmd->argv_len[cur_param] = arg_len; - + cmd->argv[cur_param] = decode_uri(arg, arg_len, &cmd->argv_len[cur_param], 1); cur_param++; } - redisAsyncCommandArgv(s->ac, fun, cmd, param_count, cmd->argv, cmd->argv_len); - return 0; -} + /* 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) { + free((char*)cmd->argv[i]); + } -formatting_fun -get_formatting_function(struct evkeyvalq *params) { + return 0; +} + +/** + * Return 2 functions, one to format the reply and + * one to transform the command before processing it. + */ +void +get_functions(struct cmd *cmd, formatting_fun *f_format, transform_fun *f_transform) { struct evkeyval *kv; - /* check for JSONP */ - TAILQ_FOREACH(kv, params, next) { - if(strcmp(kv->key, "format") == 0) { + /* defaults */ + *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) { - return raw_reply; + *f_format = raw_reply; } else if(strcmp(kv->value, "json") == 0) { - return json_reply; + *f_format = json_reply; } break; + } else if(strcmp(kv->key, "typeKey") == 0) { /* MIME type in a key. */ + cmd->mimeKey = strdup(kv->value); + *f_transform = custom_type_process_cmd; + *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; } } - - return json_reply; } int diff --git a/cmd.h b/cmd.h index e2e1857..fde50ab 100644 --- a/cmd.h +++ b/cmd.h @@ -9,8 +9,10 @@ struct evhttp_request; struct server; +struct cmd; typedef void (*formatting_fun)(redisAsyncContext *, void *, void *); +typedef void (*transform_fun)(struct cmd *); struct cmd { @@ -22,6 +24,9 @@ struct cmd { struct evkeyvalq uri_params; int started_responding; + + char *mime; + char *mimeKey; }; struct pubsub_client { @@ -39,8 +44,8 @@ int cmd_run(struct server *s, struct evhttp_request *rq, const char *uri, size_t uri_len); -formatting_fun -get_formatting_function(struct evkeyvalq *params); +void +get_functions(struct cmd *cmd, formatting_fun *f_format, transform_fun *f_transform); int cmd_is_subscribe(struct cmd *cmd); diff --git a/formats/common.c b/formats/common.c index 277354f..e87a136 100644 --- a/formats/common.c +++ b/formats/common.c @@ -12,7 +12,7 @@ format_send_reply(struct cmd *cmd, const char *p, size_t sz, const char *content /* send reply */ body = evbuffer_new(); evbuffer_add(body, p, sz); - evhttp_add_header(cmd->rq->output_headers, "Content-Type", content_type); + evhttp_add_header(cmd->rq->output_headers, "Content-Type", cmd->mime?cmd->mime:content_type); if(cmd_is_subscribe(cmd)) { free_cmd = 0; diff --git a/formats/custom-type.c b/formats/custom-type.c new file mode 100644 index 0000000..39baff9 --- /dev/null +++ b/formats/custom-type.c @@ -0,0 +1,76 @@ +#include "custom-type.h" +#include "cmd.h" +#include "common.h" + +#include +#include +#include + +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) { + 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; + } + + /* 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 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"; + } + + /* send reply */ + format_send_reply(cmd, reply->element[0]->str, reply->element[0]->len, ct); + return; +} + +/* 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); + } +} diff --git a/formats/custom-type.h b/formats/custom-type.h new file mode 100644 index 0000000..959e0be --- /dev/null +++ b/formats/custom-type.h @@ -0,0 +1,15 @@ +#ifndef CUSTOM_TYPE_H +#define CUSTOM_TYPE_H + +#include +#include + +struct cmd; + +void +custom_type_reply(redisAsyncContext *c, void *r, void *privdata); + +void +custom_type_process_cmd(struct cmd *cmd); + +#endif diff --git a/server.c b/server.c index 4370a1e..f574711 100644 --- a/server.c +++ b/server.c @@ -95,6 +95,26 @@ server_copy(const struct server *s) { return ret; } +/* Adobe flash cross-domain request */ +void +on_flash_request(struct evhttp_request *rq, void *ctx) { + + (void)ctx; + + char out[] = "\n" +"\n" +"\n" + "\n" +"\n"; + + struct evbuffer *body = evbuffer_new(); + evbuffer_add(body, out, sizeof(out) - 1); + + evhttp_add_header(rq->output_headers, "Content-Type", "application/xml"); + evhttp_send_reply(rq, 200, "OK", body); + evbuffer_free(body); +} + void on_request(struct evhttp_request *rq, void *ctx) { const char *uri = evhttp_request_uri(rq); @@ -146,6 +166,7 @@ server_start(struct server *s) { /* start http server */ slog(s->cfg->logfile,1,"Starting HTTP Server"); evhttp_bind_socket(s->http, s->cfg->http_host, s->cfg->http_port); + evhttp_set_cb(s->http, "/crossdomain.xml", on_flash_request, s); evhttp_set_gencb(s->http, on_request, s); /* drop privileges */