master
mrb 14 years ago
commit cadfcec881

@ -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

@ -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'
</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.
**Content-Type in parameter:**
<pre>
curl -v "http://127.0.0.1:7379/GET/hello.html?type=text/html"
[...]
&lt; HTTP/1.1 200 OK
&lt; Content-Type: text/html
&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;
</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"
[...]
&lt; HTTP/1.1 200 OK
&lt; Content-Type: text/html
&lt; Date: Mon, 03 Jan 2011 20:56:43 GMT
&lt; Content-Length: 137
&lt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
...
&lt;/html&gt;
</pre>

104
cmd.c

@ -5,10 +5,12 @@
#include "formats/json.h"
#include "formats/raw.h"
#include "formats/custom-type.h"
#include <stdlib.h>
#include <string.h>
#include <hiredis/hiredis.h>
#include <ctype.h>
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

@ -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);

@ -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;

@ -0,0 +1,76 @@
#include "custom-type.h"
#include "cmd.h"
#include "common.h"
#include <string.h>
#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) {
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);
}
}

@ -0,0 +1,15 @@
#ifndef CUSTOM_TYPE_H
#define CUSTOM_TYPE_H
#include <hiredis/hiredis.h>
#include <hiredis/async.h>
struct cmd;
void
custom_type_reply(redisAsyncContext *c, void *r, void *privdata);
void
custom_type_process_cmd(struct cmd *cmd);
#endif

@ -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[] = "<?xml version=\"1.0\"?>\n"
"<!DOCTYPE cross-domain-policy SYSTEM \"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd\">\n"
"<cross-domain-policy>\n"
"<allow-access-from domain=\"*\" />\n"
"</cross-domain-policy>\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 */

Loading…
Cancel
Save