From: Andriy Senkovych Date: Sun, 3 Aug 2014 23:10:55 +0000 (+0100) Subject: Import webdis_0.1.1.orig.tar.gz X-Git-Tag: archive/raspbian/0.1.4+dfsg-2+rpi1~1^2^2^2~2 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=7b0a4c78bcef98492a364b7702701dbe4b04840f;p=webdis.git Import webdis_0.1.1.orig.tar.gz [dgit import orig webdis_0.1.1.orig.tar.gz] --- 7b0a4c78bcef98492a364b7702701dbe4b04840f diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67cccf6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.log +*.swp +*.o +webdis +websocket +*.png +pubsub diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1d124ec --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +script: "make clean all" +language: c +compiler: + - gcc + - clang +before_install: + - sudo apt-get update + - sudo apt-get install libevent-dev +install: "sudo make install" diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..a9ae619 --- /dev/null +++ b/COPYING @@ -0,0 +1,24 @@ +Copyright (c) 2010-2011, Nicolas Favre-Felix + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e2b4b58 --- /dev/null +++ b/Makefile @@ -0,0 +1,66 @@ +OUT=webdis +HIREDIS_OBJ?=hiredis/hiredis.o hiredis/sds.o hiredis/net.o hiredis/async.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 +B64_OBJS?=b64/cencode.o +FORMAT_OBJS?=formats/json.o formats/raw.o formats/common.o formats/custom-type.o +HTTP_PARSER_OBJS?=http-parser/http_parser.o + +CFLAGS ?= -O0 -ggdb -Wall -Wextra -I. -Ijansson/src -Ihttp-parser +LDFLAGS ?= -levent -pthread + +# check for MessagePack +MSGPACK_LIB=$(shell ls /usr/lib/libmsgpack.so 2>/dev/null) +ifneq ($(strip $(MSGPACK_LIB)),) + FORMAT_OBJS += formats/msgpack.o + CFLAGS += -DMSGPACK=1 + LDFLAGS += -lmsgpack +endif + + +DEPS=$(FORMAT_OBJS) $(HIREDIS_OBJ) $(JANSSON_OBJ) $(HTTP_PARSER_OBJS) $(B64_OBJS) +OBJS=webdis.o cmd.o worker.o slog.o server.o acl.o md5/md5.o sha1/sha1.o http.o client.o websocket.o pool.o conf.o $(DEPS) + + + +PREFIX ?= /usr/local +CONFDIR ?= $(DESTDIR)/etc + +INSTALL_DIRS = $(DESTDIR) \ + $(DESTDIR)/$(PREFIX) \ + $(DESTDIR)/$(PREFIX)/bin \ + $(CONFDIR) + +all: $(OUT) Makefile + +$(OUT): $(OBJS) Makefile + $(CC) -o $(OUT) $(OBJS) $(LDFLAGS) + +%.o: %.c %.h Makefile + $(CC) -c $(CFLAGS) -o $@ $< + +%.o: %.c Makefile + $(CC) -c $(CFLAGS) -o $@ $< + +$(INSTALL_DIRS): + mkdir -p $@ + +clean: + rm -f $(OBJS) $(OUT) + +install: $(OUT) $(INSTALL_DIRS) + cp $(OUT) $(DESTDIR)/$(PREFIX)/bin + cp webdis.prod.json $(CONFDIR) + + +WEBDIS_PORT ?= 7379 + +test_all: test perftest + +test: + python tests/basic.py + python tests/limits.py + ./tests/pubsub -p $(WEBDIS_PORT) + +perftest: + # This is a performance test that requires apache2-utils and curl + ./tests/bench.sh diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..54ad875 --- /dev/null +++ b/README.markdown @@ -0,0 +1,321 @@ +# About + +A very simple web server providing an HTTP interface to Redis. It uses [hiredis](https://github.com/antirez/hiredis), [jansson](https://github.com/akheron/jansson), [libevent](http://monkey.org/~provos/libevent/), and [http-parser](https://github.com/ry/http-parser/). + +Webdis depends on libevent-dev. You can install it on Ubuntu by typing `sudo apt-get install libevent-dev` or on OS X by typing `brew install libevent`. +
+make clean all
+
+./webdis &
+
+curl http://127.0.0.1:7379/SET/hello/world
+→ {"SET":[true,"OK"]}
+
+curl http://127.0.0.1:7379/GET/hello
+→ {"GET":"world"}
+
+curl -d "GET/hello" http://127.0.0.1:7379/
+→ {"GET":"world"}
+
+
+ +# Features +* `GET` and `POST` are supported, as well as `PUT` for file uploads. +* JSON output by default, optional JSONP parameter (`?jsonp=myFunction` or `?callback=myFunction`). +* Raw Redis 2.0 protocol output with `.raw` suffix +* MessagePack output with `.msg` suffix +* HTTP 1.1 pipelining (70,000 http requests per second on a desktop Linux machine.) +* Multi-threaded server, configurable number of worker threads. +* WebSocket support (Currently using the “hixie-76” specification). +* 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. +* Custom Content-Type using a pre-defined file extension, or with `?type=some/thing`. +* URL-encoded parameters for binary data or slashes and question marks. For instance, `%2f` is decoded as `/` but not used as a command separator. +* Logs, with a configurable verbosity. +* Cross-origin requests, usable with XMLHttpRequest2 (Cross-Origin Resource Sharing - CORS). +* File upload with PUT. +* With the JSON output, the return value of INFO is parsed and transformed into an object. +* Optional daemonize: set `"daemonize": true` and `"pidfile": "/var/run/webdis.pid"` in webdis.json. +* Default root object: Add `"default_root": "/GET/index.html"` in webdis.json to substitute the request to `/` with a Redis request. +* HTTP request limit with `http_max_request_size` (in bytes, set to 128MB by default). +* Database selection in the URL, using e.g. `/7/GET/key` to run the command on DB 7. + +# Ideas, TODO... +* Add better support for PUT, DELETE, HEAD, OPTIONS? How? For which commands? + * This could be done using a “strict mode” with a table of commands and the verbs that can/must be used with each command. Strict mode would be optional, configurable. How would webdis know of new commands remains to be determined. +* MULTI/EXEC/DISCARD/WATCH are disabled at the moment; find a way to use them. +* Support POST of raw Redis protocol data, and execute the whole thing. This could be useful for MULTI/EXEC transactions. +* Enrich config file: + * Provide timeout (maybe for some commands only?). What should the response be? 504 Gateway Timeout? 503 Service Unavailable? +* Multi-server support, using consistent hashing. +* SSL? + * Not sure if this is such a good idea. +* SPDY? + * SPDY is mostly useful for parallel fetches. Not sure if it would make sense for Webdis. +* Send your ideas using the github tracker, on twitter [@yowgi](http://twitter.com/yowgi) or by mail to n.favrefelix@gmail.com. + +# HTTP error codes +* Unknown HTTP verb: 405 Method Not Allowed. +* Redis is unreachable: 503 Service Unavailable. +* Matching ETag sent using `If-None-Match`: 304 Not Modified. +* 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.ext` executes the command on Redis and returns the response to the client. GET, POST, and PUT are supported: + +* `GET /COMMAND/arg0/.../argN.ext` +* `POST /` with `COMMAND/arg0/.../argN` in the HTTP body. +* `PUT /COMMAND/arg0.../argN-1` with `argN` in the HTTP body (see section on [file uploads](#file-upload).) + +`.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: + +* [CIDR](http://en.wikipedia.org/wiki/CIDR) subnet + mask +* [HTTP Basic Auth](http://en.wikipedia.org/wiki/Basic_access_authentication) in the format of "user:password". + +Each ACL contains two lists of commands, `enabled` and `disabled`. All commands being enabled by default, it is up to the administrator to disable or re-enable them on a per-profile basis. +Examples: +
+{
+	"disabled":	["DEBUG", "FLUSHDB", "FLUSHALL"],
+},
+
+{
+	"http_basic_auth": "user:password",
+	"disabled":	["DEBUG", "FLUSHDB", "FLUSHALL"],
+	"enabled":	["SET"]
+},
+
+{
+	"ip": 		"192.168.10.0/24",
+	"enabled":	["SET"]
+},
+
+{
+	"http_basic_auth": "user:password",
+	"ip": 		"192.168.10.0/24",
+	"enabled":	["SET", "DEL"]
+}
+
+ACLs are interpreted in order, later authorizations superseding earlier ones if a client matches several. The special value "*" matches all commands. + +# JSON output +JSON is the default output format. Each command returns a JSON object with the command as a key and the result as a value. + +**Examples:** +
+// string
+$ curl http://127.0.0.1:7379/GET/y
+{"GET":"41"}
+
+// number
+$ curl http://127.0.0.1:7379/INCR/y
+{"INCR":42}
+
+// list
+$ curl http://127.0.0.1:7379/LRANGE/x/0/1
+{"LRANGE":["abc","def"]}
+
+// status
+$ curl http://127.0.0.1:7379/TYPE/y
+{"TYPE":[true,"string"]}
+
+// error, which is basically a status
+$ curl http://127.0.0.1:7379/MAKE-ME-COFFEE
+{"MAKE-ME-COFFEE":[false,"ERR unknown command 'MAKE-ME-COFFEE'"]}
+
+// JSONP callback:
+$ curl  "http://127.0.0.1:7379/TYPE/y?jsonp=myCustomFunction"
+myCustomFunction({"TYPE":[true,"string"]})
+
+ +# RAW output +This is the raw output of Redis; enable it with the `.raw` suffix. +
+
+// string
+$ curl http://127.0.0.1:7379/GET/z.raw
+$5
+hello
+
+// number
+curl http://127.0.0.1:7379/INCR/a.raw
+:2
+
+// list
+$ curl http://127.0.0.1:7379/LRANGE/x/0/-1.raw
+*2
+$3
+abc
+$3
+def
+
+// status
+$ 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.raw
+-ERR unknown command 'MAKE-ME-COFFEE'
+
+ +# Custom content-type +Several content-types are available: + +* `.json` for `application/json` (this is the default Content-Type). +* `.msg` for `application/x-msgpack`. See [http://msgpack.org/](http://msgpack.org/) for the specs. +* `.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. +* Add a custom separator for list responses with `?sep=,` query string. + +
+curl -v "http://127.0.0.1:7379/GET/hello.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>
+
+curl -v "http://127.0.0.1:7379/GET/hello.txt"
+[...]
+< HTTP/1.1 200 OK
+< Content-Type: text/plain
+< Date: Mon, 03 Jan 2011 20:43:36 GMT
+< Content-Length: 137
+[...]
+
+curl -v "http://127.0.0.1:7379/GET/big-file?type=application/pdf"
+[...]
+< HTTP/1.1 200 OK
+< Content-Type: application/pdf
+< Date: Mon, 03 Jan 2011 20:45:12 GMT
+[...]
+
+ +# File upload +Webdis supports file upload using HTTP PUT. The command URI is slightly different, as the last argument is taken from the HTTP body. +For example: instead of `/SET/key/value`, the URI becomes `/SET/key` and the value is the entirety of the body. This works for other commands such as LPUSH, etc. + +**Uploading a binary file to webdis**: +
+$ file redis-logo.png
+redis-logo.png: PNG image, 513 x 197, 8-bit/color RGBA, non-interlaced
+
+$ wc -c redis-logo.png
+16744 redis-logo.png
+
+$ curl -v --upload-file redis-logo.png http://127.0.0.1:7379/SET/logo
+[...]
+> PUT /SET/logo HTTP/1.1
+> User-Agent: curl/7.19.7 (x86_64-pc-linux-gnu) libcurl/7.19.7 OpenSSL/0.9.8k zlib/1.2.3.3 libidn/1.15
+> Host: 127.0.0.1:7379
+> Accept: */*
+> Content-Length: 16744
+> Expect: 100-continue
+>
+< HTTP/1.1 100 Continue
+< HTTP/1.1 200 OK
+< Content-Type: application/json
+< ETag: "0db1124cf79ffeb80aff6d199d5822f8"
+< Date: Sun, 09 Jan 2011 16:48:19 GMT
+< Content-Length: 19
+<
+{"SET":[true,"OK"]}
+
+$ curl -vs http://127.0.0.1:7379/GET/logo.png -o out.png
+> GET /GET/logo.png HTTP/1.1
+> User-Agent: curl/7.19.7 (x86_64-pc-linux-gnu) libcurl/7.19.7 OpenSSL/0.9.8k zlib/1.2.3.3 libidn/1.15
+> Host: 127.0.0.1:7379
+> Accept: */*
+>
+< HTTP/1.1 200 OK
+< Content-Type: image/png
+< ETag: "1991df597267d70bf9066a7d11969da0"
+< Date: Sun, 09 Jan 2011 16:50:51 GMT
+< Content-Length: 16744
+
+$ md5sum redis-logo.png out.png
+1991df597267d70bf9066a7d11969da0  redis-logo.png
+1991df597267d70bf9066a7d11969da0  out.png
+
+ +The file was uploaded and re-downloaded properly: it has the same hash and the content-type was set properly thanks to the `.png` extension. + +# WebSockets +Webdis supports WebSocket clients implementing [dixie-76](http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76). +Web Sockets are supported with the following formats, selected by the connection URL: + +* JSON (on `/` or `/.json`) +* Raw Redis wire protocol (on `/.raw`) + +**Example**: +
+function testJSON() {
+	var jsonSocket = new WebSocket("ws://127.0.0.1:7379/.json");
+	jsonSocket.onopen = function() {
+
+		console.log("JSON socket connected!");
+		jsonSocket.send(JSON.stringify(["SET", "hello", "world"]));
+		jsonSocket.send(JSON.stringify(["GET", "hello"]));
+	};
+	jsonSocket.onmessage = function(messageEvent) {
+		console.log("JSON received:", messageEvent.data);
+	};
+}
+testJSON();
+
+ +This produces the following output: +
+JSON socket connected!
+JSON received: {"SET":[true,"OK"]}
+JSON received: {"GET":"world"}
+
+ +# Pub/Sub with chunked transfer encoding +Webdis exposes Redis PUB/SUB channels to HTTP clients, forwarding messages in the channel as they are published by Redis. This is done using chunked transfer encoding. + +**Example using XMLHttpRequest**: +
+var previous_response_length = 0
+xhr = new XMLHttpRequest()
+xhr.open("GET", "http://127.0.0.1:7379/SUBSCRIBE/hello", true);
+xhr.onreadystatechange = checkData;
+xhr.send(null);
+
+function checkData() {
+	if(xhr.readyState == 3)  {
+    	response = xhr.responseText;
+    	chunk = response.slice(previous_response_length);
+    	previous_response_length = response.length;
+    	console.log(chunk);
+    }
+};
+
+ +Publish messages to redis to see output similar to the following: +
+{"SUBSCRIBE":["subscribe","hello",1]}
+{"SUBSCRIBE":["message","hello","some message"]}
+{"SUBSCRIBE":["message","hello","some other message"]} 
+
diff --git a/acl.c b/acl.c new file mode 100644 index 0000000..160e6e5 --- /dev/null +++ b/acl.c @@ -0,0 +1,98 @@ +#include "acl.h" +#include "cmd.h" +#include "conf.h" +#include "http.h" +#include "client.h" + +#include +#include +#include +#include + +int +acl_match_client(struct acl *a, struct http_client *client, in_addr_t *ip) { + + /* check HTTP Basic Auth */ + const char *auth; + auth = client_get_header(client, "Authorization"); + if(a->http_basic_auth) { + if(auth && strncasecmp(auth, "Basic ", 6) == 0) { /* sent auth */ + if(strcmp(auth + 6, a->http_basic_auth) != 0) { /* bad password */ + return 0; + } + } else { /* no auth sent, required to match this ACL */ + return 0; + } + } + + /* CIDR check. */ + if(a->cidr.enabled == 0) { /* none given, all match */ + return 1; + } + if(((*ip) & a->cidr.mask) == (a->cidr.subnet & a->cidr.mask)) { + return 1; + } + + return 0; +} + +int +acl_allow_command(struct cmd *cmd, struct conf *cfg, struct http_client *client) { + + char *always_off[] = {"MULTI", "EXEC", "WATCH", "DISCARD", "SELECT"}; + + unsigned int i; + int authorized = 1; + struct acl *a; + + in_addr_t client_addr; + + const char *cmd_name; + size_t cmd_len; + + if(cmd->count == 0) { + return 0; + } + + cmd_name = cmd->argv[0]; + cmd_len = cmd->argv_len[0]; + + /* some commands are always disabled, regardless of the config file. */ + for(i = 0; i < sizeof(always_off) / sizeof(always_off[0]); ++i) { + if(strncasecmp(always_off[i], cmd_name, cmd_len) == 0) { + return 0; + } + } + + /* find client's address */ + client_addr = ntohl(client->addr); + + /* go through permissions */ + for(a = cfg->perms; a; a = a->next) { + + if(!acl_match_client(a, client, &client_addr)) continue; /* match client */ + + /* go through authorized commands */ + for(i = 0; i < a->enabled.count; ++i) { + if(strncasecmp(a->enabled.commands[i], cmd_name, cmd_len) == 0) { + authorized = 1; + } + if(strncasecmp(a->enabled.commands[i], "*", 1) == 0) { + authorized = 1; + } + } + + /* go through unauthorized commands */ + for(i = 0; i < a->disabled.count; ++i) { + if(strncasecmp(a->disabled.commands[i], cmd_name, cmd_len) == 0) { + authorized = 0; + } + if(strncasecmp(a->disabled.commands[i], "*", 1) == 0) { + authorized = 0; + } + } + } + + return authorized; +} + diff --git a/acl.h b/acl.h new file mode 100644 index 0000000..2b68b93 --- /dev/null +++ b/acl.h @@ -0,0 +1,39 @@ +#ifndef ACL_H +#define ACL_H + +#include + +struct http_client; +struct cmd; +struct conf; + +struct acl_commands { + unsigned int count; + char **commands; +}; + +struct acl { + + /* CIDR subnet + mask */ + struct { + int enabled; + in_addr_t subnet; + in_addr_t mask; + } cidr; + + char *http_basic_auth; + + /* commands that have been enabled or disabled */ + struct acl_commands enabled; + struct acl_commands disabled; + + struct acl *next; +}; + +int +acl_match_client(struct acl *a, struct http_client *client, in_addr_t *ip); + +int +acl_allow_command(struct cmd *cmd, struct conf *cfg, struct http_client *client); + +#endif diff --git a/b64/cencode.c b/b64/cencode.c new file mode 100644 index 0000000..a8c8fee --- /dev/null +++ b/b64/cencode.c @@ -0,0 +1,109 @@ +/* +cencoder.c - c source to a base64 encoding algorithm implementation + +This is part of the libb64 project, and has been placed in the public domain. +For details, see http://sourceforge.net/projects/libb64 +*/ + +#include "cencode.h" + +const int CHARS_PER_LINE = 72; + +void base64_init_encodestate(base64_encodestate* state_in) +{ + state_in->step = step_A; + state_in->result = 0; + state_in->stepcount = 0; +} + +char base64_encode_value(char value_in) +{ + static const char* encoding = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + if (value_in > 63) return '='; + return encoding[(int)value_in]; +} + +int base64_encode_block(const char* plaintext_in, int length_in, char* code_out, base64_encodestate* state_in) +{ + const char* plainchar = plaintext_in; + const char* const plaintextend = plaintext_in + length_in; + char* codechar = code_out; + char result; + char fragment; + + result = state_in->result; + + switch (state_in->step) + { + while (1) + { + case step_A: + if (plainchar == plaintextend) + { + state_in->result = result; + state_in->step = step_A; + return codechar - code_out; + } + fragment = *plainchar++; + result = (fragment & 0x0fc) >> 2; + *codechar++ = base64_encode_value(result); + result = (fragment & 0x003) << 4; + case step_B: + if (plainchar == plaintextend) + { + state_in->result = result; + state_in->step = step_B; + return codechar - code_out; + } + fragment = *plainchar++; + result |= (fragment & 0x0f0) >> 4; + *codechar++ = base64_encode_value(result); + result = (fragment & 0x00f) << 2; + case step_C: + if (plainchar == plaintextend) + { + state_in->result = result; + state_in->step = step_C; + return codechar - code_out; + } + fragment = *plainchar++; + result |= (fragment & 0x0c0) >> 6; + *codechar++ = base64_encode_value(result); + result = (fragment & 0x03f) >> 0; + *codechar++ = base64_encode_value(result); + + ++(state_in->stepcount); + if (state_in->stepcount == CHARS_PER_LINE/4) + { + *codechar++ = '\n'; + state_in->stepcount = 0; + } + } + } + /* control should not reach here */ + return codechar - code_out; +} + +int base64_encode_blockend(char* code_out, base64_encodestate* state_in) +{ + char* codechar = code_out; + + switch (state_in->step) + { + case step_B: + *codechar++ = base64_encode_value(state_in->result); + *codechar++ = '='; + *codechar++ = '='; + break; + case step_C: + *codechar++ = base64_encode_value(state_in->result); + *codechar++ = '='; + break; + case step_A: + break; + } + *codechar++ = '\n'; + + return codechar - code_out; +} + diff --git a/b64/cencode.h b/b64/cencode.h new file mode 100644 index 0000000..c1e3464 --- /dev/null +++ b/b64/cencode.h @@ -0,0 +1,31 @@ +/* +cencode.h - c header for a base64 encoding algorithm + +This is part of the libb64 project, and has been placed in the public domain. +For details, see http://sourceforge.net/projects/libb64 +*/ + +#ifndef BASE64_CENCODE_H +#define BASE64_CENCODE_H + +typedef enum +{ + step_A, step_B, step_C +} base64_encodestep; + +typedef struct +{ + base64_encodestep step; + char result; + int stepcount; +} base64_encodestate; + +void base64_init_encodestate(base64_encodestate* state_in); + +char base64_encode_value(char value_in); + +int base64_encode_block(const char* plaintext_in, int length_in, char* code_out, base64_encodestate* state_in); + +int base64_encode_blockend(char* code_out, base64_encodestate* state_in); + +#endif /* BASE64_CENCODE_H */ diff --git a/client.c b/client.c new file mode 100644 index 0000000..cdcd762 --- /dev/null +++ b/client.c @@ -0,0 +1,370 @@ +#include "client.h" +#include "http_parser.h" +#include "http.h" +#include "server.h" +#include "worker.h" +#include "websocket.h" +#include "cmd.h" +#include "conf.h" + +#include +#include +#include +#include +#include +#include + +#define CHECK_ALLOC(c, ptr) if(!(ptr)) { c->failed_alloc = 1; return -1;} + +static int +http_client_on_url(struct http_parser *p, const char *at, size_t sz) { + + struct http_client *c = p->data; + + CHECK_ALLOC(c, c->path = realloc(c->path, c->path_sz + sz + 1)); + memcpy(c->path + c->path_sz, at, sz); + c->path_sz += sz; + c->path[c->path_sz] = 0; + + return 0; +} + +/* + * Called when the body is parsed. + */ +static int +http_client_on_body(struct http_parser *p, const char *at, size_t sz) { + + struct http_client *c = p->data; + return http_client_add_to_body(c, at, sz); +} + +int +http_client_add_to_body(struct http_client *c, const char *at, size_t sz) { + + CHECK_ALLOC(c, c->body = realloc(c->body, c->body_sz + sz + 1)); + memcpy(c->body + c->body_sz, at, sz); + c->body_sz += sz; + c->body[c->body_sz] = 0; + + return 0; +} + +static int +http_client_on_header_name(struct http_parser *p, const char *at, size_t sz) { + + struct http_client *c = p->data; + size_t n = c->header_count; + + /* if we're not adding to the same header name as last time, realloc to add one field. */ + if(c->last_cb != LAST_CB_KEY) { + n = ++c->header_count; + CHECK_ALLOC(c, c->headers = realloc(c->headers, n * sizeof(struct http_header))); + memset(&c->headers[n-1], 0, sizeof(struct http_header)); + } + + /* Add data to the current header name. */ + CHECK_ALLOC(c, c->headers[n-1].key = realloc(c->headers[n-1].key, + c->headers[n-1].key_sz + sz + 1)); + memcpy(c->headers[n-1].key + c->headers[n-1].key_sz, at, sz); + c->headers[n-1].key_sz += sz; + c->headers[n-1].key[c->headers[n-1].key_sz] = 0; + + c->last_cb = LAST_CB_KEY; + + return 0; +} + +static char * +wrap_filename(const char *val, size_t val_len) { + + char format[] = "attachment; filename=\""; + size_t sz = sizeof(format) - 1 + val_len + 1; + char *p = calloc(sz + 1, 1); + + memcpy(p, format, sizeof(format)-1); /* copy format */ + memcpy(p + sizeof(format)-1, val, val_len); /* copy filename */ + p[sz-1] = '"'; + + return p; +} + +/* + * Split query string into key/value pairs, process some of them. + */ +static int +http_client_on_query_string(struct http_parser *parser, const char *at, size_t sz) { + + struct http_client *c = parser->data; + const char *p = at; + + while(p < at + sz) { + + const char *key = p, *val; + int key_len, val_len; + char *eq = memchr(key, '=', sz - (p-at)); + if(!eq || eq > at + sz) { /* last argument */ + break; + } else { /* found an '=' */ + char *amp; + val = eq + 1; + key_len = eq - key; + p = eq + 1; + + amp = memchr(p, '&', sz - (p-at)); + if(!amp || amp > at + sz) { + val_len = at + sz - p; /* last arg */ + } else { + val_len = amp - val; /* cur arg */ + p = amp + 1; + } + + if(key_len == 4 && strncmp(key, "type", 4) == 0) { + c->type = calloc(1 + val_len, 1); + memcpy(c->type, val, val_len); + } else if((key_len == 5 && strncmp(key, "jsonp", 5) == 0) + || (key_len == 8 && strncmp(key, "callback", 8) == 0)) { + c->jsonp = calloc(1 + val_len, 1); + memcpy(c->jsonp, val, val_len); + } else if(key_len == 3 && strncmp(key, "sep", 3) == 0) { + c->separator = calloc(1 + val_len, 1); + memcpy(c->separator, val, val_len); + } else if(key_len == 8 && strncmp(key, "filename", 8) == 0) { + c->filename = wrap_filename(val, val_len); + } + + if(!amp) { + break; + } + } + } + return 0; +} + +static int +http_client_on_header_value(struct http_parser *p, const char *at, size_t sz) { + + struct http_client *c = p->data; + size_t n = c->header_count; + + /* Add data to the current header value. */ + CHECK_ALLOC(c, c->headers[n-1].val = realloc(c->headers[n-1].val, + c->headers[n-1].val_sz + sz + 1)); + memcpy(c->headers[n-1].val + c->headers[n-1].val_sz, at, sz); + c->headers[n-1].val_sz += sz; + c->headers[n-1].val[c->headers[n-1].val_sz] = 0; + + c->last_cb = LAST_CB_VAL; + + + /* react to some values. */ + if(strncmp("Expect", c->headers[n-1].key, c->headers[n-1].key_sz) == 0) { + if(sz == 12 && strncasecmp(at, "100-continue", sz) == 0) { + /* support HTTP file upload */ + char http100[] = "HTTP/1.1 100 Continue\r\n\r\n"; + int ret = write(c->fd, http100, sizeof(http100)-1); + (void)ret; + } + } else if(strncasecmp("Connection", c->headers[n-1].key, c->headers[n-1].key_sz) == 0) { + if(sz == 10 && strncasecmp(at, "Keep-Alive", sz) == 0) { + c->keep_alive = 1; + } + } + + return 0; +} + +static int +http_client_on_message_complete(struct http_parser *p) { + + struct http_client *c = p->data; + + /* keep-alive detection */ + if(c->parser.http_major == 1 && c->parser.http_minor == 1) { /* 1.1 */ + c->keep_alive = 1; + } + c->http_version = c->parser.http_minor; + + if(p->upgrade && c->w->s->cfg->websockets) { /* WebSocket, don't execute just yet */ + c->is_websocket = 1; + return 0; + } + + /* handle default root object */ + if(c->path_sz == 1 && *c->path == '/' && c->w->s->cfg->default_root) { /* replace */ + free(c->path); + c->path = strdup(c->w->s->cfg->default_root); + c->path_sz = strlen(c->path); + } + + + worker_process_client(c); + http_client_reset(c); + + return 0; +} + +struct http_client * +http_client_new(struct worker *w, int fd, in_addr_t addr) { + + struct http_client *c = calloc(1, sizeof(struct http_client)); + + c->fd = fd; + c->w = w; + c->addr = addr; + c->s = w->s; + + /* parser */ + http_parser_init(&c->parser, HTTP_REQUEST); + c->parser.data = c; + + /* callbacks */ + c->settings.on_url = http_client_on_url; + c->settings.on_query_string = http_client_on_query_string; + c->settings.on_body = http_client_on_body; + c->settings.on_message_complete = http_client_on_message_complete; + c->settings.on_header_field = http_client_on_header_name; + c->settings.on_header_value = http_client_on_header_value; + + c->last_cb = LAST_CB_NONE; + + return c; +} + + +void +http_client_reset(struct http_client *c) { + + int i; + + /* headers */ + for(i = 0; i < c->header_count; ++i) { + free(c->headers[i].key); + free(c->headers[i].val); + } + free(c->headers); + c->headers = NULL; + c->header_count = 0; + + /* other data */ + free(c->body); c->body = NULL; + c->body_sz = 0; + free(c->path); c->path = NULL; + c->path_sz = 0; + free(c->type); c->type = NULL; + free(c->jsonp); c->jsonp = NULL; + free(c->filename); c->filename = NULL; + c->request_sz = 0; + + /* no last known header callback */ + c->last_cb = LAST_CB_NONE; + + /* mark as broken if client doesn't support Keep-Alive. */ + if(c->keep_alive == 0) { + c->broken = 1; + } +} + +void +http_client_free(struct http_client *c) { + + http_client_reset(c); + free(c->buffer); + free(c); +} + +int +http_client_read(struct http_client *c) { + + char buffer[4096]; + int ret; + + ret = read(c->fd, buffer, sizeof(buffer)); + if(ret <= 0) { + /* broken link, free buffer and client object */ + + /* disconnect pub/sub client if there is one. */ + if(c->pub_sub && c->pub_sub->ac) { + struct cmd *cmd = c->pub_sub; + + /* disconnect from all channels */ + redisAsyncDisconnect(c->pub_sub->ac); + if(c->pub_sub) c->pub_sub->ac = NULL; + c->pub_sub = NULL; + + /* delete command object */ + cmd_free(cmd); + } + + close(c->fd); + + http_client_free(c); + return (int)CLIENT_DISCONNECTED; + } + + /* save what we've just read */ + c->buffer = realloc(c->buffer, c->sz + ret); + if(!c->buffer) { + return (int)CLIENT_OOM; + } + memcpy(c->buffer + c->sz, buffer, ret); + c->sz += ret; + + /* keep track of total sent */ + c->request_sz += ret; + + return ret; +} + +int +http_client_remove_data(struct http_client *c, size_t sz) { + + char *buffer; + if(c->sz < sz) + return -1; + + /* replace buffer */ + CHECK_ALLOC(c, buffer = malloc(c->sz - sz)); + memcpy(buffer, c->buffer + sz, c->sz - sz); + free(c->buffer); + c->buffer = buffer; + c->sz -= sz; + + return 0; +} + +int +http_client_execute(struct http_client *c) { + + int nparsed = http_parser_execute(&c->parser, &c->settings, c->buffer, c->sz); + + if(!c->is_websocket) { + /* removed consumed data, all has been copied already. */ + free(c->buffer); + c->buffer = NULL; + c->sz = 0; + } + return nparsed; +} + +/* + * Find header value, returns NULL if not found. + */ +const char * +client_get_header(struct http_client *c, const char *key) { + + int i; + size_t sz = strlen(key); + + for(i = 0; i < c->header_count; ++i) { + + if(sz == c->headers[i].key_sz && + strncasecmp(key, c->headers[i].key, sz) == 0) { + return c->headers[i].val; + } + + } + + return NULL; +} + diff --git a/client.h b/client.h new file mode 100644 index 0000000..72594ef --- /dev/null +++ b/client.h @@ -0,0 +1,93 @@ +#ifndef CLIENT_H +#define CLIENT_H + +#include +#include +#include "http_parser.h" +#include "websocket.h" + +struct http_header; +struct server; +struct cmd; + +typedef enum { + LAST_CB_NONE = 0, + LAST_CB_KEY = 1, + LAST_CB_VAL = 2} last_cb_t; + +typedef enum { + CLIENT_DISCONNECTED = -1, + CLIENT_OOM = -2} client_error_t; + +struct http_client { + + int fd; + in_addr_t addr; + struct event ev; + + struct worker *w; + struct server *s; + + + /* HTTP parsing */ + struct http_parser parser; + struct http_parser_settings settings; + char *buffer; + size_t sz; + size_t request_sz; /* accumulated so far. */ + last_cb_t last_cb; + + /* various flags. */ + char keep_alive; + char broken; + char is_websocket; + char http_version; + char failed_alloc; + + /* HTTP data */ + char *path; + size_t path_sz; + + /* headers */ + struct http_header *headers; + int header_count; + + char *body; + size_t body_sz; + + char *type; /* forced output content-type */ + char *jsonp; /* jsonp wrapper */ + char *separator; /* list separator for raw lists */ + char *filename; /* content-disposition */ + + struct cmd *pub_sub; + + struct ws_msg *frame; /* websocket frame */ +}; + +struct http_client * +http_client_new(struct worker *w, int fd, in_addr_t addr); + +void +http_client_reset(struct http_client *c); + +void +http_client_free(struct http_client *c); + +int +http_client_read(struct http_client *c); + +int +http_client_remove_data(struct http_client *c, size_t sz); + +int +http_client_execute(struct http_client *c); + +int +http_client_add_to_body(struct http_client *c, const char *at, size_t sz); + +const char * +client_get_header(struct http_client *c, const char *key); + + +#endif diff --git a/cmd.c b/cmd.c new file mode 100644 index 0000000..1c661ca --- /dev/null +++ b/cmd.c @@ -0,0 +1,379 @@ +#include "cmd.h" +#include "conf.h" +#include "acl.h" +#include "client.h" +#include "pool.h" +#include "worker.h" +#include "http.h" +#include "server.h" +#include "slog.h" + +#include "formats/json.h" +#include "formats/raw.h" +#ifdef MSGPACK +#include "formats/msgpack.h" +#endif +#include "formats/custom-type.h" + +#include +#include +#include +#include +#include + +struct cmd * +cmd_new(int count) { + + struct cmd *c = calloc(1, sizeof(struct cmd)); + + c->count = count; + + c->argv = calloc(count, sizeof(char*)); + c->argv_len = calloc(count, sizeof(size_t)); + + return c; +} + + +void +cmd_free(struct cmd *c) { + + int i; + if(!c) return; + + for(i = 0; i < c->count; ++i) { + free((char*)c->argv[i]); + } + + free(c->jsonp); + free(c->separator); + free(c->if_none_match); + if(c->mime_free) free(c->mime); + + if (c->ac && /* we have a connection */ + (c->database != c->w->s->cfg->database /* custom DB */ + || cmd_is_subscribe(c))) { + pool_free_context(c->ac); + } + free(c->argv); + free(c->argv_len); + + free(c); +} + +/* 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; +} + +/* setup headers */ +void +cmd_setup(struct cmd *cmd, struct http_client *client) { + + int i; + cmd->keep_alive = client->keep_alive; + cmd->w = client->w; /* keep track of the worker */ + + for(i = 0; i < client->header_count; ++i) { + if(strcasecmp(client->headers[i].key, "If-None-Match") == 0) { + cmd->if_none_match = calloc(1+client->headers[i].val_sz, 1); + memcpy(cmd->if_none_match, client->headers[i].val, + client->headers[i].val_sz); + } else if(strcasecmp(client->headers[i].key, "Connection") == 0 && + strcasecmp(client->headers[i].val, "Keep-Alive") == 0) { + cmd->keep_alive = 1; + } + } + + if(client->type) { /* transfer pointer ownership */ + cmd->mime = client->type; + cmd->mime_free = 1; + client->type = NULL; + } + + if(client->jsonp) { /* transfer pointer ownership */ + cmd->jsonp = client->jsonp; + client->jsonp = NULL; + } + + if(client->separator) { /* transfer pointer ownership */ + cmd->separator = client->separator; + client->separator = NULL; + } + + if(client->filename) { /* transfer pointer ownership */ + cmd->filename = client->filename; + client->filename = NULL; + } + + cmd->fd = client->fd; + cmd->http_version = client->http_version; +} + + +cmd_response_t +cmd_run(struct worker *w, struct http_client *client, + const char *uri, size_t uri_len, + const char *body, size_t body_len) { + + char *qmark = memchr(uri, '?', uri_len); + char *slash; + const char *p, *cmd_name = uri; + int cmd_len; + int param_count = 0, cur_param = 1; + + struct cmd *cmd; + formatting_fun f_format; + + /* count arguments */ + if(qmark) { + uri_len = qmark - uri; + } + for(p = uri; p && p < uri + uri_len; param_count++) { + p = memchr(p+1, '/', uri_len - (p+1-uri)); + } + + if(body && body_len) { /* PUT request */ + param_count++; + } + if(param_count == 0) { + return CMD_PARAM_ERROR; + } + + cmd = cmd_new(param_count); + cmd->fd = client->fd; + cmd->database = w->s->cfg->database; + + /* get output formatting function */ + uri_len = cmd_select_format(client, cmd, uri, uri_len, &f_format); + + /* add HTTP info */ + cmd_setup(cmd, client); + + /* check if we only have one command or more. */ + slash = memchr(uri, '/', uri_len); + if(slash) { + + /* detect DB number by checking if first arg is only numbers */ + int has_db = 1; + int db_num = 0; + for(p = uri; p < slash; ++p) { + if(*p < '0' || *p > '9') { + has_db = 0; + break; + } + db_num = db_num * 10 + (*p - '0'); + } + + /* shift to next arg if a db was set up */ + if(has_db) { + char *next; + cmd->database = db_num; + cmd->count--; /* overcounted earlier */ + cmd_name = slash + 1; + + if((next = memchr(cmd_name, '/', uri_len - (slash - uri)))) { + cmd_len = next - cmd_name; + } else { + cmd_len = uri_len - (slash - uri + 1); + } + } else { + cmd_len = slash - uri; + } + } else { + cmd_len = uri_len; + } + + /* there is always a first parameter, it's the command name */ + cmd->argv[0] = malloc(cmd_len); + memcpy(cmd->argv[0], cmd_name, cmd_len); + cmd->argv_len[0] = cmd_len; + + /* check that the client is able to run this command */ + if(!acl_allow_command(cmd, w->s->cfg, client)) { + cmd_free(cmd); + return CMD_ACL_FAIL; + } + + if(cmd_is_subscribe(cmd)) { + /* create a new connection to Redis */ + cmd->ac = (redisAsyncContext*)pool_connect(w->pool, cmd->database, 0); + + /* register with the client, used upon disconnection */ + client->pub_sub = cmd; + cmd->pub_sub_client = client; + } else if(cmd->database != w->s->cfg->database) { + /* create a new connection to Redis for custom DBs */ + cmd->ac = (redisAsyncContext*)pool_connect(w->pool, cmd->database, 0); + } else { + /* get a connection from the pool */ + cmd->ac = (redisAsyncContext*)pool_get_context(w->pool); + } + + /* no args (e.g. INFO command) */ + if(!slash) { + if(!cmd->ac) { + cmd_free(cmd); + return CMD_REDIS_UNAVAIL; + } + redisAsyncCommandArgv(cmd->ac, f_format, cmd, 1, + (const char **)cmd->argv, cmd->argv_len); + return CMD_SENT; + } + p = cmd_name + cmd_len + 1; + while(p < uri + uri_len) { + + const char *arg = p; + int arg_len; + char *next = memchr(arg, '/', uri_len - (arg-uri)); + 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; + } + + /* record argument */ + cmd->argv[cur_param] = decode_uri(arg, arg_len, &cmd->argv_len[cur_param], 1); + cur_param++; + } + + if(body && body_len) { /* PUT request */ + cmd->argv[cur_param] = malloc(body_len); + memcpy(cmd->argv[cur_param], body, body_len); + cmd->argv_len[cur_param] = body_len; + } + + /* send it off! */ + if(cmd->ac) { + cmd_send(cmd, f_format); + return CMD_SENT; + } + /* failed to find a suitable connection to Redis. */ + cmd_free(cmd); + client->pub_sub = NULL; + return CMD_REDIS_UNAVAIL; +} + +void +cmd_send(struct cmd *cmd, formatting_fun f_format) { + redisAsyncCommandArgv(cmd->ac, f_format, cmd, cmd->count, + (const char **)cmd->argv, cmd->argv_len); +} + +/** + * Select Content-Type and processing function. + */ +int +cmd_select_format(struct http_client *client, struct cmd *cmd, + const char *uri, size_t uri_len, formatting_fun *f_format) { + + const char *ext; + int ext_len = -1; + unsigned int i; + int found = 0; /* did we match it to a predefined format? */ + + /* 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"}, + +#ifdef MSGPACK + {.s = "msg", .sz = 3, .f = msgpack_reply, .ct = "application/x-msgpack"}, +#endif + + {.s = "bin", .sz = 3, .f = custom_type_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"}, + + {.s = "js", .sz = 2, .f = json_reply, .ct = "application/javascript"}, + {.s = "css", .sz = 3, .f = custom_type_reply, .ct = "text/css"}, + }; + + /* default */ + *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; + } + } + 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; + found = 1; + } + } + + /* the user can force it with ?type=some/thing */ + if(client->type) { + *f_format = custom_type_reply; + cmd->mime = strdup(client->type); + cmd->mime_free = 1; + } + + if(found) { + return uri_len - ext_len - 1; + } else { + /* no matching format, use default output with the full argument, extension included. */ + return uri_len; + } +} + +int +cmd_is_subscribe(struct cmd *cmd) { + + if(cmd->count >= 1 && cmd->argv[0] && + (strncasecmp(cmd->argv[0], "SUBSCRIBE", cmd->argv_len[0]) == 0 || + strncasecmp(cmd->argv[0], "PSUBSCRIBE", cmd->argv_len[0]) == 0)) { + return 1; + } + return 0; +} diff --git a/cmd.h b/cmd.h new file mode 100644 index 0000000..e216bde --- /dev/null +++ b/cmd.h @@ -0,0 +1,80 @@ +#ifndef CMD_H +#define CMD_H + +#include +#include +#include +#include +#include + +struct evhttp_request; +struct http_client; +struct server; +struct worker; +struct cmd; + +typedef void (*formatting_fun)(redisAsyncContext *, void *, void *); +typedef enum {CMD_SENT, + CMD_PARAM_ERROR, + CMD_ACL_FAIL, + CMD_REDIS_UNAVAIL} cmd_response_t; + +struct cmd { + int fd; + + int count; + char **argv; + size_t *argv_len; + + /* HTTP data */ + char *mime; /* forced output content-type */ + int mime_free; /* need to free mime buffer */ + + char *filename; /* content-disposition attachment */ + + char *if_none_match; /* used with ETags */ + char *jsonp; /* jsonp wrapper */ + char *separator; /* list separator for raw lists */ + int keep_alive; + + /* various flags */ + int started_responding; + int is_websocket; + int http_version; + int database; + + struct http_client *pub_sub_client; + redisAsyncContext *ac; + struct worker *w; +}; + +struct subscription { + struct server *s; + struct cmd *cmd; +}; + +struct cmd * +cmd_new(int count); + +void +cmd_free(struct cmd *c); + +cmd_response_t +cmd_run(struct worker *w, struct http_client *client, + const char *uri, size_t uri_len, + const char *body, size_t body_len); + +int +cmd_select_format(struct http_client *client, struct cmd *cmd, + const char *uri, size_t uri_len, formatting_fun *f_format); + +int +cmd_is_subscribe(struct cmd *cmd); + +void +cmd_send(struct cmd *cmd, formatting_fun f_format); + +void +cmd_setup(struct cmd *cmd, struct http_client *client); + +#endif diff --git a/conf.c b/conf.c new file mode 100644 index 0000000..4fce300 --- /dev/null +++ b/conf.c @@ -0,0 +1,233 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include "conf.h" +#include "acl.h" + +static struct acl * +conf_parse_acls(json_t *jtab); + +struct conf * +conf_read(const char *filename) { + + json_t *j; + json_error_t error; + struct conf *conf; + void *kv; + + /* defaults */ + conf = calloc(1, sizeof(struct conf)); + conf->redis_host = strdup("127.0.0.1"); + conf->redis_port = 6379; + conf->http_host = strdup("0.0.0.0"); + conf->http_port = 7379; + conf->http_max_request_size = 128*1024*1024; + conf->http_threads = 4; + conf->user = getuid(); + conf->group = getgid(); + conf->logfile = "webdis.log"; + conf->verbosity = WEBDIS_NOTICE; + conf->daemonize = 0; + conf->pidfile = "webdis.pid"; + conf->database = 0; + conf->pool_size_per_thread = 2; + + j = json_load_file(filename, 0, &error); + if(!j) { + fprintf(stderr, "Error: %s (line %d)\n", error.text, error.line); + return conf; + } + + for(kv = json_object_iter(j); kv; kv = json_object_iter_next(j, kv)) { + json_t *jtmp = json_object_iter_value(kv); + + if(strcmp(json_object_iter_key(kv), "redis_host") == 0 && json_typeof(jtmp) == JSON_STRING) { + free(conf->redis_host); + conf->redis_host = strdup(json_string_value(jtmp)); + } else if(strcmp(json_object_iter_key(kv), "redis_port") == 0 && json_typeof(jtmp) == JSON_INTEGER) { + conf->redis_port = (short)json_integer_value(jtmp); + } else if(strcmp(json_object_iter_key(kv), "redis_auth") == 0 && json_typeof(jtmp) == JSON_STRING) { + conf->redis_auth = strdup(json_string_value(jtmp)); + } else if(strcmp(json_object_iter_key(kv), "http_host") == 0 && json_typeof(jtmp) == JSON_STRING) { + free(conf->http_host); + conf->http_host = strdup(json_string_value(jtmp)); + } else if(strcmp(json_object_iter_key(kv), "http_port") == 0 && json_typeof(jtmp) == JSON_INTEGER) { + conf->http_port = (short)json_integer_value(jtmp); + } else if(strcmp(json_object_iter_key(kv), "http_max_request_size") == 0 && json_typeof(jtmp) == JSON_INTEGER) { + conf->http_max_request_size = (size_t)json_integer_value(jtmp); + } else if(strcmp(json_object_iter_key(kv), "threads") == 0 && json_typeof(jtmp) == JSON_INTEGER) { + conf->http_threads = (short)json_integer_value(jtmp); + } else if(strcmp(json_object_iter_key(kv), "acl") == 0 && json_typeof(jtmp) == JSON_ARRAY) { + conf->perms = conf_parse_acls(jtmp); + } else if(strcmp(json_object_iter_key(kv), "user") == 0 && json_typeof(jtmp) == JSON_STRING) { + struct passwd *u; + if((u = getpwnam(json_string_value(jtmp)))) { + conf->user = u->pw_uid; + } + } else if(strcmp(json_object_iter_key(kv), "group") == 0 && json_typeof(jtmp) == JSON_STRING) { + struct group *g; + if((g = getgrnam(json_string_value(jtmp)))) { + conf->group = g->gr_gid; + } + } else if(strcmp(json_object_iter_key(kv),"logfile") == 0 && json_typeof(jtmp) == JSON_STRING){ + conf->logfile = strdup(json_string_value(jtmp)); + } else if(strcmp(json_object_iter_key(kv),"verbosity") == 0 && json_typeof(jtmp) == JSON_INTEGER){ + int tmp = json_integer_value(jtmp); + if(tmp < 0) conf->verbosity = WEBDIS_ERROR; + else if(tmp > (int)WEBDIS_DEBUG) conf->verbosity = WEBDIS_DEBUG; + else conf->verbosity = (log_level)tmp; + } else if(strcmp(json_object_iter_key(kv), "daemonize") == 0 && json_typeof(jtmp) == JSON_TRUE) { + conf->daemonize = 1; + } else if(strcmp(json_object_iter_key(kv),"pidfile") == 0 && json_typeof(jtmp) == JSON_STRING){ + conf->pidfile = strdup(json_string_value(jtmp)); + } else if(strcmp(json_object_iter_key(kv), "websockets") == 0 && json_typeof(jtmp) == JSON_TRUE) { + conf->websockets = 1; + } else if(strcmp(json_object_iter_key(kv), "database") == 0 && json_typeof(jtmp) == JSON_INTEGER) { + conf->database = json_integer_value(jtmp); + } else if(strcmp(json_object_iter_key(kv), "pool_size") == 0 && json_typeof(jtmp) == JSON_INTEGER) { + conf->pool_size_per_thread = json_integer_value(jtmp); + } else if(strcmp(json_object_iter_key(kv), "default_root") == 0 && json_typeof(jtmp) == JSON_STRING) { + conf->default_root = strdup(json_string_value(jtmp)); + } + } + + json_decref(j); + + return conf; +} + +void +acl_read_commands(json_t *jlist, struct acl_commands *ac) { + + unsigned int i, n, cur; + + /* count strings in the array */ + for(i = 0, n = 0; i < json_array_size(jlist); ++i) { + json_t *jelem = json_array_get(jlist, (size_t)i); + if(json_typeof(jelem) == JSON_STRING) { + n++; + } + } + + /* allocate block */ + ac->commands = calloc((size_t)n, sizeof(char*)); + ac->count = n; + + /* add all disabled commands */ + for(i = 0, cur = 0; i < json_array_size(jlist); ++i) { + json_t *jelem = json_array_get(jlist, i); + if(json_typeof(jelem) == JSON_STRING) { + size_t sz; + const char *s = json_string_value(jelem); + sz = strlen(s); + + ac->commands[cur] = calloc(1 + sz, 1); + memcpy(ac->commands[cur], s, sz); + cur++; + } + } +} + +struct acl * +conf_parse_acl(json_t *j) { + + json_t *jcidr, *jbasic, *jlist; + unsigned short mask_bits = 0; + + struct acl *a = calloc(1, sizeof(struct acl)); + + /* parse CIDR */ + if((jcidr = json_object_get(j, "ip")) && json_typeof(jcidr) == JSON_STRING) { + const char *s; + char *p, *ip; + + s = json_string_value(jcidr); + p = strchr(s, '/'); + if(!p) { + ip = strdup(s); + } else { + ip = calloc((size_t)(p - s + 1), 1); + memcpy(ip, s, (size_t)(p - s)); + mask_bits = (unsigned short)atoi(p+1); + } + a->cidr.enabled = 1; + a->cidr.mask = (mask_bits == 0 ? 0xffffffff : (0xffffffff << (32 - mask_bits))); + a->cidr.subnet = ntohl(inet_addr(ip)) & a->cidr.mask; + free(ip); + } + + /* parse basic_auth */ + if((jbasic = json_object_get(j, "http_basic_auth")) && json_typeof(jbasic) == JSON_STRING) { + + /* base64 encode */ + base64_encodestate b64; + int pos; + char *p; + const char *plain = json_string_value(jbasic); + size_t len, plain_len = strlen(plain) + 0; + len = (plain_len + 8) * 8 / 6; + a->http_basic_auth = calloc(len, 1); + + base64_init_encodestate(&b64); + pos = base64_encode_block(plain, (int)plain_len, a->http_basic_auth, &b64); /* FIXME: check return value */ + base64_encode_blockend(a->http_basic_auth + pos, &b64); + + /* end string with \0 rather than \n */ + if((p = strchr(a->http_basic_auth + pos, '\n'))) { + *p = 0; + } + } + + /* parse enabled commands */ + if((jlist = json_object_get(j, "enabled")) && json_typeof(jlist) == JSON_ARRAY) { + acl_read_commands(jlist, &a->enabled); + } + + /* parse disabled commands */ + if((jlist = json_object_get(j, "disabled")) && json_typeof(jlist) == JSON_ARRAY) { + acl_read_commands(jlist, &a->disabled); + } + + return a; +} + +struct acl * +conf_parse_acls(json_t *jtab) { + + struct acl *head = NULL, *tail = NULL, *tmp; + + unsigned int i; + for(i = 0; i < json_array_size(jtab); ++i) { + json_t *val = json_array_get(jtab, i); + + tmp = conf_parse_acl(val); + if(head == NULL && tail == NULL) { + head = tail = tmp; + } else { + tail->next = tmp; + tail = tmp; + } + } + + return head; +} + +void +conf_free(struct conf *conf) { + + free(conf->redis_host); + free(conf->redis_auth); + + free(conf->http_host); + + free(conf); +} diff --git a/conf.h b/conf.h new file mode 100644 index 0000000..28ba115 --- /dev/null +++ b/conf.h @@ -0,0 +1,54 @@ +#ifndef CONF_H +#define CONF_H + +#include +#include "slog.h" + +struct conf { + + /* connection to Redis */ + char *redis_host; + short redis_port; + char *redis_auth; + + /* HTTP server interface */ + char *http_host; + short http_port; + short http_threads; + size_t http_max_request_size; + + /* pool size, one pool per worker thread */ + int pool_size_per_thread; + + /* daemonize process, off by default */ + int daemonize; + char *pidfile; + + /* WebSocket support, off by default */ + int websockets; + + /* database number */ + int database; + + /* ACL */ + struct acl *perms; + + /* user/group */ + uid_t user; + gid_t group; + + /* Logging */ + char *logfile; + log_level verbosity; + + /* Request to serve on “/” */ + char *default_root; +}; + +struct conf * +conf_read(const char *filename); + +void +conf_free(struct conf *conf); + +#endif /* CONF_H */ diff --git a/formats/common.c b/formats/common.c new file mode 100644 index 0000000..e0b456f --- /dev/null +++ b/formats/common.c @@ -0,0 +1,141 @@ +#include "common.h" +#include "cmd.h" +#include "http.h" +#include "client.h" +#include "websocket.h" + +#include "md5/md5.h" +#include +#include + +/* TODO: replace this with a faster hash function? */ +char *etag_new(const char *p, size_t sz) { + + md5_byte_t buf[16]; + char *etag = calloc(34 + 1, 1); + int i; + + if(!etag) + return NULL; + + md5_state_t pms; + + md5_init(&pms); + md5_append(&pms, (const md5_byte_t *)p, (int)sz); + md5_finish(&pms, buf); + + for(i = 0; i < 16; ++i) { + sprintf(etag + 1 + 2*i, "%.2x", (unsigned char)buf[i]); + } + + etag[0] = '"'; + etag[33] = '"'; + + return etag; +} + +void +format_send_error(struct cmd *cmd, short code, const char *msg) { + + struct http_response *resp; + + if(!cmd->is_websocket && !cmd->pub_sub_client) { + resp = http_response_init(cmd->w, code, msg); + resp->http_version = cmd->http_version; + http_response_set_keep_alive(resp, cmd->keep_alive); + http_response_write(resp, cmd->fd); + } + + /* for pub/sub, remove command from client */ + if(cmd->pub_sub_client) { + cmd->pub_sub_client->pub_sub = NULL; + } else { + cmd_free(cmd); + } +} + +void +format_send_reply(struct cmd *cmd, const char *p, size_t sz, const char *content_type) { + + int free_cmd = 1; + const char *ct = cmd->mime?cmd->mime:content_type; + struct http_response *resp; + + if(cmd->is_websocket) { + ws_reply(cmd, p, sz); + + /* If it's a subscribe command, there'll be more responses */ + if(!cmd_is_subscribe(cmd)) + cmd_free(cmd); + return; + } + + if(cmd_is_subscribe(cmd)) { + free_cmd = 0; + + /* start streaming */ + if(cmd->started_responding == 0) { + cmd->started_responding = 1; + resp = http_response_init(cmd->w, 200, "OK"); + resp->http_version = cmd->http_version; + if(cmd->filename) { + http_response_set_header(resp, "Content-Disposition", cmd->filename); + } + http_response_set_header(resp, "Content-Type", ct); + http_response_set_keep_alive(resp, 1); + http_response_set_header(resp, "Transfer-Encoding", "chunked"); + http_response_set_body(resp, p, sz); + http_response_write(resp, cmd->fd); + } else { + /* Asynchronous chunk write. */ + http_response_write_chunk(cmd->fd, cmd->w, p, sz); + } + + } else { + /* compute ETag */ + char *etag = etag_new(p, sz); + + if(etag) { + /* check If-None-Match */ + if(cmd->if_none_match && strcmp(cmd->if_none_match, etag) == 0) { + /* SAME! send 304. */ + resp = http_response_init(cmd->w, 304, "Not Modified"); + } else { + resp = http_response_init(cmd->w, 200, "OK"); + if(cmd->filename) { + http_response_set_header(resp, "Content-Disposition", cmd->filename); + } + http_response_set_header(resp, "Content-Type", ct); + http_response_set_header(resp, "ETag", etag); + http_response_set_body(resp, p, sz); + } + resp->http_version = cmd->http_version; + http_response_set_keep_alive(resp, cmd->keep_alive); + http_response_write(resp, cmd->fd); + free(etag); + } else { + format_send_error(cmd, 503, "Service Unavailable"); + } + } + + /* cleanup */ + if(free_cmd) { + cmd_free(cmd); + } +} + +int +integer_length(long long int i) { + int sz = 0; + int ci = abs(i); + while (ci > 0) { + ci = (ci/10); + sz += 1; + } + if(i == 0) { /* log 0 doesn't make sense. */ + sz = 1; + } else if(i < 0) { /* allow for neg sign as well. */ + sz++; + } + return sz; +} diff --git a/formats/common.h b/formats/common.h new file mode 100644 index 0000000..b4bfeb5 --- /dev/null +++ b/formats/common.h @@ -0,0 +1,18 @@ +#ifndef FORMATS_COMMON_H +#define FORMATS_COMMON_H + +#include + +struct cmd; + +void +format_send_reply(struct cmd *cmd, + const char *p, size_t sz, + const char *content_type); + +void +format_send_error(struct cmd *cmd, short code, const char *msg); +int +integer_length(long long int i); + +#endif diff --git a/formats/custom-type.c b/formats/custom-type.c new file mode 100644 index 0000000..3fc6e08 --- /dev/null +++ b/formats/custom-type.c @@ -0,0 +1,118 @@ +#include "custom-type.h" +#include "cmd.h" +#include "common.h" +#include "http.h" + +#include +#include +#include + +static char * +custom_array(struct cmd *cmd, const redisReply *r, size_t *sz); + +void +custom_type_reply(redisAsyncContext *c, void *r, void *privdata) { + + redisReply *reply = r; + struct cmd *cmd = privdata; + (void)c; + char int_buffer[50]; + char *status_buf; + int int_len; + struct http_response *resp; + size_t sz; + char *array_out; + + + if (reply == NULL) { /* broken Redis link */ + format_send_error(cmd, 503, "Service Unavailable"); + return; + } + + if(cmd->mime) { /* use the given content-type, but only for strings */ + switch(reply->type) { + + case REDIS_REPLY_NIL: /* or nil values */ + format_send_error(cmd, 404, "Not found"); + return; + + case REDIS_REPLY_STRING: + format_send_reply(cmd, reply->str, reply->len, cmd->mime); + return; + + case REDIS_REPLY_STATUS: + case REDIS_REPLY_ERROR: + status_buf = calloc(1 + reply->len, 1); + status_buf[0] = (reply->type == REDIS_REPLY_STATUS ? '+' : '-'); + memcpy(status_buf + 1, reply->str, reply->len); + format_send_reply(cmd, status_buf, 1 + reply->len, cmd->mime); + free(status_buf); + return; + + case REDIS_REPLY_INTEGER: + int_len = sprintf(int_buffer, "%lld", reply->integer); + format_send_reply(cmd, int_buffer, int_len, cmd->mime); + return; + case REDIS_REPLY_ARRAY: + array_out = custom_array(cmd, r, &sz); + format_send_reply(cmd, array_out, sz, cmd->mime); + free(array_out); + return; + } + } + + /* couldn't make sense of what the client wanted. */ + resp = http_response_init(cmd->w, 400, "Bad Request"); + http_response_set_header(resp, "Content-Length", "0"); + http_response_set_keep_alive(resp, cmd->keep_alive); + http_response_write(resp, cmd->fd); + + if(!cmd_is_subscribe(cmd)) { + cmd_free(cmd); + } +} + +static char * +custom_array(struct cmd *cmd, const redisReply *r, size_t *sz) { + + unsigned int i; + char *ret, *p; + size_t sep_len = 0; + + if(cmd->separator) + sep_len = strlen(cmd->separator); + + /* compute size */ + *sz = 0; + for(i = 0; i < r->elements; ++i) { + redisReply *e = r->element[i]; + switch(e->type) { + case REDIS_REPLY_STRING: + if(sep_len && i != 0) + *sz += sep_len; + *sz += e->len; + break; + + } + } + + /* allocate */ + p = ret = malloc(*sz); + + /* copy */ + for(i = 0; i < r->elements; ++i) { + redisReply *e = r->element[i]; + switch(e->type) { + case REDIS_REPLY_STRING: + if(sep_len && i != 0) { + memcpy(p, cmd->separator, sep_len); + p += sep_len; + } + memcpy(p, e->str, e->len); + p += e->len; + break; + } + } + + return ret; +} diff --git a/formats/custom-type.h b/formats/custom-type.h new file mode 100644 index 0000000..cb5b7be --- /dev/null +++ b/formats/custom-type.h @@ -0,0 +1,12 @@ +#ifndef CUSTOM_TYPE_H +#define CUSTOM_TYPE_H + +#include +#include + +struct cmd; + +void +custom_type_reply(redisAsyncContext *c, void *r, void *privdata); + +#endif diff --git a/formats/json.c b/formats/json.c new file mode 100644 index 0000000..56c3a95 --- /dev/null +++ b/formats/json.c @@ -0,0 +1,293 @@ +#include "json.h" +#include "common.h" +#include "cmd.h" +#include "http.h" +#include "client.h" + +#include +#include +#include + +static json_t * +json_wrap_redis_reply(const struct cmd *cmd, const redisReply *r); + +void +json_reply(redisAsyncContext *c, void *r, void *privdata) { + + redisReply *reply = r; + struct cmd *cmd = privdata; + json_t *j; + char *jstr; + (void)c; + + if(cmd == NULL) { + /* broken connection */ + return; + } + + if (reply == NULL) { /* broken Redis link */ + format_send_error(cmd, 503, "Service Unavailable"); + return; + } + + /* encode redis reply as JSON */ + j = json_wrap_redis_reply(cmd, r); + + /* get JSON as string, possibly with JSONP wrapper */ + jstr = json_string_output(j, cmd->jsonp); + + /* send reply */ + format_send_reply(cmd, jstr, strlen(jstr), "application/json"); + + /* cleanup */ + json_decref(j); + free(jstr); +} + +/** + * Parse info message and return object. + */ +static json_t * +json_info_reply(const char *s) { + const char *p = s; + size_t sz = strlen(s); + + json_t *jroot = json_object(); + + /* TODO: handle new format */ + + while(p < s + sz) { + char *key, *val, *nl, *colon; + + /* find key */ + colon = strchr(p, ':'); + if(!colon) { + break; + } + key = calloc(colon - p + 1, 1); + memcpy(key, p, colon - p); + p = colon + 1; + + /* find value */ + nl = strchr(p, '\r'); + if(!nl) { + free(key); + break; + } + val = calloc(nl - p + 1, 1); + memcpy(val, p, nl - p); + p = nl + 1; + if(*p == '\n') p++; + + /* add to object */ + json_object_set_new(jroot, key, json_string(val)); + free(key); + free(val); + } + + return jroot; +} + +static json_t * +json_hgetall_reply(const redisReply *r) { + /* zip keys and values together in a json object */ + json_t *jroot; + unsigned int i; + + if(r->elements % 2 != 0) { + return NULL; + } + + jroot = json_object(); + for(i = 0; i < r->elements; i += 2) { + redisReply *k = r->element[i], *v = r->element[i+1]; + + /* keys and values need to be strings */ + if(k->type != REDIS_REPLY_STRING || v->type != REDIS_REPLY_STRING) { + json_decref(jroot); + return NULL; + } + json_object_set_new(jroot, k->str, json_string(v->str)); + } + return jroot; +} + +static json_t * +json_wrap_redis_reply(const struct cmd *cmd, const redisReply *r) { + + unsigned int i; + json_t *jlist, *jroot = json_object(); /* that's what we return */ + + + /* copy verb, as jansson only takes a char* but not its length. */ + char *verb; + if(cmd->count) { + verb = calloc(cmd->argv_len[0]+1, 1); + memcpy(verb, cmd->argv[0], cmd->argv_len[0]); + } else { + verb = strdup(""); + } + + + switch(r->type) { + case REDIS_REPLY_STATUS: + case REDIS_REPLY_ERROR: + jlist = json_array(); + json_array_append_new(jlist, + r->type == REDIS_REPLY_ERROR ? json_false() : json_true()); + json_array_append_new(jlist, json_string(r->str)); + json_object_set_new(jroot, verb, jlist); + break; + + case REDIS_REPLY_STRING: + if(strcasecmp(verb, "INFO") == 0) { + json_object_set_new(jroot, verb, json_info_reply(r->str)); + } else { + json_object_set_new(jroot, verb, json_string(r->str)); + } + break; + + case REDIS_REPLY_INTEGER: + json_object_set_new(jroot, verb, json_integer(r->integer)); + break; + + case REDIS_REPLY_ARRAY: + if(strcasecmp(verb, "HGETALL") == 0) { + json_t *jobj = json_hgetall_reply(r); + if(jobj) { + json_object_set_new(jroot, verb, jobj); + break; + } + } + jlist = json_array(); + for(i = 0; i < r->elements; ++i) { + redisReply *e = r->element[i]; + switch(e->type) { + case REDIS_REPLY_STRING: + json_array_append_new(jlist, json_string(e->str)); + break; + case REDIS_REPLY_INTEGER: + json_array_append_new(jlist, json_integer(e->integer)); + break; + default: + json_array_append_new(jlist, json_null()); + break; + } + } + json_object_set_new(jroot, verb, jlist); + break; + + default: + json_object_set_new(jroot, verb, json_null()); + break; + } + + free(verb); + return jroot; +} + + +char * +json_string_output(json_t *j, const char *jsonp) { + + char *json_reply = json_dumps(j, JSON_COMPACT); + + /* check for JSONP */ + if(jsonp) { + size_t jsonp_len = strlen(jsonp); + size_t json_len = strlen(json_reply); + size_t ret_len = jsonp_len + 1 + json_len + 3; + char *ret = calloc(1 + ret_len, 1); + + memcpy(ret, jsonp, jsonp_len); + ret[jsonp_len]='('; + memcpy(ret + jsonp_len + 1, json_reply, json_len); + memcpy(ret + jsonp_len + 1 + json_len, ");\n", 3); + free(json_reply); + + return ret; + } + + return json_reply; +} + +/* extract JSON from WebSocket frame and fill struct cmd. */ +struct cmd * +json_ws_extract(struct http_client *c, const char *p, size_t sz) { + + struct cmd *cmd = NULL; + json_t *j; + char *jsonz; /* null-terminated */ + + unsigned int i, cur; + int argc = 0; + json_error_t jerror; + + (void)c; + + jsonz = calloc(sz + 1, 1); + memcpy(jsonz, p, sz); + j = json_loads(jsonz, sz, &jerror); + free(jsonz); + + if(!j) { + return NULL; + } + if(json_typeof(j) != JSON_ARRAY) { + json_decref(j); + return NULL; /* invalid JSON */ + } + + /* count elements */ + for(i = 0; i < json_array_size(j); ++i) { + json_t *jelem = json_array_get(j, i); + + switch(json_typeof(jelem)) { + case JSON_STRING: + case JSON_INTEGER: + argc++; + break; + + default: + break; + } + } + + if(!argc) { /* not a single item could be decoded */ + json_decref(j); + return NULL; + } + + /* create command and add args */ + cmd = cmd_new(argc); + for(i = 0, cur = 0; i < json_array_size(j); ++i) { + json_t *jelem = json_array_get(j, i); + char *tmp; + + switch(json_typeof(jelem)) { + case JSON_STRING: + tmp = strdup(json_string_value(jelem)); + + cmd->argv[cur] = tmp; + cmd->argv_len[cur] = strlen(tmp); + cur++; + break; + + case JSON_INTEGER: + tmp = malloc(40); + sprintf(tmp, "%d", (int)json_integer_value(jelem)); + + cmd->argv[cur] = tmp; + cmd->argv_len[cur] = strlen(tmp); + cur++; + break; + + default: + break; + } + } + + json_decref(j); + return cmd; +} + diff --git a/formats/json.h b/formats/json.h new file mode 100644 index 0000000..911f8f5 --- /dev/null +++ b/formats/json.h @@ -0,0 +1,20 @@ +#ifndef JSON_H +#define JSON_H + +#include +#include +#include + +struct cmd; +struct http_client; + +void +json_reply(redisAsyncContext *c, void *r, void *privdata); + +char * +json_string_output(json_t *j, const char *jsonp); + +struct cmd * +json_ws_extract(struct http_client *c, const char *p, size_t sz); + +#endif diff --git a/formats/msgpack.c b/formats/msgpack.c new file mode 100644 index 0000000..dc1ab0c --- /dev/null +++ b/formats/msgpack.c @@ -0,0 +1,236 @@ +#include "msgpack.h" +#include "common.h" +#include "cmd.h" +#include "http.h" +#include "client.h" + +#include +#include +#include + +struct msg_out { + char *p; + size_t sz; +}; + +static void +msgpack_wrap_redis_reply(const struct cmd *cmd, struct msg_out *, const redisReply *r); + +void +msgpack_reply(redisAsyncContext *c, void *r, void *privdata) { + + redisReply *reply = r; + struct cmd *cmd = privdata; + struct msg_out out; + (void)c; + + if(cmd == NULL) { + /* broken connection */ + return; + } + + if (reply == NULL) { /* broken Redis link */ + format_send_error(cmd, 503, "Service Unavailable"); + return; + } + + /* prepare data structure for output */ + out.p = NULL; + out.sz = 0; + + /* encode redis reply */ + msgpack_wrap_redis_reply(cmd, &out, r); + + /* send reply */ + format_send_reply(cmd, out.p, out.sz, "application/x-msgpack"); + + /* cleanup */ + free(out.p); +} + +static int +on_msgpack_write(void *data, const char *s, unsigned int sz) { + + struct msg_out *out = data; + + out->p = realloc(out->p, out->sz + sz); + memcpy(out->p + out->sz, s, sz); + out->sz += sz; + + return sz; +} + +/** + * Parse info message and return object. + */ +void +msg_info_reply(msgpack_packer* pk, const char *s, size_t sz) { + + const char *p = s; + unsigned int count = 0; + + /* TODO: handle new format */ + + /* count number of lines */ + while(p < s + sz) { + p = strchr(p, '\r'); + if(!p) break; + + p++; + count++; + } + + /* create msgpack object */ + msgpack_pack_map(pk, count); + + p = s; + while(p < s + sz) { + char *key, *val, *nl, *colon; + size_t key_sz, val_sz; + + /* find key */ + colon = strchr(p, ':'); + if(!colon) { + break; + } + key_sz = colon - p; + key = calloc(key_sz + 1, 1); + memcpy(key, p, key_sz); + p = colon + 1; + + /* find value */ + nl = strchr(p, '\r'); + if(!nl) { + free(key); + break; + } + val_sz = nl - p; + val = calloc(val_sz + 1, 1); + memcpy(val, p, val_sz); + p = nl + 1; + if(*p == '\n') p++; + + /* add to object */ + msgpack_pack_raw(pk, key_sz); + msgpack_pack_raw_body(pk, key, key_sz); + msgpack_pack_raw(pk, val_sz); + msgpack_pack_raw_body(pk, val, val_sz); + + free(key); + free(val); + } +} + +static void +msg_hgetall_reply(msgpack_packer* pk, const redisReply *r) { + + /* zip keys and values together in a msgpack object */ + + unsigned int i; + + if(r->elements % 2 != 0) { + return; + } + + msgpack_pack_map(pk, r->elements / 2); + for(i = 0; i < r->elements; i += 2) { + redisReply *k = r->element[i], *v = r->element[i+1]; + + /* keys and values need to be strings */ + if(k->type != REDIS_REPLY_STRING || v->type != REDIS_REPLY_STRING) { + return; + } + + /* key */ + msgpack_pack_raw(pk, k->len); + msgpack_pack_raw_body(pk, k->str, k->len); + + /* value */ + msgpack_pack_raw(pk, v->len); + msgpack_pack_raw_body(pk, v->str, v->len); + } +} + +static void +msgpack_wrap_redis_reply(const struct cmd *cmd, struct msg_out *out, const redisReply *r) { + + unsigned int i; + msgpack_packer* pk = msgpack_packer_new(out, on_msgpack_write); + + /* copy verb, as jansson only takes a char* but not its length. */ + char *verb = ""; + size_t verb_sz = 0; + if(cmd->count) { + verb_sz = cmd->argv_len[0]; + verb = cmd->argv[0]; + } + + /* Create map object */ + msgpack_pack_map(pk, 1); + + /* The single element is the verb */ + msgpack_pack_raw(pk, verb_sz); + msgpack_pack_raw_body(pk, verb, verb_sz); + + switch(r->type) { + case REDIS_REPLY_STATUS: + case REDIS_REPLY_ERROR: + msgpack_pack_array(pk, 2); + + /* first element: book */ + if(r->type == REDIS_REPLY_ERROR) + msgpack_pack_false(pk); + else + msgpack_pack_true(pk); + + /* second element: message */ + msgpack_pack_raw(pk, r->len); + msgpack_pack_raw_body(pk, r->str, r->len); + break; + + case REDIS_REPLY_STRING: + if(verb_sz ==4 && strncasecmp(verb, "INFO", 4) == 0) { + msg_info_reply(pk, r->str, r->len); + } else { + msgpack_pack_raw(pk, r->len); + msgpack_pack_raw_body(pk, r->str, r->len); + } + break; + + case REDIS_REPLY_INTEGER: + msgpack_pack_int(pk, r->integer); + break; + + case REDIS_REPLY_ARRAY: + if(verb_sz == 7 && strncasecmp(verb, "HGETALL", 7) == 0) { + msg_hgetall_reply(pk, r); + break; + } + + msgpack_pack_array(pk, r->elements); + + for(i = 0; i < r->elements; ++i) { + redisReply *e = r->element[i]; + switch(e->type) { + case REDIS_REPLY_STRING: + msgpack_pack_raw(pk, e->len); + msgpack_pack_raw_body(pk, e->str, e->len); + break; + case REDIS_REPLY_INTEGER: + msgpack_pack_int(pk, e->integer); + break; + default: + msgpack_pack_nil(pk); + break; + } + } + + break; + + default: + msgpack_pack_nil(pk); + break; + } + + msgpack_packer_free(pk); +} diff --git a/formats/msgpack.h b/formats/msgpack.h new file mode 100644 index 0000000..796caec --- /dev/null +++ b/formats/msgpack.h @@ -0,0 +1,11 @@ +#ifndef MSGPACK_H +#define MSGPACK_H + +#include +#include +#include + +void +msgpack_reply(redisAsyncContext *c, void *r, void *privdata); + +#endif diff --git a/formats/raw.c b/formats/raw.c new file mode 100644 index 0000000..fdc9051 --- /dev/null +++ b/formats/raw.c @@ -0,0 +1,192 @@ +#include "raw.h" +#include "common.h" +#include "http.h" +#include "client.h" +#include "cmd.h" + +#include +#include +#include + +static char * +raw_wrap(const redisReply *r, size_t *sz); + +void +raw_reply(redisAsyncContext *c, void *r, void *privdata) { + + redisReply *reply = r; + struct cmd *cmd = privdata; + char *raw_out; + size_t sz; + (void)c; + + if (reply == NULL) { /* broken Redis link */ + format_send_error(cmd, 503, "Service Unavailable"); + return; + } + + raw_out = raw_wrap(r, &sz); + + /* send reply */ + format_send_reply(cmd, raw_out, sz, "binary/octet-stream"); + + /* cleanup */ + free(raw_out); +} + +/* extract Redis protocol string from WebSocket frame and fill struct cmd. */ +struct cmd * +raw_ws_extract(struct http_client *c, const char *p, size_t sz) { + + struct cmd *cmd = NULL; + void *reader = NULL; + redisReply *reply = NULL; + void **reply_ptr = (void**)&reply; + unsigned int i; + (void)c; + + /* create protocol reader */ + reader = redisReaderCreate(); + + /* add data */ + redisReaderFeed(reader, (char*)p, sz); + + /* parse data into reply object */ + if(redisReaderGetReply(reader, reply_ptr) == REDIS_ERR) { + goto end; + } + + /* add data from reply object to cmd struct */ + if(reply->type != REDIS_REPLY_ARRAY) { + goto end; + } + + /* create cmd object */ + cmd = cmd_new(reply->elements); + + for(i = 0; i < reply->elements; ++i) { + redisReply *ri = reply->element[i]; + + switch(ri->type) { + case REDIS_REPLY_STRING: + cmd->argv_len[i] = ri->len; + cmd->argv[i] = calloc(cmd->argv_len[i] + 1, 1); + memcpy(cmd->argv[i], ri->str, ri->len); + break; + + case REDIS_REPLY_INTEGER: + cmd->argv_len[i] = integer_length(ri->integer); + cmd->argv[i] = calloc(cmd->argv_len[i] + 1, 1); + sprintf(cmd->argv[i], "%lld", ri->integer); + break; + + default: + cmd_free(cmd); + cmd = NULL; + goto end; + } + } + + +end: + /* free reader */ + if(reader) redisReaderFree(reader); + + /* free reply */ + if(reply) freeReplyObject(reply); + + return cmd; +} + + +static char * +raw_array(const redisReply *r, size_t *sz) { + + unsigned int i; + char *ret, *p; + + /* compute size */ + *sz = 0; + *sz += 1 + integer_length(r->elements) + 2; + for(i = 0; i < r->elements; ++i) { + redisReply *e = r->element[i]; + switch(e->type) { + case REDIS_REPLY_STRING: + *sz += 1 + integer_length(e->len) + 2 + + e->len + 2; + break; + case REDIS_REPLY_INTEGER: + *sz += 1 + integer_length(integer_length(e->integer)) + 2 + + integer_length(e->integer) + 2; + break; + + } + } + + /* allocate */ + p = ret = malloc(1+*sz); + p += sprintf(p, "*%zd\r\n", r->elements); + + /* copy */ + for(i = 0; i < r->elements; ++i) { + redisReply *e = r->element[i]; + switch(e->type) { + case REDIS_REPLY_STRING: + p += sprintf(p, "$%d\r\n", e->len); + memcpy(p, e->str, e->len); + p += e->len; + *p = '\r'; + p++; + *p = '\n'; + p++; + break; + case REDIS_REPLY_INTEGER: + p += sprintf(p, "$%d\r\n%lld\r\n", + integer_length(e->integer), e->integer); + break; + } + } + + return ret; +} + +static char * +raw_wrap(const redisReply *r, size_t *sz) { + + char *ret, *p; + + switch(r->type) { + case REDIS_REPLY_STATUS: + case REDIS_REPLY_ERROR: + *sz = 3 + r->len; + ret = malloc(*sz); + ret[0] = (r->type == REDIS_REPLY_STATUS?'+':'-'); + memcpy(ret+1, r->str, *sz-3); + memcpy(ret+*sz - 2, "\r\n", 2); + return ret; + + case REDIS_REPLY_STRING: + *sz = 1 + integer_length(r->len) + 2 + r->len + 2; + p = ret = malloc(*sz); + p += sprintf(p, "$%d\r\n", r->len); + memcpy(p, r->str, *sz - 2 - (p-ret)); + memcpy(ret + *sz - 2, "\r\n", 2); + return ret; + + case REDIS_REPLY_INTEGER: + *sz = 3 + integer_length(r->integer); + ret = malloc(4+*sz); + sprintf(ret, ":%lld\r\n", r->integer); + return ret; + + case REDIS_REPLY_ARRAY: + return raw_array(r, sz); + + default: + *sz = 5; + ret = malloc(*sz); + memcpy(ret, "$-1\r\n", 5); + return ret; + } +} + diff --git a/formats/raw.h b/formats/raw.h new file mode 100644 index 0000000..10321e3 --- /dev/null +++ b/formats/raw.h @@ -0,0 +1,16 @@ +#ifndef RAW_H +#define RAW_H + +#include +#include + +struct cmd; +struct http_client; + +void +raw_reply(redisAsyncContext *c, void *r, void *privdata); + +struct cmd * +raw_ws_extract(struct http_client *c, const char *p, size_t sz); + +#endif diff --git a/hiredis/.gitignore b/hiredis/.gitignore new file mode 100644 index 0000000..1a4d60d --- /dev/null +++ b/hiredis/.gitignore @@ -0,0 +1,6 @@ +/hiredis-test +/hiredis-example* +/*.o +/*.so +/*.dylib +/*.a diff --git a/hiredis/COPYING b/hiredis/COPYING new file mode 100644 index 0000000..a5fc973 --- /dev/null +++ b/hiredis/COPYING @@ -0,0 +1,29 @@ +Copyright (c) 2009-2011, Salvatore Sanfilippo +Copyright (c) 2010-2011, Pieter Noordhuis + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Redis nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/hiredis/Makefile b/hiredis/Makefile new file mode 100644 index 0000000..57f057e --- /dev/null +++ b/hiredis/Makefile @@ -0,0 +1,148 @@ +# Hiredis Makefile +# Copyright (C) 2010-2011 Salvatore Sanfilippo +# Copyright (C) 2010-2011 Pieter Noordhuis +# This file is released under the BSD license, see the COPYING file + +OBJ=net.o hiredis.o sds.o async.o +BINS=hiredis-example hiredis-test +LIBNAME=libhiredis + +HIREDIS_MAJOR=0 +HIREDIS_MINOR=10 + +# Fallback to gcc when $CC is not in $PATH. +CC:=$(shell sh -c 'type $(CC) >/dev/null 2>/dev/null && echo $(CC) || echo gcc') +OPTIMIZATION?=-O3 +WARNINGS=-Wall -W -Wstrict-prototypes -Wwrite-strings +DEBUG?= -g -ggdb +REAL_CFLAGS=$(OPTIMIZATION) -fPIC $(CFLAGS) $(WARNINGS) $(DEBUG) +REAL_LDFLAGS=$(LDFLAGS) + +DYLIBSUFFIX=so +STLIBSUFFIX=a +DYLIB_MINOR_NAME=$(LIBNAME).$(DYLIBSUFFIX).$(HIREDIS_MAJOR).$(HIREDIS_MINOR) +DYLIB_MAJOR_NAME=$(LIBNAME).$(DYLIBSUFFIX).$(HIREDIS_MAJOR) +DYLIBNAME=$(LIBNAME).$(DYLIBSUFFIX) +DYLIB_MAKE_CMD=$(CC) -shared -Wl,-soname,$(DYLIB_MINOR_NAME) -o $(DYLIBNAME) $(LDFLAGS) +STLIBNAME=$(LIBNAME).$(STLIBSUFFIX) +STLIB_MAKE_CMD=ar rcs $(STLIBNAME) + +# Platform-specific overrides +uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not') +ifeq ($(uname_S),SunOS) + REAL_LDFLAGS+= -ldl -lnsl -lsocket + DYLIB_MAKE_CMD=$(CC) -G -o $(DYLIBNAME) -h $(DYLIB_MINOR_NAME) $(LDFLAGS) + INSTALL= cp -r +endif +ifeq ($(uname_S),Darwin) + DYLIBSUFFIX=dylib + DYLIB_MINOR_NAME=$(LIBNAME).$(HIREDIS_MAJOR).$(HIREDIS_MINOR).$(DYLIBSUFFIX) + DYLIB_MAJOR_NAME=$(LIBNAME).$(HIREDIS_MAJOR).$(DYLIBSUFFIX) + DYLIB_MAKE_CMD=$(CC) -shared -Wl,-install_name,$(DYLIB_MINOR_NAME) -o $(DYLIBNAME) $(LDFLAGS) +endif + +all: $(DYLIBNAME) $(BINS) + +# Deps (use make dep to generate this) +net.o: net.c fmacros.h net.h hiredis.h +async.o: async.c async.h hiredis.h sds.h dict.c dict.h +example.o: example.c hiredis.h +hiredis.o: hiredis.c fmacros.h hiredis.h net.h sds.h +sds.o: sds.c sds.h +test.o: test.c hiredis.h + +$(DYLIBNAME): $(OBJ) + $(DYLIB_MAKE_CMD) $(OBJ) + +$(STLIBNAME): $(OBJ) + $(STLIB_MAKE_CMD) $(OBJ) + +dynamic: $(DYLIBNAME) +static: $(STLIBNAME) + +# Binaries: +hiredis-example-libevent: example-libevent.c adapters/libevent.h $(STLIBNAME) + $(CC) -o $@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -levent example-libevent.c $(STLIBNAME) + +hiredis-example-libev: example-libev.c adapters/libev.h $(STLIBNAME) + $(CC) -o $@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -lev example-libev.c $(STLIBNAME) + +ifndef AE_DIR +hiredis-example-ae: + @echo "Please specify AE_DIR (e.g. /src)" + @false +else +hiredis-example-ae: example-ae.c adapters/ae.h $(STLIBNAME) + $(CC) -o $@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I$(AE_DIR) $(AE_DIR)/ae.o $(AE_DIR)/zmalloc.o example-ae.c $(STLIBNAME) +endif + +hiredis-%: %.o $(STLIBNAME) + $(CC) -o $@ $(REAL_LDFLAGS) $< $(STLIBNAME) + +test: hiredis-test + ./hiredis-test + +check: hiredis-test + echo \ + "daemonize yes\n" \ + "pidfile /tmp/hiredis-test-redis.pid\n" \ + "port 56379\n" \ + "bind 127.0.0.1\n" \ + "unixsocket /tmp/hiredis-test-redis.sock" \ + | redis-server - + ./hiredis-test -h 127.0.0.1 -p 56379 -s /tmp/hiredis-test-redis.sock || \ + ( kill `cat /tmp/hiredis-test-redis.pid` && false ) + kill `cat /tmp/hiredis-test-redis.pid` + +.c.o: + $(CC) -std=c99 -pedantic -c $(REAL_CFLAGS) $< + +clean: + rm -rf $(DYLIBNAME) $(STLIBNAME) $(BINS) hiredis-example* *.o *.gcda *.gcno *.gcov + +dep: + $(CC) -MM *.c + +# Installation related variables and target +PREFIX?=/usr/local +INCLUDE_PATH?=include/hiredis +LIBRARY_PATH?=lib +INSTALL_INCLUDE_PATH= $(PREFIX)/$(INCLUDE_PATH) +INSTALL_LIBRARY_PATH= $(PREFIX)/$(LIBRARY_PATH) + +ifeq ($(uname_S),SunOS) + INSTALL?= cp -r +endif + +INSTALL?= cp -a + +install: $(DYLIBNAME) $(STLIBNAME) + mkdir -p $(INSTALL_INCLUDE_PATH) $(INSTALL_LIBRARY_PATH) + $(INSTALL) hiredis.h async.h adapters $(INSTALL_INCLUDE_PATH) + $(INSTALL) $(DYLIBNAME) $(INSTALL_LIBRARY_PATH)/$(DYLIB_MINOR_NAME) + cd $(INSTALL_LIBRARY_PATH) && ln -sf $(DYLIB_MINOR_NAME) $(DYLIB_MAJOR_NAME) + cd $(INSTALL_LIBRARY_PATH) && ln -sf $(DYLIB_MAJOR_NAME) $(DYLIBNAME) + $(INSTALL) $(STLIBNAME) $(INSTALL_LIBRARY_PATH) + +32bit: + @echo "" + @echo "WARNING: if this fails under Linux you probably need to install libc6-dev-i386" + @echo "" + $(MAKE) CFLAGS="-m32" LDFLAGS="-m32" + +gprof: + $(MAKE) CFLAGS="-pg" LDFLAGS="-pg" + +gcov: + $(MAKE) CFLAGS="-fprofile-arcs -ftest-coverage" LDFLAGS="-fprofile-arcs" + +coverage: gcov + make check + mkdir -p tmp/lcov + lcov -d . -c -o tmp/lcov/hiredis.info + genhtml --legend -o tmp/lcov/report tmp/lcov/hiredis.info + +noopt: + $(MAKE) OPTIMIZATION="" + +.PHONY: all test check clean dep install 32bit gprof gcov noopt diff --git a/hiredis/README.md b/hiredis/README.md new file mode 100644 index 0000000..a58101c --- /dev/null +++ b/hiredis/README.md @@ -0,0 +1,352 @@ +# HIREDIS + +Hiredis is a minimalistic C client library for the [Redis](http://redis.io/) database. + +It is minimalistic because it just adds minimal support for the protocol, but +at the same time it uses an high level printf-alike API in order to make it +much higher level than otherwise suggested by its minimal code base and the +lack of explicit bindings for every Redis command. + +Apart from supporting sending commands and receiving replies, it comes with +a reply parser that is decoupled from the I/O layer. It +is a stream parser designed for easy reusability, which can for instance be used +in higher level language bindings for efficient reply parsing. + +Hiredis only supports the binary-safe Redis protocol, so you can use it with any +Redis version >= 1.2.0. + +The library comes with multiple APIs. There is the +*synchronous API*, the *asynchronous API* and the *reply parsing API*. + +## UPGRADING + +Version 0.9.0 is a major overhaul of hiredis in every aspect. However, upgrading existing +code using hiredis should not be a big pain. The key thing to keep in mind when +upgrading is that hiredis >= 0.9.0 uses a `redisContext*` to keep state, in contrast to +the stateless 0.0.1 that only has a file descriptor to work with. + +## Synchronous API + +To consume the synchronous API, there are only a few function calls that need to be introduced: + + redisContext *redisConnect(const char *ip, int port); + void *redisCommand(redisContext *c, const char *format, ...); + void freeReplyObject(void *reply); + +### Connecting + +The function `redisConnect` is used to create a so-called `redisContext`. The +context is where Hiredis holds state for a connection. The `redisContext` +struct has an integer `err` field that is non-zero when an the connection is in +an error state. The field `errstr` will contain a string with a description of +the error. More information on errors can be found in the **Errors** section. +After trying to connect to Redis using `redisConnect` you should +check the `err` field to see if establishing the connection was successful: + + redisContext *c = redisConnect("127.0.0.1", 6379); + if (c->err) { + printf("Error: %s\n", c->errstr); + // handle error + } + +### Sending commands + +There are several ways to issue commands to Redis. The first that will be introduced is +`redisCommand`. This function takes a format similar to printf. In the simplest form, +it is used like this: + + reply = redisCommand(context, "SET foo bar"); + +The specifier `%s` interpolates a string in the command, and uses `strlen` to +determine the length of the string: + + reply = redisCommand(context, "SET foo %s", value); + +When you need to pass binary safe strings in a command, the `%b` specifier can be +used. Together with a pointer to the string, it requires a `size_t` length argument +of the string: + + reply = redisCommand(context, "SET foo %b", value, valuelen); + +Internally, Hiredis splits the command in different arguments and will +convert it to the protocol used to communicate with Redis. +One or more spaces separates arguments, so you can use the specifiers +anywhere in an argument: + + reply = redisCommand("SET key:%s %s", myid, value); + +### Using replies + +The return value of `redisCommand` holds a reply when the command was +successfully executed. When an error occurs, the return value is `NULL` and +the `err` field in the context will be set (see section on **Errors**). +Once an error is returned the context cannot be reused and you should set up +a new connection. + +The standard replies that `redisCommand` are of the type `redisReply`. The +`type` field in the `redisReply` should be used to test what kind of reply +was received: + +* **`REDIS_REPLY_STATUS`**: + * The command replied with a status reply. The status string can be accessed using `reply->str`. + The length of this string can be accessed using `reply->len`. + +* **`REDIS_REPLY_ERROR`**: + * The command replied with an error. The error string can be accessed identical to `REDIS_REPLY_STATUS`. + +* **`REDIS_REPLY_INTEGER`**: + * The command replied with an integer. The integer value can be accessed using the + `reply->integer` field of type `long long`. + +* **`REDIS_REPLY_NIL`**: + * The command replied with a **nil** object. There is no data to access. + +* **`REDIS_REPLY_STRING`**: + * A bulk (string) reply. The value of the reply can be accessed using `reply->str`. + The length of this string can be accessed using `reply->len`. + +* **`REDIS_REPLY_ARRAY`**: + * A multi bulk reply. The number of elements in the multi bulk reply is stored in + `reply->elements`. Every element in the multi bulk reply is a `redisReply` object as well + and can be accessed via `reply->element[..index..]`. + Redis may reply with nested arrays but this is fully supported. + +Replies should be freed using the `freeReplyObject()` function. +Note that this function will take care of freeing sub-replies objects +contained in arrays and nested arrays, so there is no need for the user to +free the sub replies (it is actually harmful and will corrupt the memory). + +**Important:** the current version of hiredis (0.10.0) free's replies when the +asynchronous API is used. This means you should not call `freeReplyObject` when +you use this API. The reply is cleaned up by hiredis _after_ the callback +returns. This behavior will probably change in future releases, so make sure to +keep an eye on the changelog when upgrading (see issue #39). + +### Cleaning up + +To disconnect and free the context the following function can be used: + + void redisFree(redisContext *c); + +This function immediately closes the socket and then free's the allocations done in +creating the context. + +### Sending commands (cont'd) + +Together with `redisCommand`, the function `redisCommandArgv` can be used to issue commands. +It has the following prototype: + + void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen); + +It takes the number of arguments `argc`, an array of strings `argv` and the lengths of the +arguments `argvlen`. For convenience, `argvlen` may be set to `NULL` and the function will +use `strlen(3)` on every argument to determine its length. Obviously, when any of the arguments +need to be binary safe, the entire array of lengths `argvlen` should be provided. + +The return value has the same semantic as `redisCommand`. + +### Pipelining + +To explain how Hiredis supports pipelining in a blocking connection, there needs to be +understanding of the internal execution flow. + +When any of the functions in the `redisCommand` family is called, Hiredis first formats the +command according to the Redis protocol. The formatted command is then put in the output buffer +of the context. This output buffer is dynamic, so it can hold any number of commands. +After the command is put in the output buffer, `redisGetReply` is called. This function has the +following two execution paths: + +1. The input buffer is non-empty: + * Try to parse a single reply from the input buffer and return it + * If no reply could be parsed, continue at *2* +2. The input buffer is empty: + * Write the **entire** output buffer to the socket + * Read from the socket until a single reply could be parsed + +The function `redisGetReply` is exported as part of the Hiredis API and can be used when a reply +is expected on the socket. To pipeline commands, the only things that needs to be done is +filling up the output buffer. For this cause, two commands can be used that are identical +to the `redisCommand` family, apart from not returning a reply: + + void redisAppendCommand(redisContext *c, const char *format, ...); + void redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen); + +After calling either function one or more times, `redisGetReply` can be used to receive the +subsequent replies. The return value for this function is either `REDIS_OK` or `REDIS_ERR`, where +the latter means an error occurred while reading a reply. Just as with the other commands, +the `err` field in the context can be used to find out what the cause of this error is. + +The following examples shows a simple pipeline (resulting in only a single call to `write(2)` and +a single call to `read(2)`): + + redisReply *reply; + redisAppendCommand(context,"SET foo bar"); + redisAppendCommand(context,"GET foo"); + redisGetReply(context,&reply); // reply for SET + freeReplyObject(reply); + redisGetReply(context,&reply); // reply for GET + freeReplyObject(reply); + +This API can also be used to implement a blocking subscriber: + + reply = redisCommand(context,"SUBSCRIBE foo"); + freeReplyObject(reply); + while(redisGetReply(context,&reply) == REDIS_OK) { + // consume message + freeReplyObject(reply); + } + +### Errors + +When a function call is not successful, depending on the function either `NULL` or `REDIS_ERR` is +returned. The `err` field inside the context will be non-zero and set to one of the +following constants: + +* **`REDIS_ERR_IO`**: + There was an I/O error while creating the connection, trying to write + to the socket or read from the socket. If you included `errno.h` in your + application, you can use the global `errno` variable to find out what is + wrong. + +* **`REDIS_ERR_EOF`**: + The server closed the connection which resulted in an empty read. + +* **`REDIS_ERR_PROTOCOL`**: + There was an error while parsing the protocol. + +* **`REDIS_ERR_OTHER`**: + Any other error. Currently, it is only used when a specified hostname to connect + to cannot be resolved. + +In every case, the `errstr` field in the context will be set to hold a string representation +of the error. + +## Asynchronous API + +Hiredis comes with an asynchronous API that works easily with any event library. +Examples are bundled that show using Hiredis with [libev](http://software.schmorp.de/pkg/libev.html) +and [libevent](http://monkey.org/~provos/libevent/). + +### Connecting + +The function `redisAsyncConnect` can be used to establish a non-blocking connection to +Redis. It returns a pointer to the newly created `redisAsyncContext` struct. The `err` field +should be checked after creation to see if there were errors creating the connection. +Because the connection that will be created is non-blocking, the kernel is not able to +instantly return if the specified host and port is able to accept a connection. + + redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379); + if (c->err) { + printf("Error: %s\n", c->errstr); + // handle error + } + +The asynchronous context can hold a disconnect callback function that is called when the +connection is disconnected (either because of an error or per user request). This function should +have the following prototype: + + void(const redisAsyncContext *c, int status); + +On a disconnect, the `status` argument is set to `REDIS_OK` when disconnection was initiated by the +user, or `REDIS_ERR` when the disconnection was caused by an error. When it is `REDIS_ERR`, the `err` +field in the context can be accessed to find out the cause of the error. + +The context object is always free'd after the disconnect callback fired. When a reconnect is needed, +the disconnect callback is a good point to do so. + +Setting the disconnect callback can only be done once per context. For subsequent calls it will +return `REDIS_ERR`. The function to set the disconnect callback has the following prototype: + + int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn); + +### Sending commands and their callbacks + +In an asynchronous context, commands are automatically pipelined due to the nature of an event loop. +Therefore, unlike the synchronous API, there is only a single way to send commands. +Because commands are sent to Redis asynchronously, issuing a command requires a callback function +that is called when the reply is received. Reply callbacks should have the following prototype: + + void(redisAsyncContext *c, void *reply, void *privdata); + +The `privdata` argument can be used to curry arbitrary data to the callback from the point where +the command is initially queued for execution. + +The functions that can be used to issue commands in an asynchronous context are: + + int redisAsyncCommand( + redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, + const char *format, ...); + int redisAsyncCommandArgv( + redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, + int argc, const char **argv, const size_t *argvlen); + +Both functions work like their blocking counterparts. The return value is `REDIS_OK` when the command +was successfully added to the output buffer and `REDIS_ERR` otherwise. Example: when the connection +is being disconnected per user-request, no new commands may be added to the output buffer and `REDIS_ERR` is +returned on calls to the `redisAsyncCommand` family. + +If the reply for a command with a `NULL` callback is read, it is immediately free'd. When the callback +for a command is non-`NULL`, the memory is free'd immediately following the callback: the reply is only +valid for the duration of the callback. + +All pending callbacks are called with a `NULL` reply when the context encountered an error. + +### Disconnecting + +An asynchronous connection can be terminated using: + + void redisAsyncDisconnect(redisAsyncContext *ac); + +When this function is called, the connection is **not** immediately terminated. Instead, new +commands are no longer accepted and the connection is only terminated when all pending commands +have been written to the socket, their respective replies have been read and their respective +callbacks have been executed. After this, the disconnection callback is executed with the +`REDIS_OK` status and the context object is free'd. + +### Hooking it up to event library *X* + +There are a few hooks that need to be set on the context object after it is created. +See the `adapters/` directory for bindings to *libev* and *libevent*. + +## Reply parsing API + +Hiredis comes with a reply parsing API that makes it easy for writing higher +level language bindings. + +The reply parsing API consists of the following functions: + + redisReader *redisReaderCreate(void); + void redisReaderFree(redisReader *reader); + int redisReaderFeed(redisReader *reader, const char *buf, size_t len); + int redisReaderGetReply(redisReader *reader, void **reply); + +### Usage + +The function `redisReaderCreate` creates a `redisReader` structure that holds a +buffer with unparsed data and state for the protocol parser. + +Incoming data -- most likely from a socket -- can be placed in the internal +buffer of the `redisReader` using `redisReaderFeed`. This function will make a +copy of the buffer pointed to by `buf` for `len` bytes. This data is parsed +when `redisReaderGetReply` is called. This function returns an integer status +and a reply object (as described above) via `void **reply`. The returned status +can be either `REDIS_OK` or `REDIS_ERR`, where the latter means something went +wrong (either a protocol error, or an out of memory error). + +### Customizing replies + +The function `redisReaderGetReply` creates `redisReply` and makes the function +argument `reply` point to the created `redisReply` variable. For instance, if +the response of type `REDIS_REPLY_STATUS` then the `str` field of `redisReply` +will hold the status as a vanilla C string. However, the functions that are +responsible for creating instances of the `redisReply` can be customized by +setting the `fn` field on the `redisReader` struct. This should be done +immediately after creating the `redisReader`. + +For example, [hiredis-rb](https://github.com/pietern/hiredis-rb/blob/master/ext/hiredis_ext/reader.c) +uses customized reply object functions to create Ruby objects. + +## AUTHORS + +Hiredis was written by Salvatore Sanfilippo (antirez at gmail) and +Pieter Noordhuis (pcnoordhuis at gmail) and is released under the BSD license. diff --git a/hiredis/TODO b/hiredis/TODO new file mode 100644 index 0000000..de70b94 --- /dev/null +++ b/hiredis/TODO @@ -0,0 +1,2 @@ +- add redisCommandVector() +- add support for pipelining diff --git a/hiredis/adapters/ae.h b/hiredis/adapters/ae.h new file mode 100644 index 0000000..65235f8 --- /dev/null +++ b/hiredis/adapters/ae.h @@ -0,0 +1,97 @@ +#ifndef __HIREDIS_AE_H__ +#define __HIREDIS_AE_H__ +#include +#include +#include "../hiredis.h" +#include "../async.h" + +typedef struct redisAeEvents { + redisAsyncContext *context; + aeEventLoop *loop; + int fd; + int reading, writing; +} redisAeEvents; + +static void redisAeReadEvent(aeEventLoop *el, int fd, void *privdata, int mask) { + ((void)el); ((void)fd); ((void)mask); + + redisAeEvents *e = (redisAeEvents*)privdata; + redisAsyncHandleRead(e->context); +} + +static void redisAeWriteEvent(aeEventLoop *el, int fd, void *privdata, int mask) { + ((void)el); ((void)fd); ((void)mask); + + redisAeEvents *e = (redisAeEvents*)privdata; + redisAsyncHandleWrite(e->context); +} + +static void redisAeAddRead(void *privdata) { + redisAeEvents *e = (redisAeEvents*)privdata; + aeEventLoop *loop = e->loop; + if (!e->reading) { + e->reading = 1; + aeCreateFileEvent(loop,e->fd,AE_READABLE,redisAeReadEvent,e); + } +} + +static void redisAeDelRead(void *privdata) { + redisAeEvents *e = (redisAeEvents*)privdata; + aeEventLoop *loop = e->loop; + if (e->reading) { + e->reading = 0; + aeDeleteFileEvent(loop,e->fd,AE_READABLE); + } +} + +static void redisAeAddWrite(void *privdata) { + redisAeEvents *e = (redisAeEvents*)privdata; + aeEventLoop *loop = e->loop; + if (!e->writing) { + e->writing = 1; + aeCreateFileEvent(loop,e->fd,AE_WRITABLE,redisAeWriteEvent,e); + } +} + +static void redisAeDelWrite(void *privdata) { + redisAeEvents *e = (redisAeEvents*)privdata; + aeEventLoop *loop = e->loop; + if (e->writing) { + e->writing = 0; + aeDeleteFileEvent(loop,e->fd,AE_WRITABLE); + } +} + +static void redisAeCleanup(void *privdata) { + redisAeEvents *e = (redisAeEvents*)privdata; + redisAeDelRead(privdata); + redisAeDelWrite(privdata); + free(e); +} + +static int redisAeAttach(aeEventLoop *loop, redisAsyncContext *ac) { + redisContext *c = &(ac->c); + redisAeEvents *e; + + /* Nothing should be attached when something is already attached */ + if (ac->ev.data != NULL) + return REDIS_ERR; + + /* Create container for context and r/w events */ + e = (redisAeEvents*)malloc(sizeof(*e)); + e->context = ac; + e->loop = loop; + e->fd = c->fd; + e->reading = e->writing = 0; + + /* Register functions to start/stop listening for events */ + ac->ev.addRead = redisAeAddRead; + ac->ev.delRead = redisAeDelRead; + ac->ev.addWrite = redisAeAddWrite; + ac->ev.delWrite = redisAeDelWrite; + ac->ev.cleanup = redisAeCleanup; + ac->ev.data = e; + + return REDIS_OK; +} +#endif diff --git a/hiredis/adapters/libev.h b/hiredis/adapters/libev.h new file mode 100644 index 0000000..534d743 --- /dev/null +++ b/hiredis/adapters/libev.h @@ -0,0 +1,117 @@ +#ifndef __HIREDIS_LIBEV_H__ +#define __HIREDIS_LIBEV_H__ +#include +#include +#include +#include "../hiredis.h" +#include "../async.h" + +typedef struct redisLibevEvents { + redisAsyncContext *context; + struct ev_loop *loop; + int reading, writing; + ev_io rev, wev; +} redisLibevEvents; + +static void redisLibevReadEvent(EV_P_ ev_io *watcher, int revents) { +#if EV_MULTIPLICITY + ((void)loop); +#endif + ((void)revents); + + redisLibevEvents *e = (redisLibevEvents*)watcher->data; + redisAsyncHandleRead(e->context); +} + +static void redisLibevWriteEvent(EV_P_ ev_io *watcher, int revents) { +#if EV_MULTIPLICITY + ((void)loop); +#endif + ((void)revents); + + redisLibevEvents *e = (redisLibevEvents*)watcher->data; + redisAsyncHandleWrite(e->context); +} + +static void redisLibevAddRead(void *privdata) { + redisLibevEvents *e = (redisLibevEvents*)privdata; + struct ev_loop *loop = e->loop; + ((void)loop); + if (!e->reading) { + e->reading = 1; + ev_io_start(EV_A_ &e->rev); + } +} + +static void redisLibevDelRead(void *privdata) { + redisLibevEvents *e = (redisLibevEvents*)privdata; + struct ev_loop *loop = e->loop; + ((void)loop); + if (e->reading) { + e->reading = 0; + ev_io_stop(EV_A_ &e->rev); + } +} + +static void redisLibevAddWrite(void *privdata) { + redisLibevEvents *e = (redisLibevEvents*)privdata; + struct ev_loop *loop = e->loop; + ((void)loop); + if (!e->writing) { + e->writing = 1; + ev_io_start(EV_A_ &e->wev); + } +} + +static void redisLibevDelWrite(void *privdata) { + redisLibevEvents *e = (redisLibevEvents*)privdata; + struct ev_loop *loop = e->loop; + ((void)loop); + if (e->writing) { + e->writing = 0; + ev_io_stop(EV_A_ &e->wev); + } +} + +static void redisLibevCleanup(void *privdata) { + redisLibevEvents *e = (redisLibevEvents*)privdata; + redisLibevDelRead(privdata); + redisLibevDelWrite(privdata); + free(e); +} + +static int redisLibevAttach(EV_P_ redisAsyncContext *ac) { + redisContext *c = &(ac->c); + redisLibevEvents *e; + + /* Nothing should be attached when something is already attached */ + if (ac->ev.data != NULL) + return REDIS_ERR; + + /* Create container for context and r/w events */ + e = (redisLibevEvents*)malloc(sizeof(*e)); + e->context = ac; +#if EV_MULTIPLICITY + e->loop = loop; +#else + e->loop = NULL; +#endif + e->reading = e->writing = 0; + e->rev.data = e; + e->wev.data = e; + + /* Register functions to start/stop listening for events */ + ac->ev.addRead = redisLibevAddRead; + ac->ev.delRead = redisLibevDelRead; + ac->ev.addWrite = redisLibevAddWrite; + ac->ev.delWrite = redisLibevDelWrite; + ac->ev.cleanup = redisLibevCleanup; + ac->ev.data = e; + + /* Initialize read/write events */ + ev_io_init(&e->rev,redisLibevReadEvent,c->fd,EV_READ); + ev_io_init(&e->wev,redisLibevWriteEvent,c->fd,EV_WRITE); + return REDIS_OK; +} + +#endif diff --git a/hiredis/adapters/libevent.h b/hiredis/adapters/libevent.h new file mode 100644 index 0000000..4055ec0 --- /dev/null +++ b/hiredis/adapters/libevent.h @@ -0,0 +1,78 @@ +#ifndef __HIREDIS_LIBEVENT_H__ +#define __HIREDIS_LIBEVENT_H__ +#include +#include "../hiredis.h" +#include "../async.h" + +typedef struct redisLibeventEvents { + redisAsyncContext *context; + struct event rev, wev; +} redisLibeventEvents; + +static void redisLibeventReadEvent(int fd, short event, void *arg) { + ((void)fd); ((void)event); + redisLibeventEvents *e = (redisLibeventEvents*)arg; + redisAsyncHandleRead(e->context); +} + +static void redisLibeventWriteEvent(int fd, short event, void *arg) { + ((void)fd); ((void)event); + redisLibeventEvents *e = (redisLibeventEvents*)arg; + redisAsyncHandleWrite(e->context); +} + +static void redisLibeventAddRead(void *privdata) { + redisLibeventEvents *e = (redisLibeventEvents*)privdata; + event_add(&e->rev,NULL); +} + +static void redisLibeventDelRead(void *privdata) { + redisLibeventEvents *e = (redisLibeventEvents*)privdata; + event_del(&e->rev); +} + +static void redisLibeventAddWrite(void *privdata) { + redisLibeventEvents *e = (redisLibeventEvents*)privdata; + event_add(&e->wev,NULL); +} + +static void redisLibeventDelWrite(void *privdata) { + redisLibeventEvents *e = (redisLibeventEvents*)privdata; + event_del(&e->wev); +} + +static void redisLibeventCleanup(void *privdata) { + redisLibeventEvents *e = (redisLibeventEvents*)privdata; + event_del(&e->rev); + event_del(&e->wev); + free(e); +} + +static int redisLibeventAttach(redisAsyncContext *ac, struct event_base *base) { + redisContext *c = &(ac->c); + redisLibeventEvents *e; + + /* Nothing should be attached when something is already attached */ + if (ac->ev.data != NULL) + return REDIS_ERR; + + /* Create container for context and r/w events */ + e = (redisLibeventEvents*)malloc(sizeof(*e)); + e->context = ac; + + /* Register functions to start/stop listening for events */ + ac->ev.addRead = redisLibeventAddRead; + ac->ev.delRead = redisLibeventDelRead; + ac->ev.addWrite = redisLibeventAddWrite; + ac->ev.delWrite = redisLibeventDelWrite; + ac->ev.cleanup = redisLibeventCleanup; + ac->ev.data = e; + + /* Initialize and install read/write events */ + event_set(&e->rev,c->fd,EV_READ,redisLibeventReadEvent,e); + event_set(&e->wev,c->fd,EV_WRITE,redisLibeventWriteEvent,e); + event_base_set(base,&e->rev); + event_base_set(base,&e->wev); + return REDIS_OK; +} +#endif diff --git a/hiredis/async.c b/hiredis/async.c new file mode 100644 index 0000000..83d88ec --- /dev/null +++ b/hiredis/async.c @@ -0,0 +1,647 @@ +/* + * Copyright (c) 2009-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "fmacros.h" +#include +#include +#include +#include +#include +#include +#include "async.h" +#include "net.h" +#include "dict.c" +#include "sds.h" + +#define _EL_ADD_READ(ctx) do { \ + if ((ctx)->ev.addRead) (ctx)->ev.addRead((ctx)->ev.data); \ + } while(0) +#define _EL_DEL_READ(ctx) do { \ + if ((ctx)->ev.delRead) (ctx)->ev.delRead((ctx)->ev.data); \ + } while(0) +#define _EL_ADD_WRITE(ctx) do { \ + if ((ctx)->ev.addWrite) (ctx)->ev.addWrite((ctx)->ev.data); \ + } while(0) +#define _EL_DEL_WRITE(ctx) do { \ + if ((ctx)->ev.delWrite) (ctx)->ev.delWrite((ctx)->ev.data); \ + } while(0) +#define _EL_CLEANUP(ctx) do { \ + if ((ctx)->ev.cleanup) (ctx)->ev.cleanup((ctx)->ev.data); \ + } while(0); + +/* Forward declaration of function in hiredis.c */ +void __redisAppendCommand(redisContext *c, char *cmd, size_t len); + +/* Functions managing dictionary of callbacks for pub/sub. */ +static unsigned int callbackHash(const void *key) { + return dictGenHashFunction((unsigned char*)key,sdslen((char*)key)); +} + +static void *callbackValDup(void *privdata, const void *src) { + ((void) privdata); + redisCallback *dup = malloc(sizeof(*dup)); + memcpy(dup,src,sizeof(*dup)); + return dup; +} + +static int callbackKeyCompare(void *privdata, const void *key1, const void *key2) { + int l1, l2; + ((void) privdata); + + l1 = sdslen((sds)key1); + l2 = sdslen((sds)key2); + if (l1 != l2) return 0; + return memcmp(key1,key2,l1) == 0; +} + +static void callbackKeyDestructor(void *privdata, void *key) { + ((void) privdata); + sdsfree((sds)key); +} + +static void callbackValDestructor(void *privdata, void *val) { + ((void) privdata); + free(val); +} + +static dictType callbackDict = { + callbackHash, + NULL, + callbackValDup, + callbackKeyCompare, + callbackKeyDestructor, + callbackValDestructor +}; + +static redisAsyncContext *redisAsyncInitialize(redisContext *c) { + redisAsyncContext *ac; + + ac = realloc(c,sizeof(redisAsyncContext)); + if (ac == NULL) + return NULL; + + c = &(ac->c); + + /* The regular connect functions will always set the flag REDIS_CONNECTED. + * For the async API, we want to wait until the first write event is + * received up before setting this flag, so reset it here. */ + c->flags &= ~REDIS_CONNECTED; + + ac->err = 0; + ac->errstr = NULL; + ac->data = NULL; + + ac->ev.data = NULL; + ac->ev.addRead = NULL; + ac->ev.delRead = NULL; + ac->ev.addWrite = NULL; + ac->ev.delWrite = NULL; + ac->ev.cleanup = NULL; + + ac->onConnect = NULL; + ac->onDisconnect = NULL; + + ac->replies.head = NULL; + ac->replies.tail = NULL; + ac->sub.invalid.head = NULL; + ac->sub.invalid.tail = NULL; + ac->sub.channels = dictCreate(&callbackDict,NULL); + ac->sub.patterns = dictCreate(&callbackDict,NULL); + return ac; +} + +/* We want the error field to be accessible directly instead of requiring + * an indirection to the redisContext struct. */ +static void __redisAsyncCopyError(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + ac->err = c->err; + ac->errstr = c->errstr; +} + +redisAsyncContext *redisAsyncConnect(const char *ip, int port) { + redisContext *c; + redisAsyncContext *ac; + + c = redisConnectNonBlock(ip,port); + if (c == NULL) + return NULL; + + ac = redisAsyncInitialize(c); + if (ac == NULL) { + redisFree(c); + return NULL; + } + + __redisAsyncCopyError(ac); + return ac; +} + +redisAsyncContext *redisAsyncConnectUnix(const char *path) { + redisContext *c; + redisAsyncContext *ac; + + c = redisConnectUnixNonBlock(path); + if (c == NULL) + return NULL; + + ac = redisAsyncInitialize(c); + __redisAsyncCopyError(ac); + return ac; +} + +int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn) { + if (ac->onConnect == NULL) { + ac->onConnect = fn; + + /* The common way to detect an established connection is to wait for + * the first write event to be fired. This assumes the related event + * library functions are already set. */ + _EL_ADD_WRITE(ac); + return REDIS_OK; + } + return REDIS_ERR; +} + +int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn) { + if (ac->onDisconnect == NULL) { + ac->onDisconnect = fn; + return REDIS_OK; + } + return REDIS_ERR; +} + +/* Helper functions to push/shift callbacks */ +static int __redisPushCallback(redisCallbackList *list, redisCallback *source) { + redisCallback *cb; + + /* Copy callback from stack to heap */ + cb = malloc(sizeof(*cb)); + if (cb == NULL) + return REDIS_ERR_OOM; + + if (source != NULL) { + memcpy(cb,source,sizeof(*cb)); + cb->next = NULL; + } + + /* Store callback in list */ + if (list->head == NULL) + list->head = cb; + if (list->tail != NULL) + list->tail->next = cb; + list->tail = cb; + return REDIS_OK; +} + +static int __redisShiftCallback(redisCallbackList *list, redisCallback *target) { + redisCallback *cb = list->head; + if (cb != NULL) { + list->head = cb->next; + if (cb == list->tail) + list->tail = NULL; + + /* Copy callback from heap to stack */ + if (target != NULL) + memcpy(target,cb,sizeof(*cb)); + free(cb); + return REDIS_OK; + } + return REDIS_ERR; +} + +static void __redisRunCallback(redisAsyncContext *ac, redisCallback *cb, redisReply *reply) { + redisContext *c = &(ac->c); + if (cb->fn != NULL) { + c->flags |= REDIS_IN_CALLBACK; + cb->fn(ac,reply,cb->privdata); + c->flags &= ~REDIS_IN_CALLBACK; + } +} + +/* Helper function to free the context. */ +static void __redisAsyncFree(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + redisCallback cb; + dictIterator *it; + dictEntry *de; + + /* Execute pending callbacks with NULL reply. */ + while (__redisShiftCallback(&ac->replies,&cb) == REDIS_OK) + __redisRunCallback(ac,&cb,NULL); + + /* Execute callbacks for invalid commands */ + while (__redisShiftCallback(&ac->sub.invalid,&cb) == REDIS_OK) + __redisRunCallback(ac,&cb,NULL); + + /* Run subscription callbacks callbacks with NULL reply */ + it = dictGetIterator(ac->sub.channels); + while ((de = dictNext(it)) != NULL) + __redisRunCallback(ac,dictGetEntryVal(de),NULL); + dictReleaseIterator(it); + dictRelease(ac->sub.channels); + + it = dictGetIterator(ac->sub.patterns); + while ((de = dictNext(it)) != NULL) + __redisRunCallback(ac,dictGetEntryVal(de),NULL); + dictReleaseIterator(it); + dictRelease(ac->sub.patterns); + + /* Signal event lib to clean up */ + _EL_CLEANUP(ac); + + /* Execute disconnect callback. When redisAsyncFree() initiated destroying + * this context, the status will always be REDIS_OK. */ + if (ac->onDisconnect && (c->flags & REDIS_CONNECTED)) { + if (c->flags & REDIS_FREEING) { + ac->onDisconnect(ac,REDIS_OK); + } else { + ac->onDisconnect(ac,(ac->err == 0) ? REDIS_OK : REDIS_ERR); + } + } + + /* Cleanup self */ + redisFree(c); +} + +/* Free the async context. When this function is called from a callback, + * control needs to be returned to redisProcessCallbacks() before actual + * free'ing. To do so, a flag is set on the context which is picked up by + * redisProcessCallbacks(). Otherwise, the context is immediately free'd. */ +void redisAsyncFree(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + c->flags |= REDIS_FREEING; + if (!(c->flags & REDIS_IN_CALLBACK)) + __redisAsyncFree(ac); +} + +/* Helper function to make the disconnect happen and clean up. */ +static void __redisAsyncDisconnect(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + + /* Make sure error is accessible if there is any */ + __redisAsyncCopyError(ac); + + if (ac->err == 0) { + /* For clean disconnects, there should be no pending callbacks. */ + assert(__redisShiftCallback(&ac->replies,NULL) == REDIS_ERR); + } else { + /* Disconnection is caused by an error, make sure that pending + * callbacks cannot call new commands. */ + c->flags |= REDIS_DISCONNECTING; + } + + /* For non-clean disconnects, __redisAsyncFree() will execute pending + * callbacks with a NULL-reply. */ + __redisAsyncFree(ac); +} + +/* Tries to do a clean disconnect from Redis, meaning it stops new commands + * from being issued, but tries to flush the output buffer and execute + * callbacks for all remaining replies. When this function is called from a + * callback, there might be more replies and we can safely defer disconnecting + * to redisProcessCallbacks(). Otherwise, we can only disconnect immediately + * when there are no pending callbacks. */ +void redisAsyncDisconnect(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + c->flags |= REDIS_DISCONNECTING; + if (!(c->flags & REDIS_IN_CALLBACK) && ac->replies.head == NULL) + __redisAsyncDisconnect(ac); +} + +static int __redisGetSubscribeCallback(redisAsyncContext *ac, redisReply *reply, redisCallback *dstcb) { + redisContext *c = &(ac->c); + dict *callbacks; + dictEntry *de; + int pvariant; + char *stype; + sds sname; + + /* Custom reply functions are not supported for pub/sub. This will fail + * very hard when they are used... */ + if (reply->type == REDIS_REPLY_ARRAY) { + assert(reply->elements >= 2); + assert(reply->element[0]->type == REDIS_REPLY_STRING); + stype = reply->element[0]->str; + pvariant = (tolower(stype[0]) == 'p') ? 1 : 0; + + if (pvariant) + callbacks = ac->sub.patterns; + else + callbacks = ac->sub.channels; + + /* Locate the right callback */ + assert(reply->element[1]->type == REDIS_REPLY_STRING); + sname = sdsnewlen(reply->element[1]->str,reply->element[1]->len); + de = dictFind(callbacks,sname); + if (de != NULL) { + memcpy(dstcb,dictGetEntryVal(de),sizeof(*dstcb)); + + /* If this is an unsubscribe message, remove it. */ + if (strcasecmp(stype+pvariant,"unsubscribe") == 0) { + dictDelete(callbacks,sname); + + /* If this was the last unsubscribe message, revert to + * non-subscribe mode. */ + assert(reply->element[2]->type == REDIS_REPLY_INTEGER); + if (reply->element[2]->integer == 0) + c->flags &= ~REDIS_SUBSCRIBED; + } + } + sdsfree(sname); + } else { + /* Shift callback for invalid commands. */ + __redisShiftCallback(&ac->sub.invalid,dstcb); + } + return REDIS_OK; +} + +void redisProcessCallbacks(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + redisCallback cb; + void *reply = NULL; + int status; + + while((status = redisGetReply(c,&reply)) == REDIS_OK) { + if (reply == NULL) { + /* When the connection is being disconnected and there are + * no more replies, this is the cue to really disconnect. */ + if (c->flags & REDIS_DISCONNECTING && sdslen(c->obuf) == 0) { + __redisAsyncDisconnect(ac); + return; + } + + /* If monitor mode, repush callback */ + if(c->flags & REDIS_MONITORING) { + __redisPushCallback(&ac->replies,&cb); + } + + /* When the connection is not being disconnected, simply stop + * trying to get replies and wait for the next loop tick. */ + break; + } + + /* Even if the context is subscribed, pending regular callbacks will + * get a reply before pub/sub messages arrive. */ + if (__redisShiftCallback(&ac->replies,&cb) != REDIS_OK) { + /* + * A spontaneous reply in a not-subscribed context can be the error + * reply that is sent when a new connection exceeds the maximum + * number of allowed connections on the server side. + * + * This is seen as an error instead of a regular reply because the + * server closes the connection after sending it. + * + * To prevent the error from being overwritten by an EOF error the + * connection is closed here. See issue #43. + * + * Another possibility is that the server is loading its dataset. + * In this case we also want to close the connection, and have the + * user wait until the server is ready to take our request. + */ + if (((redisReply*)reply)->type == REDIS_REPLY_ERROR) { + c->err = REDIS_ERR_OTHER; + snprintf(c->errstr,sizeof(c->errstr),"%s",((redisReply*)reply)->str); + __redisAsyncDisconnect(ac); + return; + } + /* No more regular callbacks and no errors, the context *must* be subscribed or monitoring. */ + assert((c->flags & REDIS_SUBSCRIBED || c->flags & REDIS_MONITORING)); + if(c->flags & REDIS_SUBSCRIBED) + __redisGetSubscribeCallback(ac,reply,&cb); + } + + if (cb.fn != NULL) { + __redisRunCallback(ac,&cb,reply); + c->reader->fn->freeObject(reply); + + /* Proceed with free'ing when redisAsyncFree() was called. */ + if (c->flags & REDIS_FREEING) { + __redisAsyncFree(ac); + return; + } + } else { + /* No callback for this reply. This can either be a NULL callback, + * or there were no callbacks to begin with. Either way, don't + * abort with an error, but simply ignore it because the client + * doesn't know what the server will spit out over the wire. */ + c->reader->fn->freeObject(reply); + } + } + + /* Disconnect when there was an error reading the reply */ + if (status != REDIS_OK) + __redisAsyncDisconnect(ac); +} + +/* Internal helper function to detect socket status the first time a read or + * write event fires. When connecting was not succesful, the connect callback + * is called with a REDIS_ERR status and the context is free'd. */ +static int __redisAsyncHandleConnect(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + + if (redisCheckSocketError(c,c->fd) == REDIS_ERR) { + /* Try again later when connect(2) is still in progress. */ + if (errno == EINPROGRESS) + return REDIS_OK; + + if (ac->onConnect) ac->onConnect(ac,REDIS_ERR); + __redisAsyncDisconnect(ac); + return REDIS_ERR; + } + + /* Mark context as connected. */ + c->flags |= REDIS_CONNECTED; + if (ac->onConnect) ac->onConnect(ac,REDIS_OK); + return REDIS_OK; +} + +/* This function should be called when the socket is readable. + * It processes all replies that can be read and executes their callbacks. + */ +void redisAsyncHandleRead(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + + if (!(c->flags & REDIS_CONNECTED)) { + /* Abort connect was not successful. */ + if (__redisAsyncHandleConnect(ac) != REDIS_OK) + return; + /* Try again later when the context is still not connected. */ + if (!(c->flags & REDIS_CONNECTED)) + return; + } + + if (redisBufferRead(c) == REDIS_ERR) { + __redisAsyncDisconnect(ac); + } else { + /* Always re-schedule reads */ + _EL_ADD_READ(ac); + redisProcessCallbacks(ac); + } +} + +void redisAsyncHandleWrite(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + int done = 0; + + if (!(c->flags & REDIS_CONNECTED)) { + /* Abort connect was not successful. */ + if (__redisAsyncHandleConnect(ac) != REDIS_OK) + return; + /* Try again later when the context is still not connected. */ + if (!(c->flags & REDIS_CONNECTED)) + return; + } + + if (redisBufferWrite(c,&done) == REDIS_ERR) { + __redisAsyncDisconnect(ac); + } else { + /* Continue writing when not done, stop writing otherwise */ + if (!done) + _EL_ADD_WRITE(ac); + else + _EL_DEL_WRITE(ac); + + /* Always schedule reads after writes */ + _EL_ADD_READ(ac); + } +} + +/* Sets a pointer to the first argument and its length starting at p. Returns + * the number of bytes to skip to get to the following argument. */ +static char *nextArgument(char *start, char **str, size_t *len) { + char *p = start; + if (p[0] != '$') { + p = strchr(p,'$'); + if (p == NULL) return NULL; + } + + *len = (int)strtol(p+1,NULL,10); + p = strchr(p,'\r'); + assert(p); + *str = p+2; + return p+2+(*len)+2; +} + +/* Helper function for the redisAsyncCommand* family of functions. Writes a + * formatted command to the output buffer and registers the provided callback + * function with the context. */ +static int __redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, char *cmd, size_t len) { + redisContext *c = &(ac->c); + redisCallback cb; + int pvariant, hasnext; + char *cstr, *astr; + size_t clen, alen; + char *p; + sds sname; + + /* Don't accept new commands when the connection is about to be closed. */ + if (c->flags & (REDIS_DISCONNECTING | REDIS_FREEING)) return REDIS_ERR; + + /* Setup callback */ + cb.fn = fn; + cb.privdata = privdata; + + /* Find out which command will be appended. */ + p = nextArgument(cmd,&cstr,&clen); + assert(p != NULL); + hasnext = (p[0] == '$'); + pvariant = (tolower(cstr[0]) == 'p') ? 1 : 0; + cstr += pvariant; + clen -= pvariant; + + if (hasnext && strncasecmp(cstr,"subscribe\r\n",11) == 0) { + c->flags |= REDIS_SUBSCRIBED; + + /* Add every channel/pattern to the list of subscription callbacks. */ + while ((p = nextArgument(p,&astr,&alen)) != NULL) { + sname = sdsnewlen(astr,alen); + if (pvariant) + dictReplace(ac->sub.patterns,sname,&cb); + else + dictReplace(ac->sub.channels,sname,&cb); + } + } else if (strncasecmp(cstr,"unsubscribe\r\n",13) == 0) { + /* It is only useful to call (P)UNSUBSCRIBE when the context is + * subscribed to one or more channels or patterns. */ + if (!(c->flags & REDIS_SUBSCRIBED)) return REDIS_ERR; + + /* (P)UNSUBSCRIBE does not have its own response: every channel or + * pattern that is unsubscribed will receive a message. This means we + * should not append a callback function for this command. */ + } else if(strncasecmp(cstr,"monitor\r\n",9) == 0) { + /* Set monitor flag and push callback */ + c->flags |= REDIS_MONITORING; + __redisPushCallback(&ac->replies,&cb); + } else { + if (c->flags & REDIS_SUBSCRIBED) + /* This will likely result in an error reply, but it needs to be + * received and passed to the callback. */ + __redisPushCallback(&ac->sub.invalid,&cb); + else + __redisPushCallback(&ac->replies,&cb); + } + + __redisAppendCommand(c,cmd,len); + + /* Always schedule a write when the write buffer is non-empty */ + _EL_ADD_WRITE(ac); + + return REDIS_OK; +} + +int redisvAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, va_list ap) { + char *cmd; + int len; + int status; + len = redisvFormatCommand(&cmd,format,ap); + status = __redisAsyncCommand(ac,fn,privdata,cmd,len); + free(cmd); + return status; +} + +int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...) { + va_list ap; + int status; + va_start(ap,format); + status = redisvAsyncCommand(ac,fn,privdata,format,ap); + va_end(ap); + return status; +} + +int redisAsyncCommandArgv(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, int argc, const char **argv, const size_t *argvlen) { + char *cmd; + int len; + int status; + len = redisFormatCommandArgv(&cmd,argc,argv,argvlen); + status = __redisAsyncCommand(ac,fn,privdata,cmd,len); + free(cmd); + return status; +} diff --git a/hiredis/async.h b/hiredis/async.h new file mode 100644 index 0000000..268274e --- /dev/null +++ b/hiredis/async.h @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2009-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __HIREDIS_ASYNC_H +#define __HIREDIS_ASYNC_H +#include "hiredis.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct redisAsyncContext; /* need forward declaration of redisAsyncContext */ +struct dict; /* dictionary header is included in async.c */ + +/* Reply callback prototype and container */ +typedef void (redisCallbackFn)(struct redisAsyncContext*, void*, void*); +typedef struct redisCallback { + struct redisCallback *next; /* simple singly linked list */ + redisCallbackFn *fn; + void *privdata; +} redisCallback; + +/* List of callbacks for either regular replies or pub/sub */ +typedef struct redisCallbackList { + redisCallback *head, *tail; +} redisCallbackList; + +/* Connection callback prototypes */ +typedef void (redisDisconnectCallback)(const struct redisAsyncContext*, int status); +typedef void (redisConnectCallback)(const struct redisAsyncContext*, int status); + +/* Context for an async connection to Redis */ +typedef struct redisAsyncContext { + /* Hold the regular context, so it can be realloc'ed. */ + redisContext c; + + /* Setup error flags so they can be used directly. */ + int err; + char *errstr; + + /* Not used by hiredis */ + void *data; + + /* Event library data and hooks */ + struct { + void *data; + + /* Hooks that are called when the library expects to start + * reading/writing. These functions should be idempotent. */ + void (*addRead)(void *privdata); + void (*delRead)(void *privdata); + void (*addWrite)(void *privdata); + void (*delWrite)(void *privdata); + void (*cleanup)(void *privdata); + } ev; + + /* Called when either the connection is terminated due to an error or per + * user request. The status is set accordingly (REDIS_OK, REDIS_ERR). */ + redisDisconnectCallback *onDisconnect; + + /* Called when the first write event was received. */ + redisConnectCallback *onConnect; + + /* Regular command callbacks */ + redisCallbackList replies; + + /* Subscription callbacks */ + struct { + redisCallbackList invalid; + struct dict *channels; + struct dict *patterns; + } sub; +} redisAsyncContext; + +/* Functions that proxy to hiredis */ +redisAsyncContext *redisAsyncConnect(const char *ip, int port); +redisAsyncContext *redisAsyncConnectUnix(const char *path); +int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn); +int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn); +void redisAsyncDisconnect(redisAsyncContext *ac); +void redisAsyncFree(redisAsyncContext *ac); + +/* Handle read/write events */ +void redisAsyncHandleRead(redisAsyncContext *ac); +void redisAsyncHandleWrite(redisAsyncContext *ac); + +/* Command functions for an async context. Write the command to the + * output buffer and register the provided callback. */ +int redisvAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, va_list ap); +int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...); +int redisAsyncCommandArgv(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, int argc, const char **argv, const size_t *argvlen); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/hiredis/dict.c b/hiredis/dict.c new file mode 100644 index 0000000..79b1041 --- /dev/null +++ b/hiredis/dict.c @@ -0,0 +1,338 @@ +/* Hash table implementation. + * + * This file implements in memory hash tables with insert/del/replace/find/ + * get-random-element operations. Hash tables will auto resize if needed + * tables of power of two in size are used, collisions are handled by + * chaining. See the source code for more information... :) + * + * Copyright (c) 2006-2010, Salvatore Sanfilippo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "fmacros.h" +#include +#include +#include +#include "dict.h" + +/* -------------------------- private prototypes ---------------------------- */ + +static int _dictExpandIfNeeded(dict *ht); +static unsigned long _dictNextPower(unsigned long size); +static int _dictKeyIndex(dict *ht, const void *key); +static int _dictInit(dict *ht, dictType *type, void *privDataPtr); + +/* -------------------------- hash functions -------------------------------- */ + +/* Generic hash function (a popular one from Bernstein). + * I tested a few and this was the best. */ +static unsigned int dictGenHashFunction(const unsigned char *buf, int len) { + unsigned int hash = 5381; + + while (len--) + hash = ((hash << 5) + hash) + (*buf++); /* hash * 33 + c */ + return hash; +} + +/* ----------------------------- API implementation ------------------------- */ + +/* Reset an hashtable already initialized with ht_init(). + * NOTE: This function should only called by ht_destroy(). */ +static void _dictReset(dict *ht) { + ht->table = NULL; + ht->size = 0; + ht->sizemask = 0; + ht->used = 0; +} + +/* Create a new hash table */ +static dict *dictCreate(dictType *type, void *privDataPtr) { + dict *ht = malloc(sizeof(*ht)); + _dictInit(ht,type,privDataPtr); + return ht; +} + +/* Initialize the hash table */ +static int _dictInit(dict *ht, dictType *type, void *privDataPtr) { + _dictReset(ht); + ht->type = type; + ht->privdata = privDataPtr; + return DICT_OK; +} + +/* Expand or create the hashtable */ +static int dictExpand(dict *ht, unsigned long size) { + dict n; /* the new hashtable */ + unsigned long realsize = _dictNextPower(size), i; + + /* the size is invalid if it is smaller than the number of + * elements already inside the hashtable */ + if (ht->used > size) + return DICT_ERR; + + _dictInit(&n, ht->type, ht->privdata); + n.size = realsize; + n.sizemask = realsize-1; + n.table = calloc(realsize,sizeof(dictEntry*)); + + /* Copy all the elements from the old to the new table: + * note that if the old hash table is empty ht->size is zero, + * so dictExpand just creates an hash table. */ + n.used = ht->used; + for (i = 0; i < ht->size && ht->used > 0; i++) { + dictEntry *he, *nextHe; + + if (ht->table[i] == NULL) continue; + + /* For each hash entry on this slot... */ + he = ht->table[i]; + while(he) { + unsigned int h; + + nextHe = he->next; + /* Get the new element index */ + h = dictHashKey(ht, he->key) & n.sizemask; + he->next = n.table[h]; + n.table[h] = he; + ht->used--; + /* Pass to the next element */ + he = nextHe; + } + } + assert(ht->used == 0); + free(ht->table); + + /* Remap the new hashtable in the old */ + *ht = n; + return DICT_OK; +} + +/* Add an element to the target hash table */ +static int dictAdd(dict *ht, void *key, void *val) { + int index; + dictEntry *entry; + + /* Get the index of the new element, or -1 if + * the element already exists. */ + if ((index = _dictKeyIndex(ht, key)) == -1) + return DICT_ERR; + + /* Allocates the memory and stores key */ + entry = malloc(sizeof(*entry)); + entry->next = ht->table[index]; + ht->table[index] = entry; + + /* Set the hash entry fields. */ + dictSetHashKey(ht, entry, key); + dictSetHashVal(ht, entry, val); + ht->used++; + return DICT_OK; +} + +/* Add an element, discarding the old if the key already exists. + * Return 1 if the key was added from scratch, 0 if there was already an + * element with such key and dictReplace() just performed a value update + * operation. */ +static int dictReplace(dict *ht, void *key, void *val) { + dictEntry *entry, auxentry; + + /* Try to add the element. If the key + * does not exists dictAdd will suceed. */ + if (dictAdd(ht, key, val) == DICT_OK) + return 1; + /* It already exists, get the entry */ + entry = dictFind(ht, key); + /* Free the old value and set the new one */ + /* Set the new value and free the old one. Note that it is important + * to do that in this order, as the value may just be exactly the same + * as the previous one. In this context, think to reference counting, + * you want to increment (set), and then decrement (free), and not the + * reverse. */ + auxentry = *entry; + dictSetHashVal(ht, entry, val); + dictFreeEntryVal(ht, &auxentry); + return 0; +} + +/* Search and remove an element */ +static int dictDelete(dict *ht, const void *key) { + unsigned int h; + dictEntry *de, *prevde; + + if (ht->size == 0) + return DICT_ERR; + h = dictHashKey(ht, key) & ht->sizemask; + de = ht->table[h]; + + prevde = NULL; + while(de) { + if (dictCompareHashKeys(ht,key,de->key)) { + /* Unlink the element from the list */ + if (prevde) + prevde->next = de->next; + else + ht->table[h] = de->next; + + dictFreeEntryKey(ht,de); + dictFreeEntryVal(ht,de); + free(de); + ht->used--; + return DICT_OK; + } + prevde = de; + de = de->next; + } + return DICT_ERR; /* not found */ +} + +/* Destroy an entire hash table */ +static int _dictClear(dict *ht) { + unsigned long i; + + /* Free all the elements */ + for (i = 0; i < ht->size && ht->used > 0; i++) { + dictEntry *he, *nextHe; + + if ((he = ht->table[i]) == NULL) continue; + while(he) { + nextHe = he->next; + dictFreeEntryKey(ht, he); + dictFreeEntryVal(ht, he); + free(he); + ht->used--; + he = nextHe; + } + } + /* Free the table and the allocated cache structure */ + free(ht->table); + /* Re-initialize the table */ + _dictReset(ht); + return DICT_OK; /* never fails */ +} + +/* Clear & Release the hash table */ +static void dictRelease(dict *ht) { + _dictClear(ht); + free(ht); +} + +static dictEntry *dictFind(dict *ht, const void *key) { + dictEntry *he; + unsigned int h; + + if (ht->size == 0) return NULL; + h = dictHashKey(ht, key) & ht->sizemask; + he = ht->table[h]; + while(he) { + if (dictCompareHashKeys(ht, key, he->key)) + return he; + he = he->next; + } + return NULL; +} + +static dictIterator *dictGetIterator(dict *ht) { + dictIterator *iter = malloc(sizeof(*iter)); + + iter->ht = ht; + iter->index = -1; + iter->entry = NULL; + iter->nextEntry = NULL; + return iter; +} + +static dictEntry *dictNext(dictIterator *iter) { + while (1) { + if (iter->entry == NULL) { + iter->index++; + if (iter->index >= + (signed)iter->ht->size) break; + iter->entry = iter->ht->table[iter->index]; + } else { + iter->entry = iter->nextEntry; + } + if (iter->entry) { + /* We need to save the 'next' here, the iterator user + * may delete the entry we are returning. */ + iter->nextEntry = iter->entry->next; + return iter->entry; + } + } + return NULL; +} + +static void dictReleaseIterator(dictIterator *iter) { + free(iter); +} + +/* ------------------------- private functions ------------------------------ */ + +/* Expand the hash table if needed */ +static int _dictExpandIfNeeded(dict *ht) { + /* If the hash table is empty expand it to the intial size, + * if the table is "full" dobule its size. */ + if (ht->size == 0) + return dictExpand(ht, DICT_HT_INITIAL_SIZE); + if (ht->used == ht->size) + return dictExpand(ht, ht->size*2); + return DICT_OK; +} + +/* Our hash table capability is a power of two */ +static unsigned long _dictNextPower(unsigned long size) { + unsigned long i = DICT_HT_INITIAL_SIZE; + + if (size >= LONG_MAX) return LONG_MAX; + while(1) { + if (i >= size) + return i; + i *= 2; + } +} + +/* Returns the index of a free slot that can be populated with + * an hash entry for the given 'key'. + * If the key already exists, -1 is returned. */ +static int _dictKeyIndex(dict *ht, const void *key) { + unsigned int h; + dictEntry *he; + + /* Expand the hashtable if needed */ + if (_dictExpandIfNeeded(ht) == DICT_ERR) + return -1; + /* Compute the key hash value */ + h = dictHashKey(ht, key) & ht->sizemask; + /* Search if this slot does not already contain the given key */ + he = ht->table[h]; + while(he) { + if (dictCompareHashKeys(ht, key, he->key)) + return -1; + he = he->next; + } + return h; +} + diff --git a/hiredis/dict.h b/hiredis/dict.h new file mode 100644 index 0000000..95fcd28 --- /dev/null +++ b/hiredis/dict.h @@ -0,0 +1,126 @@ +/* Hash table implementation. + * + * This file implements in memory hash tables with insert/del/replace/find/ + * get-random-element operations. Hash tables will auto resize if needed + * tables of power of two in size are used, collisions are handled by + * chaining. See the source code for more information... :) + * + * Copyright (c) 2006-2010, Salvatore Sanfilippo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __DICT_H +#define __DICT_H + +#define DICT_OK 0 +#define DICT_ERR 1 + +/* Unused arguments generate annoying warnings... */ +#define DICT_NOTUSED(V) ((void) V) + +typedef struct dictEntry { + void *key; + void *val; + struct dictEntry *next; +} dictEntry; + +typedef struct dictType { + unsigned int (*hashFunction)(const void *key); + void *(*keyDup)(void *privdata, const void *key); + void *(*valDup)(void *privdata, const void *obj); + int (*keyCompare)(void *privdata, const void *key1, const void *key2); + void (*keyDestructor)(void *privdata, void *key); + void (*valDestructor)(void *privdata, void *obj); +} dictType; + +typedef struct dict { + dictEntry **table; + dictType *type; + unsigned long size; + unsigned long sizemask; + unsigned long used; + void *privdata; +} dict; + +typedef struct dictIterator { + dict *ht; + int index; + dictEntry *entry, *nextEntry; +} dictIterator; + +/* This is the initial size of every hash table */ +#define DICT_HT_INITIAL_SIZE 4 + +/* ------------------------------- Macros ------------------------------------*/ +#define dictFreeEntryVal(ht, entry) \ + if ((ht)->type->valDestructor) \ + (ht)->type->valDestructor((ht)->privdata, (entry)->val) + +#define dictSetHashVal(ht, entry, _val_) do { \ + if ((ht)->type->valDup) \ + entry->val = (ht)->type->valDup((ht)->privdata, _val_); \ + else \ + entry->val = (_val_); \ +} while(0) + +#define dictFreeEntryKey(ht, entry) \ + if ((ht)->type->keyDestructor) \ + (ht)->type->keyDestructor((ht)->privdata, (entry)->key) + +#define dictSetHashKey(ht, entry, _key_) do { \ + if ((ht)->type->keyDup) \ + entry->key = (ht)->type->keyDup((ht)->privdata, _key_); \ + else \ + entry->key = (_key_); \ +} while(0) + +#define dictCompareHashKeys(ht, key1, key2) \ + (((ht)->type->keyCompare) ? \ + (ht)->type->keyCompare((ht)->privdata, key1, key2) : \ + (key1) == (key2)) + +#define dictHashKey(ht, key) (ht)->type->hashFunction(key) + +#define dictGetEntryKey(he) ((he)->key) +#define dictGetEntryVal(he) ((he)->val) +#define dictSlots(ht) ((ht)->size) +#define dictSize(ht) ((ht)->used) + +/* API */ +static unsigned int dictGenHashFunction(const unsigned char *buf, int len); +static dict *dictCreate(dictType *type, void *privDataPtr); +static int dictExpand(dict *ht, unsigned long size); +static int dictAdd(dict *ht, void *key, void *val); +static int dictReplace(dict *ht, void *key, void *val); +static int dictDelete(dict *ht, const void *key); +static void dictRelease(dict *ht); +static dictEntry * dictFind(dict *ht, const void *key); +static dictIterator *dictGetIterator(dict *ht); +static dictEntry *dictNext(dictIterator *iter); +static void dictReleaseIterator(dictIterator *iter); + +#endif /* __DICT_H */ diff --git a/hiredis/example-ae.c b/hiredis/example-ae.c new file mode 100644 index 0000000..5ed34a3 --- /dev/null +++ b/hiredis/example-ae.c @@ -0,0 +1,56 @@ +#include +#include +#include +#include +#include "hiredis.h" +#include "async.h" +#include "adapters/ae.h" + +/* Put event loop in the global scope, so it can be explicitly stopped */ +static aeEventLoop *loop; + +void getCallback(redisAsyncContext *c, void *r, void *privdata) { + redisReply *reply = r; + if (reply == NULL) return; + printf("argv[%s]: %s\n", (char*)privdata, reply->str); + + /* Disconnect after receiving the reply to GET */ + redisAsyncDisconnect(c); +} + +void connectCallback(const redisAsyncContext *c, int status) { + if (status != REDIS_OK) { + printf("Error: %s\n", c->errstr); + return; + } + printf("Connected...\n"); +} + +void disconnectCallback(const redisAsyncContext *c, int status) { + if (status != REDIS_OK) { + printf("Error: %s\n", c->errstr); + return; + } + printf("Disconnected...\n"); +} + +int main (int argc, char **argv) { + signal(SIGPIPE, SIG_IGN); + + redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379); + if (c->err) { + /* Let *c leak for now... */ + printf("Error: %s\n", c->errstr); + return 1; + } + + loop = aeCreateEventLoop(); + redisAeAttach(loop, c); + redisAsyncSetConnectCallback(c,connectCallback); + redisAsyncSetDisconnectCallback(c,disconnectCallback); + redisAsyncCommand(c, NULL, NULL, "SET key %b", argv[argc-1], strlen(argv[argc-1])); + redisAsyncCommand(c, getCallback, (char*)"end-1", "GET key"); + aeMain(loop); + return 0; +} + diff --git a/hiredis/example-libev.c b/hiredis/example-libev.c new file mode 100644 index 0000000..7894f1f --- /dev/null +++ b/hiredis/example-libev.c @@ -0,0 +1,51 @@ +#include +#include +#include +#include +#include "hiredis.h" +#include "async.h" +#include "adapters/libev.h" + +void getCallback(redisAsyncContext *c, void *r, void *privdata) { + redisReply *reply = r; + if (reply == NULL) return; + printf("argv[%s]: %s\n", (char*)privdata, reply->str); + + /* Disconnect after receiving the reply to GET */ + redisAsyncDisconnect(c); +} + +void connectCallback(const redisAsyncContext *c, int status) { + if (status != REDIS_OK) { + printf("Error: %s\n", c->errstr); + return; + } + printf("Connected...\n"); +} + +void disconnectCallback(const redisAsyncContext *c, int status) { + if (status != REDIS_OK) { + printf("Error: %s\n", c->errstr); + return; + } + printf("Disconnected...\n"); +} + +int main (int argc, char **argv) { + signal(SIGPIPE, SIG_IGN); + + redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379); + if (c->err) { + /* Let *c leak for now... */ + printf("Error: %s\n", c->errstr); + return 1; + } + + redisLibevAttach(EV_DEFAULT_ c); + redisAsyncSetConnectCallback(c,connectCallback); + redisAsyncSetDisconnectCallback(c,disconnectCallback); + redisAsyncCommand(c, NULL, NULL, "SET key %b", argv[argc-1], strlen(argv[argc-1])); + redisAsyncCommand(c, getCallback, (char*)"end-1", "GET key"); + ev_loop(EV_DEFAULT_ 0); + return 0; +} diff --git a/hiredis/example-libevent.c b/hiredis/example-libevent.c new file mode 100644 index 0000000..9da8e02 --- /dev/null +++ b/hiredis/example-libevent.c @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include "hiredis.h" +#include "async.h" +#include "adapters/libevent.h" + +void getCallback(redisAsyncContext *c, void *r, void *privdata) { + redisReply *reply = r; + if (reply == NULL) return; + printf("argv[%s]: %s\n", (char*)privdata, reply->str); + + /* Disconnect after receiving the reply to GET */ + redisAsyncDisconnect(c); +} + +void connectCallback(const redisAsyncContext *c, int status) { + if (status != REDIS_OK) { + printf("Error: %s\n", c->errstr); + return; + } + printf("Connected...\n"); +} + +void disconnectCallback(const redisAsyncContext *c, int status) { + if (status != REDIS_OK) { + printf("Error: %s\n", c->errstr); + return; + } + printf("Disconnected...\n"); +} + +int main (int argc, char **argv) { + signal(SIGPIPE, SIG_IGN); + struct event_base *base = event_base_new(); + + redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379); + if (c->err) { + /* Let *c leak for now... */ + printf("Error: %s\n", c->errstr); + return 1; + } + + redisLibeventAttach(c,base); + redisAsyncSetConnectCallback(c,connectCallback); + redisAsyncSetDisconnectCallback(c,disconnectCallback); + redisAsyncCommand(c, NULL, NULL, "SET key %b", argv[argc-1], strlen(argv[argc-1])); + redisAsyncCommand(c, getCallback, (char*)"end-1", "GET key"); + event_base_dispatch(base); + return 0; +} diff --git a/hiredis/example.c b/hiredis/example.c new file mode 100644 index 0000000..d9d7271 --- /dev/null +++ b/hiredis/example.c @@ -0,0 +1,73 @@ +#include +#include +#include + +#include "hiredis.h" + +int main(void) { + unsigned int j; + redisContext *c; + redisReply *reply; + + struct timeval timeout = { 1, 500000 }; // 1.5 seconds + c = redisConnectWithTimeout((char*)"127.0.0.1", 6379, timeout); + if (c == NULL || c->err) { + if (c) { + printf("Connection error: %s\n", c->errstr); + redisFree(c); + } else { + printf("Connection error: can't allocate redis context\n"); + } + exit(1); + } + + /* PING server */ + reply = redisCommand(c,"PING"); + printf("PING: %s\n", reply->str); + freeReplyObject(reply); + + /* Set a key */ + reply = redisCommand(c,"SET %s %s", "foo", "hello world"); + printf("SET: %s\n", reply->str); + freeReplyObject(reply); + + /* Set a key using binary safe API */ + reply = redisCommand(c,"SET %b %b", "bar", 3, "hello", 5); + printf("SET (binary API): %s\n", reply->str); + freeReplyObject(reply); + + /* Try a GET and two INCR */ + reply = redisCommand(c,"GET foo"); + printf("GET foo: %s\n", reply->str); + freeReplyObject(reply); + + reply = redisCommand(c,"INCR counter"); + printf("INCR counter: %lld\n", reply->integer); + freeReplyObject(reply); + /* again ... */ + reply = redisCommand(c,"INCR counter"); + printf("INCR counter: %lld\n", reply->integer); + freeReplyObject(reply); + + /* Create a list of numbers, from 0 to 9 */ + reply = redisCommand(c,"DEL mylist"); + freeReplyObject(reply); + for (j = 0; j < 10; j++) { + char buf[64]; + + snprintf(buf,64,"%d",j); + reply = redisCommand(c,"LPUSH mylist element-%s", buf); + freeReplyObject(reply); + } + + /* Let's check what we have inside the list */ + reply = redisCommand(c,"LRANGE mylist 0 -1"); + if (reply->type == REDIS_REPLY_ARRAY) { + for (j = 0; j < reply->elements; j++) { + printf("%u) %s\n", j, reply->element[j]->str); + } + } + freeReplyObject(reply); + + return 0; +} diff --git a/hiredis/fmacros.h b/hiredis/fmacros.h new file mode 100644 index 0000000..21cd9cf --- /dev/null +++ b/hiredis/fmacros.h @@ -0,0 +1,16 @@ +#ifndef __HIREDIS_FMACRO_H +#define __HIREDIS_FMACRO_H + +#if !defined(_BSD_SOURCE) +#define _BSD_SOURCE +#endif + +#if defined(__sun__) +#define _POSIX_C_SOURCE 200112L +#elif defined(__linux__) +#define _XOPEN_SOURCE 600 +#else +#define _XOPEN_SOURCE +#endif + +#endif diff --git a/hiredis/hiredis.c b/hiredis/hiredis.c new file mode 100644 index 0000000..958f58b --- /dev/null +++ b/hiredis/hiredis.c @@ -0,0 +1,1315 @@ +/* + * Copyright (c) 2009-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "fmacros.h" +#include +#include +#include +#include +#include +#include + +#include "hiredis.h" +#include "net.h" +#include "sds.h" + +static redisReply *createReplyObject(int type); +static void *createStringObject(const redisReadTask *task, char *str, size_t len); +static void *createArrayObject(const redisReadTask *task, int elements); +static void *createIntegerObject(const redisReadTask *task, long long value); +static void *createNilObject(const redisReadTask *task); + +/* Default set of functions to build the reply. Keep in mind that such a + * function returning NULL is interpreted as OOM. */ +static redisReplyObjectFunctions defaultFunctions = { + createStringObject, + createArrayObject, + createIntegerObject, + createNilObject, + freeReplyObject +}; + +/* Create a reply object */ +static redisReply *createReplyObject(int type) { + redisReply *r = calloc(1,sizeof(*r)); + + if (r == NULL) + return NULL; + + r->type = type; + return r; +} + +/* Free a reply object */ +void freeReplyObject(void *reply) { + redisReply *r = reply; + size_t j; + + switch(r->type) { + case REDIS_REPLY_INTEGER: + break; /* Nothing to free */ + case REDIS_REPLY_ARRAY: + if (r->element != NULL) { + for (j = 0; j < r->elements; j++) + if (r->element[j] != NULL) + freeReplyObject(r->element[j]); + free(r->element); + } + break; + case REDIS_REPLY_ERROR: + case REDIS_REPLY_STATUS: + case REDIS_REPLY_STRING: + if (r->str != NULL) + free(r->str); + break; + } + free(r); +} + +static void *createStringObject(const redisReadTask *task, char *str, size_t len) { + redisReply *r, *parent; + char *buf; + + r = createReplyObject(task->type); + if (r == NULL) + return NULL; + + buf = malloc(len+1); + if (buf == NULL) { + freeReplyObject(r); + return NULL; + } + + assert(task->type == REDIS_REPLY_ERROR || + task->type == REDIS_REPLY_STATUS || + task->type == REDIS_REPLY_STRING); + + /* Copy string value */ + memcpy(buf,str,len); + buf[len] = '\0'; + r->str = buf; + r->len = len; + + if (task->parent) { + parent = task->parent->obj; + assert(parent->type == REDIS_REPLY_ARRAY); + parent->element[task->idx] = r; + } + return r; +} + +static void *createArrayObject(const redisReadTask *task, int elements) { + redisReply *r, *parent; + + r = createReplyObject(REDIS_REPLY_ARRAY); + if (r == NULL) + return NULL; + + if (elements > 0) { + r->element = calloc(elements,sizeof(redisReply*)); + if (r->element == NULL) { + freeReplyObject(r); + return NULL; + } + } + + r->elements = elements; + + if (task->parent) { + parent = task->parent->obj; + assert(parent->type == REDIS_REPLY_ARRAY); + parent->element[task->idx] = r; + } + return r; +} + +static void *createIntegerObject(const redisReadTask *task, long long value) { + redisReply *r, *parent; + + r = createReplyObject(REDIS_REPLY_INTEGER); + if (r == NULL) + return NULL; + + r->integer = value; + + if (task->parent) { + parent = task->parent->obj; + assert(parent->type == REDIS_REPLY_ARRAY); + parent->element[task->idx] = r; + } + return r; +} + +static void *createNilObject(const redisReadTask *task) { + redisReply *r, *parent; + + r = createReplyObject(REDIS_REPLY_NIL); + if (r == NULL) + return NULL; + + if (task->parent) { + parent = task->parent->obj; + assert(parent->type == REDIS_REPLY_ARRAY); + parent->element[task->idx] = r; + } + return r; +} + +static void __redisReaderSetError(redisReader *r, int type, const char *str) { + size_t len; + + if (r->reply != NULL && r->fn && r->fn->freeObject) { + r->fn->freeObject(r->reply); + r->reply = NULL; + } + + /* Clear input buffer on errors. */ + if (r->buf != NULL) { + sdsfree(r->buf); + r->buf = NULL; + r->pos = r->len = 0; + } + + /* Reset task stack. */ + r->ridx = -1; + + /* Set error. */ + r->err = type; + len = strlen(str); + len = len < (sizeof(r->errstr)-1) ? len : (sizeof(r->errstr)-1); + memcpy(r->errstr,str,len); + r->errstr[len] = '\0'; +} + +static size_t chrtos(char *buf, size_t size, char byte) { + size_t len = 0; + + switch(byte) { + case '\\': + case '"': + len = snprintf(buf,size,"\"\\%c\"",byte); + break; + case '\n': len = snprintf(buf,size,"\"\\n\""); break; + case '\r': len = snprintf(buf,size,"\"\\r\""); break; + case '\t': len = snprintf(buf,size,"\"\\t\""); break; + case '\a': len = snprintf(buf,size,"\"\\a\""); break; + case '\b': len = snprintf(buf,size,"\"\\b\""); break; + default: + if (isprint(byte)) + len = snprintf(buf,size,"\"%c\"",byte); + else + len = snprintf(buf,size,"\"\\x%02x\"",(unsigned char)byte); + break; + } + + return len; +} + +static void __redisReaderSetErrorProtocolByte(redisReader *r, char byte) { + char cbuf[8], sbuf[128]; + + chrtos(cbuf,sizeof(cbuf),byte); + snprintf(sbuf,sizeof(sbuf), + "Protocol error, got %s as reply type byte", cbuf); + __redisReaderSetError(r,REDIS_ERR_PROTOCOL,sbuf); +} + +static void __redisReaderSetErrorOOM(redisReader *r) { + __redisReaderSetError(r,REDIS_ERR_OOM,"Out of memory"); +} + +static char *readBytes(redisReader *r, unsigned int bytes) { + char *p; + if (r->len-r->pos >= bytes) { + p = r->buf+r->pos; + r->pos += bytes; + return p; + } + return NULL; +} + +/* Find pointer to \r\n. */ +static char *seekNewline(char *s, size_t len) { + int pos = 0; + int _len = len-1; + + /* Position should be < len-1 because the character at "pos" should be + * followed by a \n. Note that strchr cannot be used because it doesn't + * allow to search a limited length and the buffer that is being searched + * might not have a trailing NULL character. */ + while (pos < _len) { + while(pos < _len && s[pos] != '\r') pos++; + if (s[pos] != '\r') { + /* Not found. */ + return NULL; + } else { + if (s[pos+1] == '\n') { + /* Found. */ + return s+pos; + } else { + /* Continue searching. */ + pos++; + } + } + } + return NULL; +} + +/* Read a long long value starting at *s, under the assumption that it will be + * terminated by \r\n. Ambiguously returns -1 for unexpected input. */ +static long long readLongLong(char *s) { + long long v = 0; + int dec, mult = 1; + char c; + + if (*s == '-') { + mult = -1; + s++; + } else if (*s == '+') { + mult = 1; + s++; + } + + while ((c = *(s++)) != '\r') { + dec = c - '0'; + if (dec >= 0 && dec < 10) { + v *= 10; + v += dec; + } else { + /* Should not happen... */ + return -1; + } + } + + return mult*v; +} + +static char *readLine(redisReader *r, int *_len) { + char *p, *s; + int len; + + p = r->buf+r->pos; + s = seekNewline(p,(r->len-r->pos)); + if (s != NULL) { + len = s-(r->buf+r->pos); + r->pos += len+2; /* skip \r\n */ + if (_len) *_len = len; + return p; + } + return NULL; +} + +static void moveToNextTask(redisReader *r) { + redisReadTask *cur, *prv; + while (r->ridx >= 0) { + /* Return a.s.a.p. when the stack is now empty. */ + if (r->ridx == 0) { + r->ridx--; + return; + } + + cur = &(r->rstack[r->ridx]); + prv = &(r->rstack[r->ridx-1]); + assert(prv->type == REDIS_REPLY_ARRAY); + if (cur->idx == prv->elements-1) { + r->ridx--; + } else { + /* Reset the type because the next item can be anything */ + assert(cur->idx < prv->elements); + cur->type = -1; + cur->elements = -1; + cur->idx++; + return; + } + } +} + +static int processLineItem(redisReader *r) { + redisReadTask *cur = &(r->rstack[r->ridx]); + void *obj; + char *p; + int len; + + if ((p = readLine(r,&len)) != NULL) { + if (cur->type == REDIS_REPLY_INTEGER) { + if (r->fn && r->fn->createInteger) + obj = r->fn->createInteger(cur,readLongLong(p)); + else + obj = (void*)REDIS_REPLY_INTEGER; + } else { + /* Type will be error or status. */ + if (r->fn && r->fn->createString) + obj = r->fn->createString(cur,p,len); + else + obj = (void*)(size_t)(cur->type); + } + + if (obj == NULL) { + __redisReaderSetErrorOOM(r); + return REDIS_ERR; + } + + /* Set reply if this is the root object. */ + if (r->ridx == 0) r->reply = obj; + moveToNextTask(r); + return REDIS_OK; + } + + return REDIS_ERR; +} + +static int processBulkItem(redisReader *r) { + redisReadTask *cur = &(r->rstack[r->ridx]); + void *obj = NULL; + char *p, *s; + long len; + unsigned long bytelen; + int success = 0; + + p = r->buf+r->pos; + s = seekNewline(p,r->len-r->pos); + if (s != NULL) { + p = r->buf+r->pos; + bytelen = s-(r->buf+r->pos)+2; /* include \r\n */ + len = readLongLong(p); + + if (len < 0) { + /* The nil object can always be created. */ + if (r->fn && r->fn->createNil) + obj = r->fn->createNil(cur); + else + obj = (void*)REDIS_REPLY_NIL; + success = 1; + } else { + /* Only continue when the buffer contains the entire bulk item. */ + bytelen += len+2; /* include \r\n */ + if (r->pos+bytelen <= r->len) { + if (r->fn && r->fn->createString) + obj = r->fn->createString(cur,s+2,len); + else + obj = (void*)REDIS_REPLY_STRING; + success = 1; + } + } + + /* Proceed when obj was created. */ + if (success) { + if (obj == NULL) { + __redisReaderSetErrorOOM(r); + return REDIS_ERR; + } + + r->pos += bytelen; + + /* Set reply if this is the root object. */ + if (r->ridx == 0) r->reply = obj; + moveToNextTask(r); + return REDIS_OK; + } + } + + return REDIS_ERR; +} + +static int processMultiBulkItem(redisReader *r) { + redisReadTask *cur = &(r->rstack[r->ridx]); + void *obj; + char *p; + long elements; + int root = 0; + + /* Set error for nested multi bulks with depth > 7 */ + if (r->ridx == 8) { + __redisReaderSetError(r,REDIS_ERR_PROTOCOL, + "No support for nested multi bulk replies with depth > 7"); + return REDIS_ERR; + } + + if ((p = readLine(r,NULL)) != NULL) { + elements = readLongLong(p); + root = (r->ridx == 0); + + if (elements == -1) { + if (r->fn && r->fn->createNil) + obj = r->fn->createNil(cur); + else + obj = (void*)REDIS_REPLY_NIL; + + if (obj == NULL) { + __redisReaderSetErrorOOM(r); + return REDIS_ERR; + } + + moveToNextTask(r); + } else { + if (r->fn && r->fn->createArray) + obj = r->fn->createArray(cur,elements); + else + obj = (void*)REDIS_REPLY_ARRAY; + + if (obj == NULL) { + __redisReaderSetErrorOOM(r); + return REDIS_ERR; + } + + /* Modify task stack when there are more than 0 elements. */ + if (elements > 0) { + cur->elements = elements; + cur->obj = obj; + r->ridx++; + r->rstack[r->ridx].type = -1; + r->rstack[r->ridx].elements = -1; + r->rstack[r->ridx].idx = 0; + r->rstack[r->ridx].obj = NULL; + r->rstack[r->ridx].parent = cur; + r->rstack[r->ridx].privdata = r->privdata; + } else { + moveToNextTask(r); + } + } + + /* Set reply if this is the root object. */ + if (root) r->reply = obj; + return REDIS_OK; + } + + return REDIS_ERR; +} + +static int processItem(redisReader *r) { + redisReadTask *cur = &(r->rstack[r->ridx]); + char *p; + + /* check if we need to read type */ + if (cur->type < 0) { + if ((p = readBytes(r,1)) != NULL) { + switch (p[0]) { + case '-': + cur->type = REDIS_REPLY_ERROR; + break; + case '+': + cur->type = REDIS_REPLY_STATUS; + break; + case ':': + cur->type = REDIS_REPLY_INTEGER; + break; + case '$': + cur->type = REDIS_REPLY_STRING; + break; + case '*': + cur->type = REDIS_REPLY_ARRAY; + break; + default: + __redisReaderSetErrorProtocolByte(r,*p); + return REDIS_ERR; + } + } else { + /* could not consume 1 byte */ + return REDIS_ERR; + } + } + + /* process typed item */ + switch(cur->type) { + case REDIS_REPLY_ERROR: + case REDIS_REPLY_STATUS: + case REDIS_REPLY_INTEGER: + return processLineItem(r); + case REDIS_REPLY_STRING: + return processBulkItem(r); + case REDIS_REPLY_ARRAY: + return processMultiBulkItem(r); + default: + assert(NULL); + return REDIS_ERR; /* Avoid warning. */ + } +} + +redisReader *redisReaderCreate(void) { + redisReader *r; + + r = calloc(sizeof(redisReader),1); + if (r == NULL) + return NULL; + + r->err = 0; + r->errstr[0] = '\0'; + r->fn = &defaultFunctions; + r->buf = sdsempty(); + r->maxbuf = REDIS_READER_MAX_BUF; + if (r->buf == NULL) { + free(r); + return NULL; + } + + r->ridx = -1; + return r; +} + +void redisReaderFree(redisReader *r) { + if (r->reply != NULL && r->fn && r->fn->freeObject) + r->fn->freeObject(r->reply); + if (r->buf != NULL) + sdsfree(r->buf); + free(r); +} + +int redisReaderFeed(redisReader *r, const char *buf, size_t len) { + sds newbuf; + + /* Return early when this reader is in an erroneous state. */ + if (r->err) + return REDIS_ERR; + + /* Copy the provided buffer. */ + if (buf != NULL && len >= 1) { + /* Destroy internal buffer when it is empty and is quite large. */ + if (r->len == 0 && r->maxbuf != 0 && sdsavail(r->buf) > r->maxbuf) { + sdsfree(r->buf); + r->buf = sdsempty(); + r->pos = 0; + + /* r->buf should not be NULL since we just free'd a larger one. */ + assert(r->buf != NULL); + } + + newbuf = sdscatlen(r->buf,buf,len); + if (newbuf == NULL) { + __redisReaderSetErrorOOM(r); + return REDIS_ERR; + } + + r->buf = newbuf; + r->len = sdslen(r->buf); + } + + return REDIS_OK; +} + +int redisReaderGetReply(redisReader *r, void **reply) { + /* Default target pointer to NULL. */ + if (reply != NULL) + *reply = NULL; + + /* Return early when this reader is in an erroneous state. */ + if (r->err) + return REDIS_ERR; + + /* When the buffer is empty, there will never be a reply. */ + if (r->len == 0) + return REDIS_OK; + + /* Set first item to process when the stack is empty. */ + if (r->ridx == -1) { + r->rstack[0].type = -1; + r->rstack[0].elements = -1; + r->rstack[0].idx = -1; + r->rstack[0].obj = NULL; + r->rstack[0].parent = NULL; + r->rstack[0].privdata = r->privdata; + r->ridx = 0; + } + + /* Process items in reply. */ + while (r->ridx >= 0) + if (processItem(r) != REDIS_OK) + break; + + /* Return ASAP when an error occurred. */ + if (r->err) + return REDIS_ERR; + + /* Discard part of the buffer when we've consumed at least 1k, to avoid + * doing unnecessary calls to memmove() in sds.c. */ + if (r->pos >= 1024) { + r->buf = sdsrange(r->buf,r->pos,-1); + r->pos = 0; + r->len = sdslen(r->buf); + } + + /* Emit a reply when there is one. */ + if (r->ridx == -1) { + if (reply != NULL) + *reply = r->reply; + r->reply = NULL; + } + return REDIS_OK; +} + +/* Calculate the number of bytes needed to represent an integer as string. */ +static int intlen(int i) { + int len = 0; + if (i < 0) { + len++; + i = -i; + } + do { + len++; + i /= 10; + } while(i); + return len; +} + +/* Helper that calculates the bulk length given a certain string length. */ +static size_t bulklen(size_t len) { + return 1+intlen(len)+2+len+2; +} + +int redisvFormatCommand(char **target, const char *format, va_list ap) { + const char *c = format; + char *cmd = NULL; /* final command */ + int pos; /* position in final command */ + sds curarg, newarg; /* current argument */ + int touched = 0; /* was the current argument touched? */ + char **curargv = NULL, **newargv = NULL; + int argc = 0; + int totlen = 0; + int j; + + /* Abort if there is not target to set */ + if (target == NULL) + return -1; + + /* Build the command string accordingly to protocol */ + curarg = sdsempty(); + if (curarg == NULL) + return -1; + + while(*c != '\0') { + if (*c != '%' || c[1] == '\0') { + if (*c == ' ') { + if (touched) { + newargv = realloc(curargv,sizeof(char*)*(argc+1)); + if (newargv == NULL) goto err; + curargv = newargv; + curargv[argc++] = curarg; + totlen += bulklen(sdslen(curarg)); + + /* curarg is put in argv so it can be overwritten. */ + curarg = sdsempty(); + if (curarg == NULL) goto err; + touched = 0; + } + } else { + newarg = sdscatlen(curarg,c,1); + if (newarg == NULL) goto err; + curarg = newarg; + touched = 1; + } + } else { + char *arg; + size_t size; + + /* Set newarg so it can be checked even if it is not touched. */ + newarg = curarg; + + switch(c[1]) { + case 's': + arg = va_arg(ap,char*); + size = strlen(arg); + if (size > 0) + newarg = sdscatlen(curarg,arg,size); + break; + case 'b': + arg = va_arg(ap,char*); + size = va_arg(ap,size_t); + if (size > 0) + newarg = sdscatlen(curarg,arg,size); + break; + case '%': + newarg = sdscat(curarg,"%"); + break; + default: + /* Try to detect printf format */ + { + static const char intfmts[] = "diouxX"; + char _format[16]; + const char *_p = c+1; + size_t _l = 0; + va_list _cpy; + + /* Flags */ + if (*_p != '\0' && *_p == '#') _p++; + if (*_p != '\0' && *_p == '0') _p++; + if (*_p != '\0' && *_p == '-') _p++; + if (*_p != '\0' && *_p == ' ') _p++; + if (*_p != '\0' && *_p == '+') _p++; + + /* Field width */ + while (*_p != '\0' && isdigit(*_p)) _p++; + + /* Precision */ + if (*_p == '.') { + _p++; + while (*_p != '\0' && isdigit(*_p)) _p++; + } + + /* Copy va_list before consuming with va_arg */ + va_copy(_cpy,ap); + + /* Integer conversion (without modifiers) */ + if (strchr(intfmts,*_p) != NULL) { + va_arg(ap,int); + goto fmt_valid; + } + + /* Double conversion (without modifiers) */ + if (strchr("eEfFgGaA",*_p) != NULL) { + va_arg(ap,double); + goto fmt_valid; + } + + /* Size: char */ + if (_p[0] == 'h' && _p[1] == 'h') { + _p += 2; + if (*_p != '\0' && strchr(intfmts,*_p) != NULL) { + va_arg(ap,int); /* char gets promoted to int */ + goto fmt_valid; + } + goto fmt_invalid; + } + + /* Size: short */ + if (_p[0] == 'h') { + _p += 1; + if (*_p != '\0' && strchr(intfmts,*_p) != NULL) { + va_arg(ap,int); /* short gets promoted to int */ + goto fmt_valid; + } + goto fmt_invalid; + } + + /* Size: long long */ + if (_p[0] == 'l' && _p[1] == 'l') { + _p += 2; + if (*_p != '\0' && strchr(intfmts,*_p) != NULL) { + va_arg(ap,long long); + goto fmt_valid; + } + goto fmt_invalid; + } + + /* Size: long */ + if (_p[0] == 'l') { + _p += 1; + if (*_p != '\0' && strchr(intfmts,*_p) != NULL) { + va_arg(ap,long); + goto fmt_valid; + } + goto fmt_invalid; + } + + fmt_invalid: + va_end(_cpy); + goto err; + + fmt_valid: + _l = (_p+1)-c; + if (_l < sizeof(_format)-2) { + memcpy(_format,c,_l); + _format[_l] = '\0'; + newarg = sdscatvprintf(curarg,_format,_cpy); + + /* Update current position (note: outer blocks + * increment c twice so compensate here) */ + c = _p-1; + } + + va_end(_cpy); + break; + } + } + + if (newarg == NULL) goto err; + curarg = newarg; + + touched = 1; + c++; + } + c++; + } + + /* Add the last argument if needed */ + if (touched) { + newargv = realloc(curargv,sizeof(char*)*(argc+1)); + if (newargv == NULL) goto err; + curargv = newargv; + curargv[argc++] = curarg; + totlen += bulklen(sdslen(curarg)); + } else { + sdsfree(curarg); + } + + /* Clear curarg because it was put in curargv or was free'd. */ + curarg = NULL; + + /* Add bytes needed to hold multi bulk count */ + totlen += 1+intlen(argc)+2; + + /* Build the command at protocol level */ + cmd = malloc(totlen+1); + if (cmd == NULL) goto err; + + pos = sprintf(cmd,"*%d\r\n",argc); + for (j = 0; j < argc; j++) { + pos += sprintf(cmd+pos,"$%zu\r\n",sdslen(curargv[j])); + memcpy(cmd+pos,curargv[j],sdslen(curargv[j])); + pos += sdslen(curargv[j]); + sdsfree(curargv[j]); + cmd[pos++] = '\r'; + cmd[pos++] = '\n'; + } + assert(pos == totlen); + cmd[pos] = '\0'; + + free(curargv); + *target = cmd; + return totlen; + +err: + while(argc--) + sdsfree(curargv[argc]); + free(curargv); + + if (curarg != NULL) + sdsfree(curarg); + + /* No need to check cmd since it is the last statement that can fail, + * but do it anyway to be as defensive as possible. */ + if (cmd != NULL) + free(cmd); + + return -1; +} + +/* Format a command according to the Redis protocol. This function + * takes a format similar to printf: + * + * %s represents a C null terminated string you want to interpolate + * %b represents a binary safe string + * + * When using %b you need to provide both the pointer to the string + * and the length in bytes. Examples: + * + * len = redisFormatCommand(target, "GET %s", mykey); + * len = redisFormatCommand(target, "SET %s %b", mykey, myval, myvallen); + */ +int redisFormatCommand(char **target, const char *format, ...) { + va_list ap; + int len; + va_start(ap,format); + len = redisvFormatCommand(target,format,ap); + va_end(ap); + return len; +} + +/* Format a command according to the Redis protocol. This function takes the + * number of arguments, an array with arguments and an array with their + * lengths. If the latter is set to NULL, strlen will be used to compute the + * argument lengths. + */ +int redisFormatCommandArgv(char **target, int argc, const char **argv, const size_t *argvlen) { + char *cmd = NULL; /* final command */ + int pos; /* position in final command */ + size_t len; + int totlen, j; + + /* Calculate number of bytes needed for the command */ + totlen = 1+intlen(argc)+2; + for (j = 0; j < argc; j++) { + len = argvlen ? argvlen[j] : strlen(argv[j]); + totlen += bulklen(len); + } + + /* Build the command at protocol level */ + cmd = malloc(totlen+1); + if (cmd == NULL) + return -1; + + pos = sprintf(cmd,"*%d\r\n",argc); + for (j = 0; j < argc; j++) { + len = argvlen ? argvlen[j] : strlen(argv[j]); + pos += sprintf(cmd+pos,"$%zu\r\n",len); + memcpy(cmd+pos,argv[j],len); + pos += len; + cmd[pos++] = '\r'; + cmd[pos++] = '\n'; + } + assert(pos == totlen); + cmd[pos] = '\0'; + + *target = cmd; + return totlen; +} + +void __redisSetError(redisContext *c, int type, const char *str) { + size_t len; + + c->err = type; + if (str != NULL) { + len = strlen(str); + len = len < (sizeof(c->errstr)-1) ? len : (sizeof(c->errstr)-1); + memcpy(c->errstr,str,len); + c->errstr[len] = '\0'; + } else { + /* Only REDIS_ERR_IO may lack a description! */ + assert(type == REDIS_ERR_IO); + strerror_r(errno,c->errstr,sizeof(c->errstr)); + } +} + +static redisContext *redisContextInit(void) { + redisContext *c; + + c = calloc(1,sizeof(redisContext)); + if (c == NULL) + return NULL; + + c->err = 0; + c->errstr[0] = '\0'; + c->obuf = sdsempty(); + c->reader = redisReaderCreate(); + return c; +} + +void redisFree(redisContext *c) { + if (c->fd > 0) + close(c->fd); + if (c->obuf != NULL) + sdsfree(c->obuf); + if (c->reader != NULL) + redisReaderFree(c->reader); + free(c); +} + +/* Connect to a Redis instance. On error the field error in the returned + * context will be set to the return value of the error function. + * When no set of reply functions is given, the default set will be used. */ +redisContext *redisConnect(const char *ip, int port) { + redisContext *c; + + c = redisContextInit(); + if (c == NULL) + return NULL; + + c->flags |= REDIS_BLOCK; + redisContextConnectTcp(c,ip,port,NULL); + return c; +} + +redisContext *redisConnectWithTimeout(const char *ip, int port, struct timeval tv) { + redisContext *c; + + c = redisContextInit(); + if (c == NULL) + return NULL; + + c->flags |= REDIS_BLOCK; + redisContextConnectTcp(c,ip,port,&tv); + return c; +} + +redisContext *redisConnectNonBlock(const char *ip, int port) { + redisContext *c; + + c = redisContextInit(); + if (c == NULL) + return NULL; + + c->flags &= ~REDIS_BLOCK; + redisContextConnectTcp(c,ip,port,NULL); + return c; +} + +redisContext *redisConnectUnix(const char *path) { + redisContext *c; + + c = redisContextInit(); + if (c == NULL) + return NULL; + + c->flags |= REDIS_BLOCK; + redisContextConnectUnix(c,path,NULL); + return c; +} + +redisContext *redisConnectUnixWithTimeout(const char *path, struct timeval tv) { + redisContext *c; + + c = redisContextInit(); + if (c == NULL) + return NULL; + + c->flags |= REDIS_BLOCK; + redisContextConnectUnix(c,path,&tv); + return c; +} + +redisContext *redisConnectUnixNonBlock(const char *path) { + redisContext *c; + + c = redisContextInit(); + if (c == NULL) + return NULL; + + c->flags &= ~REDIS_BLOCK; + redisContextConnectUnix(c,path,NULL); + return c; +} + +/* Set read/write timeout on a blocking socket. */ +int redisSetTimeout(redisContext *c, struct timeval tv) { + if (c->flags & REDIS_BLOCK) + return redisContextSetTimeout(c,tv); + return REDIS_ERR; +} + +/* Use this function to handle a read event on the descriptor. It will try + * and read some bytes from the socket and feed them to the reply parser. + * + * After this function is called, you may use redisContextReadReply to + * see if there is a reply available. */ +int redisBufferRead(redisContext *c) { + char buf[1024*16]; + int nread; + + /* Return early when the context has seen an error. */ + if (c->err) + return REDIS_ERR; + + nread = read(c->fd,buf,sizeof(buf)); + if (nread == -1) { + if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { + /* Try again later */ + } else { + __redisSetError(c,REDIS_ERR_IO,NULL); + return REDIS_ERR; + } + } else if (nread == 0) { + __redisSetError(c,REDIS_ERR_EOF,"Server closed the connection"); + return REDIS_ERR; + } else { + if (redisReaderFeed(c->reader,buf,nread) != REDIS_OK) { + __redisSetError(c,c->reader->err,c->reader->errstr); + return REDIS_ERR; + } + } + return REDIS_OK; +} + +/* Write the output buffer to the socket. + * + * Returns REDIS_OK when the buffer is empty, or (a part of) the buffer was + * succesfully written to the socket. When the buffer is empty after the + * write operation, "done" is set to 1 (if given). + * + * Returns REDIS_ERR if an error occured trying to write and sets + * c->errstr to hold the appropriate error string. + */ +int redisBufferWrite(redisContext *c, int *done) { + int nwritten; + + /* Return early when the context has seen an error. */ + if (c->err) + return REDIS_ERR; + + if (sdslen(c->obuf) > 0) { + nwritten = write(c->fd,c->obuf,sdslen(c->obuf)); + if (nwritten == -1) { + if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { + /* Try again later */ + } else { + __redisSetError(c,REDIS_ERR_IO,NULL); + return REDIS_ERR; + } + } else if (nwritten > 0) { + if (nwritten == (signed)sdslen(c->obuf)) { + sdsfree(c->obuf); + c->obuf = sdsempty(); + } else { + c->obuf = sdsrange(c->obuf,nwritten,-1); + } + } + } + if (done != NULL) *done = (sdslen(c->obuf) == 0); + return REDIS_OK; +} + +/* Internal helper function to try and get a reply from the reader, + * or set an error in the context otherwise. */ +int redisGetReplyFromReader(redisContext *c, void **reply) { + if (redisReaderGetReply(c->reader,reply) == REDIS_ERR) { + __redisSetError(c,c->reader->err,c->reader->errstr); + return REDIS_ERR; + } + return REDIS_OK; +} + +int redisGetReply(redisContext *c, void **reply) { + int wdone = 0; + void *aux = NULL; + + /* Try to read pending replies */ + if (redisGetReplyFromReader(c,&aux) == REDIS_ERR) + return REDIS_ERR; + + /* For the blocking context, flush output buffer and read reply */ + if (aux == NULL && c->flags & REDIS_BLOCK) { + /* Write until done */ + do { + if (redisBufferWrite(c,&wdone) == REDIS_ERR) + return REDIS_ERR; + } while (!wdone); + + /* Read until there is a reply */ + do { + if (redisBufferRead(c) == REDIS_ERR) + return REDIS_ERR; + if (redisGetReplyFromReader(c,&aux) == REDIS_ERR) + return REDIS_ERR; + } while (aux == NULL); + } + + /* Set reply object */ + if (reply != NULL) *reply = aux; + return REDIS_OK; +} + + +/* Helper function for the redisAppendCommand* family of functions. + * + * Write a formatted command to the output buffer. When this family + * is used, you need to call redisGetReply yourself to retrieve + * the reply (or replies in pub/sub). + */ +int __redisAppendCommand(redisContext *c, char *cmd, size_t len) { + sds newbuf; + + newbuf = sdscatlen(c->obuf,cmd,len); + if (newbuf == NULL) { + __redisSetError(c,REDIS_ERR_OOM,"Out of memory"); + return REDIS_ERR; + } + + c->obuf = newbuf; + return REDIS_OK; +} + +int redisvAppendCommand(redisContext *c, const char *format, va_list ap) { + char *cmd; + int len; + + len = redisvFormatCommand(&cmd,format,ap); + if (len == -1) { + __redisSetError(c,REDIS_ERR_OOM,"Out of memory"); + return REDIS_ERR; + } + + if (__redisAppendCommand(c,cmd,len) != REDIS_OK) { + free(cmd); + return REDIS_ERR; + } + + free(cmd); + return REDIS_OK; +} + +int redisAppendCommand(redisContext *c, const char *format, ...) { + va_list ap; + int ret; + + va_start(ap,format); + ret = redisvAppendCommand(c,format,ap); + va_end(ap); + return ret; +} + +int redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen) { + char *cmd; + int len; + + len = redisFormatCommandArgv(&cmd,argc,argv,argvlen); + if (len == -1) { + __redisSetError(c,REDIS_ERR_OOM,"Out of memory"); + return REDIS_ERR; + } + + if (__redisAppendCommand(c,cmd,len) != REDIS_OK) { + free(cmd); + return REDIS_ERR; + } + + free(cmd); + return REDIS_OK; +} + +/* Helper function for the redisCommand* family of functions. + * + * Write a formatted command to the output buffer. If the given context is + * blocking, immediately read the reply into the "reply" pointer. When the + * context is non-blocking, the "reply" pointer will not be used and the + * command is simply appended to the write buffer. + * + * Returns the reply when a reply was succesfully retrieved. Returns NULL + * otherwise. When NULL is returned in a blocking context, the error field + * in the context will be set. + */ +static void *__redisBlockForReply(redisContext *c) { + void *reply; + + if (c->flags & REDIS_BLOCK) { + if (redisGetReply(c,&reply) != REDIS_OK) + return NULL; + return reply; + } + return NULL; +} + +void *redisvCommand(redisContext *c, const char *format, va_list ap) { + if (redisvAppendCommand(c,format,ap) != REDIS_OK) + return NULL; + return __redisBlockForReply(c); +} + +void *redisCommand(redisContext *c, const char *format, ...) { + va_list ap; + void *reply = NULL; + va_start(ap,format); + reply = redisvCommand(c,format,ap); + va_end(ap); + return reply; +} + +void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen) { + if (redisAppendCommandArgv(c,argc,argv,argvlen) != REDIS_OK) + return NULL; + return __redisBlockForReply(c); +} diff --git a/hiredis/hiredis.h b/hiredis/hiredis.h new file mode 100644 index 0000000..aadcf35 --- /dev/null +++ b/hiredis/hiredis.h @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2009-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __HIREDIS_H +#define __HIREDIS_H +#include /* for size_t */ +#include /* for va_list */ +#include /* for struct timeval */ + +#define HIREDIS_MAJOR 0 +#define HIREDIS_MINOR 11 +#define HIREDIS_PATCH 0 + +#define REDIS_ERR -1 +#define REDIS_OK 0 + +/* When an error occurs, the err flag in a context is set to hold the type of + * error that occured. REDIS_ERR_IO means there was an I/O error and you + * should use the "errno" variable to find out what is wrong. + * For other values, the "errstr" field will hold a description. */ +#define REDIS_ERR_IO 1 /* Error in read or write */ +#define REDIS_ERR_EOF 3 /* End of file */ +#define REDIS_ERR_PROTOCOL 4 /* Protocol error */ +#define REDIS_ERR_OOM 5 /* Out of memory */ +#define REDIS_ERR_OTHER 2 /* Everything else... */ + +/* Connection type can be blocking or non-blocking and is set in the + * least significant bit of the flags field in redisContext. */ +#define REDIS_BLOCK 0x1 + +/* Connection may be disconnected before being free'd. The second bit + * in the flags field is set when the context is connected. */ +#define REDIS_CONNECTED 0x2 + +/* The async API might try to disconnect cleanly and flush the output + * buffer and read all subsequent replies before disconnecting. + * This flag means no new commands can come in and the connection + * should be terminated once all replies have been read. */ +#define REDIS_DISCONNECTING 0x4 + +/* Flag specific to the async API which means that the context should be clean + * up as soon as possible. */ +#define REDIS_FREEING 0x8 + +/* Flag that is set when an async callback is executed. */ +#define REDIS_IN_CALLBACK 0x10 + +/* Flag that is set when the async context has one or more subscriptions. */ +#define REDIS_SUBSCRIBED 0x20 + +/* Flag that is set when monitor mode is active */ +#define REDIS_MONITORING 0x40 + +#define REDIS_REPLY_STRING 1 +#define REDIS_REPLY_ARRAY 2 +#define REDIS_REPLY_INTEGER 3 +#define REDIS_REPLY_NIL 4 +#define REDIS_REPLY_STATUS 5 +#define REDIS_REPLY_ERROR 6 + +#define REDIS_READER_MAX_BUF (1024*16) /* Default max unused reader buffer. */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is the reply object returned by redisCommand() */ +typedef struct redisReply { + int type; /* REDIS_REPLY_* */ + long long integer; /* The integer when type is REDIS_REPLY_INTEGER */ + int len; /* Length of string */ + char *str; /* Used for both REDIS_REPLY_ERROR and REDIS_REPLY_STRING */ + size_t elements; /* number of elements, for REDIS_REPLY_ARRAY */ + struct redisReply **element; /* elements vector for REDIS_REPLY_ARRAY */ +} redisReply; + +typedef struct redisReadTask { + int type; + int elements; /* number of elements in multibulk container */ + int idx; /* index in parent (array) object */ + void *obj; /* holds user-generated value for a read task */ + struct redisReadTask *parent; /* parent task */ + void *privdata; /* user-settable arbitrary field */ +} redisReadTask; + +typedef struct redisReplyObjectFunctions { + void *(*createString)(const redisReadTask*, char*, size_t); + void *(*createArray)(const redisReadTask*, int); + void *(*createInteger)(const redisReadTask*, long long); + void *(*createNil)(const redisReadTask*); + void (*freeObject)(void*); +} redisReplyObjectFunctions; + +/* State for the protocol parser */ +typedef struct redisReader { + int err; /* Error flags, 0 when there is no error */ + char errstr[128]; /* String representation of error when applicable */ + + char *buf; /* Read buffer */ + size_t pos; /* Buffer cursor */ + size_t len; /* Buffer length */ + size_t maxbuf; /* Max length of unused buffer */ + + redisReadTask rstack[9]; + int ridx; /* Index of current read task */ + void *reply; /* Temporary reply pointer */ + + redisReplyObjectFunctions *fn; + void *privdata; +} redisReader; + +/* Public API for the protocol parser. */ +redisReader *redisReaderCreate(void); +void redisReaderFree(redisReader *r); +int redisReaderFeed(redisReader *r, const char *buf, size_t len); +int redisReaderGetReply(redisReader *r, void **reply); + +/* Backwards compatibility, can be removed on big version bump. */ +#define redisReplyReaderCreate redisReaderCreate +#define redisReplyReaderFree redisReaderFree +#define redisReplyReaderFeed redisReaderFeed +#define redisReplyReaderGetReply redisReaderGetReply +#define redisReplyReaderSetPrivdata(_r, _p) (int)(((redisReader*)(_r))->privdata = (_p)) +#define redisReplyReaderGetObject(_r) (((redisReader*)(_r))->reply) +#define redisReplyReaderGetError(_r) (((redisReader*)(_r))->errstr) + +/* Function to free the reply objects hiredis returns by default. */ +void freeReplyObject(void *reply); + +/* Functions to format a command according to the protocol. */ +int redisvFormatCommand(char **target, const char *format, va_list ap); +int redisFormatCommand(char **target, const char *format, ...); +int redisFormatCommandArgv(char **target, int argc, const char **argv, const size_t *argvlen); + +/* Context for a connection to Redis */ +typedef struct redisContext { + int err; /* Error flags, 0 when there is no error */ + char errstr[128]; /* String representation of error when applicable */ + int fd; + int flags; + char *obuf; /* Write buffer */ + redisReader *reader; /* Protocol reader */ +} redisContext; + +redisContext *redisConnect(const char *ip, int port); +redisContext *redisConnectWithTimeout(const char *ip, int port, struct timeval tv); +redisContext *redisConnectNonBlock(const char *ip, int port); +redisContext *redisConnectUnix(const char *path); +redisContext *redisConnectUnixWithTimeout(const char *path, struct timeval tv); +redisContext *redisConnectUnixNonBlock(const char *path); +int redisSetTimeout(redisContext *c, struct timeval tv); +void redisFree(redisContext *c); +int redisBufferRead(redisContext *c); +int redisBufferWrite(redisContext *c, int *done); + +/* In a blocking context, this function first checks if there are unconsumed + * replies to return and returns one if so. Otherwise, it flushes the output + * buffer to the socket and reads until it has a reply. In a non-blocking + * context, it will return unconsumed replies until there are no more. */ +int redisGetReply(redisContext *c, void **reply); +int redisGetReplyFromReader(redisContext *c, void **reply); + +/* Write a command to the output buffer. Use these functions in blocking mode + * to get a pipeline of commands. */ +int redisvAppendCommand(redisContext *c, const char *format, va_list ap); +int redisAppendCommand(redisContext *c, const char *format, ...); +int redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen); + +/* Issue a command to Redis. In a blocking context, it is identical to calling + * redisAppendCommand, followed by redisGetReply. The function will return + * NULL if there was an error in performing the request, otherwise it will + * return the reply. In a non-blocking context, it is identical to calling + * only redisAppendCommand and will always return NULL. */ +void *redisvCommand(redisContext *c, const char *format, va_list ap); +void *redisCommand(redisContext *c, const char *format, ...); +void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/hiredis/net.c b/hiredis/net.c new file mode 100644 index 0000000..82ab2b4 --- /dev/null +++ b/hiredis/net.c @@ -0,0 +1,291 @@ +/* Extracted from anet.c to work properly with Hiredis error reporting. + * + * Copyright (c) 2006-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "fmacros.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "net.h" +#include "sds.h" + +/* Defined in hiredis.c */ +void __redisSetError(redisContext *c, int type, const char *str); + +static void __redisSetErrorFromErrno(redisContext *c, int type, const char *prefix) { + char buf[128]; + size_t len = 0; + + if (prefix != NULL) + len = snprintf(buf,sizeof(buf),"%s: ",prefix); + strerror_r(errno,buf+len,sizeof(buf)-len); + __redisSetError(c,type,buf); +} + +static int redisSetReuseAddr(redisContext *c, int fd) { + int on = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); + close(fd); + return REDIS_ERR; + } + return REDIS_OK; +} + +static int redisCreateSocket(redisContext *c, int type) { + int s; + if ((s = socket(type, SOCK_STREAM, 0)) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); + return REDIS_ERR; + } + if (type == AF_INET) { + if (redisSetReuseAddr(c,s) == REDIS_ERR) { + return REDIS_ERR; + } + } + return s; +} + +static int redisSetBlocking(redisContext *c, int fd, int blocking) { + int flags; + + /* Set the socket nonblocking. + * Note that fcntl(2) for F_GETFL and F_SETFL can't be + * interrupted by a signal. */ + if ((flags = fcntl(fd, F_GETFL)) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,"fcntl(F_GETFL)"); + close(fd); + return REDIS_ERR; + } + + if (blocking) + flags &= ~O_NONBLOCK; + else + flags |= O_NONBLOCK; + + if (fcntl(fd, F_SETFL, flags) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,"fcntl(F_SETFL)"); + close(fd); + return REDIS_ERR; + } + return REDIS_OK; +} + +static int redisSetTcpNoDelay(redisContext *c, int fd) { + int yes = 1; + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes)) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(TCP_NODELAY)"); + close(fd); + return REDIS_ERR; + } + return REDIS_OK; +} + +#define __MAX_MSEC (((LONG_MAX) - 999) / 1000) + +static int redisContextWaitReady(redisContext *c, int fd, const struct timeval *timeout) { + struct pollfd wfd[1]; + long msec; + + msec = -1; + wfd[0].fd = fd; + wfd[0].events = POLLOUT; + + /* Only use timeout when not NULL. */ + if (timeout != NULL) { + if (timeout->tv_usec > 1000000 || timeout->tv_sec > __MAX_MSEC) { + close(fd); + return REDIS_ERR; + } + + msec = (timeout->tv_sec * 1000) + ((timeout->tv_usec + 999) / 1000); + + if (msec < 0 || msec > INT_MAX) { + msec = INT_MAX; + } + } + + if (errno == EINPROGRESS) { + int res; + + if ((res = poll(wfd, 1, msec)) == -1) { + __redisSetErrorFromErrno(c, REDIS_ERR_IO, "poll(2)"); + close(fd); + return REDIS_ERR; + } else if (res == 0) { + errno = ETIMEDOUT; + __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); + close(fd); + return REDIS_ERR; + } + + if (redisCheckSocketError(c, fd) != REDIS_OK) + return REDIS_ERR; + + return REDIS_OK; + } + + __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); + close(fd); + return REDIS_ERR; +} + +int redisCheckSocketError(redisContext *c, int fd) { + int err = 0; + socklen_t errlen = sizeof(err); + + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,"getsockopt(SO_ERROR)"); + close(fd); + return REDIS_ERR; + } + + if (err) { + errno = err; + __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); + close(fd); + return REDIS_ERR; + } + + return REDIS_OK; +} + +int redisContextSetTimeout(redisContext *c, struct timeval tv) { + if (setsockopt(c->fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(SO_RCVTIMEO)"); + return REDIS_ERR; + } + if (setsockopt(c->fd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv)) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(SO_SNDTIMEO)"); + return REDIS_ERR; + } + return REDIS_OK; +} + +int redisContextConnectTcp(redisContext *c, const char *addr, int port, struct timeval *timeout) { + int s, rv; + char _port[6]; /* strlen("65535"); */ + struct addrinfo hints, *servinfo, *p; + int blocking = (c->flags & REDIS_BLOCK); + + snprintf(_port, 6, "%d", port); + memset(&hints,0,sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + if ((rv = getaddrinfo(addr,_port,&hints,&servinfo)) != 0) { + __redisSetError(c,REDIS_ERR_OTHER,gai_strerror(rv)); + return REDIS_ERR; + } + for (p = servinfo; p != NULL; p = p->ai_next) { + if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1) + continue; + + if (redisSetBlocking(c,s,0) != REDIS_OK) + goto error; + if (connect(s,p->ai_addr,p->ai_addrlen) == -1) { + if (errno == EHOSTUNREACH) { + close(s); + continue; + } else if (errno == EINPROGRESS && !blocking) { + /* This is ok. */ + } else { + if (redisContextWaitReady(c,s,timeout) != REDIS_OK) + goto error; + } + } + if (blocking && redisSetBlocking(c,s,1) != REDIS_OK) + goto error; + if (redisSetTcpNoDelay(c,s) != REDIS_OK) + goto error; + + c->fd = s; + c->flags |= REDIS_CONNECTED; + rv = REDIS_OK; + goto end; + } + if (p == NULL) { + char buf[128]; + snprintf(buf,sizeof(buf),"Can't create socket: %s",strerror(errno)); + __redisSetError(c,REDIS_ERR_OTHER,buf); + goto error; + } + +error: + rv = REDIS_ERR; +end: + freeaddrinfo(servinfo); + return rv; // Need to return REDIS_OK if alright +} + +int redisContextConnectUnix(redisContext *c, const char *path, struct timeval *timeout) { + int s; + int blocking = (c->flags & REDIS_BLOCK); + struct sockaddr_un sa; + + if ((s = redisCreateSocket(c,AF_LOCAL)) < 0) + return REDIS_ERR; + if (redisSetBlocking(c,s,0) != REDIS_OK) + return REDIS_ERR; + + sa.sun_family = AF_LOCAL; + strncpy(sa.sun_path,path,sizeof(sa.sun_path)-1); + if (connect(s, (struct sockaddr*)&sa, sizeof(sa)) == -1) { + if (errno == EINPROGRESS && !blocking) { + /* This is ok. */ + } else { + if (redisContextWaitReady(c,s,timeout) != REDIS_OK) + return REDIS_ERR; + } + } + + /* Reset socket to be blocking after connect(2). */ + if (blocking && redisSetBlocking(c,s,1) != REDIS_OK) + return REDIS_ERR; + + c->fd = s; + c->flags |= REDIS_CONNECTED; + return REDIS_OK; +} diff --git a/hiredis/net.h b/hiredis/net.h new file mode 100644 index 0000000..eb8a0a1 --- /dev/null +++ b/hiredis/net.h @@ -0,0 +1,47 @@ +/* Extracted from anet.c to work properly with Hiredis error reporting. + * + * Copyright (c) 2006-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __NET_H +#define __NET_H + +#include "hiredis.h" + +#if defined(__sun) +#define AF_LOCAL AF_UNIX +#endif + +int redisCheckSocketError(redisContext *c, int fd); +int redisContextSetTimeout(redisContext *c, struct timeval tv); +int redisContextConnectTcp(redisContext *c, const char *addr, int port, struct timeval *timeout); +int redisContextConnectUnix(redisContext *c, const char *path, struct timeval *timeout); + +#endif diff --git a/hiredis/sds.c b/hiredis/sds.c new file mode 100644 index 0000000..0af9c67 --- /dev/null +++ b/hiredis/sds.c @@ -0,0 +1,605 @@ +/* SDSLib, A C dynamic strings library + * + * Copyright (c) 2006-2010, Salvatore Sanfilippo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include "sds.h" + +#ifdef SDS_ABORT_ON_OOM +static void sdsOomAbort(void) { + fprintf(stderr,"SDS: Out Of Memory (SDS_ABORT_ON_OOM defined)\n"); + abort(); +} +#endif + +sds sdsnewlen(const void *init, size_t initlen) { + struct sdshdr *sh; + + sh = malloc(sizeof(struct sdshdr)+initlen+1); +#ifdef SDS_ABORT_ON_OOM + if (sh == NULL) sdsOomAbort(); +#else + if (sh == NULL) return NULL; +#endif + sh->len = initlen; + sh->free = 0; + if (initlen) { + if (init) memcpy(sh->buf, init, initlen); + else memset(sh->buf,0,initlen); + } + sh->buf[initlen] = '\0'; + return (char*)sh->buf; +} + +sds sdsempty(void) { + return sdsnewlen("",0); +} + +sds sdsnew(const char *init) { + size_t initlen = (init == NULL) ? 0 : strlen(init); + return sdsnewlen(init, initlen); +} + +sds sdsdup(const sds s) { + return sdsnewlen(s, sdslen(s)); +} + +void sdsfree(sds s) { + if (s == NULL) return; + free(s-sizeof(struct sdshdr)); +} + +void sdsupdatelen(sds s) { + struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); + int reallen = strlen(s); + sh->free += (sh->len-reallen); + sh->len = reallen; +} + +static sds sdsMakeRoomFor(sds s, size_t addlen) { + struct sdshdr *sh, *newsh; + size_t free = sdsavail(s); + size_t len, newlen; + + if (free >= addlen) return s; + len = sdslen(s); + sh = (void*) (s-(sizeof(struct sdshdr))); + newlen = (len+addlen)*2; + newsh = realloc(sh, sizeof(struct sdshdr)+newlen+1); +#ifdef SDS_ABORT_ON_OOM + if (newsh == NULL) sdsOomAbort(); +#else + if (newsh == NULL) return NULL; +#endif + + newsh->free = newlen - len; + return newsh->buf; +} + +/* Grow the sds to have the specified length. Bytes that were not part of + * the original length of the sds will be set to zero. */ +sds sdsgrowzero(sds s, size_t len) { + struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); + size_t totlen, curlen = sh->len; + + if (len <= curlen) return s; + s = sdsMakeRoomFor(s,len-curlen); + if (s == NULL) return NULL; + + /* Make sure added region doesn't contain garbage */ + sh = (void*)(s-(sizeof(struct sdshdr))); + memset(s+curlen,0,(len-curlen+1)); /* also set trailing \0 byte */ + totlen = sh->len+sh->free; + sh->len = len; + sh->free = totlen-sh->len; + return s; +} + +sds sdscatlen(sds s, const void *t, size_t len) { + struct sdshdr *sh; + size_t curlen = sdslen(s); + + s = sdsMakeRoomFor(s,len); + if (s == NULL) return NULL; + sh = (void*) (s-(sizeof(struct sdshdr))); + memcpy(s+curlen, t, len); + sh->len = curlen+len; + sh->free = sh->free-len; + s[curlen+len] = '\0'; + return s; +} + +sds sdscat(sds s, const char *t) { + return sdscatlen(s, t, strlen(t)); +} + +sds sdscpylen(sds s, char *t, size_t len) { + struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); + size_t totlen = sh->free+sh->len; + + if (totlen < len) { + s = sdsMakeRoomFor(s,len-sh->len); + if (s == NULL) return NULL; + sh = (void*) (s-(sizeof(struct sdshdr))); + totlen = sh->free+sh->len; + } + memcpy(s, t, len); + s[len] = '\0'; + sh->len = len; + sh->free = totlen-len; + return s; +} + +sds sdscpy(sds s, char *t) { + return sdscpylen(s, t, strlen(t)); +} + +sds sdscatvprintf(sds s, const char *fmt, va_list ap) { + va_list cpy; + char *buf, *t; + size_t buflen = 16; + + while(1) { + buf = malloc(buflen); +#ifdef SDS_ABORT_ON_OOM + if (buf == NULL) sdsOomAbort(); +#else + if (buf == NULL) return NULL; +#endif + buf[buflen-2] = '\0'; + va_copy(cpy,ap); + vsnprintf(buf, buflen, fmt, cpy); + if (buf[buflen-2] != '\0') { + free(buf); + buflen *= 2; + continue; + } + break; + } + t = sdscat(s, buf); + free(buf); + return t; +} + +sds sdscatprintf(sds s, const char *fmt, ...) { + va_list ap; + char *t; + va_start(ap, fmt); + t = sdscatvprintf(s,fmt,ap); + va_end(ap); + return t; +} + +sds sdstrim(sds s, const char *cset) { + struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); + char *start, *end, *sp, *ep; + size_t len; + + sp = start = s; + ep = end = s+sdslen(s)-1; + while(sp <= end && strchr(cset, *sp)) sp++; + while(ep > start && strchr(cset, *ep)) ep--; + len = (sp > ep) ? 0 : ((ep-sp)+1); + if (sh->buf != sp) memmove(sh->buf, sp, len); + sh->buf[len] = '\0'; + sh->free = sh->free+(sh->len-len); + sh->len = len; + return s; +} + +sds sdsrange(sds s, int start, int end) { + struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); + size_t newlen, len = sdslen(s); + + if (len == 0) return s; + if (start < 0) { + start = len+start; + if (start < 0) start = 0; + } + if (end < 0) { + end = len+end; + if (end < 0) end = 0; + } + newlen = (start > end) ? 0 : (end-start)+1; + if (newlen != 0) { + if (start >= (signed)len) { + newlen = 0; + } else if (end >= (signed)len) { + end = len-1; + newlen = (start > end) ? 0 : (end-start)+1; + } + } else { + start = 0; + } + if (start && newlen) memmove(sh->buf, sh->buf+start, newlen); + sh->buf[newlen] = 0; + sh->free = sh->free+(sh->len-newlen); + sh->len = newlen; + return s; +} + +void sdstolower(sds s) { + int len = sdslen(s), j; + + for (j = 0; j < len; j++) s[j] = tolower(s[j]); +} + +void sdstoupper(sds s) { + int len = sdslen(s), j; + + for (j = 0; j < len; j++) s[j] = toupper(s[j]); +} + +int sdscmp(sds s1, sds s2) { + size_t l1, l2, minlen; + int cmp; + + l1 = sdslen(s1); + l2 = sdslen(s2); + minlen = (l1 < l2) ? l1 : l2; + cmp = memcmp(s1,s2,minlen); + if (cmp == 0) return l1-l2; + return cmp; +} + +/* Split 's' with separator in 'sep'. An array + * of sds strings is returned. *count will be set + * by reference to the number of tokens returned. + * + * On out of memory, zero length string, zero length + * separator, NULL is returned. + * + * Note that 'sep' is able to split a string using + * a multi-character separator. For example + * sdssplit("foo_-_bar","_-_"); will return two + * elements "foo" and "bar". + * + * This version of the function is binary-safe but + * requires length arguments. sdssplit() is just the + * same function but for zero-terminated strings. + */ +sds *sdssplitlen(char *s, int len, char *sep, int seplen, int *count) { + int elements = 0, slots = 5, start = 0, j; + + sds *tokens = malloc(sizeof(sds)*slots); +#ifdef SDS_ABORT_ON_OOM + if (tokens == NULL) sdsOomAbort(); +#endif + if (seplen < 1 || len < 0 || tokens == NULL) return NULL; + if (len == 0) { + *count = 0; + return tokens; + } + for (j = 0; j < (len-(seplen-1)); j++) { + /* make sure there is room for the next element and the final one */ + if (slots < elements+2) { + sds *newtokens; + + slots *= 2; + newtokens = realloc(tokens,sizeof(sds)*slots); + if (newtokens == NULL) { +#ifdef SDS_ABORT_ON_OOM + sdsOomAbort(); +#else + goto cleanup; +#endif + } + tokens = newtokens; + } + /* search the separator */ + if ((seplen == 1 && *(s+j) == sep[0]) || (memcmp(s+j,sep,seplen) == 0)) { + tokens[elements] = sdsnewlen(s+start,j-start); + if (tokens[elements] == NULL) { +#ifdef SDS_ABORT_ON_OOM + sdsOomAbort(); +#else + goto cleanup; +#endif + } + elements++; + start = j+seplen; + j = j+seplen-1; /* skip the separator */ + } + } + /* Add the final element. We are sure there is room in the tokens array. */ + tokens[elements] = sdsnewlen(s+start,len-start); + if (tokens[elements] == NULL) { +#ifdef SDS_ABORT_ON_OOM + sdsOomAbort(); +#else + goto cleanup; +#endif + } + elements++; + *count = elements; + return tokens; + +#ifndef SDS_ABORT_ON_OOM +cleanup: + { + int i; + for (i = 0; i < elements; i++) sdsfree(tokens[i]); + free(tokens); + return NULL; + } +#endif +} + +void sdsfreesplitres(sds *tokens, int count) { + if (!tokens) return; + while(count--) + sdsfree(tokens[count]); + free(tokens); +} + +sds sdsfromlonglong(long long value) { + char buf[32], *p; + unsigned long long v; + + v = (value < 0) ? -value : value; + p = buf+31; /* point to the last character */ + do { + *p-- = '0'+(v%10); + v /= 10; + } while(v); + if (value < 0) *p-- = '-'; + p++; + return sdsnewlen(p,32-(p-buf)); +} + +sds sdscatrepr(sds s, char *p, size_t len) { + s = sdscatlen(s,"\"",1); + if (s == NULL) return NULL; + + while(len--) { + switch(*p) { + case '\\': + case '"': + s = sdscatprintf(s,"\\%c",*p); + break; + case '\n': s = sdscatlen(s,"\\n",2); break; + case '\r': s = sdscatlen(s,"\\r",2); break; + case '\t': s = sdscatlen(s,"\\t",2); break; + case '\a': s = sdscatlen(s,"\\a",2); break; + case '\b': s = sdscatlen(s,"\\b",2); break; + default: + if (isprint(*p)) + s = sdscatprintf(s,"%c",*p); + else + s = sdscatprintf(s,"\\x%02x",(unsigned char)*p); + break; + } + p++; + if (s == NULL) return NULL; + } + return sdscatlen(s,"\"",1); +} + +/* Split a line into arguments, where every argument can be in the + * following programming-language REPL-alike form: + * + * foo bar "newline are supported\n" and "\xff\x00otherstuff" + * + * The number of arguments is stored into *argc, and an array + * of sds is returned. The caller should sdsfree() all the returned + * strings and finally free() the array itself. + * + * Note that sdscatrepr() is able to convert back a string into + * a quoted string in the same format sdssplitargs() is able to parse. + */ +sds *sdssplitargs(char *line, int *argc) { + char *p = line; + char *current = NULL; + char **vector = NULL, **_vector = NULL; + + *argc = 0; + while(1) { + /* skip blanks */ + while(*p && isspace(*p)) p++; + if (*p) { + /* get a token */ + int inq=0; /* set to 1 if we are in "quotes" */ + int done=0; + + if (current == NULL) { + current = sdsempty(); + if (current == NULL) goto err; + } + + while(!done) { + if (inq) { + if (*p == '\\' && *(p+1)) { + char c; + + p++; + switch(*p) { + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'b': c = '\b'; break; + case 'a': c = '\a'; break; + default: c = *p; break; + } + current = sdscatlen(current,&c,1); + } else if (*p == '"') { + /* closing quote must be followed by a space */ + if (*(p+1) && !isspace(*(p+1))) goto err; + done=1; + } else if (!*p) { + /* unterminated quotes */ + goto err; + } else { + current = sdscatlen(current,p,1); + } + } else { + switch(*p) { + case ' ': + case '\n': + case '\r': + case '\t': + case '\0': + done=1; + break; + case '"': + inq=1; + break; + default: + current = sdscatlen(current,p,1); + break; + } + } + if (*p) p++; + if (current == NULL) goto err; + } + /* add the token to the vector */ + _vector = realloc(vector,((*argc)+1)*sizeof(char*)); + if (_vector == NULL) goto err; + + vector = _vector; + vector[*argc] = current; + (*argc)++; + current = NULL; + } else { + return vector; + } + } + +err: + while((*argc)--) + sdsfree(vector[*argc]); + if (vector != NULL) free(vector); + if (current != NULL) sdsfree(current); + return NULL; +} + +#ifdef SDS_TEST_MAIN +#include + +int __failed_tests = 0; +int __test_num = 0; +#define test_cond(descr,_c) do { \ + __test_num++; printf("%d - %s: ", __test_num, descr); \ + if(_c) printf("PASSED\n"); else {printf("FAILED\n"); __failed_tests++;} \ +} while(0); +#define test_report() do { \ + printf("%d tests, %d passed, %d failed\n", __test_num, \ + __test_num-__failed_tests, __failed_tests); \ + if (__failed_tests) { \ + printf("=== WARNING === We have failed tests here...\n"); \ + } \ +} while(0); + +int main(void) { + { + sds x = sdsnew("foo"), y; + + test_cond("Create a string and obtain the length", + sdslen(x) == 3 && memcmp(x,"foo\0",4) == 0) + + sdsfree(x); + x = sdsnewlen("foo",2); + test_cond("Create a string with specified length", + sdslen(x) == 2 && memcmp(x,"fo\0",3) == 0) + + x = sdscat(x,"bar"); + test_cond("Strings concatenation", + sdslen(x) == 5 && memcmp(x,"fobar\0",6) == 0); + + x = sdscpy(x,"a"); + test_cond("sdscpy() against an originally longer string", + sdslen(x) == 1 && memcmp(x,"a\0",2) == 0) + + x = sdscpy(x,"xyzxxxxxxxxxxyyyyyyyyyykkkkkkkkkk"); + test_cond("sdscpy() against an originally shorter string", + sdslen(x) == 33 && + memcmp(x,"xyzxxxxxxxxxxyyyyyyyyyykkkkkkkkkk\0",33) == 0) + + sdsfree(x); + x = sdscatprintf(sdsempty(),"%d",123); + test_cond("sdscatprintf() seems working in the base case", + sdslen(x) == 3 && memcmp(x,"123\0",4) ==0) + + sdsfree(x); + x = sdstrim(sdsnew("xxciaoyyy"),"xy"); + test_cond("sdstrim() correctly trims characters", + sdslen(x) == 4 && memcmp(x,"ciao\0",5) == 0) + + y = sdsrange(sdsdup(x),1,1); + test_cond("sdsrange(...,1,1)", + sdslen(y) == 1 && memcmp(y,"i\0",2) == 0) + + sdsfree(y); + y = sdsrange(sdsdup(x),1,-1); + test_cond("sdsrange(...,1,-1)", + sdslen(y) == 3 && memcmp(y,"iao\0",4) == 0) + + sdsfree(y); + y = sdsrange(sdsdup(x),-2,-1); + test_cond("sdsrange(...,-2,-1)", + sdslen(y) == 2 && memcmp(y,"ao\0",3) == 0) + + sdsfree(y); + y = sdsrange(sdsdup(x),2,1); + test_cond("sdsrange(...,2,1)", + sdslen(y) == 0 && memcmp(y,"\0",1) == 0) + + sdsfree(y); + y = sdsrange(sdsdup(x),1,100); + test_cond("sdsrange(...,1,100)", + sdslen(y) == 3 && memcmp(y,"iao\0",4) == 0) + + sdsfree(y); + y = sdsrange(sdsdup(x),100,100); + test_cond("sdsrange(...,100,100)", + sdslen(y) == 0 && memcmp(y,"\0",1) == 0) + + sdsfree(y); + sdsfree(x); + x = sdsnew("foo"); + y = sdsnew("foa"); + test_cond("sdscmp(foo,foa)", sdscmp(x,y) > 0) + + sdsfree(y); + sdsfree(x); + x = sdsnew("bar"); + y = sdsnew("bar"); + test_cond("sdscmp(bar,bar)", sdscmp(x,y) == 0) + + sdsfree(y); + sdsfree(x); + x = sdsnew("aar"); + y = sdsnew("bar"); + test_cond("sdscmp(bar,bar)", sdscmp(x,y) < 0) + } + test_report() +} +#endif diff --git a/hiredis/sds.h b/hiredis/sds.h new file mode 100644 index 0000000..94f5871 --- /dev/null +++ b/hiredis/sds.h @@ -0,0 +1,88 @@ +/* SDSLib, A C dynamic strings library + * + * Copyright (c) 2006-2010, Salvatore Sanfilippo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __SDS_H +#define __SDS_H + +#include +#include + +typedef char *sds; + +struct sdshdr { + int len; + int free; + char buf[]; +}; + +static inline size_t sdslen(const sds s) { + struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); + return sh->len; +} + +static inline size_t sdsavail(const sds s) { + struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); + return sh->free; +} + +sds sdsnewlen(const void *init, size_t initlen); +sds sdsnew(const char *init); +sds sdsempty(void); +size_t sdslen(const sds s); +sds sdsdup(const sds s); +void sdsfree(sds s); +size_t sdsavail(sds s); +sds sdsgrowzero(sds s, size_t len); +sds sdscatlen(sds s, const void *t, size_t len); +sds sdscat(sds s, const char *t); +sds sdscpylen(sds s, char *t, size_t len); +sds sdscpy(sds s, char *t); + +sds sdscatvprintf(sds s, const char *fmt, va_list ap); +#ifdef __GNUC__ +sds sdscatprintf(sds s, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +#else +sds sdscatprintf(sds s, const char *fmt, ...); +#endif + +sds sdstrim(sds s, const char *cset); +sds sdsrange(sds s, int start, int end); +void sdsupdatelen(sds s); +int sdscmp(sds s1, sds s2); +sds *sdssplitlen(char *s, int len, char *sep, int seplen, int *count); +void sdsfreesplitres(sds *tokens, int count); +void sdstolower(sds s); +void sdstoupper(sds s); +sds sdsfromlonglong(long long value); +sds sdscatrepr(sds s, char *p, size_t len); +sds *sdssplitargs(char *line, int *argc); + +#endif diff --git a/hiredis/test.c b/hiredis/test.c new file mode 100644 index 0000000..6786003 --- /dev/null +++ b/hiredis/test.c @@ -0,0 +1,659 @@ +#include "fmacros.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hiredis.h" + +enum connection_type { + CONN_TCP, + CONN_UNIX +}; + +struct config { + enum connection_type type; + + struct { + const char *host; + int port; + } tcp; + + struct { + const char *path; + } unix; +}; + +/* The following lines make up our testing "framework" :) */ +static int tests = 0, fails = 0; +#define test(_s) { printf("#%02d ", ++tests); printf(_s); } +#define test_cond(_c) if(_c) printf("\033[0;32mPASSED\033[0;0m\n"); else {printf("\033[0;31mFAILED\033[0;0m\n"); fails++;} + +static long long usec(void) { + struct timeval tv; + gettimeofday(&tv,NULL); + return (((long long)tv.tv_sec)*1000000)+tv.tv_usec; +} + +static redisContext *select_database(redisContext *c) { + redisReply *reply; + + /* Switch to DB 9 for testing, now that we know we can chat. */ + reply = redisCommand(c,"SELECT 9"); + assert(reply != NULL); + freeReplyObject(reply); + + /* Make sure the DB is emtpy */ + reply = redisCommand(c,"DBSIZE"); + assert(reply != NULL); + if (reply->type == REDIS_REPLY_INTEGER && reply->integer == 0) { + /* Awesome, DB 9 is empty and we can continue. */ + freeReplyObject(reply); + } else { + printf("Database #9 is not empty, test can not continue\n"); + exit(1); + } + + return c; +} + +static void disconnect(redisContext *c) { + redisReply *reply; + + /* Make sure we're on DB 9. */ + reply = redisCommand(c,"SELECT 9"); + assert(reply != NULL); + freeReplyObject(reply); + reply = redisCommand(c,"FLUSHDB"); + assert(reply != NULL); + freeReplyObject(reply); + + /* Free the context as well. */ + redisFree(c); +} + +static redisContext *connect(struct config config) { + redisContext *c = NULL; + + if (config.type == CONN_TCP) { + c = redisConnect(config.tcp.host, config.tcp.port); + } else if (config.type == CONN_UNIX) { + c = redisConnectUnix(config.unix.path); + } else { + assert(NULL); + } + + if (c == NULL) { + printf("Connection error: can't allocate redis context\n"); + exit(1); + } else if (c->err) { + printf("Connection error: %s\n", c->errstr); + exit(1); + } + + return select_database(c); +} + +static void test_format_commands(void) { + char *cmd; + int len; + + test("Format command without interpolation: "); + len = redisFormatCommand(&cmd,"SET foo bar"); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 && + len == 4+4+(3+2)+4+(3+2)+4+(3+2)); + free(cmd); + + test("Format command with %%s string interpolation: "); + len = redisFormatCommand(&cmd,"SET %s %s","foo","bar"); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 && + len == 4+4+(3+2)+4+(3+2)+4+(3+2)); + free(cmd); + + test("Format command with %%s and an empty string: "); + len = redisFormatCommand(&cmd,"SET %s %s","foo",""); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$0\r\n\r\n",len) == 0 && + len == 4+4+(3+2)+4+(3+2)+4+(0+2)); + free(cmd); + + test("Format command with an empty string in between proper interpolations: "); + len = redisFormatCommand(&cmd,"SET %s %s","","foo"); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$0\r\n\r\n$3\r\nfoo\r\n",len) == 0 && + len == 4+4+(3+2)+4+(0+2)+4+(3+2)); + free(cmd); + + test("Format command with %%b string interpolation: "); + len = redisFormatCommand(&cmd,"SET %b %b","foo",3,"b\0r",3); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nb\0r\r\n",len) == 0 && + len == 4+4+(3+2)+4+(3+2)+4+(3+2)); + free(cmd); + + test("Format command with %%b and an empty string: "); + len = redisFormatCommand(&cmd,"SET %b %b","foo",3,"",0); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$0\r\n\r\n",len) == 0 && + len == 4+4+(3+2)+4+(3+2)+4+(0+2)); + free(cmd); + + test("Format command with literal %%: "); + len = redisFormatCommand(&cmd,"SET %% %%"); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$1\r\n%\r\n$1\r\n%\r\n",len) == 0 && + len == 4+4+(3+2)+4+(1+2)+4+(1+2)); + free(cmd); + + /* Vararg width depends on the type. These tests make sure that the + * width is correctly determined using the format and subsequent varargs + * can correctly be interpolated. */ +#define INTEGER_WIDTH_TEST(fmt, type) do { \ + type value = 123; \ + test("Format command with printf-delegation (" #type "): "); \ + len = redisFormatCommand(&cmd,"key:%08" fmt " str:%s", value, "hello"); \ + test_cond(strncmp(cmd,"*2\r\n$12\r\nkey:00000123\r\n$9\r\nstr:hello\r\n",len) == 0 && \ + len == 4+5+(12+2)+4+(9+2)); \ + free(cmd); \ +} while(0) + +#define FLOAT_WIDTH_TEST(type) do { \ + type value = 123.0; \ + test("Format command with printf-delegation (" #type "): "); \ + len = redisFormatCommand(&cmd,"key:%08.3f str:%s", value, "hello"); \ + test_cond(strncmp(cmd,"*2\r\n$12\r\nkey:0123.000\r\n$9\r\nstr:hello\r\n",len) == 0 && \ + len == 4+5+(12+2)+4+(9+2)); \ + free(cmd); \ +} while(0) + + INTEGER_WIDTH_TEST("d", int); + INTEGER_WIDTH_TEST("hhd", char); + INTEGER_WIDTH_TEST("hd", short); + INTEGER_WIDTH_TEST("ld", long); + INTEGER_WIDTH_TEST("lld", long long); + INTEGER_WIDTH_TEST("u", unsigned int); + INTEGER_WIDTH_TEST("hhu", unsigned char); + INTEGER_WIDTH_TEST("hu", unsigned short); + INTEGER_WIDTH_TEST("lu", unsigned long); + INTEGER_WIDTH_TEST("llu", unsigned long long); + FLOAT_WIDTH_TEST(float); + FLOAT_WIDTH_TEST(double); + + test("Format command with invalid printf format: "); + len = redisFormatCommand(&cmd,"key:%08p %b",(void*)1234,"foo",3); + test_cond(len == -1); + + const char *argv[3]; + argv[0] = "SET"; + argv[1] = "foo\0xxx"; + argv[2] = "bar"; + size_t lens[3] = { 3, 7, 3 }; + int argc = 3; + + test("Format command by passing argc/argv without lengths: "); + len = redisFormatCommandArgv(&cmd,argc,argv,NULL); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 && + len == 4+4+(3+2)+4+(3+2)+4+(3+2)); + free(cmd); + + test("Format command by passing argc/argv with lengths: "); + len = redisFormatCommandArgv(&cmd,argc,argv,lens); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$7\r\nfoo\0xxx\r\n$3\r\nbar\r\n",len) == 0 && + len == 4+4+(3+2)+4+(7+2)+4+(3+2)); + free(cmd); +} + +static void test_reply_reader(void) { + redisReader *reader; + void *reply; + int ret; + int i; + + test("Error handling in reply parser: "); + reader = redisReaderCreate(); + redisReaderFeed(reader,(char*)"@foo\r\n",6); + ret = redisReaderGetReply(reader,NULL); + test_cond(ret == REDIS_ERR && + strcasecmp(reader->errstr,"Protocol error, got \"@\" as reply type byte") == 0); + redisReaderFree(reader); + + /* when the reply already contains multiple items, they must be free'd + * on an error. valgrind will bark when this doesn't happen. */ + test("Memory cleanup in reply parser: "); + reader = redisReaderCreate(); + redisReaderFeed(reader,(char*)"*2\r\n",4); + redisReaderFeed(reader,(char*)"$5\r\nhello\r\n",11); + redisReaderFeed(reader,(char*)"@foo\r\n",6); + ret = redisReaderGetReply(reader,NULL); + test_cond(ret == REDIS_ERR && + strcasecmp(reader->errstr,"Protocol error, got \"@\" as reply type byte") == 0); + redisReaderFree(reader); + + test("Set error on nested multi bulks with depth > 7: "); + reader = redisReaderCreate(); + + for (i = 0; i < 9; i++) { + redisReaderFeed(reader,(char*)"*1\r\n",4); + } + + ret = redisReaderGetReply(reader,NULL); + test_cond(ret == REDIS_ERR && + strncasecmp(reader->errstr,"No support for",14) == 0); + redisReaderFree(reader); + + test("Works with NULL functions for reply: "); + reader = redisReaderCreate(); + reader->fn = NULL; + redisReaderFeed(reader,(char*)"+OK\r\n",5); + ret = redisReaderGetReply(reader,&reply); + test_cond(ret == REDIS_OK && reply == (void*)REDIS_REPLY_STATUS); + redisReaderFree(reader); + + test("Works when a single newline (\\r\\n) covers two calls to feed: "); + reader = redisReaderCreate(); + reader->fn = NULL; + redisReaderFeed(reader,(char*)"+OK\r",4); + ret = redisReaderGetReply(reader,&reply); + assert(ret == REDIS_OK && reply == NULL); + redisReaderFeed(reader,(char*)"\n",1); + ret = redisReaderGetReply(reader,&reply); + test_cond(ret == REDIS_OK && reply == (void*)REDIS_REPLY_STATUS); + redisReaderFree(reader); + + test("Don't reset state after protocol error: "); + reader = redisReaderCreate(); + reader->fn = NULL; + redisReaderFeed(reader,(char*)"x",1); + ret = redisReaderGetReply(reader,&reply); + assert(ret == REDIS_ERR); + ret = redisReaderGetReply(reader,&reply); + test_cond(ret == REDIS_ERR && reply == NULL); + redisReaderFree(reader); + + /* Regression test for issue #45 on GitHub. */ + test("Don't do empty allocation for empty multi bulk: "); + reader = redisReaderCreate(); + redisReaderFeed(reader,(char*)"*0\r\n",4); + ret = redisReaderGetReply(reader,&reply); + test_cond(ret == REDIS_OK && + ((redisReply*)reply)->type == REDIS_REPLY_ARRAY && + ((redisReply*)reply)->elements == 0); + freeReplyObject(reply); + redisReaderFree(reader); +} + +static void test_blocking_connection_errors(void) { + redisContext *c; + + test("Returns error when host cannot be resolved: "); + c = redisConnect((char*)"idontexist.local", 6379); + test_cond(c->err == REDIS_ERR_OTHER && + (strcmp(c->errstr,"Name or service not known") == 0 || + strcmp(c->errstr,"Can't resolve: idontexist.local") == 0)); + redisFree(c); + + test("Returns error when the port is not open: "); + c = redisConnect((char*)"localhost", 1); + test_cond(c->err == REDIS_ERR_IO && + strcmp(c->errstr,"Connection refused") == 0); + redisFree(c); + + test("Returns error when the unix socket path doesn't accept connections: "); + c = redisConnectUnix((char*)"/tmp/idontexist.sock"); + test_cond(c->err == REDIS_ERR_IO); /* Don't care about the message... */ + redisFree(c); +} + +static void test_blocking_connection(struct config config) { + redisContext *c; + redisReply *reply; + + c = connect(config); + + test("Is able to deliver commands: "); + reply = redisCommand(c,"PING"); + test_cond(reply->type == REDIS_REPLY_STATUS && + strcasecmp(reply->str,"pong") == 0) + freeReplyObject(reply); + + test("Is a able to send commands verbatim: "); + reply = redisCommand(c,"SET foo bar"); + test_cond (reply->type == REDIS_REPLY_STATUS && + strcasecmp(reply->str,"ok") == 0) + freeReplyObject(reply); + + test("%%s String interpolation works: "); + reply = redisCommand(c,"SET %s %s","foo","hello world"); + freeReplyObject(reply); + reply = redisCommand(c,"GET foo"); + test_cond(reply->type == REDIS_REPLY_STRING && + strcmp(reply->str,"hello world") == 0); + freeReplyObject(reply); + + test("%%b String interpolation works: "); + reply = redisCommand(c,"SET %b %b","foo",3,"hello\x00world",11); + freeReplyObject(reply); + reply = redisCommand(c,"GET foo"); + test_cond(reply->type == REDIS_REPLY_STRING && + memcmp(reply->str,"hello\x00world",11) == 0) + + test("Binary reply length is correct: "); + test_cond(reply->len == 11) + freeReplyObject(reply); + + test("Can parse nil replies: "); + reply = redisCommand(c,"GET nokey"); + test_cond(reply->type == REDIS_REPLY_NIL) + freeReplyObject(reply); + + /* test 7 */ + test("Can parse integer replies: "); + reply = redisCommand(c,"INCR mycounter"); + test_cond(reply->type == REDIS_REPLY_INTEGER && reply->integer == 1) + freeReplyObject(reply); + + test("Can parse multi bulk replies: "); + freeReplyObject(redisCommand(c,"LPUSH mylist foo")); + freeReplyObject(redisCommand(c,"LPUSH mylist bar")); + reply = redisCommand(c,"LRANGE mylist 0 -1"); + test_cond(reply->type == REDIS_REPLY_ARRAY && + reply->elements == 2 && + !memcmp(reply->element[0]->str,"bar",3) && + !memcmp(reply->element[1]->str,"foo",3)) + freeReplyObject(reply); + + /* m/e with multi bulk reply *before* other reply. + * specifically test ordering of reply items to parse. */ + test("Can handle nested multi bulk replies: "); + freeReplyObject(redisCommand(c,"MULTI")); + freeReplyObject(redisCommand(c,"LRANGE mylist 0 -1")); + freeReplyObject(redisCommand(c,"PING")); + reply = (redisCommand(c,"EXEC")); + test_cond(reply->type == REDIS_REPLY_ARRAY && + reply->elements == 2 && + reply->element[0]->type == REDIS_REPLY_ARRAY && + reply->element[0]->elements == 2 && + !memcmp(reply->element[0]->element[0]->str,"bar",3) && + !memcmp(reply->element[0]->element[1]->str,"foo",3) && + reply->element[1]->type == REDIS_REPLY_STATUS && + strcasecmp(reply->element[1]->str,"pong") == 0); + freeReplyObject(reply); + + disconnect(c); +} + +static void test_blocking_io_errors(struct config config) { + redisContext *c; + redisReply *reply; + void *_reply; + int major, minor; + + /* Connect to target given by config. */ + c = connect(config); + { + /* Find out Redis version to determine the path for the next test */ + const char *field = "redis_version:"; + char *p, *eptr; + + reply = redisCommand(c,"INFO"); + p = strstr(reply->str,field); + major = strtol(p+strlen(field),&eptr,10); + p = eptr+1; /* char next to the first "." */ + minor = strtol(p,&eptr,10); + freeReplyObject(reply); + } + + test("Returns I/O error when the connection is lost: "); + reply = redisCommand(c,"QUIT"); + if (major >= 2 && minor > 0) { + /* > 2.0 returns OK on QUIT and read() should be issued once more + * to know the descriptor is at EOF. */ + test_cond(strcasecmp(reply->str,"OK") == 0 && + redisGetReply(c,&_reply) == REDIS_ERR); + freeReplyObject(reply); + } else { + test_cond(reply == NULL); + } + + /* On 2.0, QUIT will cause the connection to be closed immediately and + * the read(2) for the reply on QUIT will set the error to EOF. + * On >2.0, QUIT will return with OK and another read(2) needed to be + * issued to find out the socket was closed by the server. In both + * conditions, the error will be set to EOF. */ + assert(c->err == REDIS_ERR_EOF && + strcmp(c->errstr,"Server closed the connection") == 0); + redisFree(c); + + c = connect(config); + test("Returns I/O error on socket timeout: "); + struct timeval tv = { 0, 1000 }; + assert(redisSetTimeout(c,tv) == REDIS_OK); + test_cond(redisGetReply(c,&_reply) == REDIS_ERR && + c->err == REDIS_ERR_IO && errno == EAGAIN); + redisFree(c); +} + +static void test_throughput(struct config config) { + redisContext *c = connect(config); + redisReply **replies; + int i, num; + long long t1, t2; + + test("Throughput:\n"); + for (i = 0; i < 500; i++) + freeReplyObject(redisCommand(c,"LPUSH mylist foo")); + + num = 1000; + replies = malloc(sizeof(redisReply*)*num); + t1 = usec(); + for (i = 0; i < num; i++) { + replies[i] = redisCommand(c,"PING"); + assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_STATUS); + } + t2 = usec(); + for (i = 0; i < num; i++) freeReplyObject(replies[i]); + free(replies); + printf("\t(%dx PING: %.3fs)\n", num, (t2-t1)/1000000.0); + + replies = malloc(sizeof(redisReply*)*num); + t1 = usec(); + for (i = 0; i < num; i++) { + replies[i] = redisCommand(c,"LRANGE mylist 0 499"); + assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_ARRAY); + assert(replies[i] != NULL && replies[i]->elements == 500); + } + t2 = usec(); + for (i = 0; i < num; i++) freeReplyObject(replies[i]); + free(replies); + printf("\t(%dx LRANGE with 500 elements: %.3fs)\n", num, (t2-t1)/1000000.0); + + num = 10000; + replies = malloc(sizeof(redisReply*)*num); + for (i = 0; i < num; i++) + redisAppendCommand(c,"PING"); + t1 = usec(); + for (i = 0; i < num; i++) { + assert(redisGetReply(c, (void*)&replies[i]) == REDIS_OK); + assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_STATUS); + } + t2 = usec(); + for (i = 0; i < num; i++) freeReplyObject(replies[i]); + free(replies); + printf("\t(%dx PING (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0); + + replies = malloc(sizeof(redisReply*)*num); + for (i = 0; i < num; i++) + redisAppendCommand(c,"LRANGE mylist 0 499"); + t1 = usec(); + for (i = 0; i < num; i++) { + assert(redisGetReply(c, (void*)&replies[i]) == REDIS_OK); + assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_ARRAY); + assert(replies[i] != NULL && replies[i]->elements == 500); + } + t2 = usec(); + for (i = 0; i < num; i++) freeReplyObject(replies[i]); + free(replies); + printf("\t(%dx LRANGE with 500 elements (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0); + + disconnect(c); +} + +// static long __test_callback_flags = 0; +// static void __test_callback(redisContext *c, void *privdata) { +// ((void)c); +// /* Shift to detect execution order */ +// __test_callback_flags <<= 8; +// __test_callback_flags |= (long)privdata; +// } +// +// static void __test_reply_callback(redisContext *c, redisReply *reply, void *privdata) { +// ((void)c); +// /* Shift to detect execution order */ +// __test_callback_flags <<= 8; +// __test_callback_flags |= (long)privdata; +// if (reply) freeReplyObject(reply); +// } +// +// static redisContext *__connect_nonblock() { +// /* Reset callback flags */ +// __test_callback_flags = 0; +// return redisConnectNonBlock("127.0.0.1", port, NULL); +// } +// +// static void test_nonblocking_connection() { +// redisContext *c; +// int wdone = 0; +// +// test("Calls command callback when command is issued: "); +// c = __connect_nonblock(); +// redisSetCommandCallback(c,__test_callback,(void*)1); +// redisCommand(c,"PING"); +// test_cond(__test_callback_flags == 1); +// redisFree(c); +// +// test("Calls disconnect callback on redisDisconnect: "); +// c = __connect_nonblock(); +// redisSetDisconnectCallback(c,__test_callback,(void*)2); +// redisDisconnect(c); +// test_cond(__test_callback_flags == 2); +// redisFree(c); +// +// test("Calls disconnect callback and free callback on redisFree: "); +// c = __connect_nonblock(); +// redisSetDisconnectCallback(c,__test_callback,(void*)2); +// redisSetFreeCallback(c,__test_callback,(void*)4); +// redisFree(c); +// test_cond(__test_callback_flags == ((2 << 8) | 4)); +// +// test("redisBufferWrite against empty write buffer: "); +// c = __connect_nonblock(); +// test_cond(redisBufferWrite(c,&wdone) == REDIS_OK && wdone == 1); +// redisFree(c); +// +// test("redisBufferWrite against not yet connected fd: "); +// c = __connect_nonblock(); +// redisCommand(c,"PING"); +// test_cond(redisBufferWrite(c,NULL) == REDIS_ERR && +// strncmp(c->error,"write:",6) == 0); +// redisFree(c); +// +// test("redisBufferWrite against closed fd: "); +// c = __connect_nonblock(); +// redisCommand(c,"PING"); +// redisDisconnect(c); +// test_cond(redisBufferWrite(c,NULL) == REDIS_ERR && +// strncmp(c->error,"write:",6) == 0); +// redisFree(c); +// +// test("Process callbacks in the right sequence: "); +// c = __connect_nonblock(); +// redisCommandWithCallback(c,__test_reply_callback,(void*)1,"PING"); +// redisCommandWithCallback(c,__test_reply_callback,(void*)2,"PING"); +// redisCommandWithCallback(c,__test_reply_callback,(void*)3,"PING"); +// +// /* Write output buffer */ +// wdone = 0; +// while(!wdone) { +// usleep(500); +// redisBufferWrite(c,&wdone); +// } +// +// /* Read until at least one callback is executed (the 3 replies will +// * arrive in a single packet, causing all callbacks to be executed in +// * a single pass). */ +// while(__test_callback_flags == 0) { +// assert(redisBufferRead(c) == REDIS_OK); +// redisProcessCallbacks(c); +// } +// test_cond(__test_callback_flags == 0x010203); +// redisFree(c); +// +// test("redisDisconnect executes pending callbacks with NULL reply: "); +// c = __connect_nonblock(); +// redisSetDisconnectCallback(c,__test_callback,(void*)1); +// redisCommandWithCallback(c,__test_reply_callback,(void*)2,"PING"); +// redisDisconnect(c); +// test_cond(__test_callback_flags == 0x0201); +// redisFree(c); +// } + +int main(int argc, char **argv) { + struct config cfg = { + .tcp = { + .host = "127.0.0.1", + .port = 6379 + }, + .unix = { + .path = "/tmp/redis.sock" + } + }; + int throughput = 1; + + /* Ignore broken pipe signal (for I/O error tests). */ + signal(SIGPIPE, SIG_IGN); + + /* Parse command line options. */ + argv++; argc--; + while (argc) { + if (argc >= 2 && !strcmp(argv[0],"-h")) { + argv++; argc--; + cfg.tcp.host = argv[0]; + } else if (argc >= 2 && !strcmp(argv[0],"-p")) { + argv++; argc--; + cfg.tcp.port = atoi(argv[0]); + } else if (argc >= 2 && !strcmp(argv[0],"-s")) { + argv++; argc--; + cfg.unix.path = argv[0]; + } else if (argc >= 1 && !strcmp(argv[0],"--skip-throughput")) { + throughput = 0; + } else { + fprintf(stderr, "Invalid argument: %s\n", argv[0]); + exit(1); + } + argv++; argc--; + } + + test_format_commands(); + test_reply_reader(); + test_blocking_connection_errors(); + + printf("\nTesting against TCP connection (%s:%d):\n", cfg.tcp.host, cfg.tcp.port); + cfg.type = CONN_TCP; + test_blocking_connection(cfg); + test_blocking_io_errors(cfg); + if (throughput) test_throughput(cfg); + + printf("\nTesting against Unix socket connection (%s):\n", cfg.unix.path); + cfg.type = CONN_UNIX; + test_blocking_connection(cfg); + test_blocking_io_errors(cfg); + if (throughput) test_throughput(cfg); + + if (fails) { + printf("*** %d TESTS FAILED ***\n", fails); + return 1; + } + + printf("ALL TESTS PASSED\n"); + return 0; +} diff --git a/hiredis/util.h b/hiredis/util.h new file mode 100644 index 0000000..f192990 --- /dev/null +++ b/hiredis/util.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2009-2010, Salvatore Sanfilippo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __UTIL_H +#define __UTIL_H +#include + +/* Abort on out of memory */ +static void redisOOM(void) { + fprintf(stderr,"Out of memory in hiredis"); + exit(1); +} + +#endif diff --git a/http-parser/.gitignore b/http-parser/.gitignore new file mode 100644 index 0000000..73fe6a4 --- /dev/null +++ b/http-parser/.gitignore @@ -0,0 +1,4 @@ +tags +*.o +test +test_g diff --git a/http-parser/CONTRIBUTIONS b/http-parser/CONTRIBUTIONS new file mode 100644 index 0000000..11ba31e --- /dev/null +++ b/http-parser/CONTRIBUTIONS @@ -0,0 +1,4 @@ +Contributors must agree to the Contributor License Agreement before patches +can be accepted. + +http://spreadsheets2.google.com/viewform?hl=en&formkey=dDJXOGUwbzlYaWM4cHN1MERwQS1CSnc6MQ diff --git a/http-parser/LICENSE-MIT b/http-parser/LICENSE-MIT new file mode 100644 index 0000000..f30a31d --- /dev/null +++ b/http-parser/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright 2009,2010 Ryan Dahl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/http-parser/README.md b/http-parser/README.md new file mode 100644 index 0000000..72332fb --- /dev/null +++ b/http-parser/README.md @@ -0,0 +1,171 @@ +HTTP Parser +=========== + +This is a parser for HTTP messages written in C. It parses both requests and +responses. The parser is designed to be used in performance HTTP +applications. It does not make any syscalls nor allocations, it does not +buffer data, it can be interrupted at anytime. Depending on your +architecture, it only requires about 40 bytes of data per message +stream (in a web server that is per connection). + +Features: + + * No dependencies + * Handles persistent streams (keep-alive). + * Decodes chunked encoding. + * Upgrade support + * Defends against buffer overflow attacks. + +The parser extracts the following information from HTTP messages: + + * Header fields and values + * Content-Length + * Request method + * Response status code + * Transfer-Encoding + * HTTP version + * Request path, query string, fragment + * Message body + + +Usage +----- + +One `http_parser` object is used per TCP connection. Initialize the struct +using `http_parser_init()` and set the callbacks. That might look something +like this for a request parser: + + http_parser_settings settings; + settings.on_path = my_path_callback; + settings.on_header_field = my_header_field_callback; + /* ... */ + + http_parser *parser = malloc(sizeof(http_parser)); + http_parser_init(parser, HTTP_REQUEST); + parser->data = my_socket; + +When data is received on the socket execute the parser and check for errors. + + size_t len = 80*1024, nparsed; + char buf[len]; + ssize_t recved; + + recved = recv(fd, buf, len, 0); + + if (recved < 0) { + /* Handle error. */ + } + + /* Start up / continue the parser. + * Note we pass recved==0 to signal that EOF has been recieved. + */ + nparsed = http_parser_execute(parser, &settings, buf, recved); + + if (parser->upgrade) { + /* handle new protocol */ + } else if (nparsed != recved) { + /* Handle error. Usually just close the connection. */ + } + +HTTP needs to know where the end of the stream is. For example, sometimes +servers send responses without Content-Length and expect the client to +consume input (for the body) until EOF. To tell http_parser about EOF, give +`0` as the forth parameter to `http_parser_execute()`. Callbacks and errors +can still be encountered during an EOF, so one must still be prepared +to receive them. + +Scalar valued message information such as `status_code`, `method`, and the +HTTP version are stored in the parser structure. This data is only +temporally stored in `http_parser` and gets reset on each new message. If +this information is needed later, copy it out of the structure during the +`headers_complete` callback. + +The parser decodes the transfer-encoding for both requests and responses +transparently. That is, a chunked encoding is decoded before being sent to +the on_body callback. + + +The Special Problem of Upgrade +------------------------------ + +HTTP supports upgrading the connection to a different protocol. An +increasingly common example of this is the Web Socket protocol which sends +a request like + + GET /demo HTTP/1.1 + Upgrade: WebSocket + Connection: Upgrade + Host: example.com + Origin: http://example.com + WebSocket-Protocol: sample + +followed by non-HTTP data. + +(See http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75 for more +information the Web Socket protocol.) + +To support this, the parser will treat this as a normal HTTP message without a +body. Issuing both on_headers_complete and on_message_complete callbacks. However +http_parser_execute() will stop parsing at the end of the headers and return. + +The user is expected to check if `parser->upgrade` has been set to 1 after +`http_parser_execute()` returns. Non-HTTP data begins at the buffer supplied +offset by the return value of `http_parser_execute()`. + + +Callbacks +--------- + +During the `http_parser_execute()` call, the callbacks set in +`http_parser_settings` will be executed. The parser maintains state and +never looks behind, so buffering the data is not necessary. If you need to +save certain data for later usage, you can do that from the callbacks. + +There are two types of callbacks: + +* notification `typedef int (*http_cb) (http_parser*);` + Callbacks: on_message_begin, on_headers_complete, on_message_complete. +* data `typedef int (*http_data_cb) (http_parser*, const char *at, size_t length);` + Callbacks: (requests only) on_path, on_query_string, on_uri, on_fragment, + (common) on_header_field, on_header_value, on_body; + +Callbacks must return 0 on success. Returning a non-zero value indicates +error to the parser, making it exit immediately. + +In case you parse HTTP message in chunks (i.e. `read()` request line +from socket, parse, read half headers, parse, etc) your data callbacks +may be called more than once. Http-parser guarantees that data pointer is only +valid for the lifetime of callback. You can also `read()` into a heap allocated +buffer to avoid copying memory around if this fits your application. + +Reading headers may be a tricky task if you read/parse headers partially. +Basically, you need to remember whether last header callback was field or value +and apply following logic: + + (on_header_field and on_header_value shortened to on_h_*) + ------------------------ ------------ -------------------------------------------- + | State (prev. callback) | Callback | Description/action | + ------------------------ ------------ -------------------------------------------- + | nothing (first call) | on_h_field | Allocate new buffer and copy callback data | + | | | into it | + ------------------------ ------------ -------------------------------------------- + | value | on_h_field | New header started. | + | | | Copy current name,value buffers to headers | + | | | list and allocate new buffer for new name | + ------------------------ ------------ -------------------------------------------- + | field | on_h_field | Previous name continues. Reallocate name | + | | | buffer and append callback data to it | + ------------------------ ------------ -------------------------------------------- + | field | on_h_value | Value for current header started. Allocate | + | | | new buffer and copy callback data to it | + ------------------------ ------------ -------------------------------------------- + | value | on_h_value | Value continues. Reallocate value buffer | + | | | and append callback data to it | + ------------------------ ------------ -------------------------------------------- + + +See examples of reading in headers: + +* [partial example](http://gist.github.com/155877) in C +* [from http-parser tests](http://github.com/ry/http-parser/blob/37a0ff8928fb0d83cec0d0d8909c5a4abcd221af/test.c#L403) in C +* [from Node library](http://github.com/ry/node/blob/842eaf446d2fdcb33b296c67c911c32a0dabc747/src/http.js#L284) in Javascript diff --git a/http-parser/http_parser.c b/http-parser/http_parser.c new file mode 100644 index 0000000..5a0972a --- /dev/null +++ b/http-parser/http_parser.c @@ -0,0 +1,1601 @@ +/* Copyright 2009,2010 Ryan Dahl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#include +#include +#include + + +#ifndef MIN +# define MIN(a,b) ((a) < (b) ? (a) : (b)) +#endif + + +#define CALLBACK2(FOR) \ +do { \ + if (settings->on_##FOR) { \ + if (0 != settings->on_##FOR(parser)) return (p - data); \ + } \ +} while (0) + + +#define MARK(FOR) \ +do { \ + FOR##_mark = p; \ +} while (0) + +#define CALLBACK_NOCLEAR(FOR) \ +do { \ + if (FOR##_mark) { \ + if (settings->on_##FOR) { \ + if (0 != settings->on_##FOR(parser, \ + FOR##_mark, \ + p - FOR##_mark)) \ + { \ + return (p - data); \ + } \ + } \ + } \ +} while (0) + + +#define CALLBACK(FOR) \ +do { \ + CALLBACK_NOCLEAR(FOR); \ + FOR##_mark = NULL; \ +} while (0) + + +#define PROXY_CONNECTION "proxy-connection" +#define CONNECTION "connection" +#define CONTENT_LENGTH "content-length" +#define TRANSFER_ENCODING "transfer-encoding" +#define UPGRADE "upgrade" +#define CHUNKED "chunked" +#define KEEP_ALIVE "keep-alive" +#define CLOSE "close" + + +static const char *method_strings[] = + { "DELETE" + , "GET" + , "HEAD" + , "POST" + , "PUT" + , "CONNECT" + , "OPTIONS" + , "TRACE" + , "COPY" + , "LOCK" + , "MKCOL" + , "MOVE" + , "PROPFIND" + , "PROPPATCH" + , "UNLOCK" + , "REPORT" + , "MKACTIVITY" + , "CHECKOUT" + , "MERGE" + , "M-SEARCH" + , "NOTIFY" + , "SUBSCRIBE" + , "UNSUBSCRIBE" + }; + + +/* Tokens as defined by rfc 2616. Also lowercases them. + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + */ +static const char tokens[256] = { +/* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ + ' ', '!', '"', '#', '$', '%', '&', '\'', +/* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ + 0, 0, '*', '+', 0, '-', '.', '/', +/* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ + '0', '1', '2', '3', '4', '5', '6', '7', +/* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ + '8', '9', 0, 0, 0, 0, 0, 0, +/* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ + 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', +/* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', +/* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', +/* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ + 'x', 'y', 'z', 0, 0, 0, '^', '_', +/* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ + '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', +/* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', +/* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', +/* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ + 'x', 'y', 'z', 0, '|', '}', '~', 0 }; + + +static const int8_t unhex[256] = + {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + , 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1,-1,-1,-1,-1 + ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + }; + + +static const uint8_t normal_url_char[256] = { +/* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ + 0, 1, 1, 0, 1, 1, 1, 1, +/* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ + 1, 1, 1, 1, 1, 1, 1, 1, +/* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ + 1, 1, 1, 1, 1, 1, 1, 1, +/* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ + 1, 1, 1, 1, 1, 1, 1, 0, +/* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ + 1, 1, 1, 1, 1, 1, 1, 1, +/* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ + 1, 1, 1, 1, 1, 1, 1, 1, +/* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ + 1, 1, 1, 1, 1, 1, 1, 1, +/* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ + 1, 1, 1, 1, 1, 1, 1, 1, +/* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ + 1, 1, 1, 1, 1, 1, 1, 1, +/* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ + 1, 1, 1, 1, 1, 1, 1, 1, +/* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ + 1, 1, 1, 1, 1, 1, 1, 1, +/* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ + 1, 1, 1, 1, 1, 1, 1, 0 }; + + +enum state + { s_dead = 1 /* important that this is > 0 */ + + , s_start_req_or_res + , s_res_or_resp_H + , s_start_res + , s_res_H + , s_res_HT + , s_res_HTT + , s_res_HTTP + , s_res_first_http_major + , s_res_http_major + , s_res_first_http_minor + , s_res_http_minor + , s_res_first_status_code + , s_res_status_code + , s_res_status + , s_res_line_almost_done + + , s_start_req + + , s_req_method + , s_req_spaces_before_url + , s_req_schema + , s_req_schema_slash + , s_req_schema_slash_slash + , s_req_host + , s_req_port + , s_req_path + , s_req_query_string_start + , s_req_query_string + , s_req_fragment_start + , s_req_fragment + , s_req_http_start + , s_req_http_H + , s_req_http_HT + , s_req_http_HTT + , s_req_http_HTTP + , s_req_first_http_major + , s_req_http_major + , s_req_first_http_minor + , s_req_http_minor + , s_req_line_almost_done + + , s_header_field_start + , s_header_field + , s_header_value_start + , s_header_value + + , s_header_almost_done + + , s_headers_almost_done + /* Important: 's_headers_almost_done' must be the last 'header' state. All + * states beyond this must be 'body' states. It is used for overflow + * checking. See the PARSING_HEADER() macro. + */ + , s_chunk_size_start + , s_chunk_size + , s_chunk_size_almost_done + , s_chunk_parameters + , s_chunk_data + , s_chunk_data_almost_done + , s_chunk_data_done + + , s_body_identity + , s_body_identity_eof + }; + + +#define PARSING_HEADER(state) (state <= s_headers_almost_done && 0 == (parser->flags & F_TRAILING)) + + +enum header_states + { h_general = 0 + , h_C + , h_CO + , h_CON + + , h_matching_connection + , h_matching_proxy_connection + , h_matching_content_length + , h_matching_transfer_encoding + , h_matching_upgrade + + , h_connection + , h_content_length + , h_transfer_encoding + , h_upgrade + + , h_matching_transfer_encoding_chunked + , h_matching_connection_keep_alive + , h_matching_connection_close + + , h_transfer_encoding_chunked + , h_connection_keep_alive + , h_connection_close + }; + + +enum flags + { F_CHUNKED = 1 << 0 + , F_CONNECTION_KEEP_ALIVE = 1 << 1 + , F_CONNECTION_CLOSE = 1 << 2 + , F_TRAILING = 1 << 3 + , F_UPGRADE = 1 << 4 + , F_SKIPBODY = 1 << 5 + }; + + +#define CR '\r' +#define LF '\n' +#define LOWER(c) (unsigned char)(c | 0x20) +#define TOKEN(c) tokens[(unsigned char)c] + + +#define start_state (parser->type == HTTP_REQUEST ? s_start_req : s_start_res) + + +#if HTTP_PARSER_STRICT +# define STRICT_CHECK(cond) if (cond) goto error +# define NEW_MESSAGE() (http_should_keep_alive(parser) ? start_state : s_dead) +#else +# define STRICT_CHECK(cond) +# define NEW_MESSAGE() start_state +#endif + + +size_t http_parser_execute (http_parser *parser, + const http_parser_settings *settings, + const char *data, + size_t len) +{ + char c, ch; + const char *p = data, *pe; + int64_t to_read; + + enum state state = (enum state) parser->state; + enum header_states header_state = (enum header_states) parser->header_state; + uint64_t index = parser->index; + uint64_t nread = parser->nread; + + if (len == 0) { + if (state == s_body_identity_eof) { + CALLBACK2(message_complete); + } + return 0; + } + + /* technically we could combine all of these (except for url_mark) into one + variable, saving stack space, but it seems more clear to have them + separated. */ + const char *header_field_mark = 0; + const char *header_value_mark = 0; + const char *fragment_mark = 0; + const char *query_string_mark = 0; + const char *path_mark = 0; + const char *url_mark = 0; + + if (state == s_header_field) + header_field_mark = data; + if (state == s_header_value) + header_value_mark = data; + if (state == s_req_fragment) + fragment_mark = data; + if (state == s_req_query_string) + query_string_mark = data; + if (state == s_req_path) + path_mark = data; + if (state == s_req_path || state == s_req_schema || state == s_req_schema_slash + || state == s_req_schema_slash_slash || state == s_req_port + || state == s_req_query_string_start || state == s_req_query_string + || state == s_req_host + || state == s_req_fragment_start || state == s_req_fragment) + url_mark = data; + + for (p=data, pe=data+len; p != pe; p++) { + ch = *p; + + if (PARSING_HEADER(state)) { + ++nread; + /* Buffer overflow attack */ + if (nread > HTTP_MAX_HEADER_SIZE) goto error; + } + + switch (state) { + + case s_dead: + /* this state is used after a 'Connection: close' message + * the parser will error out if it reads another message + */ + goto error; + + case s_start_req_or_res: + { + if (ch == CR || ch == LF) + break; + parser->flags = 0; + parser->content_length = -1; + + CALLBACK2(message_begin); + + if (ch == 'H') + state = s_res_or_resp_H; + else { + parser->type = HTTP_REQUEST; + goto start_req_method_assign; + } + break; + } + + case s_res_or_resp_H: + if (ch == 'T') { + parser->type = HTTP_RESPONSE; + state = s_res_HT; + } else { + if (ch != 'E') goto error; + parser->type = HTTP_REQUEST; + parser->method = HTTP_HEAD; + index = 2; + state = s_req_method; + } + break; + + case s_start_res: + { + parser->flags = 0; + parser->content_length = -1; + + CALLBACK2(message_begin); + + switch (ch) { + case 'H': + state = s_res_H; + break; + + case CR: + case LF: + break; + + default: + goto error; + } + break; + } + + case s_res_H: + STRICT_CHECK(ch != 'T'); + state = s_res_HT; + break; + + case s_res_HT: + STRICT_CHECK(ch != 'T'); + state = s_res_HTT; + break; + + case s_res_HTT: + STRICT_CHECK(ch != 'P'); + state = s_res_HTTP; + break; + + case s_res_HTTP: + STRICT_CHECK(ch != '/'); + state = s_res_first_http_major; + break; + + case s_res_first_http_major: + if (ch < '1' || ch > '9') goto error; + parser->http_major = ch - '0'; + state = s_res_http_major; + break; + + /* major HTTP version or dot */ + case s_res_http_major: + { + if (ch == '.') { + state = s_res_first_http_minor; + break; + } + + if (ch < '0' || ch > '9') goto error; + + parser->http_major *= 10; + parser->http_major += ch - '0'; + + if (parser->http_major > 999) goto error; + break; + } + + /* first digit of minor HTTP version */ + case s_res_first_http_minor: + if (ch < '0' || ch > '9') goto error; + parser->http_minor = ch - '0'; + state = s_res_http_minor; + break; + + /* minor HTTP version or end of request line */ + case s_res_http_minor: + { + if (ch == ' ') { + state = s_res_first_status_code; + break; + } + + if (ch < '0' || ch > '9') goto error; + + parser->http_minor *= 10; + parser->http_minor += ch - '0'; + + if (parser->http_minor > 999) goto error; + break; + } + + case s_res_first_status_code: + { + if (ch < '0' || ch > '9') { + if (ch == ' ') { + break; + } + goto error; + } + parser->status_code = ch - '0'; + state = s_res_status_code; + break; + } + + case s_res_status_code: + { + if (ch < '0' || ch > '9') { + switch (ch) { + case ' ': + state = s_res_status; + break; + case CR: + state = s_res_line_almost_done; + break; + case LF: + state = s_header_field_start; + break; + default: + goto error; + } + break; + } + + parser->status_code *= 10; + parser->status_code += ch - '0'; + + if (parser->status_code > 999) goto error; + break; + } + + case s_res_status: + /* the human readable status. e.g. "NOT FOUND" + * we are not humans so just ignore this */ + if (ch == CR) { + state = s_res_line_almost_done; + break; + } + + if (ch == LF) { + state = s_header_field_start; + break; + } + break; + + case s_res_line_almost_done: + STRICT_CHECK(ch != LF); + state = s_header_field_start; + break; + + case s_start_req: + { + if (ch == CR || ch == LF) + break; + parser->flags = 0; + parser->content_length = -1; + + CALLBACK2(message_begin); + + if (ch < 'A' || 'Z' < ch) goto error; + + start_req_method_assign: + parser->method = (enum http_method) 0; + index = 1; + switch (ch) { + case 'C': parser->method = HTTP_CONNECT; /* or COPY, CHECKOUT */ break; + case 'D': parser->method = HTTP_DELETE; break; + case 'G': parser->method = HTTP_GET; break; + case 'H': parser->method = HTTP_HEAD; break; + case 'L': parser->method = HTTP_LOCK; break; + case 'M': parser->method = HTTP_MKCOL; /* or MOVE, MKACTIVITY, MERGE, M-SEARCH */ break; + case 'N': parser->method = HTTP_NOTIFY; break; + case 'O': parser->method = HTTP_OPTIONS; break; + case 'P': parser->method = HTTP_POST; /* or PROPFIND or PROPPATCH or PUT */ break; + case 'R': parser->method = HTTP_REPORT; break; + case 'S': parser->method = HTTP_SUBSCRIBE; break; + case 'T': parser->method = HTTP_TRACE; break; + case 'U': parser->method = HTTP_UNLOCK; /* or UNSUBSCRIBE */ break; + default: goto error; + } + state = s_req_method; + break; + } + + case s_req_method: + { + if (ch == '\0') + goto error; + + const char *matcher = method_strings[parser->method]; + if (ch == ' ' && matcher[index] == '\0') { + state = s_req_spaces_before_url; + } else if (ch == matcher[index]) { + ; /* nada */ + } else if (parser->method == HTTP_CONNECT) { + if (index == 1 && ch == 'H') { + parser->method = HTTP_CHECKOUT; + } else if (index == 2 && ch == 'P') { + parser->method = HTTP_COPY; + } + } else if (parser->method == HTTP_MKCOL) { + if (index == 1 && ch == 'O') { + parser->method = HTTP_MOVE; + } else if (index == 1 && ch == 'E') { + parser->method = HTTP_MERGE; + } else if (index == 1 && ch == '-') { + parser->method = HTTP_MSEARCH; + } else if (index == 2 && ch == 'A') { + parser->method = HTTP_MKACTIVITY; + } + } else if (index == 1 && parser->method == HTTP_POST && ch == 'R') { + parser->method = HTTP_PROPFIND; /* or HTTP_PROPPATCH */ + } else if (index == 1 && parser->method == HTTP_POST && ch == 'U') { + parser->method = HTTP_PUT; + } else if (index == 2 && parser->method == HTTP_UNLOCK && ch == 'S') { + parser->method = HTTP_UNSUBSCRIBE; + } else if (index == 4 && parser->method == HTTP_PROPFIND && ch == 'P') { + parser->method = HTTP_PROPPATCH; + } else { + goto error; + } + + ++index; + break; + } + case s_req_spaces_before_url: + { + if (ch == ' ') break; + + if (ch == '/' || ch == '*') { + MARK(url); + MARK(path); + state = s_req_path; + break; + } + + c = LOWER(ch); + + if (c >= 'a' && c <= 'z') { + MARK(url); + state = s_req_schema; + break; + } + + goto error; + } + + case s_req_schema: + { + c = LOWER(ch); + + if (c >= 'a' && c <= 'z') break; + + if (ch == ':') { + state = s_req_schema_slash; + break; + } else if (ch == '.') { + state = s_req_host; + break; + } else if ('0' <= ch && ch <= '9') { + state = s_req_host; + break; + } + + goto error; + } + + case s_req_schema_slash: + STRICT_CHECK(ch != '/'); + state = s_req_schema_slash_slash; + break; + + case s_req_schema_slash_slash: + STRICT_CHECK(ch != '/'); + state = s_req_host; + break; + + case s_req_host: + { + c = LOWER(ch); + if (c >= 'a' && c <= 'z') break; + if ((ch >= '0' && ch <= '9') || ch == '.' || ch == '-') break; + switch (ch) { + case ':': + state = s_req_port; + break; + case '/': + MARK(path); + state = s_req_path; + break; + case ' ': + /* The request line looks like: + * "GET http://foo.bar.com HTTP/1.1" + * That is, there is no path. + */ + CALLBACK(url); + state = s_req_http_start; + break; + default: + goto error; + } + break; + } + + case s_req_port: + { + if (ch >= '0' && ch <= '9') break; + switch (ch) { + case '/': + MARK(path); + state = s_req_path; + break; + case ' ': + /* The request line looks like: + * "GET http://foo.bar.com:1234 HTTP/1.1" + * That is, there is no path. + */ + CALLBACK(url); + state = s_req_http_start; + break; + default: + goto error; + } + break; + } + + case s_req_path: + { + if (normal_url_char[(unsigned char)ch]) break; + + switch (ch) { + case ' ': + CALLBACK(url); + CALLBACK(path); + state = s_req_http_start; + break; + case CR: + CALLBACK(url); + CALLBACK(path); + parser->http_major = 0; + parser->http_minor = 9; + state = s_req_line_almost_done; + break; + case LF: + CALLBACK(url); + CALLBACK(path); + parser->http_major = 0; + parser->http_minor = 9; + state = s_header_field_start; + break; + case '?': + CALLBACK(path); + state = s_req_query_string_start; + break; + case '#': + CALLBACK(path); + state = s_req_fragment_start; + break; + default: + goto error; + } + break; + } + + case s_req_query_string_start: + { + if (normal_url_char[(unsigned char)ch]) { + MARK(query_string); + state = s_req_query_string; + break; + } + + switch (ch) { + case '?': + break; /* XXX ignore extra '?' ... is this right? */ + case ' ': + CALLBACK(url); + state = s_req_http_start; + break; + case CR: + CALLBACK(url); + parser->http_major = 0; + parser->http_minor = 9; + state = s_req_line_almost_done; + break; + case LF: + CALLBACK(url); + parser->http_major = 0; + parser->http_minor = 9; + state = s_header_field_start; + break; + case '#': + state = s_req_fragment_start; + break; + default: + goto error; + } + break; + } + + case s_req_query_string: + { + if (normal_url_char[(unsigned char)ch]) break; + + switch (ch) { + case '?': + /* allow extra '?' in query string */ + break; + case ' ': + CALLBACK(url); + CALLBACK(query_string); + state = s_req_http_start; + break; + case CR: + CALLBACK(url); + CALLBACK(query_string); + parser->http_major = 0; + parser->http_minor = 9; + state = s_req_line_almost_done; + break; + case LF: + CALLBACK(url); + CALLBACK(query_string); + parser->http_major = 0; + parser->http_minor = 9; + state = s_header_field_start; + break; + case '#': + CALLBACK(query_string); + state = s_req_fragment_start; + break; + default: + goto error; + } + break; + } + + case s_req_fragment_start: + { + if (normal_url_char[(unsigned char)ch]) { + MARK(fragment); + state = s_req_fragment; + break; + } + + switch (ch) { + case ' ': + CALLBACK(url); + state = s_req_http_start; + break; + case CR: + CALLBACK(url); + parser->http_major = 0; + parser->http_minor = 9; + state = s_req_line_almost_done; + break; + case LF: + CALLBACK(url); + parser->http_major = 0; + parser->http_minor = 9; + state = s_header_field_start; + break; + case '?': + MARK(fragment); + state = s_req_fragment; + break; + case '#': + break; + default: + goto error; + } + break; + } + + case s_req_fragment: + { + if (normal_url_char[(unsigned char)ch]) break; + + switch (ch) { + case ' ': + CALLBACK(url); + CALLBACK(fragment); + state = s_req_http_start; + break; + case CR: + CALLBACK(url); + CALLBACK(fragment); + parser->http_major = 0; + parser->http_minor = 9; + state = s_req_line_almost_done; + break; + case LF: + CALLBACK(url); + CALLBACK(fragment); + parser->http_major = 0; + parser->http_minor = 9; + state = s_header_field_start; + break; + case '?': + case '#': + break; + default: + goto error; + } + break; + } + + case s_req_http_start: + switch (ch) { + case 'H': + state = s_req_http_H; + break; + case ' ': + break; + default: + goto error; + } + break; + + case s_req_http_H: + STRICT_CHECK(ch != 'T'); + state = s_req_http_HT; + break; + + case s_req_http_HT: + STRICT_CHECK(ch != 'T'); + state = s_req_http_HTT; + break; + + case s_req_http_HTT: + STRICT_CHECK(ch != 'P'); + state = s_req_http_HTTP; + break; + + case s_req_http_HTTP: + STRICT_CHECK(ch != '/'); + state = s_req_first_http_major; + break; + + /* first digit of major HTTP version */ + case s_req_first_http_major: + if (ch < '1' || ch > '9') goto error; + parser->http_major = ch - '0'; + state = s_req_http_major; + break; + + /* major HTTP version or dot */ + case s_req_http_major: + { + if (ch == '.') { + state = s_req_first_http_minor; + break; + } + + if (ch < '0' || ch > '9') goto error; + + parser->http_major *= 10; + parser->http_major += ch - '0'; + + if (parser->http_major > 999) goto error; + break; + } + + /* first digit of minor HTTP version */ + case s_req_first_http_minor: + if (ch < '0' || ch > '9') goto error; + parser->http_minor = ch - '0'; + state = s_req_http_minor; + break; + + /* minor HTTP version or end of request line */ + case s_req_http_minor: + { + if (ch == CR) { + state = s_req_line_almost_done; + break; + } + + if (ch == LF) { + state = s_header_field_start; + break; + } + + /* XXX allow spaces after digit? */ + + if (ch < '0' || ch > '9') goto error; + + parser->http_minor *= 10; + parser->http_minor += ch - '0'; + + if (parser->http_minor > 999) goto error; + break; + } + + /* end of request line */ + case s_req_line_almost_done: + { + if (ch != LF) goto error; + state = s_header_field_start; + break; + } + + case s_header_field_start: + { + if (ch == CR) { + state = s_headers_almost_done; + break; + } + + if (ch == LF) { + /* they might be just sending \n instead of \r\n so this would be + * the second \n to denote the end of headers*/ + state = s_headers_almost_done; + goto headers_almost_done; + } + + c = TOKEN(ch); + + if (!c) goto error; + + MARK(header_field); + + index = 0; + state = s_header_field; + + switch (c) { + case 'c': + header_state = h_C; + break; + + case 'p': + header_state = h_matching_proxy_connection; + break; + + case 't': + header_state = h_matching_transfer_encoding; + break; + + case 'u': + header_state = h_matching_upgrade; + break; + + default: + header_state = h_general; + break; + } + break; + } + + case s_header_field: + { + c = TOKEN(ch); + + if (c) { + switch (header_state) { + case h_general: + break; + + case h_C: + index++; + header_state = (c == 'o' ? h_CO : h_general); + break; + + case h_CO: + index++; + header_state = (c == 'n' ? h_CON : h_general); + break; + + case h_CON: + index++; + switch (c) { + case 'n': + header_state = h_matching_connection; + break; + case 't': + header_state = h_matching_content_length; + break; + default: + header_state = h_general; + break; + } + break; + + /* connection */ + + case h_matching_connection: + index++; + if (index > sizeof(CONNECTION)-1 + || c != CONNECTION[index]) { + header_state = h_general; + } else if (index == sizeof(CONNECTION)-2) { + header_state = h_connection; + } + break; + + /* proxy-connection */ + + case h_matching_proxy_connection: + index++; + if (index > sizeof(PROXY_CONNECTION)-1 + || c != PROXY_CONNECTION[index]) { + header_state = h_general; + } else if (index == sizeof(PROXY_CONNECTION)-2) { + header_state = h_connection; + } + break; + + /* content-length */ + + case h_matching_content_length: + index++; + if (index > sizeof(CONTENT_LENGTH)-1 + || c != CONTENT_LENGTH[index]) { + header_state = h_general; + } else if (index == sizeof(CONTENT_LENGTH)-2) { + header_state = h_content_length; + } + break; + + /* transfer-encoding */ + + case h_matching_transfer_encoding: + index++; + if (index > sizeof(TRANSFER_ENCODING)-1 + || c != TRANSFER_ENCODING[index]) { + header_state = h_general; + } else if (index == sizeof(TRANSFER_ENCODING)-2) { + header_state = h_transfer_encoding; + } + break; + + /* upgrade */ + + case h_matching_upgrade: + index++; + if (index > sizeof(UPGRADE)-1 + || c != UPGRADE[index]) { + header_state = h_general; + } else if (index == sizeof(UPGRADE)-2) { + header_state = h_upgrade; + } + break; + + case h_connection: + case h_content_length: + case h_transfer_encoding: + case h_upgrade: + if (ch != ' ') header_state = h_general; + break; + + default: + assert(0 && "Unknown header_state"); + break; + } + break; + } + + if (ch == ':') { + CALLBACK(header_field); + state = s_header_value_start; + break; + } + + if (ch == CR) { + state = s_header_almost_done; + CALLBACK(header_field); + break; + } + + if (ch == LF) { + CALLBACK(header_field); + state = s_header_field_start; + break; + } + + goto error; + } + + case s_header_value_start: + { + if (ch == ' ') break; + + MARK(header_value); + + state = s_header_value; + index = 0; + + c = LOWER(ch); + + if (ch == CR) { + CALLBACK(header_value); + header_state = h_general; + state = s_header_almost_done; + break; + } + + if (ch == LF) { + CALLBACK(header_value); + state = s_header_field_start; + break; + } + + switch (header_state) { + case h_upgrade: + parser->flags |= F_UPGRADE; + header_state = h_general; + break; + + case h_transfer_encoding: + /* looking for 'Transfer-Encoding: chunked' */ + if ('c' == c) { + header_state = h_matching_transfer_encoding_chunked; + } else { + header_state = h_general; + } + break; + + case h_content_length: + if (ch < '0' || ch > '9') goto error; + parser->content_length = ch - '0'; + break; + + case h_connection: + /* looking for 'Connection: keep-alive' */ + if (c == 'k') { + header_state = h_matching_connection_keep_alive; + /* looking for 'Connection: close' */ + } else if (c == 'c') { + header_state = h_matching_connection_close; + } else { + header_state = h_general; + } + break; + + default: + header_state = h_general; + break; + } + break; + } + + case s_header_value: + { + c = LOWER(ch); + + if (ch == CR) { + CALLBACK(header_value); + state = s_header_almost_done; + break; + } + + if (ch == LF) { + CALLBACK(header_value); + goto header_almost_done; + } + + switch (header_state) { + case h_general: + break; + + case h_connection: + case h_transfer_encoding: + assert(0 && "Shouldn't get here."); + break; + + case h_content_length: + if (ch == ' ') break; + if (ch < '0' || ch > '9') goto error; + parser->content_length *= 10; + parser->content_length += ch - '0'; + break; + + /* Transfer-Encoding: chunked */ + case h_matching_transfer_encoding_chunked: + index++; + if (index > sizeof(CHUNKED)-1 + || c != CHUNKED[index]) { + header_state = h_general; + } else if (index == sizeof(CHUNKED)-2) { + header_state = h_transfer_encoding_chunked; + } + break; + + /* looking for 'Connection: keep-alive' */ + case h_matching_connection_keep_alive: + index++; + if (index > sizeof(KEEP_ALIVE)-1 + || c != KEEP_ALIVE[index]) { + header_state = h_general; + } else if (index == sizeof(KEEP_ALIVE)-2) { + header_state = h_connection_keep_alive; + } + break; + + /* looking for 'Connection: close' */ + case h_matching_connection_close: + index++; + if (index > sizeof(CLOSE)-1 || c != CLOSE[index]) { + header_state = h_general; + } else if (index == sizeof(CLOSE)-2) { + header_state = h_connection_close; + } + break; + + case h_transfer_encoding_chunked: + case h_connection_keep_alive: + case h_connection_close: + if (ch != ' ') header_state = h_general; + break; + + default: + state = s_header_value; + header_state = h_general; + break; + } + break; + } + + case s_header_almost_done: + header_almost_done: + { + STRICT_CHECK(ch != LF); + + state = s_header_field_start; + + switch (header_state) { + case h_connection_keep_alive: + parser->flags |= F_CONNECTION_KEEP_ALIVE; + break; + case h_connection_close: + parser->flags |= F_CONNECTION_CLOSE; + break; + case h_transfer_encoding_chunked: + parser->flags |= F_CHUNKED; + break; + default: + break; + } + break; + } + + case s_headers_almost_done: + headers_almost_done: + { + STRICT_CHECK(ch != LF); + + if (parser->flags & F_TRAILING) { + /* End of a chunked request */ + CALLBACK2(message_complete); + state = NEW_MESSAGE(); + break; + } + + nread = 0; + + if (parser->flags & F_UPGRADE || parser->method == HTTP_CONNECT) { + parser->upgrade = 1; + } + + /* Here we call the headers_complete callback. This is somewhat + * different than other callbacks because if the user returns 1, we + * will interpret that as saying that this message has no body. This + * is needed for the annoying case of recieving a response to a HEAD + * request. + */ + if (settings->on_headers_complete) { + switch (settings->on_headers_complete(parser)) { + case 0: + break; + + case 1: + parser->flags |= F_SKIPBODY; + break; + + default: + return p - data; /* Error */ + } + } + + /* Exit, the rest of the connect is in a different protocol. */ + if (parser->upgrade) { + CALLBACK2(message_complete); + return (p - data); + } + + if (parser->flags & F_SKIPBODY) { + CALLBACK2(message_complete); + state = NEW_MESSAGE(); + } else if (parser->flags & F_CHUNKED) { + /* chunked encoding - ignore Content-Length header */ + state = s_chunk_size_start; + } else { + if (parser->content_length == 0) { + /* Content-Length header given but zero: Content-Length: 0\r\n */ + CALLBACK2(message_complete); + state = NEW_MESSAGE(); + } else if (parser->content_length > 0) { + /* Content-Length header given and non-zero */ + state = s_body_identity; + } else { + if (parser->type == HTTP_REQUEST || http_should_keep_alive(parser)) { + /* Assume content-length 0 - read the next */ + CALLBACK2(message_complete); + state = NEW_MESSAGE(); + } else { + /* Read body until EOF */ + state = s_body_identity_eof; + } + } + } + + break; + } + + case s_body_identity: + to_read = MIN(pe - p, (int64_t)parser->content_length); + if (to_read > 0) { + if (settings->on_body) settings->on_body(parser, p, to_read); + p += to_read - 1; + parser->content_length -= to_read; + if (parser->content_length == 0) { + CALLBACK2(message_complete); + state = NEW_MESSAGE(); + } + } + break; + + /* read until EOF */ + case s_body_identity_eof: + to_read = pe - p; + if (to_read > 0) { + if (settings->on_body) settings->on_body(parser, p, to_read); + p += to_read - 1; + } + break; + + case s_chunk_size_start: + { + assert(parser->flags & F_CHUNKED); + + c = unhex[(unsigned char)ch]; + if (c == -1) goto error; + parser->content_length = c; + state = s_chunk_size; + break; + } + + case s_chunk_size: + { + assert(parser->flags & F_CHUNKED); + + if (ch == CR) { + state = s_chunk_size_almost_done; + break; + } + + c = unhex[(unsigned char)ch]; + + if (c == -1) { + if (ch == ';' || ch == ' ') { + state = s_chunk_parameters; + break; + } + goto error; + } + + parser->content_length *= 16; + parser->content_length += c; + break; + } + + case s_chunk_parameters: + { + assert(parser->flags & F_CHUNKED); + /* just ignore this shit. TODO check for overflow */ + if (ch == CR) { + state = s_chunk_size_almost_done; + break; + } + break; + } + + case s_chunk_size_almost_done: + { + assert(parser->flags & F_CHUNKED); + STRICT_CHECK(ch != LF); + + if (parser->content_length == 0) { + parser->flags |= F_TRAILING; + state = s_header_field_start; + } else { + state = s_chunk_data; + } + break; + } + + case s_chunk_data: + { + assert(parser->flags & F_CHUNKED); + + to_read = MIN(pe - p, (int64_t)(parser->content_length)); + + if (to_read > 0) { + if (settings->on_body) settings->on_body(parser, p, to_read); + p += to_read - 1; + } + + if (to_read == parser->content_length) { + state = s_chunk_data_almost_done; + } + + parser->content_length -= to_read; + break; + } + + case s_chunk_data_almost_done: + assert(parser->flags & F_CHUNKED); + STRICT_CHECK(ch != CR); + state = s_chunk_data_done; + break; + + case s_chunk_data_done: + assert(parser->flags & F_CHUNKED); + STRICT_CHECK(ch != LF); + state = s_chunk_size_start; + break; + + default: + assert(0 && "unhandled state"); + goto error; + } + } + + CALLBACK_NOCLEAR(header_field); + CALLBACK_NOCLEAR(header_value); + CALLBACK_NOCLEAR(fragment); + CALLBACK_NOCLEAR(query_string); + CALLBACK_NOCLEAR(path); + CALLBACK_NOCLEAR(url); + + parser->state = state; + parser->header_state = header_state; + parser->index = index; + parser->nread = nread; + + return len; + +error: + parser->state = s_dead; + return (p - data); +} + + +int +http_should_keep_alive (http_parser *parser) +{ + if (parser->http_major > 0 && parser->http_minor > 0) { + /* HTTP/1.1 */ + if (parser->flags & F_CONNECTION_CLOSE) { + return 0; + } else { + return 1; + } + } else { + /* HTTP/1.0 or earlier */ + if (parser->flags & F_CONNECTION_KEEP_ALIVE) { + return 1; + } else { + return 0; + } + } +} + + +const char * http_method_str (enum http_method m) +{ + return method_strings[m]; +} + + +void +http_parser_init (http_parser *parser, enum http_parser_type t) +{ + parser->type = t; + parser->state = (t == HTTP_REQUEST ? s_start_req : (t == HTTP_RESPONSE ? s_start_res : s_start_req_or_res)); + parser->nread = 0; + parser->upgrade = 0; + parser->flags = 0; + parser->method = 0; +} diff --git a/http-parser/http_parser.h b/http-parser/http_parser.h new file mode 100644 index 0000000..c03ec05 --- /dev/null +++ b/http-parser/http_parser.h @@ -0,0 +1,181 @@ +/* Copyright 2009,2010 Ryan Dahl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#ifndef http_parser_h +#define http_parser_h +#ifdef __cplusplus +extern "C" { +#endif + + +#include +#if defined(_WIN32) && !defined(__MINGW32__) +typedef __int8 int8_t; +typedef unsigned __int8 uint8_t; +typedef __int16 int16_t; +typedef unsigned __int16 uint16_t; +typedef __int32 int32_t; +typedef unsigned __int32 uint32_t; +typedef __int64 int64_t; +typedef unsigned __int64 uint64_t; + +typedef unsigned int size_t; +typedef int ssize_t; +#else +#include +#endif + +/* Compile with -DHTTP_PARSER_STRICT=0 to make less checks, but run + * faster + */ +#ifndef HTTP_PARSER_STRICT +# define HTTP_PARSER_STRICT 1 +#else +# define HTTP_PARSER_STRICT 0 +#endif + + +/* Maximium header size allowed */ +#define HTTP_MAX_HEADER_SIZE (80*1024) + + +typedef struct http_parser http_parser; +typedef struct http_parser_settings http_parser_settings; + + +/* Callbacks should return non-zero to indicate an error. The parser will + * then halt execution. + * + * The one exception is on_headers_complete. In a HTTP_RESPONSE parser + * returning '1' from on_headers_complete will tell the parser that it + * should not expect a body. This is used when receiving a response to a + * HEAD request which may contain 'Content-Length' or 'Transfer-Encoding: + * chunked' headers that indicate the presence of a body. + * + * http_data_cb does not return data chunks. It will be call arbitrarally + * many times for each string. E.G. you might get 10 callbacks for "on_path" + * each providing just a few characters more data. + */ +typedef int (*http_data_cb) (http_parser*, const char *at, size_t length); +typedef int (*http_cb) (http_parser*); + + +/* Request Methods */ +enum http_method + { HTTP_DELETE = 0 + , HTTP_GET + , HTTP_HEAD + , HTTP_POST + , HTTP_PUT + /* pathological */ + , HTTP_CONNECT + , HTTP_OPTIONS + , HTTP_TRACE + /* webdav */ + , HTTP_COPY + , HTTP_LOCK + , HTTP_MKCOL + , HTTP_MOVE + , HTTP_PROPFIND + , HTTP_PROPPATCH + , HTTP_UNLOCK + /* subversion */ + , HTTP_REPORT + , HTTP_MKACTIVITY + , HTTP_CHECKOUT + , HTTP_MERGE + /* upnp */ + , HTTP_MSEARCH + , HTTP_NOTIFY + , HTTP_SUBSCRIBE + , HTTP_UNSUBSCRIBE + }; + + +enum http_parser_type { HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH }; + + +struct http_parser { + /** PRIVATE **/ + unsigned char type : 2; + unsigned char flags : 6; + unsigned char state; + unsigned char header_state; + unsigned char index; + + uint32_t nread; + int64_t content_length; + + /** READ-ONLY **/ + unsigned short http_major; + unsigned short http_minor; + unsigned short status_code; /* responses only */ + unsigned char method; /* requests only */ + + /* 1 = Upgrade header was present and the parser has exited because of that. + * 0 = No upgrade header present. + * Should be checked when http_parser_execute() returns in addition to + * error checking. + */ + char upgrade; + + /** PUBLIC **/ + void *data; /* A pointer to get hook to the "connection" or "socket" object */ +}; + + +struct http_parser_settings { + http_cb on_message_begin; + http_data_cb on_path; + http_data_cb on_query_string; + http_data_cb on_url; + http_data_cb on_fragment; + http_data_cb on_header_field; + http_data_cb on_header_value; + http_cb on_headers_complete; + http_data_cb on_body; + http_cb on_message_complete; +}; + + +void http_parser_init(http_parser *parser, enum http_parser_type type); + + +size_t http_parser_execute(http_parser *parser, + const http_parser_settings *settings, + const char *data, + size_t len); + + +/* If http_should_keep_alive() in the on_headers_complete or + * on_message_complete callback returns true, then this will be should be + * the last message on the connection. + * If you are the server, respond with the "Connection: close" header. + * If you are the client, close the connection. + */ +int http_should_keep_alive(http_parser *parser); + +/* Returns a string version of the HTTP method. */ +const char *http_method_str(enum http_method); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/http.c b/http.c new file mode 100644 index 0000000..5bd3143 --- /dev/null +++ b/http.c @@ -0,0 +1,330 @@ +#include "http.h" +#include "server.h" +#include "worker.h" +#include "client.h" + +#include +#include +#include +#include + +/* HTTP Response */ + +struct http_response * +http_response_init(struct worker *w, int code, const char *msg) { + + /* create object */ + struct http_response *r = calloc(1, sizeof(struct http_response)); + + r->code = code; + r->msg = msg; + r->w = w; + r->keep_alive = 0; /* default */ + + http_response_set_header(r, "Server", "Webdis"); + + /* Cross-Origin Resource Sharing, CORS. */ + http_response_set_header(r, "Allow", "GET,POST,PUT,OPTIONS"); + /* + Chrome doesn't support Allow and requires + Access-Control-Allow-Methods + */ + http_response_set_header(r, "Access-Control-Allow-Methods", "GET,POST,PUT,OPTIONS"); + http_response_set_header(r, "Access-Control-Allow-Origin", "*"); + /* + According to + http://www.w3.org/TR/cors/#access-control-allow-headers-response-header + Access-Control-Allow-Headers cannot be a wildcard and must be set + with explicit names + */ + http_response_set_header(r, "Access-Control-Allow-Headers", "X-Requested-With, Content-Type"); + + return r; +} + + +void +http_response_set_header(struct http_response *r, const char *k, const char *v) { + + int i, pos = r->header_count; + size_t key_sz = strlen(k); + size_t val_sz = strlen(v); + + for(i = 0; i < r->header_count; ++i) { + if(strncmp(r->headers[i].key, k, key_sz) == 0) { + pos = i; + /* free old value before replacing it. */ + free(r->headers[i].key); + free(r->headers[i].val); + break; + } + } + + /* extend array */ + if(pos == r->header_count) { + r->headers = realloc(r->headers, + sizeof(struct http_header)*(r->header_count + 1)); + r->header_count++; + } + + /* copy key */ + r->headers[pos].key = calloc(key_sz + 1, 1); + memcpy(r->headers[pos].key, k, key_sz); + r->headers[pos].key_sz = key_sz; + + /* copy val */ + r->headers[pos].val = calloc(val_sz + 1, 1); + memcpy(r->headers[pos].val, v, val_sz); + r->headers[pos].val_sz = val_sz; + + if(!r->chunked && !strcmp(k, "Transfer-Encoding") && !strcmp(v, "chunked")) { + r->chunked = 1; + } +} + +void +http_response_set_body(struct http_response *r, const char *body, size_t body_len) { + + r->body = body; + r->body_len = body_len; +} + +static void +http_response_cleanup(struct http_response *r, int fd, int success) { + + int i; + + /* cleanup buffer */ + free(r->out); + if(!r->keep_alive || !success) { + /* Close fd is client doesn't support Keep-Alive. */ + close(fd); + } + + /* cleanup response object */ + for(i = 0; i < r->header_count; ++i) { + free(r->headers[i].key); + free(r->headers[i].val); + } + free(r->headers); + + free(r); +} + +static void +http_can_write(int fd, short event, void *p) { + + int ret; + struct http_response *r = p; + + (void)event; + + ret = write(fd, r->out + r->sent, r->out_sz - r->sent); + + if(ret > 0) + r->sent += ret; + + if(ret <= 0 || r->out_sz - r->sent == 0) { /* error or done */ + http_response_cleanup(r, fd, (int)r->out_sz == r->sent ? 1 : 0); + } else { /* reschedule write */ + http_schedule_write(fd, r); + } +} + +void +http_schedule_write(int fd, struct http_response *r) { + + if(r->w) { /* async */ + event_set(&r->ev, fd, EV_WRITE, http_can_write, r); + event_base_set(r->w->base, &r->ev); + event_add(&r->ev, NULL); + } else { /* blocking */ + http_can_write(fd, 0, r); + } + +} + +static char * +format_chunk(const char *p, size_t sz, size_t *out_sz) { + + char *out, tmp[64]; + int chunk_size; + + /* calculate format size */ + chunk_size = sprintf(tmp, "%x\r\n", (int)sz); + + *out_sz = chunk_size + sz + 2; + out = malloc(*out_sz); + memcpy(out, tmp, chunk_size); + memcpy(out + chunk_size, p, sz); + memcpy(out + chunk_size + sz, "\r\n", 2); + + return out; +} + +void +http_response_write(struct http_response *r, int fd) { + + char *p; + int i, ret; + + /*r->keep_alive = 0;*/ + r->out_sz = sizeof("HTTP/1.x xxx ")-1 + strlen(r->msg) + 2; + r->out = calloc(r->out_sz + 1, 1); + + ret = sprintf(r->out, "HTTP/1.%d %d %s\r\n", (r->http_version?1:0), r->code, r->msg); + (void)ret; + p = r->out; + + if(!r->chunked) { + if(r->code == 200 && r->body) { + char content_length[10]; + sprintf(content_length, "%zd", r->body_len); + http_response_set_header(r, "Content-Length", content_length); + } else { + http_response_set_header(r, "Content-Length", "0"); + } + } + + for(i = 0; i < r->header_count; ++i) { + /* "Key: Value\r\n" */ + size_t header_sz = r->headers[i].key_sz + 2 + r->headers[i].val_sz + 2; + r->out = realloc(r->out, r->out_sz + header_sz); + p = r->out + r->out_sz; + + /* add key */ + memcpy(p, r->headers[i].key, r->headers[i].key_sz); + p += r->headers[i].key_sz; + + /* add ": " */ + *(p++) = ':'; + *(p++) = ' '; + + /* add value */ + memcpy(p, r->headers[i].val, r->headers[i].val_sz); + p += r->headers[i].val_sz; + + /* add "\r\n" */ + *(p++) = '\r'; + *(p++) = '\n'; + + r->out_sz += header_sz; + + if(strncasecmp("Connection", r->headers[i].key, r->headers[i].key_sz) == 0 && + strncasecmp("Keep-Alive", r->headers[i].val, r->headers[i].val_sz) == 0) { + r->keep_alive = 1; + } + } + + /* end of headers */ + r->out = realloc(r->out, r->out_sz + 2); + memcpy(r->out + r->out_sz, "\r\n", 2); + r->out_sz += 2; + + /* append body if there is one. */ + if(r->body && r->body_len) { + + char *tmp = (char*)r->body; + size_t tmp_len = r->body_len; + if(r->chunked) { /* replace body with formatted chunk */ + tmp = format_chunk(r->body, r->body_len, &tmp_len); + } + + r->out = realloc(r->out, r->out_sz + tmp_len); + memcpy(r->out + r->out_sz, tmp, tmp_len); + r->out_sz += tmp_len; + + if(r->chunked) { /* need to free the chunk */ + free(tmp); + } + } + + /* send buffer to client */ + r->sent = 0; + http_schedule_write(fd, r); +} + +static void +http_response_set_connection_header(struct http_client *c, struct http_response *r) { + http_response_set_keep_alive(r, c->keep_alive); +} + + + +/* Adobe flash cross-domain request */ +void +http_crossdomain(struct http_client *c) { + + struct http_response *resp = http_response_init(NULL, 200, "OK"); + char out[] = "\n" +"\n" +"\n" + "\n" +"\n"; + + resp->http_version = c->http_version; + http_response_set_connection_header(c, resp); + http_response_set_header(resp, "Content-Type", "application/xml"); + http_response_set_body(resp, out, sizeof(out)-1); + + http_response_write(resp, c->fd); + http_client_reset(c); +} + +/* Simple error response */ +void +http_send_error(struct http_client *c, short code, const char *msg) { + + struct http_response *resp = http_response_init(NULL, code, msg); + resp->http_version = c->http_version; + http_response_set_connection_header(c, resp); + http_response_set_body(resp, NULL, 0); + + http_response_write(resp, c->fd); + http_client_reset(c); +} + +/** + * Set Connection field, either Keep-Alive or Close. + */ +void +http_response_set_keep_alive(struct http_response *r, int enabled) { + r->keep_alive = enabled; + if(enabled) { + http_response_set_header(r, "Connection", "Keep-Alive"); + } else { + http_response_set_header(r, "Connection", "Close"); + } +} + +/* Response to HTTP OPTIONS */ +void +http_send_options(struct http_client *c) { + + struct http_response *resp = http_response_init(NULL, 200, "OK"); + resp->http_version = c->http_version; + http_response_set_connection_header(c, resp); + + http_response_set_header(resp, "Content-Type", "text/html"); + http_response_set_header(resp, "Content-Length", "0"); + + http_response_write(resp, c->fd); + http_client_reset(c); +} + +/** + * Write HTTP chunk. + */ +void +http_response_write_chunk(int fd, struct worker *w, const char *p, size_t sz) { + + struct http_response *r = http_response_init(w, 0, NULL); + r->keep_alive = 1; /* chunks are always keep-alive */ + + /* format packet */ + r->out = format_chunk(p, sz, &r->out_sz); + + /* send async write */ + http_schedule_write(fd, r); +} + diff --git a/http.h b/http.h new file mode 100644 index 0000000..eac1ffe --- /dev/null +++ b/http.h @@ -0,0 +1,75 @@ +#ifndef HTTP_H +#define HTTP_H + +#include +#include + +struct http_client; +struct worker; + +struct http_header { + char *key; + size_t key_sz; + + char *val; + size_t val_sz; +}; + + +struct http_response { + + struct event ev; + + short code; + const char *msg; + + struct http_header *headers; + int header_count; + + const char *body; + size_t body_len; + + char *out; + size_t out_sz; + + int chunked; + int http_version; + int keep_alive; + int sent; + + struct worker *w; +}; + +/* HTTP response */ + +struct http_response * +http_response_init(struct worker *w, int code, const char *msg); + +void +http_response_set_header(struct http_response *r, const char *k, const char *v); + +void +http_response_set_body(struct http_response *r, const char *body, size_t body_len); + +void +http_response_write(struct http_response *r, int fd); + +void +http_schedule_write(int fd, struct http_response *r); + +void +http_crossdomain(struct http_client *c); + +void +http_send_error(struct http_client *c, short code, const char *msg); + +void +http_send_options(struct http_client *c); + +void +http_response_write_chunk(int fd, struct worker *w, const char *p, size_t sz); + +void +http_response_set_keep_alive(struct http_response *r, int enabled); + +#endif diff --git a/jansson/.gitignore b/jansson/.gitignore new file mode 100644 index 0000000..3b4bffe --- /dev/null +++ b/jansson/.gitignore @@ -0,0 +1,27 @@ +*~ +*.o +*.a +.libs +.deps +Makefile +Makefile.in +aclocal.m4 +autom4te.cache +config.guess +config.h +config.h.in +config.log +config.status +config.sub +configure +depcomp +install-sh +libtool +ltmain.sh +missing +*.lo +*.la +stamp-h1 +*.pyc +*.pc +/src/jansson_config.h diff --git a/jansson/CHANGES b/jansson/CHANGES new file mode 100644 index 0000000..0cadf5c --- /dev/null +++ b/jansson/CHANGES @@ -0,0 +1,206 @@ +Version 1.3 +=========== + +Released 2010-06-13 + +* New functions: + + - `json_object_iter_set()`, `json_object_iter_set_new()`: Change + object contents while iterating over it. + + - `json_object_iter_at()`: Return an iterator that points to a + specific object item. + +* New encoding flags: + + - ``JSON_PRESERVE_ORDER``: Preserve the insertion order of object + keys. + +* Bug fixes: + + - Fix an error that occured when an array or object was first + encoded as empty, then populated with some data, and then + re-encoded + + - Fix the situation like above, but when the first encoding resulted + in an error + +* Documentation: + + - Clarify the documentation on reference stealing, providing an + example usage pattern + + +Version 1.2.1 +============= + +Released 2010-04-03 + +* Bug fixes: + + - Fix reference counting on ``true``, ``false`` and ``null`` + - Estimate real number underflows in decoder with 0.0 instead of + issuing an error + +* Portability: + + - Make ``int32_t`` available on all systems + - Support compilers that don't have the ``inline`` keyword + - Require Autoconf 2.60 (for ``int32_t``) + +* Tests: + + - Print test names correctly when ``VERBOSE=1`` + - ``test/suites/api``: Fail when a test fails + - Enhance tests for iterators + - Enhance tests for decoding texts that contain null bytes + +* Documentation: + + - Don't remove ``changes.rst`` in ``make clean`` + - Add a chapter on RFC conformance + + +Version 1.2 +=========== + +Released 2010-01-21 + +* New functions: + + - `json_equal()`: Test whether two JSON values are equal + - `json_copy()` and `json_deep_copy()`: Make shallow and deep copies + of JSON values + - Add a version of all functions taking a string argument that + doesn't check for valid UTF-8: `json_string_nocheck()`, + `json_string_set_nocheck()`, `json_object_set_nocheck()`, + `json_object_set_new_nocheck()` + +* New encoding flags: + + - ``JSON_SORT_KEYS``: Sort objects by key + - ``JSON_ENSURE_ASCII``: Escape all non-ASCII Unicode characters + - ``JSON_COMPACT``: Use a compact representation with all unneeded + whitespace stripped + +* Bug fixes: + + - Revise and unify whitespace usage in encoder: Add spaces between + array and object items, never append newline to output. + - Remove const qualifier from the ``json_t`` parameter in + `json_string_set()`, `json_integer_set()` and `json_real_set`. + - Use ``int32_t`` internally for representing Unicode code points + (int is not enough on all platforms) + +* Other changes: + + - Convert ``CHANGES`` (this file) to reStructured text and add it to + HTML documentation + - The test system has been refactored. Python is no longer required + to run the tests. + - Documentation can now be built by invoking ``make html`` + - Support for pkg-config + + +Version 1.1.3 +============= + +Released 2009-12-18 + +* Encode reals correctly, so that first encoding and then decoding a + real always produces the same value +* Don't export private symbols in ``libjansson.so`` + + +Version 1.1.2 +============= + +Released 2009-11-08 + +* Fix a bug where an error message was not produced if the input file + could not be opened in `json_load_file()` +* Fix an assertion failure in decoder caused by a minus sign without a + digit after it +* Remove an unneeded include of ``stdint.h`` in ``jansson.h`` + + +Version 1.1.1 +============= + +Released 2009-10-26 + +* All documentation files were not distributed with v1.1; build + documentation in make distcheck to prevent this in the future +* Fix v1.1 release date in ``CHANGES`` + + +Version 1.1 +=========== + +Released 2009-10-20 + +* API additions and improvements: + + - Extend array and object APIs + - Add functions to modify integer, real and string values + - Improve argument validation + - Use unsigned int instead of ``uint32_t`` for encoding flags + +* Enhance documentation + + - Add getting started guide and tutorial + - Fix some typos + - General clarifications and cleanup + +* Check for integer and real overflows and underflows in decoder +* Make singleton values thread-safe (``true``, ``false`` and ``null``) +* Enhance circular reference handling +* Don't define ``-std=c99`` in ``AM_CFLAGS`` +* Add C++ guards to ``jansson.h`` +* Minor performance and portability improvements +* Expand test coverage + + +Version 1.0.4 +============= + +Released 2009-10-11 + +* Relax Autoconf version requirement to 2.59 +* Make Jansson compile on platforms where plain ``char`` is unsigned +* Fix API tests for object + + +Version 1.0.3 +============= + +Released 2009-09-14 + +* Check for integer and real overflows and underflows in decoder +* Use the Python json module for tests, or simplejson if the json + module is not found +* Distribute changelog (this file) + + +Version 1.0.2 +============= + +Released 2009-09-08 + +* Handle EOF correctly in decoder + + +Version 1.0.1 +============= + +Released 2009-09-04 + +* Fixed broken `json_is_boolean()` + + +Version 1.0 +=========== + +Released 2009-08-25 + +* Initial release diff --git a/jansson/LICENSE b/jansson/LICENSE new file mode 100644 index 0000000..552b349 --- /dev/null +++ b/jansson/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2009, 2010 Petri Lehtinen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/jansson/Makefile.am b/jansson/Makefile.am new file mode 100644 index 0000000..173c7de --- /dev/null +++ b/jansson/Makefile.am @@ -0,0 +1,8 @@ +EXTRA_DIST = CHANGES LICENSE README.rst +SUBDIRS = doc src test + +check-doc: + $(MAKE) SPHINXOPTS_EXTRA=-W html + +pkgconfigdir = $(libdir)/pkgconfig +pkgconfig_DATA = jansson.pc diff --git a/jansson/README.rst b/jansson/README.rst new file mode 100644 index 0000000..27706dd --- /dev/null +++ b/jansson/README.rst @@ -0,0 +1,59 @@ +Jansson README +============== + +Jansson_ is a C library for encoding, decoding and manipulating JSON +data. Its main features and design principles are: + +- Simple and intuitive API and data model + +- Comprehensive documentation + +- No dependencies on other libraries + +- Full Unicode support (UTF-8) + +- Extensive test suite + +Jansson is licensed under the `MIT license`_; see LICENSE in the +source distribution for details. + + +Compilation and Installation +---------------------------- + +If you obtained a source tarball, just use the standard autotools +commands:: + + $ ./configure && make && make install + +If the source has been checked out from a Git repository, the +./configure script has to be generated fist. The easiest way is to use +autoreconf:: + + $ autoreconf -i + +To run the test suite, invoke:: + + $ make check + + +Documentation +------------- + +Documentation is in the ``doc/`` subdirectory. It's written in +reStructuredText_ with Sphinx_ annotations, so reading it in plain may +be inconvenient. For this reason, prebuilt HTML documentation is +available at http://www.digip.org/jansson/doc/. + +To generate HTML documentation yourself, invoke:: + + make html + +and point your browser to ``doc/_build/html/index.html``. Sphinx_ is +required to generate the documentation. + + +.. _Jansson: http://www.digip.org/jansson/ +.. _`MIT license`: http://www.opensource.org/licenses/mit-license.php +.. _reStructuredText: http://docutils.sourceforge.net/rst.html +.. _Sphinx: http://sphinx.pocoo.org/ diff --git a/jansson/configure.ac b/jansson/configure.ac new file mode 100644 index 0000000..cabc732 --- /dev/null +++ b/jansson/configure.ac @@ -0,0 +1,49 @@ +AC_PREREQ([2.60]) +AC_INIT([jansson], [2.0pre], [petri@digip.org]) + +AM_INIT_AUTOMAKE([1.10 foreign]) + +AC_CONFIG_SRCDIR([src/value.c]) +AC_CONFIG_HEADERS([config.h]) + +# Checks for programs. +AC_PROG_CC +AC_PROG_LIBTOOL +AM_CONDITIONAL([GCC], [test x$GCC = xyes]) + +# Checks for libraries. + +# Checks for header files. + +# Checks for typedefs, structures, and compiler characteristics. +AC_TYPE_INT32_T + +AC_TYPE_LONG_LONG_INT +case $ac_cv_type_long_long_int in + yes) json_have_long_long=1;; + *) json_have_long_long=0;; +esac +AC_SUBST([json_have_long_long]) + +AC_C_INLINE +case $ac_cv_c_inline in + yes) json_inline=inline;; + no) json_inline=;; + *) json_inline=$ac_cv_c_inline;; +esac +AC_SUBST([json_inline]) + +# Checks for library functions. + +AC_CONFIG_FILES([ + jansson.pc + Makefile + doc/Makefile + src/Makefile + src/jansson_config.h + test/Makefile + test/bin/Makefile + test/suites/Makefile + test/suites/api/Makefile +]) +AC_OUTPUT diff --git a/jansson/jansson.pc.in b/jansson/jansson.pc.in new file mode 100644 index 0000000..d9bf4da --- /dev/null +++ b/jansson/jansson.pc.in @@ -0,0 +1,10 @@ +prefix=@prefix@ +exec_prefix=@exec_prefix@ +libdir=@libdir@ +includedir=${prefix}/include + +Name: Jansson +Description: Library for encoding, decoding and manipulating JSON data +Version: @VERSION@ +Libs: -L${libdir} -ljansson +Cflags: -I${includedir} diff --git a/jansson/src/Makefile.am b/jansson/src/Makefile.am new file mode 100644 index 0000000..be8fcb9 --- /dev/null +++ b/jansson/src/Makefile.am @@ -0,0 +1,24 @@ +include_HEADERS = jansson.h jansson_config.h + +lib_LTLIBRARIES = libjansson.la +libjansson_la_SOURCES = \ + dump.c \ + error.c \ + hashtable.c \ + hashtable.h \ + jansson_private.h \ + load.c \ + strbuffer.c \ + strbuffer.h \ + utf.c \ + utf.h \ + value.c \ + variadic.c +libjansson_la_LDFLAGS = \ + -export-symbols-regex '^json_' \ + -version-info 3:0:3 + +if GCC +# These flags are gcc specific +AM_CFLAGS = -Wall -Wextra -Werror +endif diff --git a/jansson/src/dump.c b/jansson/src/dump.c new file mode 100644 index 0000000..42eb256 --- /dev/null +++ b/jansson/src/dump.c @@ -0,0 +1,461 @@ +/* + * Copyright (c) 2009, 2010 Petri Lehtinen + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include + +#include +#include "jansson_private.h" +#include "strbuffer.h" +#include "utf.h" + +#define MAX_INTEGER_STR_LENGTH 100 +#define MAX_REAL_STR_LENGTH 100 + +typedef int (*dump_func)(const char *buffer, int size, void *data); + +struct string +{ + char *buffer; + int length; + int size; +}; + +static int dump_to_strbuffer(const char *buffer, int size, void *data) +{ + return strbuffer_append_bytes((strbuffer_t *)data, buffer, size); +} + +static int dump_to_file(const char *buffer, int size, void *data) +{ + FILE *dest = (FILE *)data; + if(fwrite(buffer, size, 1, dest) != 1) + return -1; + return 0; +} + +/* 32 spaces (the maximum indentation size) */ +static char whitespace[] = " "; + +static int dump_indent(size_t flags, int depth, int space, dump_func dump, void *data) +{ + if(JSON_INDENT(flags) > 0) + { + int i, ws_count = JSON_INDENT(flags); + + if(dump("\n", 1, data)) + return -1; + + for(i = 0; i < depth; i++) + { + if(dump(whitespace, ws_count, data)) + return -1; + } + } + else if(space && !(flags & JSON_COMPACT)) + { + return dump(" ", 1, data); + } + return 0; +} + +static int dump_string(const char *str, int ascii, dump_func dump, void *data) +{ + const char *pos, *end; + int32_t codepoint; + + if(dump("\"", 1, data)) + return -1; + + end = pos = str; + while(1) + { + const char *text; + char seq[13]; + int length; + + while(*end) + { + end = utf8_iterate(pos, &codepoint); + if(!end) + return -1; + + /* mandatory escape or control char */ + if(codepoint == '\\' || codepoint == '"' || codepoint < 0x20) + break; + + /* non-ASCII */ + if(ascii && codepoint > 0x7F) + break; + + pos = end; + } + + if(pos != str) { + if(dump(str, pos - str, data)) + return -1; + } + + if(end == pos) + break; + + /* handle \, ", and control codes */ + length = 2; + switch(codepoint) + { + case '\\': text = "\\\\"; break; + case '\"': text = "\\\""; break; + case '\b': text = "\\b"; break; + case '\f': text = "\\f"; break; + case '\n': text = "\\n"; break; + case '\r': text = "\\r"; break; + case '\t': text = "\\t"; break; + default: + { + /* codepoint is in BMP */ + if(codepoint < 0x10000) + { + sprintf(seq, "\\u%04x", codepoint); + length = 6; + } + + /* not in BMP -> construct a UTF-16 surrogate pair */ + else + { + int32_t first, last; + + codepoint -= 0x10000; + first = 0xD800 | ((codepoint & 0xffc00) >> 10); + last = 0xDC00 | (codepoint & 0x003ff); + + sprintf(seq, "\\u%04x\\u%04x", first, last); + length = 12; + } + + text = seq; + break; + } + } + + if(dump(text, length, data)) + return -1; + + str = pos = end; + } + + return dump("\"", 1, data); +} + +static int object_key_compare_keys(const void *key1, const void *key2) +{ + return strcmp((*(const object_key_t **)key1)->key, + (*(const object_key_t **)key2)->key); +} + +static int object_key_compare_serials(const void *key1, const void *key2) +{ + return (*(const object_key_t **)key1)->serial - + (*(const object_key_t **)key2)->serial; +} + +static int do_dump(const json_t *json, size_t flags, int depth, + dump_func dump, void *data) +{ + int ascii = flags & JSON_ENSURE_ASCII ? 1 : 0; + + switch(json_typeof(json)) { + case JSON_NULL: + return dump("null", 4, data); + + case JSON_TRUE: + return dump("true", 4, data); + + case JSON_FALSE: + return dump("false", 5, data); + + case JSON_INTEGER: + { + char buffer[MAX_INTEGER_STR_LENGTH]; + int size; + + size = snprintf(buffer, MAX_INTEGER_STR_LENGTH, + "%" JSON_INTEGER_FORMAT, + json_integer_value(json)); + if(size >= MAX_INTEGER_STR_LENGTH) + return -1; + + return dump(buffer, size, data); + } + + case JSON_REAL: + { + char buffer[MAX_REAL_STR_LENGTH]; + int size; + + size = snprintf(buffer, MAX_REAL_STR_LENGTH, "%.17g", + json_real_value(json)); + if(size >= MAX_REAL_STR_LENGTH) + return -1; + + /* Make sure there's a dot or 'e' in the output. Otherwise + a real is converted to an integer when decoding */ + if(strchr(buffer, '.') == NULL && + strchr(buffer, 'e') == NULL) + { + if(size + 2 >= MAX_REAL_STR_LENGTH) { + /* No space to append ".0" */ + return -1; + } + buffer[size] = '.'; + buffer[size + 1] = '0'; + size += 2; + } + + return dump(buffer, size, data); + } + + case JSON_STRING: + return dump_string(json_string_value(json), ascii, dump, data); + + case JSON_ARRAY: + { + int i; + int n; + json_array_t *array; + + /* detect circular references */ + array = json_to_array(json); + if(array->visited) + goto array_error; + array->visited = 1; + + n = json_array_size(json); + + if(dump("[", 1, data)) + goto array_error; + if(n == 0) { + array->visited = 0; + return dump("]", 1, data); + } + if(dump_indent(flags, depth + 1, 0, dump, data)) + goto array_error; + + for(i = 0; i < n; ++i) { + if(do_dump(json_array_get(json, i), flags, depth + 1, + dump, data)) + goto array_error; + + if(i < n - 1) + { + if(dump(",", 1, data) || + dump_indent(flags, depth + 1, 1, dump, data)) + goto array_error; + } + else + { + if(dump_indent(flags, depth, 0, dump, data)) + goto array_error; + } + } + + array->visited = 0; + return dump("]", 1, data); + + array_error: + array->visited = 0; + return -1; + } + + case JSON_OBJECT: + { + json_object_t *object; + void *iter; + const char *separator; + int separator_length; + + if(flags & JSON_COMPACT) { + separator = ":"; + separator_length = 1; + } + else { + separator = ": "; + separator_length = 2; + } + + /* detect circular references */ + object = json_to_object(json); + if(object->visited) + goto object_error; + object->visited = 1; + + iter = json_object_iter((json_t *)json); + + if(dump("{", 1, data)) + goto object_error; + if(!iter) { + object->visited = 0; + return dump("}", 1, data); + } + if(dump_indent(flags, depth + 1, 0, dump, data)) + goto object_error; + + if(flags & JSON_SORT_KEYS || flags & JSON_PRESERVE_ORDER) + { + const object_key_t **keys; + size_t size, i; + int (*cmp_func)(const void *, const void *); + + size = json_object_size(json); + keys = malloc(size * sizeof(object_key_t *)); + if(!keys) + goto object_error; + + i = 0; + while(iter) + { + keys[i] = jsonp_object_iter_fullkey(iter); + iter = json_object_iter_next((json_t *)json, iter); + i++; + } + assert(i == size); + + if(flags & JSON_SORT_KEYS) + cmp_func = object_key_compare_keys; + else + cmp_func = object_key_compare_serials; + + qsort(keys, size, sizeof(object_key_t *), cmp_func); + + for(i = 0; i < size; i++) + { + const char *key; + json_t *value; + + key = keys[i]->key; + value = json_object_get(json, key); + assert(value); + + dump_string(key, ascii, dump, data); + if(dump(separator, separator_length, data) || + do_dump(value, flags, depth + 1, dump, data)) + { + free(keys); + goto object_error; + } + + if(i < size - 1) + { + if(dump(",", 1, data) || + dump_indent(flags, depth + 1, 1, dump, data)) + { + free(keys); + goto object_error; + } + } + else + { + if(dump_indent(flags, depth, 0, dump, data)) + { + free(keys); + goto object_error; + } + } + } + + free(keys); + } + else + { + /* Don't sort keys */ + + while(iter) + { + void *next = json_object_iter_next((json_t *)json, iter); + + dump_string(json_object_iter_key(iter), ascii, dump, data); + if(dump(separator, separator_length, data) || + do_dump(json_object_iter_value(iter), flags, depth + 1, + dump, data)) + goto object_error; + + if(next) + { + if(dump(",", 1, data) || + dump_indent(flags, depth + 1, 1, dump, data)) + goto object_error; + } + else + { + if(dump_indent(flags, depth, 0, dump, data)) + goto object_error; + } + + iter = next; + } + } + + object->visited = 0; + return dump("}", 1, data); + + object_error: + object->visited = 0; + return -1; + } + + default: + /* not reached */ + return -1; + } +} + + +char *json_dumps(const json_t *json, size_t flags) +{ + strbuffer_t strbuff; + char *result; + + if(!json_is_array(json) && !json_is_object(json)) + return NULL; + + if(strbuffer_init(&strbuff)) + return NULL; + + if(do_dump(json, flags, 0, dump_to_strbuffer, (void *)&strbuff)) { + strbuffer_close(&strbuff); + return NULL; + } + + result = strdup(strbuffer_value(&strbuff)); + strbuffer_close(&strbuff); + + return result; +} + +int json_dumpf(const json_t *json, FILE *output, size_t flags) +{ + if(!json_is_array(json) && !json_is_object(json)) + return -1; + + return do_dump(json, flags, 0, dump_to_file, (void *)output); +} + +int json_dump_file(const json_t *json, const char *path, size_t flags) +{ + int result; + + FILE *output = fopen(path, "w"); + if(!output) + return -1; + + result = json_dumpf(json, output, flags); + + fclose(output); + return result; +} diff --git a/jansson/src/error.c b/jansson/src/error.c new file mode 100644 index 0000000..b8c78db --- /dev/null +++ b/jansson/src/error.c @@ -0,0 +1,38 @@ +#include +#include + +#include "jansson_private.h" + +void jsonp_error_init(json_error_t *error, const char *source) +{ + if(error) + { + error->text[0] = '\0'; + error->line = -1; + error->column = -1; + + strncpy(error->source, source, JSON_ERROR_SOURCE_LENGTH); + error->source[JSON_ERROR_SOURCE_LENGTH - 1] = '\0'; + } +} + +void jsonp_error_set(json_error_t *error, int line, int column, + const char *msg, ...) +{ + va_list ap; + + if(!error) + return; + + if(error->text[0] != '\0') { + /* error already set */ + return; + } + + error->line = line; + error->column = column; + + va_start(ap, msg); + vsnprintf(error->text, JSON_ERROR_TEXT_LENGTH, msg, ap); + va_end(ap); +} diff --git a/jansson/src/hashtable.c b/jansson/src/hashtable.c new file mode 100644 index 0000000..77d2b80 --- /dev/null +++ b/jansson/src/hashtable.c @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2009, 2010 Petri Lehtinen + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#include +#include /* for JSON_INLINE */ +#include "jansson_private.h" /* for container_of() */ +#include "hashtable.h" + +typedef struct hashtable_list list_t; +typedef struct hashtable_pair pair_t; +typedef struct hashtable_bucket bucket_t; + +#define list_to_pair(list_) container_of(list_, pair_t, list) + +static JSON_INLINE void list_init(list_t *list) +{ + list->next = list; + list->prev = list; +} + +static JSON_INLINE void list_insert(list_t *list, list_t *node) +{ + node->next = list; + node->prev = list->prev; + list->prev->next = node; + list->prev = node; +} + +static JSON_INLINE void list_remove(list_t *list) +{ + list->prev->next = list->next; + list->next->prev = list->prev; +} + +static JSON_INLINE int bucket_is_empty(hashtable_t *hashtable, bucket_t *bucket) +{ + return bucket->first == &hashtable->list && bucket->first == bucket->last; +} + +static void insert_to_bucket(hashtable_t *hashtable, bucket_t *bucket, + list_t *list) +{ + if(bucket_is_empty(hashtable, bucket)) + { + list_insert(&hashtable->list, list); + bucket->first = bucket->last = list; + } + else + { + list_insert(bucket->first, list); + bucket->first = list; + } +} + +static size_t primes[] = { + 5, 13, 23, 53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593, + 49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469, + 12582917, 25165843, 50331653, 100663319, 201326611, 402653189, + 805306457, 1610612741 +}; +static const size_t num_primes = sizeof(primes) / sizeof(size_t); + +static JSON_INLINE size_t num_buckets(hashtable_t *hashtable) +{ + return primes[hashtable->num_buckets]; +} + + +static pair_t *hashtable_find_pair(hashtable_t *hashtable, bucket_t *bucket, + const void *key, size_t hash) +{ + list_t *list; + pair_t *pair; + + if(bucket_is_empty(hashtable, bucket)) + return NULL; + + list = bucket->first; + while(1) + { + pair = list_to_pair(list); + if(pair->hash == hash && hashtable->cmp_keys(pair->key, key)) + return pair; + + if(list == bucket->last) + break; + + list = list->next; + } + + return NULL; +} + +/* returns 0 on success, -1 if key was not found */ +static int hashtable_do_del(hashtable_t *hashtable, + const void *key, size_t hash) +{ + pair_t *pair; + bucket_t *bucket; + size_t index; + + index = hash % num_buckets(hashtable); + bucket = &hashtable->buckets[index]; + + pair = hashtable_find_pair(hashtable, bucket, key, hash); + if(!pair) + return -1; + + if(&pair->list == bucket->first && &pair->list == bucket->last) + bucket->first = bucket->last = &hashtable->list; + + else if(&pair->list == bucket->first) + bucket->first = pair->list.next; + + else if(&pair->list == bucket->last) + bucket->last = pair->list.prev; + + list_remove(&pair->list); + + if(hashtable->free_key) + hashtable->free_key(pair->key); + if(hashtable->free_value) + hashtable->free_value(pair->value); + + free(pair); + hashtable->size--; + + return 0; +} + +static void hashtable_do_clear(hashtable_t *hashtable) +{ + list_t *list, *next; + pair_t *pair; + + for(list = hashtable->list.next; list != &hashtable->list; list = next) + { + next = list->next; + pair = list_to_pair(list); + if(hashtable->free_key) + hashtable->free_key(pair->key); + if(hashtable->free_value) + hashtable->free_value(pair->value); + free(pair); + } +} + +static int hashtable_do_rehash(hashtable_t *hashtable) +{ + list_t *list, *next; + pair_t *pair; + size_t i, index, new_size; + + free(hashtable->buckets); + + hashtable->num_buckets++; + new_size = num_buckets(hashtable); + + hashtable->buckets = malloc(new_size * sizeof(bucket_t)); + if(!hashtable->buckets) + return -1; + + for(i = 0; i < num_buckets(hashtable); i++) + { + hashtable->buckets[i].first = hashtable->buckets[i].last = + &hashtable->list; + } + + list = hashtable->list.next; + list_init(&hashtable->list); + + for(; list != &hashtable->list; list = next) { + next = list->next; + pair = list_to_pair(list); + index = pair->hash % new_size; + insert_to_bucket(hashtable, &hashtable->buckets[index], &pair->list); + } + + return 0; +} + + +hashtable_t *hashtable_create(key_hash_fn hash_key, key_cmp_fn cmp_keys, + free_fn free_key, free_fn free_value) +{ + hashtable_t *hashtable = malloc(sizeof(hashtable_t)); + if(!hashtable) + return NULL; + + if(hashtable_init(hashtable, hash_key, cmp_keys, free_key, free_value)) + { + free(hashtable); + return NULL; + } + + return hashtable; +} + +void hashtable_destroy(hashtable_t *hashtable) +{ + hashtable_close(hashtable); + free(hashtable); +} + +int hashtable_init(hashtable_t *hashtable, + key_hash_fn hash_key, key_cmp_fn cmp_keys, + free_fn free_key, free_fn free_value) +{ + size_t i; + + hashtable->size = 0; + hashtable->num_buckets = 0; /* index to primes[] */ + hashtable->buckets = malloc(num_buckets(hashtable) * sizeof(bucket_t)); + if(!hashtable->buckets) + return -1; + + list_init(&hashtable->list); + + hashtable->hash_key = hash_key; + hashtable->cmp_keys = cmp_keys; + hashtable->free_key = free_key; + hashtable->free_value = free_value; + + for(i = 0; i < num_buckets(hashtable); i++) + { + hashtable->buckets[i].first = hashtable->buckets[i].last = + &hashtable->list; + } + + return 0; +} + +void hashtable_close(hashtable_t *hashtable) +{ + hashtable_do_clear(hashtable); + free(hashtable->buckets); +} + +int hashtable_set(hashtable_t *hashtable, void *key, void *value) +{ + pair_t *pair; + bucket_t *bucket; + size_t hash, index; + + /* rehash if the load ratio exceeds 1 */ + if(hashtable->size >= num_buckets(hashtable)) + if(hashtable_do_rehash(hashtable)) + return -1; + + hash = hashtable->hash_key(key); + index = hash % num_buckets(hashtable); + bucket = &hashtable->buckets[index]; + pair = hashtable_find_pair(hashtable, bucket, key, hash); + + if(pair) + { + if(hashtable->free_key) + hashtable->free_key(key); + if(hashtable->free_value) + hashtable->free_value(pair->value); + pair->value = value; + } + else + { + pair = malloc(sizeof(pair_t)); + if(!pair) + return -1; + + pair->key = key; + pair->value = value; + pair->hash = hash; + list_init(&pair->list); + + insert_to_bucket(hashtable, bucket, &pair->list); + + hashtable->size++; + } + return 0; +} + +void *hashtable_get(hashtable_t *hashtable, const void *key) +{ + pair_t *pair; + size_t hash; + bucket_t *bucket; + + hash = hashtable->hash_key(key); + bucket = &hashtable->buckets[hash % num_buckets(hashtable)]; + + pair = hashtable_find_pair(hashtable, bucket, key, hash); + if(!pair) + return NULL; + + return pair->value; +} + +int hashtable_del(hashtable_t *hashtable, const void *key) +{ + size_t hash = hashtable->hash_key(key); + return hashtable_do_del(hashtable, key, hash); +} + +void hashtable_clear(hashtable_t *hashtable) +{ + size_t i; + + hashtable_do_clear(hashtable); + + for(i = 0; i < num_buckets(hashtable); i++) + { + hashtable->buckets[i].first = hashtable->buckets[i].last = + &hashtable->list; + } + + list_init(&hashtable->list); + hashtable->size = 0; +} + +void *hashtable_iter(hashtable_t *hashtable) +{ + return hashtable_iter_next(hashtable, &hashtable->list); +} + +void *hashtable_iter_at(hashtable_t *hashtable, const void *key) +{ + pair_t *pair; + size_t hash; + bucket_t *bucket; + + hash = hashtable->hash_key(key); + bucket = &hashtable->buckets[hash % num_buckets(hashtable)]; + + pair = hashtable_find_pair(hashtable, bucket, key, hash); + if(!pair) + return NULL; + + return &pair->list; +} + +void *hashtable_iter_next(hashtable_t *hashtable, void *iter) +{ + list_t *list = (list_t *)iter; + if(list->next == &hashtable->list) + return NULL; + return list->next; +} + +void *hashtable_iter_key(void *iter) +{ + pair_t *pair = list_to_pair((list_t *)iter); + return pair->key; +} + +void *hashtable_iter_value(void *iter) +{ + pair_t *pair = list_to_pair((list_t *)iter); + return pair->value; +} + +void hashtable_iter_set(hashtable_t *hashtable, void *iter, void *value) +{ + pair_t *pair = list_to_pair((list_t *)iter); + + if(hashtable->free_value) + hashtable->free_value(pair->value); + + pair->value = value; +} diff --git a/jansson/src/hashtable.h b/jansson/src/hashtable.h new file mode 100644 index 0000000..aba5134 --- /dev/null +++ b/jansson/src/hashtable.h @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2009, 2010 Petri Lehtinen + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#ifndef HASHTABLE_H +#define HASHTABLE_H + +typedef size_t (*key_hash_fn)(const void *key); +typedef int (*key_cmp_fn)(const void *key1, const void *key2); +typedef void (*free_fn)(void *key); + +struct hashtable_list { + struct hashtable_list *prev; + struct hashtable_list *next; +}; + +struct hashtable_pair { + void *key; + void *value; + size_t hash; + struct hashtable_list list; +}; + +struct hashtable_bucket { + struct hashtable_list *first; + struct hashtable_list *last; +}; + +typedef struct hashtable { + size_t size; + struct hashtable_bucket *buckets; + size_t num_buckets; /* index to primes[] */ + struct hashtable_list list; + + key_hash_fn hash_key; + key_cmp_fn cmp_keys; /* returns non-zero for equal keys */ + free_fn free_key; + free_fn free_value; +} hashtable_t; + +/** + * hashtable_create - Create a hashtable object + * + * @hash_key: The key hashing function + * @cmp_keys: The key compare function. Returns non-zero for equal and + * zero for unequal unequal keys + * @free_key: If non-NULL, called for a key that is no longer referenced. + * @free_value: If non-NULL, called for a value that is no longer referenced. + * + * Returns a new hashtable object that should be freed with + * hashtable_destroy when it's no longer used, or NULL on failure (out + * of memory). + */ +hashtable_t *hashtable_create(key_hash_fn hash_key, key_cmp_fn cmp_keys, + free_fn free_key, free_fn free_value); + +/** + * hashtable_destroy - Destroy a hashtable object + * + * @hashtable: The hashtable + * + * Destroys a hashtable created with hashtable_create(). + */ +void hashtable_destroy(hashtable_t *hashtable); + +/** + * hashtable_init - Initialize a hashtable object + * + * @hashtable: The (statically allocated) hashtable object + * @hash_key: The key hashing function + * @cmp_keys: The key compare function. Returns non-zero for equal and + * zero for unequal unequal keys + * @free_key: If non-NULL, called for a key that is no longer referenced. + * @free_value: If non-NULL, called for a value that is no longer referenced. + * + * Initializes a statically allocated hashtable object. The object + * should be cleared with hashtable_close when it's no longer used. + * + * Returns 0 on success, -1 on error (out of memory). + */ +int hashtable_init(hashtable_t *hashtable, + key_hash_fn hash_key, key_cmp_fn cmp_keys, + free_fn free_key, free_fn free_value); + +/** + * hashtable_close - Release all resources used by a hashtable object + * + * @hashtable: The hashtable + * + * Destroys a statically allocated hashtable object. + */ +void hashtable_close(hashtable_t *hashtable); + +/** + * hashtable_set - Add/modify value in hashtable + * + * @hashtable: The hashtable object + * @key: The key + * @value: The value + * + * If a value with the given key already exists, its value is replaced + * with the new value. + * + * Key and value are "stealed" in the sense that hashtable frees them + * automatically when they are no longer used. The freeing is + * accomplished by calling free_key and free_value functions that were + * supplied to hashtable_new. In case one or both of the free + * functions is NULL, the corresponding item is not "stealed". + * + * Returns 0 on success, -1 on failure (out of memory). + */ +int hashtable_set(hashtable_t *hashtable, void *key, void *value); + +/** + * hashtable_get - Get a value associated with a key + * + * @hashtable: The hashtable object + * @key: The key + * + * Returns value if it is found, or NULL otherwise. + */ +void *hashtable_get(hashtable_t *hashtable, const void *key); + +/** + * hashtable_del - Remove a value from the hashtable + * + * @hashtable: The hashtable object + * @key: The key + * + * Returns 0 on success, or -1 if the key was not found. + */ +int hashtable_del(hashtable_t *hashtable, const void *key); + +/** + * hashtable_clear - Clear hashtable + * + * @hashtable: The hashtable object + * + * Removes all items from the hashtable. + */ +void hashtable_clear(hashtable_t *hashtable); + +/** + * hashtable_iter - Iterate over hashtable + * + * @hashtable: The hashtable object + * + * Returns an opaque iterator to the first element in the hashtable. + * The iterator should be passed to hashtable_iter_* functions. + * The hashtable items are not iterated over in any particular order. + * + * There's no need to free the iterator in any way. The iterator is + * valid as long as the item that is referenced by the iterator is not + * deleted. Other values may be added or deleted. In particular, + * hashtable_iter_next() may be called on an iterator, and after that + * the key/value pair pointed by the old iterator may be deleted. + */ +void *hashtable_iter(hashtable_t *hashtable); + +/** + * hashtable_iter_at - Return an iterator at a specific key + * + * @hashtable: The hashtable object + * @key: The key that the iterator should point to + * + * Like hashtable_iter() but returns an iterator pointing to a + * specific key. + */ +void *hashtable_iter_at(hashtable_t *hashtable, const void *key); + +/** + * hashtable_iter_next - Advance an iterator + * + * @hashtable: The hashtable object + * @iter: The iterator + * + * Returns a new iterator pointing to the next element in the + * hashtable or NULL if the whole hastable has been iterated over. + */ +void *hashtable_iter_next(hashtable_t *hashtable, void *iter); + +/** + * hashtable_iter_key - Retrieve the key pointed by an iterator + * + * @iter: The iterator + */ +void *hashtable_iter_key(void *iter); + +/** + * hashtable_iter_value - Retrieve the value pointed by an iterator + * + * @iter: The iterator + */ +void *hashtable_iter_value(void *iter); + +/** + * hashtable_iter_set - Set the value pointed by an iterator + * + * @iter: The iterator + * @value: The value to set + */ +void hashtable_iter_set(hashtable_t *hashtable, void *iter, void *value); + +#endif diff --git a/jansson/src/jansson.h b/jansson/src/jansson.h new file mode 100644 index 0000000..1e6fe6e --- /dev/null +++ b/jansson/src/jansson.h @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2009, 2010 Petri Lehtinen + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#ifndef JANSSON_H +#define JANSSON_H + +#include +#include /* for size_t */ +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* version */ + +#define JANSSON_MAJOR_VERSION 1 +#define JANSSON_MINOR_VERSION 3 +#define JANSSON_MICRO_VERSION 0 + +/* Micro version is omitted if it's 0 */ +#define JANSSON_VERSION "1.3" + +/* Version as a 3-byte hex number, e.g. 0x010201 == 1.2.1. Use this + for numeric comparisons, e.g. #if JANSSON_VERSION_HEX >= ... */ +#define JANSSON_VERSION_HEX ((JANSSON_MAJOR_VERSION << 16) | \ + (JANSSON_MINOR_VERSION << 8) | \ + (JANSSON_MICRO_VERSION << 0))) + + +/* types */ + +typedef enum { + JSON_OBJECT, + JSON_ARRAY, + JSON_STRING, + JSON_INTEGER, + JSON_REAL, + JSON_TRUE, + JSON_FALSE, + JSON_NULL +} json_type; + +typedef struct { + json_type type; + size_t refcount; +} json_t; + +#if JSON_INTEGER_IS_LONG_LONG +#define JSON_INTEGER_FORMAT "lld" +typedef long long json_int_t; +#else +#define JSON_INTEGER_FORMAT "ld" +typedef long json_int_t; +#endif /* JSON_INTEGER_IS_LONG_LONG */ + +#define json_typeof(json) ((json)->type) +#define json_is_object(json) (json && json_typeof(json) == JSON_OBJECT) +#define json_is_array(json) (json && json_typeof(json) == JSON_ARRAY) +#define json_is_string(json) (json && json_typeof(json) == JSON_STRING) +#define json_is_integer(json) (json && json_typeof(json) == JSON_INTEGER) +#define json_is_real(json) (json && json_typeof(json) == JSON_REAL) +#define json_is_number(json) (json_is_integer(json) || json_is_real(json)) +#define json_is_true(json) (json && json_typeof(json) == JSON_TRUE) +#define json_is_false(json) (json && json_typeof(json) == JSON_FALSE) +#define json_is_boolean(json) (json_is_true(json) || json_is_false(json)) +#define json_is_null(json) (json && json_typeof(json) == JSON_NULL) + +/* construction, destruction, reference counting */ + +json_t *json_object(void); +json_t *json_array(void); +json_t *json_string(const char *value); +json_t *json_string_nocheck(const char *value); +json_t *json_integer(json_int_t value); +json_t *json_real(double value); +json_t *json_true(void); +json_t *json_false(void); +json_t *json_null(void); + +static JSON_INLINE +json_t *json_incref(json_t *json) +{ + if(json && json->refcount != (size_t)-1) + ++json->refcount; + return json; +} + +/* do not call json_delete directly */ +void json_delete(json_t *json); + +static JSON_INLINE +void json_decref(json_t *json) +{ + if(json && json->refcount != (size_t)-1 && --json->refcount == 0) + json_delete(json); +} + + +/* error reporting */ + +#define JSON_ERROR_TEXT_LENGTH 160 +#define JSON_ERROR_SOURCE_LENGTH 80 + +typedef struct { + char text[JSON_ERROR_TEXT_LENGTH]; + int line; + int column; + char source[JSON_ERROR_SOURCE_LENGTH]; +} json_error_t; + + +/* getters, setters, manipulation */ + +size_t json_object_size(const json_t *object); +json_t *json_object_get(const json_t *object, const char *key); +int json_object_set_new(json_t *object, const char *key, json_t *value); +int json_object_set_new_nocheck(json_t *object, const char *key, json_t *value); +int json_object_del(json_t *object, const char *key); +int json_object_clear(json_t *object); +int json_object_update(json_t *object, json_t *other); +void *json_object_iter(json_t *object); +void *json_object_iter_at(json_t *object, const char *key); +void *json_object_iter_next(json_t *object, void *iter); +const char *json_object_iter_key(void *iter); +json_t *json_object_iter_value(void *iter); +int json_object_iter_set_new(json_t *object, void *iter, json_t *value); + +static JSON_INLINE +int json_object_set(json_t *object, const char *key, json_t *value) +{ + return json_object_set_new(object, key, json_incref(value)); +} + +static JSON_INLINE +int json_object_set_nocheck(json_t *object, const char *key, json_t *value) +{ + return json_object_set_new_nocheck(object, key, json_incref(value)); +} + +static JSON_INLINE +int json_object_iter_set(json_t *object, void *iter, json_t *value) +{ + return json_object_iter_set_new(object, iter, json_incref(value)); +} + +size_t json_array_size(const json_t *array); +json_t *json_array_get(const json_t *array, size_t index); +int json_array_set_new(json_t *array, size_t index, json_t *value); +int json_array_append_new(json_t *array, json_t *value); +int json_array_insert_new(json_t *array, size_t index, json_t *value); +int json_array_remove(json_t *array, size_t index); +int json_array_clear(json_t *array); +int json_array_extend(json_t *array, json_t *other); + +static JSON_INLINE +int json_array_set(json_t *array, size_t index, json_t *value) +{ + return json_array_set_new(array, index, json_incref(value)); +} + +static JSON_INLINE +int json_array_append(json_t *array, json_t *value) +{ + return json_array_append_new(array, json_incref(value)); +} + +static JSON_INLINE +int json_array_insert(json_t *array, size_t index, json_t *value) +{ + return json_array_insert_new(array, index, json_incref(value)); +} + +const char *json_string_value(const json_t *string); +json_int_t json_integer_value(const json_t *integer); +double json_real_value(const json_t *real); +double json_number_value(const json_t *json); + +int json_string_set(json_t *string, const char *value); +int json_string_set_nocheck(json_t *string, const char *value); +int json_integer_set(json_t *integer, json_int_t value); +int json_real_set(json_t *real, double value); + +json_t *json_pack(json_error_t *error, const char *fmt, ...); +int json_unpack(json_t *root, json_error_t *error, const char *fmt, ...); + +/* equality */ + +int json_equal(json_t *value1, json_t *value2); + + +/* copying */ + +json_t *json_copy(json_t *value); +json_t *json_deep_copy(json_t *value); + + +/* loading, printing */ + +json_t *json_loads(const char *input, size_t flags, json_error_t *error); +json_t *json_loadf(FILE *input, size_t flags, json_error_t *error); +json_t *json_load_file(const char *path, size_t flags, json_error_t *error); + +#define JSON_INDENT(n) (n & 0x1F) +#define JSON_COMPACT 0x20 +#define JSON_ENSURE_ASCII 0x40 +#define JSON_SORT_KEYS 0x80 +#define JSON_PRESERVE_ORDER 0x100 + +char *json_dumps(const json_t *json, size_t flags); +int json_dumpf(const json_t *json, FILE *output, size_t flags); +int json_dump_file(const json_t *json, const char *path, size_t flags); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/jansson/src/jansson_config.h b/jansson/src/jansson_config.h new file mode 100644 index 0000000..ff0f43f --- /dev/null +++ b/jansson/src/jansson_config.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010 Petri Lehtinen + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + * + * + * This file specifies a part of the site-specific configuration for + * Jansson, namely those things that affect the public API in + * jansson.h. + * + * The configure script copies this file to jansson_config.h and + * replaces @var@ substitutions by values that fit your system. If you + * cannot run the configure script, you can do the value substitution + * by hand. + */ + +#ifndef JANSSON_CONFIG_H +#define JANSSON_CONFIG_H + +/* If your compiler supports the inline keyword in C, JSON_INLINE is + defined to `inline', otherwise empty. In C++, the inline is always + supported. */ +#ifdef __cplusplus +#define JSON_INLINE inline +#else +#define JSON_INLINE inline +#endif + +/* If your compiler supports the `long long` type, + JSON_INTEGER_IS_LONG_LONG is defined to 1, otherwise to 0. */ +#define JSON_INTEGER_IS_LONG_LONG 1 + +#endif diff --git a/jansson/src/jansson_config.h.in b/jansson/src/jansson_config.h.in new file mode 100644 index 0000000..d2a9392 --- /dev/null +++ b/jansson/src/jansson_config.h.in @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010 Petri Lehtinen + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + * + * + * This file specifies a part of the site-specific configuration for + * Jansson, namely those things that affect the public API in + * jansson.h. + * + * The configure script copies this file to jansson_config.h and + * replaces @var@ substitutions by values that fit your system. If you + * cannot run the configure script, you can do the value substitution + * by hand. + */ + +#ifndef JANSSON_CONFIG_H +#define JANSSON_CONFIG_H + +/* If your compiler supports the inline keyword in C, JSON_INLINE is + defined to `inline', otherwise empty. In C++, the inline is always + supported. */ +#ifdef __cplusplus +#define JSON_INLINE inline +#else +#define JSON_INLINE @json_inline@ +#endif + +/* If your compiler supports the `long long` type, + JSON_INTEGER_IS_LONG_LONG is defined to 1, otherwise to 0. */ +#define JSON_INTEGER_IS_LONG_LONG @json_have_long_long@ + +#endif diff --git a/jansson/src/jansson_config.h.win32 b/jansson/src/jansson_config.h.win32 new file mode 100644 index 0000000..ffb512f --- /dev/null +++ b/jansson/src/jansson_config.h.win32 @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010 Petri Lehtinen + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + * + * + * This file specifies a part of the site-specific configuration for + * Jansson, namely those things that affect the public API in + * jansson.h. + * + * The configure script copies this file to jansson_config.h and + * replaces @var@ substitutions by values that fit your system. If you + * cannot run the configure script, you can do the value substitution + * by hand. + */ + +#ifndef JANSSON_CONFIG_H +#define JANSSON_CONFIG_H + +/* If your compiler supports the inline keyword in C, JSON_INLINE is + defined to `inline', otherwise empty. In C++, the inline is always + supported. */ +#ifdef __cplusplus +#define JSON_INLINE inline +#else +#define JSON_INLINE +#endif + +/* If your compiler supports the `long long` type, + JSON_INTEGER_IS_LONG_LONG is defined to 1, otherwise to 0. */ +#define JSON_INTEGER_IS_LONG_LONG 1 + +#endif diff --git a/jansson/src/jansson_private.h b/jansson/src/jansson_private.h new file mode 100644 index 0000000..951780c --- /dev/null +++ b/jansson/src/jansson_private.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2009, 2010 Petri Lehtinen + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#ifndef JANSSON_PRIVATE_H +#define JANSSON_PRIVATE_H + +#include +#include "jansson.h" +#include "hashtable.h" + +#define container_of(ptr_, type_, member_) \ + ((type_ *)((char *)ptr_ - offsetof(type_, member_))) + +/* On some platforms, max() may already be defined */ +#ifndef max +#define max(a, b) ((a) > (b) ? (a) : (b)) +#endif + +typedef struct { + json_t json; + hashtable_t hashtable; + size_t serial; + int visited; +} json_object_t; + +typedef struct { + json_t json; + size_t size; + size_t entries; + json_t **table; + int visited; +} json_array_t; + +typedef struct { + json_t json; + char *value; +} json_string_t; + +typedef struct { + json_t json; + double value; +} json_real_t; + +typedef struct { + json_t json; + json_int_t value; +} json_integer_t; + +#define json_to_object(json_) container_of(json_, json_object_t, json) +#define json_to_array(json_) container_of(json_, json_array_t, json) +#define json_to_string(json_) container_of(json_, json_string_t, json) +#define json_to_real(json_) container_of(json_, json_real_t, json) +#define json_to_integer(json_) container_of(json_, json_integer_t, json) + +typedef struct { + size_t serial; + char key[1]; +} object_key_t; + +const object_key_t *jsonp_object_iter_fullkey(void *iter); + +void jsonp_error_init(json_error_t *error, const char *source); +void jsonp_error_set(json_error_t *error, int line, int column, + const char *msg, ...); + +#endif diff --git a/jansson/src/load.c b/jansson/src/load.c new file mode 100644 index 0000000..f05230b --- /dev/null +++ b/jansson/src/load.c @@ -0,0 +1,885 @@ +/* + * Copyright (c) 2009, 2010 Petri Lehtinen + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "jansson_private.h" +#include "strbuffer.h" +#include "utf.h" + +#define TOKEN_INVALID -1 +#define TOKEN_EOF 0 +#define TOKEN_STRING 256 +#define TOKEN_INTEGER 257 +#define TOKEN_REAL 258 +#define TOKEN_TRUE 259 +#define TOKEN_FALSE 260 +#define TOKEN_NULL 261 + +/* read one byte from stream, return EOF on end of file */ +typedef int (*get_func)(void *data); + +/* return non-zero if end of file has been reached */ +typedef int (*eof_func)(void *data); + +typedef struct { + get_func get; + eof_func eof; + void *data; + int stream_pos; + char buffer[5]; + int buffer_pos; +} stream_t; + + +typedef struct { + stream_t stream; + strbuffer_t saved_text; + int token; + int line, column; + union { + char *string; + json_int_t integer; + double real; + } value; +} lex_t; + + +/*** error reporting ***/ + +static void error_set(json_error_t *error, const lex_t *lex, + const char *msg, ...) +{ + va_list ap; + char msg_text[JSON_ERROR_TEXT_LENGTH]; + + int line = -1, col = -1; + const char *result = msg_text; + + if(!error) + return; + + va_start(ap, msg); + vsnprintf(msg_text, JSON_ERROR_TEXT_LENGTH, msg, ap); + va_end(ap); + + if(lex) + { + const char *saved_text = strbuffer_value(&lex->saved_text); + char msg_with_context[JSON_ERROR_TEXT_LENGTH]; + + line = lex->line; + + if(saved_text && saved_text[0]) + { + if(lex->saved_text.length <= 20) { + snprintf(msg_with_context, JSON_ERROR_TEXT_LENGTH, + "%s near '%s'", msg_text, saved_text); + result = msg_with_context; + } + } + else + { + snprintf(msg_with_context, JSON_ERROR_TEXT_LENGTH, + "%s near end of file", msg_text); + result = msg_with_context; + } + } + + jsonp_error_set(error, line, col, "%s", result); +} + + +/*** lexical analyzer ***/ + +static void +stream_init(stream_t *stream, get_func get, eof_func eof, void *data) +{ + stream->get = get; + stream->eof = eof; + stream->data = data; + stream->stream_pos = 0; + stream->buffer[0] = '\0'; + stream->buffer_pos = 0; +} + +static char stream_get(stream_t *stream, json_error_t *error) +{ + char c; + + if(!stream->buffer[stream->buffer_pos]) + { + stream->buffer[0] = stream->get(stream->data); + stream->buffer_pos = 0; + + c = stream->buffer[0]; + + if((unsigned char)c >= 0x80 && c != (char)EOF) + { + /* multi-byte UTF-8 sequence */ + int i, count; + + count = utf8_check_first(c); + if(!count) + goto out; + + assert(count >= 2); + + for(i = 1; i < count; i++) + stream->buffer[i] = stream->get(stream->data); + + if(!utf8_check_full(stream->buffer, count, NULL)) + goto out; + + stream->stream_pos += count; + stream->buffer[count] = '\0'; + } + else { + stream->buffer[1] = '\0'; + stream->stream_pos++; + } + } + + return stream->buffer[stream->buffer_pos++]; + +out: + error_set(error, NULL, "unable to decode byte 0x%x at position %d", + (unsigned char)c, stream->stream_pos); + + stream->buffer[0] = EOF; + stream->buffer[1] = '\0'; + stream->buffer_pos = 1; + + return EOF; +} + +static void stream_unget(stream_t *stream, char c) +{ + assert(stream->buffer_pos > 0); + stream->buffer_pos--; + assert(stream->buffer[stream->buffer_pos] == c); +} + + +static int lex_get(lex_t *lex, json_error_t *error) +{ + return stream_get(&lex->stream, error); +} + +static int lex_eof(lex_t *lex) +{ + return lex->stream.eof(lex->stream.data); +} + +static void lex_save(lex_t *lex, char c) +{ + strbuffer_append_byte(&lex->saved_text, c); +} + +static int lex_get_save(lex_t *lex, json_error_t *error) +{ + char c = stream_get(&lex->stream, error); + lex_save(lex, c); + return c; +} + +static void lex_unget_unsave(lex_t *lex, char c) +{ + char d; + stream_unget(&lex->stream, c); + d = strbuffer_pop(&lex->saved_text); + assert(c == d); +} + +static void lex_save_cached(lex_t *lex) +{ + while(lex->stream.buffer[lex->stream.buffer_pos] != '\0') + { + lex_save(lex, lex->stream.buffer[lex->stream.buffer_pos]); + lex->stream.buffer_pos++; + } +} + +/* assumes that str points to 'u' plus at least 4 valid hex digits */ +static int32_t decode_unicode_escape(const char *str) +{ + int i; + int32_t value = 0; + + assert(str[0] == 'u'); + + for(i = 1; i <= 4; i++) { + char c = str[i]; + value <<= 4; + if(isdigit(c)) + value += c - '0'; + else if(islower(c)) + value += c - 'a' + 10; + else if(isupper(c)) + value += c - 'A' + 10; + else + assert(0); + } + + return value; +} + +static void lex_scan_string(lex_t *lex, json_error_t *error) +{ + char c; + const char *p; + char *t; + int i; + + lex->value.string = NULL; + lex->token = TOKEN_INVALID; + + c = lex_get_save(lex, error); + + while(c != '"') { + if(c == (char)EOF) { + lex_unget_unsave(lex, c); + if(lex_eof(lex)) + error_set(error, lex, "premature end of input"); + goto out; + } + + else if((unsigned char)c <= 0x1F) { + /* control character */ + lex_unget_unsave(lex, c); + if(c == '\n') + error_set(error, lex, "unexpected newline", c); + else + error_set(error, lex, "control character 0x%x", c); + goto out; + } + + else if(c == '\\') { + c = lex_get_save(lex, error); + if(c == 'u') { + c = lex_get_save(lex, error); + for(i = 0; i < 4; i++) { + if(!isxdigit(c)) { + lex_unget_unsave(lex, c); + error_set(error, lex, "invalid escape"); + goto out; + } + c = lex_get_save(lex, error); + } + } + else if(c == '"' || c == '\\' || c == '/' || c == 'b' || + c == 'f' || c == 'n' || c == 'r' || c == 't') + c = lex_get_save(lex, error); + else { + lex_unget_unsave(lex, c); + error_set(error, lex, "invalid escape"); + goto out; + } + } + else + c = lex_get_save(lex, error); + } + + /* the actual value is at most of the same length as the source + string, because: + - shortcut escapes (e.g. "\t") (length 2) are converted to 1 byte + - a single \uXXXX escape (length 6) is converted to at most 3 bytes + - two \uXXXX escapes (length 12) forming an UTF-16 surrogate pair + are converted to 4 bytes + */ + lex->value.string = malloc(lex->saved_text.length + 1); + if(!lex->value.string) { + /* this is not very nice, since TOKEN_INVALID is returned */ + goto out; + } + + /* the target */ + t = lex->value.string; + + /* + 1 to skip the " */ + p = strbuffer_value(&lex->saved_text) + 1; + + while(*p != '"') { + if(*p == '\\') { + p++; + if(*p == 'u') { + char buffer[4]; + int length; + int32_t value; + + value = decode_unicode_escape(p); + p += 5; + + if(0xD800 <= value && value <= 0xDBFF) { + /* surrogate pair */ + if(*p == '\\' && *(p + 1) == 'u') { + int32_t value2 = decode_unicode_escape(++p); + p += 5; + + if(0xDC00 <= value2 && value2 <= 0xDFFF) { + /* valid second surrogate */ + value = + ((value - 0xD800) << 10) + + (value2 - 0xDC00) + + 0x10000; + } + else { + /* invalid second surrogate */ + error_set(error, lex, + "invalid Unicode '\\u%04X\\u%04X'", + value, value2); + goto out; + } + } + else { + /* no second surrogate */ + error_set(error, lex, "invalid Unicode '\\u%04X'", + value); + goto out; + } + } + else if(0xDC00 <= value && value <= 0xDFFF) { + error_set(error, lex, "invalid Unicode '\\u%04X'", value); + goto out; + } + else if(value == 0) + { + error_set(error, lex, "\\u0000 is not allowed"); + goto out; + } + + if(utf8_encode(value, buffer, &length)) + assert(0); + + memcpy(t, buffer, length); + t += length; + } + else { + switch(*p) { + case '"': case '\\': case '/': + *t = *p; break; + case 'b': *t = '\b'; break; + case 'f': *t = '\f'; break; + case 'n': *t = '\n'; break; + case 'r': *t = '\r'; break; + case 't': *t = '\t'; break; + default: assert(0); + } + t++; + p++; + } + } + else + *(t++) = *(p++); + } + *t = '\0'; + lex->token = TOKEN_STRING; + return; + +out: + free(lex->value.string); +} + +#if JSON_INTEGER_IS_LONG_LONG +#define json_strtoint strtoll +#else +#define json_strtoint strtol +#endif + +static int lex_scan_number(lex_t *lex, char c, json_error_t *error) +{ + const char *saved_text; + char *end; + double value; + + lex->token = TOKEN_INVALID; + + if(c == '-') + c = lex_get_save(lex, error); + + if(c == '0') { + c = lex_get_save(lex, error); + if(isdigit(c)) { + lex_unget_unsave(lex, c); + goto out; + } + } + else if(isdigit(c)) { + c = lex_get_save(lex, error); + while(isdigit(c)) + c = lex_get_save(lex, error); + } + else { + lex_unget_unsave(lex, c); + goto out; + } + + if(c != '.' && c != 'E' && c != 'e') { + json_int_t value; + + lex_unget_unsave(lex, c); + + saved_text = strbuffer_value(&lex->saved_text); + + errno = 0; + value = json_strtoint(saved_text, &end, 10); + if(errno == ERANGE) { + if(value < 0) + error_set(error, lex, "too big negative integer"); + else + error_set(error, lex, "too big integer"); + goto out; + } + + assert(end == saved_text + lex->saved_text.length); + + lex->token = TOKEN_INTEGER; + lex->value.integer = value; + return 0; + } + + if(c == '.') { + c = lex_get(lex, error); + if(!isdigit(c)) + goto out; + lex_save(lex, c); + + c = lex_get_save(lex, error); + while(isdigit(c)) + c = lex_get_save(lex, error); + } + + if(c == 'E' || c == 'e') { + c = lex_get_save(lex, error); + if(c == '+' || c == '-') + c = lex_get_save(lex, error); + + if(!isdigit(c)) { + lex_unget_unsave(lex, c); + goto out; + } + + c = lex_get_save(lex, error); + while(isdigit(c)) + c = lex_get_save(lex, error); + } + + lex_unget_unsave(lex, c); + + saved_text = strbuffer_value(&lex->saved_text); + value = strtod(saved_text, &end); + assert(end == saved_text + lex->saved_text.length); + + if(errno == ERANGE && value != 0) { + error_set(error, lex, "real number overflow"); + goto out; + } + + lex->token = TOKEN_REAL; + lex->value.real = value; + return 0; + +out: + return -1; +} + +static int lex_scan(lex_t *lex, json_error_t *error) +{ + char c; + + strbuffer_clear(&lex->saved_text); + + if(lex->token == TOKEN_STRING) { + free(lex->value.string); + lex->value.string = NULL; + } + + c = lex_get(lex, error); + while(c == ' ' || c == '\t' || c == '\n' || c == '\r') + { + if(c == '\n') + lex->line++; + + c = lex_get(lex, error); + } + + if(c == (char)EOF) { + if(lex_eof(lex)) + lex->token = TOKEN_EOF; + else + lex->token = TOKEN_INVALID; + goto out; + } + + lex_save(lex, c); + + if(c == '{' || c == '}' || c == '[' || c == ']' || c == ':' || c == ',') + lex->token = c; + + else if(c == '"') + lex_scan_string(lex, error); + + else if(isdigit(c) || c == '-') { + if(lex_scan_number(lex, c, error)) + goto out; + } + + else if(isupper(c) || islower(c)) { + /* eat up the whole identifier for clearer error messages */ + const char *saved_text; + + c = lex_get_save(lex, error); + while(isupper(c) || islower(c)) + c = lex_get_save(lex, error); + lex_unget_unsave(lex, c); + + saved_text = strbuffer_value(&lex->saved_text); + + if(strcmp(saved_text, "true") == 0) + lex->token = TOKEN_TRUE; + else if(strcmp(saved_text, "false") == 0) + lex->token = TOKEN_FALSE; + else if(strcmp(saved_text, "null") == 0) + lex->token = TOKEN_NULL; + else + lex->token = TOKEN_INVALID; + } + + else { + /* save the rest of the input UTF-8 sequence to get an error + message of valid UTF-8 */ + lex_save_cached(lex); + lex->token = TOKEN_INVALID; + } + +out: + return lex->token; +} + +static char *lex_steal_string(lex_t *lex) +{ + char *result = NULL; + if(lex->token == TOKEN_STRING) + { + result = lex->value.string; + lex->value.string = NULL; + } + return result; +} + +static int lex_init(lex_t *lex, get_func get, eof_func eof, void *data) +{ + stream_init(&lex->stream, get, eof, data); + if(strbuffer_init(&lex->saved_text)) + return -1; + + lex->token = TOKEN_INVALID; + lex->line = 1; + + return 0; +} + +static void lex_close(lex_t *lex) +{ + if(lex->token == TOKEN_STRING) + free(lex->value.string); + strbuffer_close(&lex->saved_text); +} + + +/*** parser ***/ + +static json_t *parse_value(lex_t *lex, json_error_t *error); + +static json_t *parse_object(lex_t *lex, json_error_t *error) +{ + json_t *object = json_object(); + if(!object) + return NULL; + + lex_scan(lex, error); + if(lex->token == '}') + return object; + + while(1) { + char *key; + json_t *value; + + if(lex->token != TOKEN_STRING) { + error_set(error, lex, "string or '}' expected"); + goto error; + } + + key = lex_steal_string(lex); + if(!key) + return NULL; + + lex_scan(lex, error); + if(lex->token != ':') { + free(key); + error_set(error, lex, "':' expected"); + goto error; + } + + lex_scan(lex, error); + value = parse_value(lex, error); + if(!value) { + free(key); + goto error; + } + + if(json_object_set_nocheck(object, key, value)) { + free(key); + json_decref(value); + goto error; + } + + json_decref(value); + free(key); + + lex_scan(lex, error); + if(lex->token != ',') + break; + + lex_scan(lex, error); + } + + if(lex->token != '}') { + error_set(error, lex, "'}' expected"); + goto error; + } + + return object; + +error: + json_decref(object); + return NULL; +} + +static json_t *parse_array(lex_t *lex, json_error_t *error) +{ + json_t *array = json_array(); + if(!array) + return NULL; + + lex_scan(lex, error); + if(lex->token == ']') + return array; + + while(lex->token) { + json_t *elem = parse_value(lex, error); + if(!elem) + goto error; + + if(json_array_append(array, elem)) { + json_decref(elem); + goto error; + } + json_decref(elem); + + lex_scan(lex, error); + if(lex->token != ',') + break; + + lex_scan(lex, error); + } + + if(lex->token != ']') { + error_set(error, lex, "']' expected"); + goto error; + } + + return array; + +error: + json_decref(array); + return NULL; +} + +static json_t *parse_value(lex_t *lex, json_error_t *error) +{ + json_t *json; + + switch(lex->token) { + case TOKEN_STRING: { + json = json_string_nocheck(lex->value.string); + break; + } + + case TOKEN_INTEGER: { + json = json_integer(lex->value.integer); + break; + } + + case TOKEN_REAL: { + json = json_real(lex->value.real); + break; + } + + case TOKEN_TRUE: + json = json_true(); + break; + + case TOKEN_FALSE: + json = json_false(); + break; + + case TOKEN_NULL: + json = json_null(); + break; + + case '{': + json = parse_object(lex, error); + break; + + case '[': + json = parse_array(lex, error); + break; + + case TOKEN_INVALID: + error_set(error, lex, "invalid token"); + return NULL; + + default: + error_set(error, lex, "unexpected token"); + return NULL; + } + + if(!json) + return NULL; + + return json; +} + +static json_t *parse_json(lex_t *lex, json_error_t *error) +{ + lex_scan(lex, error); + if(lex->token != '[' && lex->token != '{') { + error_set(error, lex, "'[' or '{' expected"); + return NULL; + } + + return parse_value(lex, error); +} + +typedef struct +{ + const char *data; + int pos; +} string_data_t; + +static int string_get(void *data) +{ + char c; + string_data_t *stream = (string_data_t *)data; + c = stream->data[stream->pos]; + if(c == '\0') + return EOF; + else + { + stream->pos++; + return c; + } +} + +static int string_eof(void *data) +{ + string_data_t *stream = (string_data_t *)data; + return (stream->data[stream->pos] == '\0'); +} + +json_t *json_loads(const char *string, size_t flags, json_error_t *error) +{ + lex_t lex; + json_t *result; + (void)flags; /* unused */ + + string_data_t stream_data = {string, 0}; + + if(lex_init(&lex, string_get, string_eof, (void *)&stream_data)) + return NULL; + + jsonp_error_init(error, ""); + + result = parse_json(&lex, error); + if(!result) + goto out; + + lex_scan(&lex, error); + if(lex.token != TOKEN_EOF) { + error_set(error, &lex, "end of file expected"); + json_decref(result); + result = NULL; + } + +out: + lex_close(&lex); + return result; +} + +json_t *json_loadf(FILE *input, size_t flags, json_error_t *error) +{ + lex_t lex; + const char *source; + json_t *result; + (void)flags; /* unused */ + + if(lex_init(&lex, (get_func)fgetc, (eof_func)feof, input)) + return NULL; + + if(input == stdin) + source = ""; + else + source = ""; + + jsonp_error_init(error, source); + + result = parse_json(&lex, error); + if(!result) + goto out; + + lex_scan(&lex, error); + if(lex.token != TOKEN_EOF) { + error_set(error, &lex, "end of file expected"); + json_decref(result); + result = NULL; + } + +out: + lex_close(&lex); + return result; +} + +json_t *json_load_file(const char *path, size_t flags, json_error_t *error) +{ + json_t *result; + FILE *fp; + + jsonp_error_init(error, path); + + fp = fopen(path, "r"); + if(!fp) + { + error_set(error, NULL, "unable to open %s: %s", + path, strerror(errno)); + return NULL; + } + + result = json_loadf(fp, flags, error); + + fclose(fp); + return result; +} diff --git a/jansson/src/strbuffer.c b/jansson/src/strbuffer.c new file mode 100644 index 0000000..4e866bd --- /dev/null +++ b/jansson/src/strbuffer.c @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2009, 2010 Petri Lehtinen + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#define _GNU_SOURCE +#include +#include +#include "jansson_private.h" +#include "strbuffer.h" + +#define STRBUFFER_MIN_SIZE 16 +#define STRBUFFER_FACTOR 2 + +int strbuffer_init(strbuffer_t *strbuff) +{ + strbuff->size = STRBUFFER_MIN_SIZE; + strbuff->length = 0; + + strbuff->value = malloc(strbuff->size); + if(!strbuff->value) + return -1; + + /* initialize to empty */ + strbuff->value[0] = '\0'; + return 0; +} + +void strbuffer_close(strbuffer_t *strbuff) +{ + free(strbuff->value); + strbuff->size = 0; + strbuff->length = 0; + strbuff->value = NULL; +} + +void strbuffer_clear(strbuffer_t *strbuff) +{ + strbuff->length = 0; + strbuff->value[0] = '\0'; +} + +const char *strbuffer_value(const strbuffer_t *strbuff) +{ + return strbuff->value; +} + +char *strbuffer_steal_value(strbuffer_t *strbuff) +{ + char *result = strbuff->value; + strbuffer_init(strbuff); + return result; +} + +int strbuffer_append(strbuffer_t *strbuff, const char *string) +{ + return strbuffer_append_bytes(strbuff, string, strlen(string)); +} + +int strbuffer_append_byte(strbuffer_t *strbuff, char byte) +{ + return strbuffer_append_bytes(strbuff, &byte, 1); +} + +int strbuffer_append_bytes(strbuffer_t *strbuff, const char *data, int size) +{ + if(strbuff->length + size >= strbuff->size) + { + strbuff->size = max(strbuff->size * STRBUFFER_FACTOR, + strbuff->length + size + 1); + + strbuff->value = realloc(strbuff->value, strbuff->size); + if(!strbuff->value) + return -1; + } + + memcpy(strbuff->value + strbuff->length, data, size); + strbuff->length += size; + strbuff->value[strbuff->length] = '\0'; + + return 0; +} + +char strbuffer_pop(strbuffer_t *strbuff) +{ + if(strbuff->length > 0) { + char c = strbuff->value[--strbuff->length]; + strbuff->value[strbuff->length] = '\0'; + return c; + } + else + return '\0'; +} diff --git a/jansson/src/strbuffer.h b/jansson/src/strbuffer.h new file mode 100644 index 0000000..f4c5f77 --- /dev/null +++ b/jansson/src/strbuffer.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2009, 2010 Petri Lehtinen + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#ifndef STRBUFFER_H +#define STRBUFFER_H + +typedef struct { + char *value; + int length; /* bytes used */ + int size; /* bytes allocated */ +} strbuffer_t; + +int strbuffer_init(strbuffer_t *strbuff); +void strbuffer_close(strbuffer_t *strbuff); + +void strbuffer_clear(strbuffer_t *strbuff); + +const char *strbuffer_value(const strbuffer_t *strbuff); +char *strbuffer_steal_value(strbuffer_t *strbuff); + +int strbuffer_append(strbuffer_t *strbuff, const char *string); +int strbuffer_append_byte(strbuffer_t *strbuff, char byte); +int strbuffer_append_bytes(strbuffer_t *strbuff, const char *data, int size); + +char strbuffer_pop(strbuffer_t *strbuff); + +#endif diff --git a/jansson/src/utf.c b/jansson/src/utf.c new file mode 100644 index 0000000..92484d0 --- /dev/null +++ b/jansson/src/utf.c @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2009, 2010 Petri Lehtinen + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#include +#include "utf.h" + +int utf8_encode(int32_t codepoint, char *buffer, int *size) +{ + if(codepoint < 0) + return -1; + else if(codepoint < 0x80) + { + buffer[0] = (char)codepoint; + *size = 1; + } + else if(codepoint < 0x800) + { + buffer[0] = 0xC0 + ((codepoint & 0x7C0) >> 6); + buffer[1] = 0x80 + ((codepoint & 0x03F)); + *size = 2; + } + else if(codepoint < 0x10000) + { + buffer[0] = 0xE0 + ((codepoint & 0xF000) >> 12); + buffer[1] = 0x80 + ((codepoint & 0x0FC0) >> 6); + buffer[2] = 0x80 + ((codepoint & 0x003F)); + *size = 3; + } + else if(codepoint <= 0x10FFFF) + { + buffer[0] = 0xF0 + ((codepoint & 0x1C0000) >> 18); + buffer[1] = 0x80 + ((codepoint & 0x03F000) >> 12); + buffer[2] = 0x80 + ((codepoint & 0x000FC0) >> 6); + buffer[3] = 0x80 + ((codepoint & 0x00003F)); + *size = 4; + } + else + return -1; + + return 0; +} + +int utf8_check_first(char byte) +{ + unsigned char u = (unsigned char)byte; + + if(u < 0x80) + return 1; + + if(0x80 <= u && u <= 0xBF) { + /* second, third or fourth byte of a multi-byte + sequence, i.e. a "continuation byte" */ + return 0; + } + else if(u == 0xC0 || u == 0xC1) { + /* overlong encoding of an ASCII byte */ + return 0; + } + else if(0xC2 <= u && u <= 0xDF) { + /* 2-byte sequence */ + return 2; + } + + else if(0xE0 <= u && u <= 0xEF) { + /* 3-byte sequence */ + return 3; + } + else if(0xF0 <= u && u <= 0xF4) { + /* 4-byte sequence */ + return 4; + } + else { /* u >= 0xF5 */ + /* Restricted (start of 4-, 5- or 6-byte sequence) or invalid + UTF-8 */ + return 0; + } +} + +int utf8_check_full(const char *buffer, int size, int32_t *codepoint) +{ + int i; + int32_t value = 0; + unsigned char u = (unsigned char)buffer[0]; + + if(size == 2) + { + value = u & 0x1F; + } + else if(size == 3) + { + value = u & 0xF; + } + else if(size == 4) + { + value = u & 0x7; + } + else + return 0; + + for(i = 1; i < size; i++) + { + u = (unsigned char)buffer[i]; + + if(u < 0x80 || u > 0xBF) { + /* not a continuation byte */ + return 0; + } + + value = (value << 6) + (u & 0x3F); + } + + if(value > 0x10FFFF) { + /* not in Unicode range */ + return 0; + } + + else if(0xD800 <= value && value <= 0xDFFF) { + /* invalid code point (UTF-16 surrogate halves) */ + return 0; + } + + else if((size == 2 && value < 0x80) || + (size == 3 && value < 0x800) || + (size == 4 && value < 0x10000)) { + /* overlong encoding */ + return 0; + } + + if(codepoint) + *codepoint = value; + + return 1; +} + +const char *utf8_iterate(const char *buffer, int32_t *codepoint) +{ + int count; + int32_t value; + + if(!*buffer) + return buffer; + + count = utf8_check_first(buffer[0]); + if(count <= 0) + return NULL; + + if(count == 1) + value = (unsigned char)buffer[0]; + else + { + if(!utf8_check_full(buffer, count, &value)) + return NULL; + } + + if(codepoint) + *codepoint = value; + + return buffer + count; +} + +int utf8_check_string(const char *string, int length) +{ + int i; + + if(length == -1) + length = strlen(string); + + for(i = 0; i < length; i++) + { + int count = utf8_check_first(string[i]); + if(count == 0) + return 0; + else if(count > 1) + { + if(i + count > length) + return 0; + + if(!utf8_check_full(&string[i], count, NULL)) + return 0; + + i += count - 1; + } + } + + return 1; +} diff --git a/jansson/src/utf.h b/jansson/src/utf.h new file mode 100644 index 0000000..862c548 --- /dev/null +++ b/jansson/src/utf.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2009, 2010 Petri Lehtinen + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#ifndef UTF_H +#define UTF_H + +#ifdef HAVE_CONFIG_H +#include + +#ifdef HAVE_INTTYPES_H +/* inttypes.h includes stdint.h in a standard environment, so there's +no need to include stdint.h separately. If inttypes.h doesn't define +int32_t, it's defined in config.h. */ +#include +#endif /* HAVE_INTTYPES_H */ + +#else /* !HAVE_CONFIG_H */ +#ifdef _WIN32 +typedef int int32_t; +#else /* !_WIN32 */ +/* Assume a standard environment */ +#include +#endif /* _WIN32 */ + +#endif /* HAVE_CONFIG_H */ + +int utf8_encode(int codepoint, char *buffer, int *size); + +int utf8_check_first(char byte); +int utf8_check_full(const char *buffer, int size, int32_t *codepoint); +const char *utf8_iterate(const char *buffer, int32_t *codepoint); + +int utf8_check_string(const char *string, int length); + +#endif diff --git a/jansson/src/value.c b/jansson/src/value.c new file mode 100644 index 0000000..89d7b71 --- /dev/null +++ b/jansson/src/value.c @@ -0,0 +1,967 @@ +/* + * Copyright (c) 2009, 2010 Petri Lehtinen + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#define _GNU_SOURCE + +#include +#include +#include + +#include +#include "hashtable.h" +#include "jansson_private.h" +#include "utf.h" + + +static JSON_INLINE void json_init(json_t *json, json_type type) +{ + json->type = type; + json->refcount = 1; +} + + +/*** object ***/ + +/* This macro just returns a pointer that's a few bytes backwards from + string. This makes it possible to pass a pointer to object_key_t + when only the string inside it is used, without actually creating + an object_key_t instance. */ +#define string_to_key(string) container_of(string, object_key_t, key) + +static size_t hash_key(const void *ptr) +{ + const char *str = ((const object_key_t *)ptr)->key; + + size_t hash = 5381; + size_t c; + + while((c = (size_t)*str)) + { + hash = ((hash << 5) + hash) + c; + str++; + } + + return hash; +} + +static int key_equal(const void *ptr1, const void *ptr2) +{ + return strcmp(((const object_key_t *)ptr1)->key, + ((const object_key_t *)ptr2)->key) == 0; +} + +static void value_decref(void *value) +{ + json_decref((json_t *)value); +} + +json_t *json_object(void) +{ + json_object_t *object = malloc(sizeof(json_object_t)); + if(!object) + return NULL; + json_init(&object->json, JSON_OBJECT); + + if(hashtable_init(&object->hashtable, hash_key, key_equal, + free, value_decref)) + { + free(object); + return NULL; + } + + object->serial = 0; + object->visited = 0; + + return &object->json; +} + +static void json_delete_object(json_object_t *object) +{ + hashtable_close(&object->hashtable); + free(object); +} + +size_t json_object_size(const json_t *json) +{ + json_object_t *object; + + if(!json_is_object(json)) + return -1; + + object = json_to_object(json); + return object->hashtable.size; +} + +json_t *json_object_get(const json_t *json, const char *key) +{ + json_object_t *object; + + if(!json_is_object(json)) + return NULL; + + object = json_to_object(json); + return hashtable_get(&object->hashtable, string_to_key(key)); +} + +int json_object_set_new_nocheck(json_t *json, const char *key, json_t *value) +{ + json_object_t *object; + object_key_t *k; + + if(!key || !value) + return -1; + + if(!json_is_object(json) || json == value) + { + json_decref(value); + return -1; + } + object = json_to_object(json); + + /* offsetof(...) returns the size of object_key_t without the + last, flexible member. This way, the correct amount is + allocated. */ + k = malloc(offsetof(object_key_t, key) + + strlen(key) + 1); if(!k) return -1; + + k->serial = object->serial++; + strcpy(k->key, key); + + if(hashtable_set(&object->hashtable, k, value)) + { + json_decref(value); + return -1; + } + + return 0; +} + +int json_object_set_new(json_t *json, const char *key, json_t *value) +{ + if(!key || !utf8_check_string(key, -1)) + { + json_decref(value); + return -1; + } + + return json_object_set_new_nocheck(json, key, value); +} + +int json_object_del(json_t *json, const char *key) +{ + json_object_t *object; + + if(!json_is_object(json)) + return -1; + + object = json_to_object(json); + return hashtable_del(&object->hashtable, string_to_key(key)); +} + +int json_object_clear(json_t *json) +{ + json_object_t *object; + + if(!json_is_object(json)) + return -1; + + object = json_to_object(json); + hashtable_clear(&object->hashtable); + + return 0; +} + +int json_object_update(json_t *object, json_t *other) +{ + void *iter; + + if(!json_is_object(object) || !json_is_object(other)) + return -1; + + iter = json_object_iter(other); + while(iter) { + const char *key; + json_t *value; + + key = json_object_iter_key(iter); + value = json_object_iter_value(iter); + + if(json_object_set_nocheck(object, key, value)) + return -1; + + iter = json_object_iter_next(other, iter); + } + + return 0; +} + +void *json_object_iter(json_t *json) +{ + json_object_t *object; + + if(!json_is_object(json)) + return NULL; + + object = json_to_object(json); + return hashtable_iter(&object->hashtable); +} + +void *json_object_iter_at(json_t *json, const char *key) +{ + json_object_t *object; + + if(!key || !json_is_object(json)) + return NULL; + + object = json_to_object(json); + return hashtable_iter_at(&object->hashtable, string_to_key(key)); +} + +void *json_object_iter_next(json_t *json, void *iter) +{ + json_object_t *object; + + if(!json_is_object(json) || iter == NULL) + return NULL; + + object = json_to_object(json); + return hashtable_iter_next(&object->hashtable, iter); +} + +const object_key_t *jsonp_object_iter_fullkey(void *iter) +{ + if(!iter) + return NULL; + + return hashtable_iter_key(iter); +} + +const char *json_object_iter_key(void *iter) +{ + if(!iter) + return NULL; + + return jsonp_object_iter_fullkey(iter)->key; +} + +json_t *json_object_iter_value(void *iter) +{ + if(!iter) + return NULL; + + return (json_t *)hashtable_iter_value(iter); +} + +int json_object_iter_set_new(json_t *json, void *iter, json_t *value) +{ + json_object_t *object; + + if(!json_is_object(json) || !iter || !value) + return -1; + + object = json_to_object(json); + hashtable_iter_set(&object->hashtable, iter, value); + + return 0; +} + +static int json_object_equal(json_t *object1, json_t *object2) +{ + void *iter; + + if(json_object_size(object1) != json_object_size(object2)) + return 0; + + iter = json_object_iter(object1); + while(iter) + { + const char *key; + json_t *value1, *value2; + + key = json_object_iter_key(iter); + value1 = json_object_iter_value(iter); + value2 = json_object_get(object2, key); + + if(!json_equal(value1, value2)) + return 0; + + iter = json_object_iter_next(object1, iter); + } + + return 1; +} + +static json_t *json_object_copy(json_t *object) +{ + json_t *result; + void *iter; + + result = json_object(); + if(!result) + return NULL; + + iter = json_object_iter(object); + while(iter) + { + const char *key; + json_t *value; + + key = json_object_iter_key(iter); + value = json_object_iter_value(iter); + json_object_set_nocheck(result, key, value); + + iter = json_object_iter_next(object, iter); + } + + return result; +} + +static json_t *json_object_deep_copy(json_t *object) +{ + json_t *result; + void *iter; + + result = json_object(); + if(!result) + return NULL; + + iter = json_object_iter(object); + while(iter) + { + const char *key; + json_t *value; + + key = json_object_iter_key(iter); + value = json_object_iter_value(iter); + json_object_set_new_nocheck(result, key, json_deep_copy(value)); + + iter = json_object_iter_next(object, iter); + } + + return result; +} + + +/*** array ***/ + +json_t *json_array(void) +{ + json_array_t *array = malloc(sizeof(json_array_t)); + if(!array) + return NULL; + json_init(&array->json, JSON_ARRAY); + + array->entries = 0; + array->size = 8; + + array->table = malloc(array->size * sizeof(json_t *)); + if(!array->table) { + free(array); + return NULL; + } + + array->visited = 0; + + return &array->json; +} + +static void json_delete_array(json_array_t *array) +{ + size_t i; + + for(i = 0; i < array->entries; i++) + json_decref(array->table[i]); + + free(array->table); + free(array); +} + +size_t json_array_size(const json_t *json) +{ + if(!json_is_array(json)) + return 0; + + return json_to_array(json)->entries; +} + +json_t *json_array_get(const json_t *json, size_t index) +{ + json_array_t *array; + if(!json_is_array(json)) + return NULL; + array = json_to_array(json); + + if(index >= array->entries) + return NULL; + + return array->table[index]; +} + +int json_array_set_new(json_t *json, size_t index, json_t *value) +{ + json_array_t *array; + + if(!value) + return -1; + + if(!json_is_array(json) || json == value) + { + json_decref(value); + return -1; + } + array = json_to_array(json); + + if(index >= array->entries) + { + json_decref(value); + return -1; + } + + json_decref(array->table[index]); + array->table[index] = value; + + return 0; +} + +static void array_move(json_array_t *array, size_t dest, + size_t src, size_t count) +{ + memmove(&array->table[dest], &array->table[src], count * sizeof(json_t *)); +} + +static void array_copy(json_t **dest, size_t dpos, + json_t **src, size_t spos, + size_t count) +{ + memcpy(&dest[dpos], &src[spos], count * sizeof(json_t *)); +} + +static json_t **json_array_grow(json_array_t *array, + size_t amount, + int copy) +{ + size_t new_size; + json_t **old_table, **new_table; + + if(array->entries + amount <= array->size) + return array->table; + + old_table = array->table; + + new_size = max(array->size + amount, array->size * 2); + new_table = malloc(new_size * sizeof(json_t *)); + if(!new_table) + return NULL; + + array->size = new_size; + array->table = new_table; + + if(copy) { + array_copy(array->table, 0, old_table, 0, array->entries); + free(old_table); + return array->table; + } + + return old_table; +} + +int json_array_append_new(json_t *json, json_t *value) +{ + json_array_t *array; + + if(!value) + return -1; + + if(!json_is_array(json) || json == value) + { + json_decref(value); + return -1; + } + array = json_to_array(json); + + if(!json_array_grow(array, 1, 1)) { + json_decref(value); + return -1; + } + + array->table[array->entries] = value; + array->entries++; + + return 0; +} + +int json_array_insert_new(json_t *json, size_t index, json_t *value) +{ + json_array_t *array; + json_t **old_table; + + if(!value) + return -1; + + if(!json_is_array(json) || json == value) { + json_decref(value); + return -1; + } + array = json_to_array(json); + + if(index > array->entries) { + json_decref(value); + return -1; + } + + old_table = json_array_grow(array, 1, 0); + if(!old_table) { + json_decref(value); + return -1; + } + + if(old_table != array->table) { + array_copy(array->table, 0, old_table, 0, index); + array_copy(array->table, index + 1, old_table, index, + array->entries - index); + free(old_table); + } + else + array_move(array, index + 1, index, array->entries - index); + + array->table[index] = value; + array->entries++; + + return 0; +} + +int json_array_remove(json_t *json, size_t index) +{ + json_array_t *array; + + if(!json_is_array(json)) + return -1; + array = json_to_array(json); + + if(index >= array->entries) + return -1; + + json_decref(array->table[index]); + + array_move(array, index, index + 1, array->entries - index); + array->entries--; + + return 0; +} + +int json_array_clear(json_t *json) +{ + json_array_t *array; + size_t i; + + if(!json_is_array(json)) + return -1; + array = json_to_array(json); + + for(i = 0; i < array->entries; i++) + json_decref(array->table[i]); + + array->entries = 0; + return 0; +} + +int json_array_extend(json_t *json, json_t *other_json) +{ + json_array_t *array, *other; + size_t i; + + if(!json_is_array(json) || !json_is_array(other_json)) + return -1; + array = json_to_array(json); + other = json_to_array(other_json); + + if(!json_array_grow(array, other->entries, 1)) + return -1; + + for(i = 0; i < other->entries; i++) + json_incref(other->table[i]); + + array_copy(array->table, array->entries, other->table, 0, other->entries); + + array->entries += other->entries; + return 0; +} + +static int json_array_equal(json_t *array1, json_t *array2) +{ + size_t i, size; + + size = json_array_size(array1); + if(size != json_array_size(array2)) + return 0; + + for(i = 0; i < size; i++) + { + json_t *value1, *value2; + + value1 = json_array_get(array1, i); + value2 = json_array_get(array2, i); + + if(!json_equal(value1, value2)) + return 0; + } + + return 1; +} + +static json_t *json_array_copy(json_t *array) +{ + json_t *result; + size_t i; + + result = json_array(); + if(!result) + return NULL; + + for(i = 0; i < json_array_size(array); i++) + json_array_append(result, json_array_get(array, i)); + + return result; +} + +static json_t *json_array_deep_copy(json_t *array) +{ + json_t *result; + size_t i; + + result = json_array(); + if(!result) + return NULL; + + for(i = 0; i < json_array_size(array); i++) + json_array_append_new(result, json_deep_copy(json_array_get(array, i))); + + return result; +} + +/*** string ***/ + +json_t *json_string_nocheck(const char *value) +{ + json_string_t *string; + + if(!value) + return NULL; + + string = malloc(sizeof(json_string_t)); + if(!string) + return NULL; + json_init(&string->json, JSON_STRING); + + string->value = strdup(value); + if(!string->value) { + free(string); + return NULL; + } + + return &string->json; +} + +json_t *json_string(const char *value) +{ + if(!value || !utf8_check_string(value, -1)) + return NULL; + + return json_string_nocheck(value); +} + +const char *json_string_value(const json_t *json) +{ + if(!json_is_string(json)) + return NULL; + + return json_to_string(json)->value; +} + +int json_string_set_nocheck(json_t *json, const char *value) +{ + char *dup; + json_string_t *string; + + dup = strdup(value); + if(!dup) + return -1; + + string = json_to_string(json); + free(string->value); + string->value = dup; + + return 0; +} + +int json_string_set(json_t *json, const char *value) +{ + if(!value || !utf8_check_string(value, -1)) + return -1; + + return json_string_set_nocheck(json, value); +} + +static void json_delete_string(json_string_t *string) +{ + free(string->value); + free(string); +} + +static int json_string_equal(json_t *string1, json_t *string2) +{ + return strcmp(json_string_value(string1), json_string_value(string2)) == 0; +} + +static json_t *json_string_copy(json_t *string) +{ + return json_string_nocheck(json_string_value(string)); +} + + +/*** integer ***/ + +json_t *json_integer(json_int_t value) +{ + json_integer_t *integer = malloc(sizeof(json_integer_t)); + if(!integer) + return NULL; + json_init(&integer->json, JSON_INTEGER); + + integer->value = value; + return &integer->json; +} + +json_int_t json_integer_value(const json_t *json) +{ + if(!json_is_integer(json)) + return 0; + + return json_to_integer(json)->value; +} + +int json_integer_set(json_t *json, json_int_t value) +{ + if(!json_is_integer(json)) + return -1; + + json_to_integer(json)->value = value; + + return 0; +} + +static void json_delete_integer(json_integer_t *integer) +{ + free(integer); +} + +static int json_integer_equal(json_t *integer1, json_t *integer2) +{ + return json_integer_value(integer1) == json_integer_value(integer2); +} + +static json_t *json_integer_copy(json_t *integer) +{ + return json_integer(json_integer_value(integer)); +} + + +/*** real ***/ + +json_t *json_real(double value) +{ + json_real_t *real = malloc(sizeof(json_real_t)); + if(!real) + return NULL; + json_init(&real->json, JSON_REAL); + + real->value = value; + return &real->json; +} + +double json_real_value(const json_t *json) +{ + if(!json_is_real(json)) + return 0; + + return json_to_real(json)->value; +} + +int json_real_set(json_t *json, double value) +{ + if(!json_is_real(json)) + return 0; + + json_to_real(json)->value = value; + + return 0; +} + +static void json_delete_real(json_real_t *real) +{ + free(real); +} + +static int json_real_equal(json_t *real1, json_t *real2) +{ + return json_real_value(real1) == json_real_value(real2); +} + +static json_t *json_real_copy(json_t *real) +{ + return json_real(json_real_value(real)); +} + + +/*** number ***/ + +double json_number_value(const json_t *json) +{ + if(json_is_integer(json)) + return json_integer_value(json); + else if(json_is_real(json)) + return json_real_value(json); + else + return 0.0; +} + + +/*** simple values ***/ + +json_t *json_true(void) +{ + static json_t the_true = {JSON_TRUE, (size_t)-1}; + return &the_true; +} + + +json_t *json_false(void) +{ + static json_t the_false = {JSON_FALSE, (size_t)-1}; + return &the_false; +} + + +json_t *json_null(void) +{ + static json_t the_null = {JSON_NULL, (size_t)-1}; + return &the_null; +} + + +/*** deletion ***/ + +void json_delete(json_t *json) +{ + if(json_is_object(json)) + json_delete_object(json_to_object(json)); + + else if(json_is_array(json)) + json_delete_array(json_to_array(json)); + + else if(json_is_string(json)) + json_delete_string(json_to_string(json)); + + else if(json_is_integer(json)) + json_delete_integer(json_to_integer(json)); + + else if(json_is_real(json)) + json_delete_real(json_to_real(json)); + + /* json_delete is not called for true, false or null */ +} + + +/*** equality ***/ + +int json_equal(json_t *json1, json_t *json2) +{ + if(!json1 || !json2) + return 0; + + if(json_typeof(json1) != json_typeof(json2)) + return 0; + + /* this covers true, false and null as they are singletons */ + if(json1 == json2) + return 1; + + if(json_is_object(json1)) + return json_object_equal(json1, json2); + + if(json_is_array(json1)) + return json_array_equal(json1, json2); + + if(json_is_string(json1)) + return json_string_equal(json1, json2); + + if(json_is_integer(json1)) + return json_integer_equal(json1, json2); + + if(json_is_real(json1)) + return json_real_equal(json1, json2); + + return 0; +} + + +/*** copying ***/ + +json_t *json_copy(json_t *json) +{ + if(!json) + return NULL; + + if(json_is_object(json)) + return json_object_copy(json); + + if(json_is_array(json)) + return json_array_copy(json); + + if(json_is_string(json)) + return json_string_copy(json); + + if(json_is_integer(json)) + return json_integer_copy(json); + + if(json_is_real(json)) + return json_real_copy(json); + + if(json_is_true(json) || json_is_false(json) || json_is_null(json)) + return json; + + return NULL; +} + +json_t *json_deep_copy(json_t *json) +{ + if(!json) + return NULL; + + if(json_is_object(json)) + return json_object_deep_copy(json); + + if(json_is_array(json)) + return json_array_deep_copy(json); + + /* for the rest of the types, deep copying doesn't differ from + shallow copying */ + + if(json_is_string(json)) + return json_string_copy(json); + + if(json_is_integer(json)) + return json_integer_copy(json); + + if(json_is_real(json)) + return json_real_copy(json); + + if(json_is_true(json) || json_is_false(json) || json_is_null(json)) + return json; + + return NULL; +} diff --git a/jansson/src/variadic.c b/jansson/src/variadic.c new file mode 100644 index 0000000..89ff1da --- /dev/null +++ b/jansson/src/variadic.c @@ -0,0 +1,568 @@ +/* + * Copyright (c) 2009, 2010 Petri Lehtinen + * Copyright (c) 2010 Graeme Smecher + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#include +#include +#include + +#include +#include "jansson_private.h" + +json_t *json_pack(json_error_t *error, const char *fmt, ...) { + int fmt_length = strlen(fmt); + va_list ap; + + /* Keep a stack of containers (lists and objects) */ + int depth = 0; + json_t **stack = NULL; + + /* Keep a list of objects we create in case of error */ + int free_count = 0; + json_t **free_list = NULL; + + json_t *cur = NULL; /* Current container */ + json_t *root = NULL; /* root object */ + json_t *obj = NULL; + + char *key = NULL; /* Current key in an object */ + char *s; + + int line = 1; + + /* Allocation provisioned for worst case */ + stack = calloc(fmt_length, sizeof(json_t *)); + free_list = calloc(fmt_length, sizeof(json_t *)); + + jsonp_error_init(error, ""); + + if(!stack || !free_list) + goto out; + + va_start(ap, fmt); + while(*fmt) { + switch(*fmt) { + case '\n': + line++; + break; + + case ' ': /* Whitespace */ + break; + + case ',': /* Element spacer */ + if(!root) + { + jsonp_error_set(error, line, -1, + "Unexpected COMMA precedes root element!"); + root = NULL; + goto out; + } + + if(!cur) + { + jsonp_error_set(error, line, -1, + "Unexpected COMMA outside a list or object!"); + root = NULL; + goto out; + } + + if(key) + { + jsonp_error_set(error, line, -1, + "Expected KEY, got COMMA!"); + root = NULL; + goto out; + } + break; + + case ':': /* Key/value separator */ + if(!key) + { + jsonp_error_set(error, line, -1, + "Got key/value separator without " + "a key preceding it!"); + root = NULL; + goto out; + } + + if(!json_is_object(cur)) + { + jsonp_error_set(error, line, -1, + "Got a key/value separator " + "(':') outside an object!"); + root = NULL; + goto out; + } + + break; + + case ']': /* Close array or object */ + case '}': + + if(key) + { + jsonp_error_set(error, line, -1, + "OBJECT or ARRAY ended with an " + "incomplete key/value pair!"); + root = NULL; + goto out; + } + + if(depth <= 0) + { + jsonp_error_set(error, line, -1, + "Too many close-brackets '%c'", *fmt); + root = NULL; + goto out; + } + + if(*fmt == ']' && !json_is_array(cur)) + { + jsonp_error_set(error, line, -1, + "Stray close-array ']' character"); + root = NULL; + goto out; + } + + if(*fmt == '}' && !json_is_object(cur)) + { + jsonp_error_set(error, line, -1, + "Stray close-object '}' character"); + root = NULL; + goto out; + } + + cur = stack[--depth]; + break; + + case '[': + obj = json_array(); + goto obj_common; + + case '{': + obj = json_object(); + goto obj_common; + + case 's': /* string */ + s = va_arg(ap, char*); + + if(!s) + { + jsonp_error_set(error, line, -1, + "Refusing to handle a NULL string"); + root = NULL; + goto out; + } + + if(json_is_object(cur) && !key) + { + /* It's a key */ + key = s; + break; + } + + obj = json_string(s); + goto obj_common; + + case 'n': /* null */ + obj = json_null(); + goto obj_common; + + case 'b': /* boolean */ + obj = va_arg(ap, int) ? + json_true() : json_false(); + goto obj_common; + + case 'i': /* integer */ + obj = json_integer(va_arg(ap, int)); + goto obj_common; + + case 'f': /* double-precision float */ + obj = json_real(va_arg(ap, double)); + goto obj_common; + + case 'O': /* a json_t object; increments refcount */ + obj = va_arg(ap, json_t *); + json_incref(obj); + goto obj_common; + + case 'o': /* a json_t object; doesn't increment refcount */ + obj = va_arg(ap, json_t *); + goto obj_common; + +obj_common: free_list[free_count++] = obj; + + /* Root this object to its parent */ + if(json_is_object(cur)) { + if(!key) + { + jsonp_error_set(error, line, -1, + "Expected key, got identifier '%c'!", *fmt); + root = NULL; + goto out; + } + + json_object_set_new(cur, key, obj); + key = NULL; + } + else if(json_is_array(cur)) + { + json_array_append_new(cur, obj); + } + else if(!root) + { + printf("Rooting\n"); + root = obj; + } + else + { + jsonp_error_set(error, line, -1, + "Can't figure out where to attach " + "'%c' object!", *fmt); + root = NULL; + goto out; + } + + /* If it was a container ('[' or '{'), descend on the stack */ + if(json_is_array(obj) || json_is_object(obj)) + { + stack[depth++] = cur; + cur = obj; + } + + break; + } + fmt++; + } + va_end(ap); + + if(depth != 0) { + jsonp_error_set(error, line, -1, + "Missing object or array close-brackets in format string"); + root = NULL; + goto out; + } + + /* Success: don't free everything we just built! */ + free_count = 0; + +out: + while(free_count) + json_decref(free_list[--free_count]); + + if(free_list) + free(free_list); + + if(stack) + free(stack); + + return(root); +} + +int json_unpack(json_t *root, json_error_t *error, const char *fmt, ...) { + va_list ap; + + int rv=0; /* Return value */ + int line = 1; /* Line number */ + + /* Keep a stack of containers (lists and objects) */ + int depth = 0; + json_t **stack; + + int array_index = 0; + char *key = NULL; /* Current key in an object */ + + json_t *cur = NULL; /* Current container */ + json_t *obj = NULL; + + int fmt_length = strlen(fmt); + + jsonp_error_init(error, ""); + + /* Allocation provisioned for worst case */ + stack = calloc(fmt_length, sizeof(json_t *)); + if(!stack) + { + jsonp_error_set(error, line, -1, "Out of memory!"); + rv = -1; + goto out; + } + + /* Even if we're successful, we need to know if the number of + * arguments provided matches the number of JSON objects. + * We can do this by counting the elements in every array or + * object we open up, and decrementing the count as we visit + * their children. */ + int unvisited = 0; + + va_start(ap, fmt); + while(*fmt) + { + switch(*fmt) + { + case ' ': /* Whitespace */ + break; + + case '\n': /* Line break */ + line++; + break; + + case ',': /* Element spacer */ + + if(!cur) + { + jsonp_error_set(error, line, -1, + "Unexpected COMMA outside a list or object!"); + rv = -1; + goto out; + } + + if(key) + { + jsonp_error_set(error, line, -1, + "Expected KEY, got COMMA!"); + rv = -1; + goto out; + } + break; + + case ':': /* Key/value separator */ + if(!json_is_object(cur) || !key) + { + jsonp_error_set(error, line, -1, "Unexpected ':'"); + rv = -1; + goto out; + } + break; + + case '[': + case '{': + /* Fetch object */ + if(!cur) + { + obj = root; + } + else if(json_is_object(cur)) + { + if(!key) + { + jsonp_error_set(error, line, -1, + "Objects can't be keys"); + rv = -1; + goto out; + } + obj = json_object_get(cur, key); + unvisited--; + key = NULL; + } + else if(json_is_array(cur)) + { + obj = json_array_get(cur, array_index); + unvisited--; + array_index++; + } + else + { + assert(0); + } + + /* Make sure we got what we expected */ + if(*fmt=='{' && !json_is_object(obj)) + { + rv = -2; + goto out; + } + + if(*fmt=='[' && !json_is_array(obj)) + { + rv = -2; + goto out; + } + + unvisited += json_is_object(obj) ? + json_object_size(obj) : + json_array_size(obj); + + /* Descend */ + stack[depth++] = cur; + cur = obj; + + key = NULL; + + break; + + + case ']': + case '}': + + if(json_is_array(cur) && *fmt!=']') + { + jsonp_error_set(error, line, -1, "Missing ']'"); + rv = -1; + goto out; + } + + if(json_is_object(cur) && *fmt!='}') + { + jsonp_error_set(error, line, -1, "Missing '}'"); + rv = -1; + goto out; + } + + if(key) + { + jsonp_error_set(error, line, -1, "Unexpected '%c'", *fmt); + rv = -1; + goto out; + } + + if(depth <= 0) + { + jsonp_error_set(error, line, -1, "Unexpected '%c'", *fmt); + rv = -1; + goto out; + } + + cur = stack[--depth]; + + break; + + case 's': + if(!key && json_is_object(cur)) + { + /* constant string for key */ + key = va_arg(ap, char*); + break; + } + /* fall through */ + + case 'i': /* integer */ + case 'f': /* double-precision float */ + case 'O': /* a json_t object; increments refcount */ + case 'o': /* a json_t object; borrowed reference */ + case 'b': /* boolean */ + case 'n': /* null */ + + /* Fetch object */ + if(!cur) + { + obj = root; + } + else if(json_is_object(cur)) + { + if(!key) + { + jsonp_error_set(error, line, -1, + "Only strings may be used as keys!"); + rv = -1; + goto out; + } + + obj = json_object_get(cur, key); + unvisited--; + key = NULL; + } + else if(json_is_array(cur)) + { + obj = json_array_get(cur, array_index); + unvisited--; + array_index++; + } + else + { + jsonp_error_set(error, line, -1, + "Unsure how to retrieve JSON object '%c'", + *fmt); + rv = -1; + goto out; + } + + switch(*fmt) + { + case 's': + if(!json_is_string(obj)) + { + jsonp_error_set(error, line, -1, + "Type mismatch! Object wasn't a string."); + rv = -2; + goto out; + } + *va_arg(ap, const char**) = json_string_value(obj); + break; + + case 'i': + if(!json_is_integer(obj)) + { + jsonp_error_set(error, line, -1, + "Type mismatch! Object wasn't an integer."); + rv = -2; + goto out; + } + *va_arg(ap, int*) = json_integer_value(obj); + break; + + case 'b': + if(!json_is_boolean(obj)) + { + jsonp_error_set(error, line, -1, + "Type mismatch! Object wasn't a boolean."); + rv = -2; + goto out; + } + *va_arg(ap, int*) = json_is_true(obj); + break; + + case 'f': + if(!json_is_number(obj)) + { + jsonp_error_set(error, line, -1, + "Type mismatch! Object wasn't a real."); + rv = -2; + goto out; + } + *va_arg(ap, double*) = json_number_value(obj); + break; + + case 'O': + json_incref(obj); + /* Fall through */ + + case 'o': + *va_arg(ap, json_t**) = obj; + break; + + case 'n': + /* Don't actually assign anything; we're just happy + * the null turned up as promised in the format + * string. */ + break; + + default: + jsonp_error_set(error, line, -1, + "Unknown format character '%c'", *fmt); + rv = -1; + goto out; + } + } + fmt++; + } + + /* Return 0 if everything was matched; otherwise the number of JSON + * objects we didn't get to. */ + rv = unvisited; + +out: + va_end(ap); + + if(stack) + free(stack); + + return(rv); +} + +/* vim: ts=4:expandtab:sw=4 + */ diff --git a/md5/md5.c b/md5/md5.c new file mode 100644 index 0000000..c35d96c --- /dev/null +++ b/md5/md5.c @@ -0,0 +1,381 @@ +/* + Copyright (C) 1999, 2000, 2002 Aladdin Enterprises. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + L. Peter Deutsch + ghost@aladdin.com + + */ +/* $Id: md5.c,v 1.6 2002/04/13 19:20:28 lpd Exp $ */ +/* + Independent implementation of MD5 (RFC 1321). + + This code implements the MD5 Algorithm defined in RFC 1321, whose + text is available at + http://www.ietf.org/rfc/rfc1321.txt + The code is derived from the text of the RFC, including the test suite + (section A.5) but excluding the rest of Appendix A. It does not include + any code or documentation that is identified in the RFC as being + copyrighted. + + The original and principal author of md5.c is L. Peter Deutsch + . Other authors are noted in the change history + that follows (in reverse chronological order): + + 2002-04-13 lpd Clarified derivation from RFC 1321; now handles byte order + either statically or dynamically; added missing #include + in library. + 2002-03-11 lpd Corrected argument list for main(), and added int return + type, in test program and T value program. + 2002-02-21 lpd Added missing #include in test program. + 2000-07-03 lpd Patched to eliminate warnings about "constant is + unsigned in ANSI C, signed in traditional"; made test program + self-checking. + 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. + 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5). + 1999-05-03 lpd Original version. + */ + +#include "md5.h" +#include + +#undef BYTE_ORDER /* 1 = big-endian, -1 = little-endian, 0 = unknown */ +#ifdef ARCH_IS_BIG_ENDIAN +# define BYTE_ORDER (ARCH_IS_BIG_ENDIAN ? 1 : -1) +#else +# define BYTE_ORDER 0 +#endif + +#define T_MASK ((md5_word_t)~0) +#define T1 /* 0xd76aa478 */ (T_MASK ^ 0x28955b87) +#define T2 /* 0xe8c7b756 */ (T_MASK ^ 0x173848a9) +#define T3 0x242070db +#define T4 /* 0xc1bdceee */ (T_MASK ^ 0x3e423111) +#define T5 /* 0xf57c0faf */ (T_MASK ^ 0x0a83f050) +#define T6 0x4787c62a +#define T7 /* 0xa8304613 */ (T_MASK ^ 0x57cfb9ec) +#define T8 /* 0xfd469501 */ (T_MASK ^ 0x02b96afe) +#define T9 0x698098d8 +#define T10 /* 0x8b44f7af */ (T_MASK ^ 0x74bb0850) +#define T11 /* 0xffff5bb1 */ (T_MASK ^ 0x0000a44e) +#define T12 /* 0x895cd7be */ (T_MASK ^ 0x76a32841) +#define T13 0x6b901122 +#define T14 /* 0xfd987193 */ (T_MASK ^ 0x02678e6c) +#define T15 /* 0xa679438e */ (T_MASK ^ 0x5986bc71) +#define T16 0x49b40821 +#define T17 /* 0xf61e2562 */ (T_MASK ^ 0x09e1da9d) +#define T18 /* 0xc040b340 */ (T_MASK ^ 0x3fbf4cbf) +#define T19 0x265e5a51 +#define T20 /* 0xe9b6c7aa */ (T_MASK ^ 0x16493855) +#define T21 /* 0xd62f105d */ (T_MASK ^ 0x29d0efa2) +#define T22 0x02441453 +#define T23 /* 0xd8a1e681 */ (T_MASK ^ 0x275e197e) +#define T24 /* 0xe7d3fbc8 */ (T_MASK ^ 0x182c0437) +#define T25 0x21e1cde6 +#define T26 /* 0xc33707d6 */ (T_MASK ^ 0x3cc8f829) +#define T27 /* 0xf4d50d87 */ (T_MASK ^ 0x0b2af278) +#define T28 0x455a14ed +#define T29 /* 0xa9e3e905 */ (T_MASK ^ 0x561c16fa) +#define T30 /* 0xfcefa3f8 */ (T_MASK ^ 0x03105c07) +#define T31 0x676f02d9 +#define T32 /* 0x8d2a4c8a */ (T_MASK ^ 0x72d5b375) +#define T33 /* 0xfffa3942 */ (T_MASK ^ 0x0005c6bd) +#define T34 /* 0x8771f681 */ (T_MASK ^ 0x788e097e) +#define T35 0x6d9d6122 +#define T36 /* 0xfde5380c */ (T_MASK ^ 0x021ac7f3) +#define T37 /* 0xa4beea44 */ (T_MASK ^ 0x5b4115bb) +#define T38 0x4bdecfa9 +#define T39 /* 0xf6bb4b60 */ (T_MASK ^ 0x0944b49f) +#define T40 /* 0xbebfbc70 */ (T_MASK ^ 0x4140438f) +#define T41 0x289b7ec6 +#define T42 /* 0xeaa127fa */ (T_MASK ^ 0x155ed805) +#define T43 /* 0xd4ef3085 */ (T_MASK ^ 0x2b10cf7a) +#define T44 0x04881d05 +#define T45 /* 0xd9d4d039 */ (T_MASK ^ 0x262b2fc6) +#define T46 /* 0xe6db99e5 */ (T_MASK ^ 0x1924661a) +#define T47 0x1fa27cf8 +#define T48 /* 0xc4ac5665 */ (T_MASK ^ 0x3b53a99a) +#define T49 /* 0xf4292244 */ (T_MASK ^ 0x0bd6ddbb) +#define T50 0x432aff97 +#define T51 /* 0xab9423a7 */ (T_MASK ^ 0x546bdc58) +#define T52 /* 0xfc93a039 */ (T_MASK ^ 0x036c5fc6) +#define T53 0x655b59c3 +#define T54 /* 0x8f0ccc92 */ (T_MASK ^ 0x70f3336d) +#define T55 /* 0xffeff47d */ (T_MASK ^ 0x00100b82) +#define T56 /* 0x85845dd1 */ (T_MASK ^ 0x7a7ba22e) +#define T57 0x6fa87e4f +#define T58 /* 0xfe2ce6e0 */ (T_MASK ^ 0x01d3191f) +#define T59 /* 0xa3014314 */ (T_MASK ^ 0x5cfebceb) +#define T60 0x4e0811a1 +#define T61 /* 0xf7537e82 */ (T_MASK ^ 0x08ac817d) +#define T62 /* 0xbd3af235 */ (T_MASK ^ 0x42c50dca) +#define T63 0x2ad7d2bb +#define T64 /* 0xeb86d391 */ (T_MASK ^ 0x14792c6e) + + +static void +md5_process(md5_state_t *pms, const md5_byte_t *data /*[64]*/) +{ + md5_word_t + a = pms->abcd[0], b = pms->abcd[1], + c = pms->abcd[2], d = pms->abcd[3]; + md5_word_t t; +#if BYTE_ORDER > 0 + /* Define storage only for big-endian CPUs. */ + md5_word_t X[16]; +#else + /* Define storage for little-endian or both types of CPUs. */ + md5_word_t xbuf[16]; + const md5_word_t *X; +#endif + + { +#if BYTE_ORDER == 0 + /* + * Determine dynamically whether this is a big-endian or + * little-endian machine, since we can use a more efficient + * algorithm on the latter. + */ + static const int w = 1; + + if (*((const md5_byte_t *)&w)) /* dynamic little-endian */ +#endif +#if BYTE_ORDER <= 0 /* little-endian */ + { + /* + * On little-endian machines, we can process properly aligned + * data without copying it. + */ + if (!((data - (const md5_byte_t *)0) & 3)) { + /* data are properly aligned */ + X = (const md5_word_t *)data; + } else { + /* not aligned */ + memcpy(xbuf, data, 64); + X = xbuf; + } + } +#endif +#if BYTE_ORDER == 0 + else /* dynamic big-endian */ +#endif +#if BYTE_ORDER >= 0 /* big-endian */ + { + /* + * On big-endian machines, we must arrange the bytes in the + * right order. + */ + const md5_byte_t *xp = data; + int i; + +# if BYTE_ORDER == 0 + X = xbuf; /* (dynamic only) */ +# else +# define xbuf X /* (static only) */ +# endif + for (i = 0; i < 16; ++i, xp += 4) + xbuf[i] = xp[0] + (xp[1] << 8) + (xp[2] << 16) + (xp[3] << 24); + } +#endif + } + +#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n)))) + + /* Round 1. */ + /* Let [abcd k s i] denote the operation + a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). */ +#define F(x, y, z) (((x) & (y)) | (~(x) & (z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + F(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 0, 7, T1); + SET(d, a, b, c, 1, 12, T2); + SET(c, d, a, b, 2, 17, T3); + SET(b, c, d, a, 3, 22, T4); + SET(a, b, c, d, 4, 7, T5); + SET(d, a, b, c, 5, 12, T6); + SET(c, d, a, b, 6, 17, T7); + SET(b, c, d, a, 7, 22, T8); + SET(a, b, c, d, 8, 7, T9); + SET(d, a, b, c, 9, 12, T10); + SET(c, d, a, b, 10, 17, T11); + SET(b, c, d, a, 11, 22, T12); + SET(a, b, c, d, 12, 7, T13); + SET(d, a, b, c, 13, 12, T14); + SET(c, d, a, b, 14, 17, T15); + SET(b, c, d, a, 15, 22, T16); +#undef SET + + /* Round 2. */ + /* Let [abcd k s i] denote the operation + a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). */ +#define G(x, y, z) (((x) & (z)) | ((y) & ~(z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + G(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 1, 5, T17); + SET(d, a, b, c, 6, 9, T18); + SET(c, d, a, b, 11, 14, T19); + SET(b, c, d, a, 0, 20, T20); + SET(a, b, c, d, 5, 5, T21); + SET(d, a, b, c, 10, 9, T22); + SET(c, d, a, b, 15, 14, T23); + SET(b, c, d, a, 4, 20, T24); + SET(a, b, c, d, 9, 5, T25); + SET(d, a, b, c, 14, 9, T26); + SET(c, d, a, b, 3, 14, T27); + SET(b, c, d, a, 8, 20, T28); + SET(a, b, c, d, 13, 5, T29); + SET(d, a, b, c, 2, 9, T30); + SET(c, d, a, b, 7, 14, T31); + SET(b, c, d, a, 12, 20, T32); +#undef SET + + /* Round 3. */ + /* Let [abcd k s t] denote the operation + a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). */ +#define H(x, y, z) ((x) ^ (y) ^ (z)) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + H(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 5, 4, T33); + SET(d, a, b, c, 8, 11, T34); + SET(c, d, a, b, 11, 16, T35); + SET(b, c, d, a, 14, 23, T36); + SET(a, b, c, d, 1, 4, T37); + SET(d, a, b, c, 4, 11, T38); + SET(c, d, a, b, 7, 16, T39); + SET(b, c, d, a, 10, 23, T40); + SET(a, b, c, d, 13, 4, T41); + SET(d, a, b, c, 0, 11, T42); + SET(c, d, a, b, 3, 16, T43); + SET(b, c, d, a, 6, 23, T44); + SET(a, b, c, d, 9, 4, T45); + SET(d, a, b, c, 12, 11, T46); + SET(c, d, a, b, 15, 16, T47); + SET(b, c, d, a, 2, 23, T48); +#undef SET + + /* Round 4. */ + /* Let [abcd k s t] denote the operation + a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). */ +#define I(x, y, z) ((y) ^ ((x) | ~(z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + I(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 0, 6, T49); + SET(d, a, b, c, 7, 10, T50); + SET(c, d, a, b, 14, 15, T51); + SET(b, c, d, a, 5, 21, T52); + SET(a, b, c, d, 12, 6, T53); + SET(d, a, b, c, 3, 10, T54); + SET(c, d, a, b, 10, 15, T55); + SET(b, c, d, a, 1, 21, T56); + SET(a, b, c, d, 8, 6, T57); + SET(d, a, b, c, 15, 10, T58); + SET(c, d, a, b, 6, 15, T59); + SET(b, c, d, a, 13, 21, T60); + SET(a, b, c, d, 4, 6, T61); + SET(d, a, b, c, 11, 10, T62); + SET(c, d, a, b, 2, 15, T63); + SET(b, c, d, a, 9, 21, T64); +#undef SET + + /* Then perform the following additions. (That is increment each + of the four registers by the value it had before this block + was started.) */ + pms->abcd[0] += a; + pms->abcd[1] += b; + pms->abcd[2] += c; + pms->abcd[3] += d; +} + +void +md5_init(md5_state_t *pms) +{ + pms->count[0] = pms->count[1] = 0; + pms->abcd[0] = 0x67452301; + pms->abcd[1] = /*0xefcdab89*/ T_MASK ^ 0x10325476; + pms->abcd[2] = /*0x98badcfe*/ T_MASK ^ 0x67452301; + pms->abcd[3] = 0x10325476; +} + +void +md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes) +{ + const md5_byte_t *p = data; + int left = nbytes; + int offset = (pms->count[0] >> 3) & 63; + md5_word_t nbits = (md5_word_t)(nbytes << 3); + + if (nbytes <= 0) + return; + + /* Update the message length. */ + pms->count[1] += nbytes >> 29; + pms->count[0] += nbits; + if (pms->count[0] < nbits) + pms->count[1]++; + + /* Process an initial partial block. */ + if (offset) { + int copy = (offset + nbytes > 64 ? 64 - offset : nbytes); + + memcpy(pms->buf + offset, p, copy); + if (offset + copy < 64) + return; + p += copy; + left -= copy; + md5_process(pms, pms->buf); + } + + /* Process full blocks. */ + for (; left >= 64; p += 64, left -= 64) + md5_process(pms, p); + + /* Process a final partial block. */ + if (left) + memcpy(pms->buf, p, left); +} + +void +md5_finish(md5_state_t *pms, md5_byte_t digest[16]) +{ + static const md5_byte_t pad[64] = { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + md5_byte_t data[8]; + int i; + + /* Save the length before padding. */ + for (i = 0; i < 8; ++i) + data[i] = (md5_byte_t)(pms->count[i >> 2] >> ((i & 3) << 3)); + /* Pad to 56 bytes mod 64. */ + md5_append(pms, pad, ((55 - (pms->count[0] >> 3)) & 63) + 1); + /* Append the length. */ + md5_append(pms, data, 8); + for (i = 0; i < 16; ++i) + digest[i] = (md5_byte_t)(pms->abcd[i >> 2] >> ((i & 3) << 3)); +} diff --git a/md5/md5.h b/md5/md5.h new file mode 100644 index 0000000..7143a36 --- /dev/null +++ b/md5/md5.h @@ -0,0 +1,84 @@ +/* + Copyright (C) 1999, 2002 Aladdin Enterprises. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + L. Peter Deutsch + ghost@aladdin.com + + */ +/* $Id: md5.h,v 1.4 2002/04/13 19:20:28 lpd Exp $ */ +/* + Independent implementation of MD5 (RFC 1321). + + This code implements the MD5 Algorithm defined in RFC 1321, whose + text is available at + http://www.ietf.org/rfc/rfc1321.txt + The code is derived from the text of the RFC, including the test suite + (section A.5) but excluding the rest of Appendix A. It does not include + any code or documentation that is identified in the RFC as being + copyrighted. + + The original and principal author of md5.h is L. Peter Deutsch + . Other authors are noted in the change history + that follows (in reverse chronological order): + + 2002-04-13 lpd Removed support for non-ANSI compilers; removed + references to Ghostscript; clarified derivation from RFC 1321; + now handles byte order either statically or dynamically. + 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. + 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5); + added conditionalization for C++ compilation from Martin + Purschke . + 1999-05-03 lpd Original version. + */ + +#ifndef md5_INCLUDED +#define md5_INCLUDED + +/* + * This package supports both compile-time and run-time determination of CPU + * byte order. If ARCH_IS_BIG_ENDIAN is defined as 0, the code will be + * compiled to run only on little-endian CPUs; if ARCH_IS_BIG_ENDIAN is + * defined as non-zero, the code will be compiled to run only on big-endian + * CPUs; if ARCH_IS_BIG_ENDIAN is not defined, the code will be compiled to + * run on either big- or little-endian CPUs, but will run slightly less + * efficiently on either one than if ARCH_IS_BIG_ENDIAN is defined. + */ + +typedef unsigned char md5_byte_t; /* 8-bit byte */ +typedef unsigned int md5_word_t; /* 32-bit word */ + +/* Define the state of the MD5 Algorithm. */ +typedef struct md5_state_s { + md5_word_t count[2]; /* message length in bits, lsw first */ + md5_word_t abcd[4]; /* digest buffer */ + md5_byte_t buf[64]; /* accumulate block */ +} md5_state_t; + + +/* Initialize the algorithm. */ +void md5_init(md5_state_t *pms); + +/* Append a string to the message. */ +void md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes); + +/* Finish the message and return the digest. */ +void md5_finish(md5_state_t *pms, md5_byte_t digest[16]); + + +#endif /* md5_INCLUDED */ diff --git a/pool.c b/pool.c new file mode 100644 index 0000000..65ff84d --- /dev/null +++ b/pool.c @@ -0,0 +1,175 @@ +#include "pool.h" +#include "worker.h" +#include "conf.h" +#include "server.h" + +#include +#include +#include +#include + +struct pool * +pool_new(struct worker *w, int count) { + + struct pool *p = calloc(1, sizeof(struct pool)); + + p->count = count; + p->ac = calloc(count, sizeof(redisAsyncContext*)); + + p->w = w; + p->cfg = w->s->cfg; + + return p; +} + +void +pool_free_context(redisAsyncContext *ac) { + + if (ac) { + redisAsyncDisconnect(ac); + redisAsyncFree(ac); + } +} + +static void +pool_on_connect(const redisAsyncContext *ac, int status) { + struct pool *p = ac->data; + int i = 0; + + if(!p || status == REDIS_ERR || ac->err) { + return; + } + /* connected to redis! */ + + /* add to pool */ + for(i = 0; i < p->count; ++i) { + if(p->ac[i] == NULL) { + p->ac[i] = ac; + return; + } + } +} + +struct pool_reconnect { + struct event ev; + struct pool *p; + + struct timeval tv; +}; + +static void +pool_can_connect(int fd, short event, void *ptr) { + struct pool_reconnect *pr = ptr; + struct pool *p = pr->p; + + (void)fd; + (void)event; + + free(pr); + + pool_connect(p, p->cfg->database, 1); +} +static void +pool_schedule_reconnect(struct pool *p) { + + struct pool_reconnect *pr = malloc(sizeof(struct pool_reconnect)); + pr->p = p; + + pr->tv.tv_sec = 0; + pr->tv.tv_usec = 100*1000; /* 0.1 sec*/ + + evtimer_set(&pr->ev, pool_can_connect, pr); + event_base_set(p->w->base, &pr->ev); + evtimer_add(&pr->ev, &pr->tv); +} + + + +static void +pool_on_disconnect(const redisAsyncContext *ac, int status) { + + struct pool *p = ac->data; + int i = 0; + if (status != REDIS_OK) { + /* fprintf(stderr, "Error: %s\n", ac->errstr); */ + } + + if(p == NULL) { /* no need to clean anything here. */ + return; + } + + /* remove from the pool */ + for(i = 0; i < p->count; ++i) { + if(p->ac[i] == ac) { + p->ac[i] = NULL; + break; + } + } + + /* schedule reconnect */ + pool_schedule_reconnect(p); +} + +/** + * Create new connection. + */ +redisAsyncContext * +pool_connect(struct pool *p, int db_num, int attach) { + + struct redisAsyncContext *ac; + if(p->cfg->redis_host[0] == '/') { /* unix socket */ + ac = redisAsyncConnectUnix(p->cfg->redis_host); + } else { + ac = redisAsyncConnect(p->cfg->redis_host, p->cfg->redis_port); + } + + if(attach) { + ac->data = p; + } else { + ac->data = NULL; + } + + if(ac->err) { + char msg[] = "Connection failed: %s"; + size_t errlen = strlen(ac->errstr); + char *err = malloc(sizeof(msg) + errlen); + if (err) { + size_t sz = sprintf(err, msg, ac->errstr); + slog(p->w->s, WEBDIS_ERROR, err, sz); + free(err); + } + redisAsyncFree(ac); + pool_schedule_reconnect(p); + return NULL; + } + + redisLibeventAttach(ac, p->w->base); + redisAsyncSetConnectCallback(ac, pool_on_connect); + redisAsyncSetDisconnectCallback(ac, pool_on_disconnect); + + if(p->cfg->redis_auth) { /* authenticate. */ + redisAsyncCommand(ac, NULL, NULL, "AUTH %s", p->cfg->redis_auth); + } + if(db_num) { /* change database. */ + redisAsyncCommand(ac, NULL, NULL, "SELECT %d", db_num); + } + return ac; +} + +const redisAsyncContext * +pool_get_context(struct pool *p) { + + int orig = p->cur++; + + do { + p->cur++; + p->cur %= p->count; + if(p->ac[p->cur] != NULL) { + return p->ac[p->cur]; + } + } while(p->cur != orig); + + return NULL; + +} + diff --git a/pool.h b/pool.h new file mode 100644 index 0000000..bd24ee9 --- /dev/null +++ b/pool.h @@ -0,0 +1,33 @@ +#ifndef POOL_H +#define POOL_H + +#include + +struct conf; +struct worker; + +struct pool { + + struct worker *w; + struct conf *cfg; + + const redisAsyncContext **ac; + int count; + int cur; + +}; + + +struct pool * +pool_new(struct worker *w, int count); + +void +pool_free_context(redisAsyncContext *ac); + +redisAsyncContext * +pool_connect(struct pool *p, int db_num, int attach); + +const redisAsyncContext * +pool_get_context(struct pool *p); + +#endif diff --git a/server.c b/server.c new file mode 100644 index 0000000..8cc667c --- /dev/null +++ b/server.c @@ -0,0 +1,241 @@ +#include "server.h" +#include "worker.h" +#include "client.h" +#include "conf.h" +#include "version.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * Sets up a non-blocking socket + */ +static int +socket_setup(struct server *s, const char *ip, short port) { + + int reuse = 1; + struct sockaddr_in addr; + int fd, ret; + + memset(&addr, 0, sizeof(addr)); +#if defined __BSD__ + addr.sin_len = sizeof(struct sockaddr_in); +#endif + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + + addr.sin_addr.s_addr = inet_addr(ip); + + /* this sad list of tests could use a Maybe monad... */ + + /* create socket */ + fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (-1 == fd) { + slog(s, WEBDIS_ERROR, strerror(errno), 0); + return -1; + } + + /* reuse address if we've bound to it before. */ + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, + sizeof(reuse)) < 0) { + slog(s, WEBDIS_ERROR, strerror(errno), 0); + return -1; + } + + /* set socket as non-blocking. */ + ret = fcntl(fd, F_SETFD, O_NONBLOCK); + if (0 != ret) { + slog(s, WEBDIS_ERROR, strerror(errno), 0); + return -1; + } + + /* bind */ + ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); + if (0 != ret) { + slog(s, WEBDIS_ERROR, strerror(errno), 0); + return -1; + } + + /* listen */ + ret = listen(fd, SOMAXCONN); + if (0 != ret) { + slog(s, WEBDIS_ERROR, strerror(errno), 0); + return -1; + } + + /* there you go, ready to accept! */ + return fd; +} + +struct server * +server_new(const char *cfg_file) { + + int i; + struct server *s = calloc(1, sizeof(struct server)); + + s->log.fd = -1; + s->cfg = conf_read(cfg_file); + + /* workers */ + s->w = calloc(s->cfg->http_threads, sizeof(struct worker*)); + for(i = 0; i < s->cfg->http_threads; ++i) { + s->w[i] = worker_new(s); + } + return s; +} + +static void +server_can_accept(int fd, short event, void *ptr) { + + struct server *s = ptr; + struct worker *w; + struct http_client *c; + int client_fd; + struct sockaddr_in addr; + socklen_t addr_sz = sizeof(addr); + char on = 1; + (void)event; + + /* select worker to send the client to */ + w = s->w[s->next_worker]; + + /* accept client */ + client_fd = accept(fd, (struct sockaddr*)&addr, &addr_sz); + + /* make non-blocking */ + ioctl(client_fd, (int)FIONBIO, (char *)&on); + + /* create client and send to worker. */ + if(client_fd > 0) { + c = http_client_new(w, client_fd, addr.sin_addr.s_addr); + worker_add_client(w, c); + + /* loop over ring of workers */ + s->next_worker = (s->next_worker + 1) % s->cfg->http_threads; + } else { /* too many connections */ + slog(s, WEBDIS_NOTICE, "Too many connections", 0); + } +} + +/** + * Daemonize server. + * (taken from Redis) + */ +static void +server_daemonize(const char *pidfile) { + int fd; + + if (fork() != 0) exit(0); /* parent exits */ + setsid(); /* create a new session */ + + /* Every output goes to /dev/null. */ + if ((fd = open("/dev/null", O_RDWR, 0)) != -1) { + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + if (fd > STDERR_FILENO) close(fd); + } + + /* write pidfile */ + if (pidfile) { + FILE *f = fopen(pidfile, "w"); + if (f) { + fprintf(f, "%d\n", (int)getpid()); + fclose(f); + } + } +} + +/* global pointer to the server object, used in signal handlers */ +static struct server *__server; + +static void +server_handle_signal(int id) { + + switch(id) { + case SIGHUP: + slog_init(__server); + break; + case SIGTERM: + case SIGINT: + slog(__server, WEBDIS_INFO, "Webdis terminating", 0); + exit(0); + break; + default: + break; + } +} + +static void +server_install_signal_handlers(struct server *s) { + __server = s; + + signal(SIGHUP, server_handle_signal); + signal(SIGTERM, server_handle_signal); + signal(SIGINT, server_handle_signal); +} + +int +server_start(struct server *s) { + + int i, ret; + + /* initialize libevent */ + s->base = event_base_new(); + + if(s->cfg->daemonize) { + server_daemonize(s->cfg->pidfile); + + /* sometimes event mech gets lost on fork */ + if(event_reinit(s->base) != 0) { + fprintf(stderr, "Error: event_reinit failed after fork"); + } + } + + /* ignore sigpipe */ +#ifdef SIGPIPE + signal(SIGPIPE, SIG_IGN); +#endif + + slog_init(s); + + /* install signal handlers */ + server_install_signal_handlers(s); + + /* start worker threads */ + for(i = 0; i < s->cfg->http_threads; ++i) { + worker_start(s->w[i]); + } + + /* create socket */ + s->fd = socket_setup(s, s->cfg->http_host, s->cfg->http_port); + if(s->fd < 0) { + return -1; + } + + /* start http server */ + event_set(&s->ev, s->fd, EV_READ | EV_PERSIST, server_can_accept, s); + event_base_set(s->base, &s->ev); + ret = event_add(&s->ev, NULL); + + if(ret < 0) { + slog(s, WEBDIS_ERROR, "Error calling event_add on socket", 0); + return -1; + } + + slog(s, WEBDIS_INFO, "Webdis " WEBDIS_VERSION " up and running", 0); + event_base_dispatch(s->base); + + return 0; +} + diff --git a/server.h b/server.h new file mode 100644 index 0000000..42438b3 --- /dev/null +++ b/server.h @@ -0,0 +1,37 @@ +#ifndef SERVER_H +#define SERVER_H + +#include +#include +#include + +struct worker; +struct conf; + +struct server { + + int fd; + struct event ev; + struct event_base *base; + + struct conf *cfg; + + /* worker threads */ + struct worker **w; + int next_worker; + + /* log lock */ + struct { + pid_t self; + int fd; + } log; +}; + +struct server * +server_new(const char *cfg_file); + +int +server_start(struct server *s); + +#endif + diff --git a/sha1/sha1.c b/sha1/sha1.c new file mode 100644 index 0000000..d87c7f4 --- /dev/null +++ b/sha1/sha1.c @@ -0,0 +1,371 @@ +/* + * sha1.c + * + * Copyright (C) 1998, 2009 + * Paul E. Jones + * All Rights Reserved + * + ***************************************************************************** + * $Id: sha1.c 12 2009-06-22 19:34:25Z paulej $ + ***************************************************************************** + * + * Description: + * This file implements the Secure Hashing Standard as defined + * in FIPS PUB 180-1 published April 17, 1995. + * + * The Secure Hashing Standard, which uses the Secure Hashing + * Algorithm (SHA), produces a 160-bit message digest for a + * given data stream. In theory, it is highly improbable that + * two messages will produce the same message digest. Therefore, + * this algorithm can serve as a means of providing a "fingerprint" + * for a message. + * + * Portability Issues: + * SHA-1 is defined in terms of 32-bit "words". This code was + * written with the expectation that the processor has at least + * a 32-bit machine word size. If the machine word size is larger, + * the code should still function properly. One caveat to that + * is that the input functions taking characters and character + * arrays assume that only 8 bits of information are stored in each + * character. + * + * Caveats: + * SHA-1 is designed to work with messages less than 2^64 bits + * long. Although SHA-1 allows a message digest to be generated for + * messages of any number of bits less than 2^64, this + * implementation only works with messages with a length that is a + * multiple of the size of an 8-bit character. + * + */ + +#include "sha1.h" + +/* + * Define the circular shift macro + */ +#define SHA1CircularShift(bits,word) \ + ((((word) << (bits)) & 0xFFFFFFFF) | \ + ((word) >> (32-(bits)))) + +/* Function prototypes */ +void SHA1ProcessMessageBlock(SHA1Context *); +void SHA1PadMessage(SHA1Context *); + +/* + * SHA1Reset + * + * Description: + * This function will initialize the SHA1Context in preparation + * for computing a new message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1Reset(SHA1Context *context) +{ + context->Length_Low = 0; + context->Length_High = 0; + context->Message_Block_Index = 0; + + context->Message_Digest[0] = 0x67452301; + context->Message_Digest[1] = 0xEFCDAB89; + context->Message_Digest[2] = 0x98BADCFE; + context->Message_Digest[3] = 0x10325476; + context->Message_Digest[4] = 0xC3D2E1F0; + + context->Computed = 0; + context->Corrupted = 0; +} + +/* + * SHA1Result + * + * Description: + * This function will return the 160-bit message digest into the + * Message_Digest array within the SHA1Context provided + * + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA-1 hash. + * + * Returns: + * 1 if successful, 0 if it failed. + * + * Comments: + * + */ +int SHA1Result(SHA1Context *context) +{ + + if (context->Corrupted) + { + return 0; + } + + if (!context->Computed) + { + SHA1PadMessage(context); + context->Computed = 1; + } + + return 1; +} + +/* + * SHA1Input + * + * Description: + * This function accepts an array of octets as the next portion of + * the message. + * + * Parameters: + * context: [in/out] + * The SHA-1 context to update + * message_array: [in] + * An array of characters representing the next portion of the + * message. + * length: [in] + * The length of the message in message_array + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1Input( SHA1Context *context, + const unsigned char *message_array, + unsigned length) +{ + if (!length) + { + return; + } + + if (context->Computed || context->Corrupted) + { + context->Corrupted = 1; + return; + } + + while(length-- && !context->Corrupted) + { + context->Message_Block[context->Message_Block_Index++] = + (*message_array & 0xFF); + + context->Length_Low += 8; + /* Force it to 32 bits */ + context->Length_Low &= 0xFFFFFFFF; + if (context->Length_Low == 0) + { + context->Length_High++; + /* Force it to 32 bits */ + context->Length_High &= 0xFFFFFFFF; + if (context->Length_High == 0) + { + /* Message is too long */ + context->Corrupted = 1; + } + } + + if (context->Message_Block_Index == 64) + { + SHA1ProcessMessageBlock(context); + } + + message_array++; + } +} + +/* + * SHA1ProcessMessageBlock + * + * Description: + * This function will process the next 512 bits of the message + * stored in the Message_Block array. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * Many of the variable names in the SHAContext, especially the + * single character names, were used because those were the names + * used in the publication. + * + * + */ +void SHA1ProcessMessageBlock(SHA1Context *context) +{ + const unsigned K[] = /* Constants defined in SHA-1 */ + { + 0x5A827999, + 0x6ED9EBA1, + 0x8F1BBCDC, + 0xCA62C1D6 + }; + int t; /* Loop counter */ + unsigned temp; /* Temporary word value */ + unsigned W[80]; /* Word sequence */ + unsigned A, B, C, D, E; /* Word buffers */ + + /* + * Initialize the first 16 words in the array W + */ + for(t = 0; t < 16; t++) + { + W[t] = ((unsigned) context->Message_Block[t * 4]) << 24; + W[t] |= ((unsigned) context->Message_Block[t * 4 + 1]) << 16; + W[t] |= ((unsigned) context->Message_Block[t * 4 + 2]) << 8; + W[t] |= ((unsigned) context->Message_Block[t * 4 + 3]); + } + + for(t = 16; t < 80; t++) + { + W[t] = SHA1CircularShift(1,W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16]); + } + + A = context->Message_Digest[0]; + B = context->Message_Digest[1]; + C = context->Message_Digest[2]; + D = context->Message_Digest[3]; + E = context->Message_Digest[4]; + + for(t = 0; t < 20; t++) + { + temp = SHA1CircularShift(5,A) + + ((B & C) | ((~B) & D)) + E + W[t] + K[0]; + temp &= 0xFFFFFFFF; + E = D; + D = C; + C = SHA1CircularShift(30,B); + B = A; + A = temp; + } + + for(t = 20; t < 40; t++) + { + temp = SHA1CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[1]; + temp &= 0xFFFFFFFF; + E = D; + D = C; + C = SHA1CircularShift(30,B); + B = A; + A = temp; + } + + for(t = 40; t < 60; t++) + { + temp = SHA1CircularShift(5,A) + + ((B & C) | (B & D) | (C & D)) + E + W[t] + K[2]; + temp &= 0xFFFFFFFF; + E = D; + D = C; + C = SHA1CircularShift(30,B); + B = A; + A = temp; + } + + for(t = 60; t < 80; t++) + { + temp = SHA1CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[3]; + temp &= 0xFFFFFFFF; + E = D; + D = C; + C = SHA1CircularShift(30,B); + B = A; + A = temp; + } + + context->Message_Digest[0] = + (context->Message_Digest[0] + A) & 0xFFFFFFFF; + context->Message_Digest[1] = + (context->Message_Digest[1] + B) & 0xFFFFFFFF; + context->Message_Digest[2] = + (context->Message_Digest[2] + C) & 0xFFFFFFFF; + context->Message_Digest[3] = + (context->Message_Digest[3] + D) & 0xFFFFFFFF; + context->Message_Digest[4] = + (context->Message_Digest[4] + E) & 0xFFFFFFFF; + + context->Message_Block_Index = 0; +} + +/* + * SHA1PadMessage + * + * Description: + * According to the standard, the message must be padded to an even + * 512 bits. The first padding bit must be a '1'. The last 64 + * bits represent the length of the original message. All bits in + * between should be 0. This function will pad the message + * according to those rules by filling the Message_Block array + * accordingly. It will also call SHA1ProcessMessageBlock() + * appropriately. When it returns, it can be assumed that the + * message digest has been computed. + * + * Parameters: + * context: [in/out] + * The context to pad + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1PadMessage(SHA1Context *context) +{ + /* + * Check to see if the current message block is too small to hold + * the initial padding bits and length. If so, we will pad the + * block, process it, and then continue padding into a second + * block. + */ + if (context->Message_Block_Index > 55) + { + context->Message_Block[context->Message_Block_Index++] = 0x80; + while(context->Message_Block_Index < 64) + { + context->Message_Block[context->Message_Block_Index++] = 0; + } + + SHA1ProcessMessageBlock(context); + + while(context->Message_Block_Index < 56) + { + context->Message_Block[context->Message_Block_Index++] = 0; + } + } + else + { + context->Message_Block[context->Message_Block_Index++] = 0x80; + while(context->Message_Block_Index < 56) + { + context->Message_Block[context->Message_Block_Index++] = 0; + } + } + + /* + * Store the message length as the last 8 octets + */ + context->Message_Block[56] = (context->Length_High >> 24) & 0xFF; + context->Message_Block[57] = (context->Length_High >> 16) & 0xFF; + context->Message_Block[58] = (context->Length_High >> 8) & 0xFF; + context->Message_Block[59] = (context->Length_High) & 0xFF; + context->Message_Block[60] = (context->Length_Low >> 24) & 0xFF; + context->Message_Block[61] = (context->Length_Low >> 16) & 0xFF; + context->Message_Block[62] = (context->Length_Low >> 8) & 0xFF; + context->Message_Block[63] = (context->Length_Low) & 0xFF; + + SHA1ProcessMessageBlock(context); +} diff --git a/sha1/sha1.h b/sha1/sha1.h new file mode 100644 index 0000000..1ca4b10 --- /dev/null +++ b/sha1/sha1.h @@ -0,0 +1,54 @@ +/* + * sha1.h + * + * Copyright (C) 1998, 2009 + * Paul E. Jones + * All Rights Reserved + * + ***************************************************************************** + * $Id: sha1.h 12 2009-06-22 19:34:25Z paulej $ + ***************************************************************************** + * + * Description: + * This class implements the Secure Hashing Standard as defined + * in FIPS PUB 180-1 published April 17, 1995. + * + * Many of the variable names in the SHA1Context, especially the + * single character names, were used because those were the names + * used in the publication. + * + * Please read the file sha1.c for more information. + * + */ + +#ifndef _SHA1_H_ +#define _SHA1_H_ + +/* + * This structure will hold context information for the hashing + * operation + */ +typedef struct SHA1Context +{ + unsigned Message_Digest[5]; /* Message Digest (output) */ + + unsigned Length_Low; /* Message length in bits */ + unsigned Length_High; /* Message length in bits */ + + unsigned char Message_Block[64]; /* 512-bit message blocks */ + int Message_Block_Index; /* Index into message block array */ + + int Computed; /* Is the digest computed? */ + int Corrupted; /* Is the message digest corruped? */ +} SHA1Context; + +/* + * Function Prototypes + */ +void SHA1Reset(SHA1Context *); +int SHA1Result(SHA1Context *); +void SHA1Input( SHA1Context *, + const unsigned char *, + unsigned); + +#endif diff --git a/slog.c b/slog.c new file mode 100644 index 0000000..0d7fd21 --- /dev/null +++ b/slog.c @@ -0,0 +1,80 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "slog.h" +#include "server.h" +#include "conf.h" + +/** + * Initialize log writer. + */ +void +slog_init(struct server *s) { + + s->log.self = getpid(); + + if(s->cfg->logfile) { + + int old_fd = s->log.fd; + + s->log.fd = open(s->cfg->logfile, + O_WRONLY | O_APPEND | O_CREAT, S_IRUSR|S_IWUSR); + + /* close old log */ + if (old_fd != -1) { + close(old_fd); + } + + if (s->log.fd != -1) + return; + + fprintf(stderr, "Could not open %s: %s\n", s->cfg->logfile, + strerror(errno)); + } + s->log.fd = 2; /* stderr */ +} + +/** + * Write log message to disk, or stderr. + */ +void +slog(struct server *s, log_level level, + const char *body, size_t sz) { + + const char *c = ".-*#"; + time_t now; + char time_buf[64]; + char msg[124]; + char line[256]; /* bounds are checked. */ + int line_sz, ret; + + if(level > s->cfg->verbosity) return; /* too verbose */ + + if(!s->log.fd) return; + + /* limit message size */ + sz = sz ? sz:strlen(body); + snprintf(msg, sz + 1 > sizeof(msg) ? sizeof(msg) : sz + 1, "%s", body); + + /* get current time */ + now = time(NULL); + strftime(time_buf, sizeof(time_buf), "%d %b %H:%M:%S", localtime(&now)); + + /* generate output line. */ + line_sz = snprintf(line, sizeof(line), + "[%d] %s %d %s\n", (int)s->log.self, time_buf, c[level], msg); + + /* write to log and flush to disk. */ + ret = write(s->log.fd, line, line_sz); + ret = fsync(s->log.fd); + + (void)ret; +} diff --git a/slog.h b/slog.h new file mode 100644 index 0000000..25d0d89 --- /dev/null +++ b/slog.h @@ -0,0 +1,23 @@ +#ifndef SLOG_H +#define SLOG_H + +typedef enum { + WEBDIS_ERROR = 0, + WEBDIS_WARNING, + WEBDIS_NOTICE, + WEBDIS_INFO, + WEBDIS_DEBUG +} log_level; + +struct server; + +void +slog_reload(); + +void +slog_init(struct server *s); + +void slog(struct server *s, log_level level, + const char *body, size_t sz); + +#endif diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..a0b8a11 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,18 @@ +OUT=websocket pubsub +CFLAGS=-O3 -Wall -Wextra +LDFLAGS=-levent -lpthread -lrt + +all: $(OUT) Makefile + +websocket: websocket.o + $(CC) -o $@ $< $(LDFLAGS) + +pubsub: pubsub.o + $(CC) -o $@ $< $(LDFLAGS) + +%.o: %.c Makefile + $(CC) -c $(CFLAGS) -o $@ $< + +clean: + rm -f *.o $(OUT) + diff --git a/tests/README.tests b/tests/README.tests new file mode 100644 index 0000000..b47cd80 --- /dev/null +++ b/tests/README.tests @@ -0,0 +1,6 @@ +This directory contains a few test programs for Webdis: + +* basic.py: Unit tests. +* bench.sh: Benchmark of several functions. +* pubsub (run `make' to compile): Tests pub/sub channels; run `./pubsub -h` for options. +* websocket (run `make' to compile): Tests HTML5 WebSockets; run `./websocket -h` for options. diff --git a/tests/basic.py b/tests/basic.py new file mode 100755 index 0000000..89d664e --- /dev/null +++ b/tests/basic.py @@ -0,0 +1,227 @@ +#!/usr/bin/python +import urllib2, unittest, json, hashlib +from functools import wraps +try: + import msgpack +except: + msgpack = None + +import os +host = os.getenv('WEBDIS_HOST', '127.0.0.1') +port = int(os.getenv('WEBDIS_PORT', 7379)) + +class TestWebdis(unittest.TestCase): + + def wrap(self,url): + return 'http://%s:%d/%s' % (host, port, url) + + def query(self, url, data = None, headers={}): + r = urllib2.Request(self.wrap(url), data, headers) + return urllib2.urlopen(r) + +class TestBasics(TestWebdis): + + def test_crossdomain(self): + f = self.query('crossdomain.xml') + self.assertTrue(f.headers.getheader('Content-Type') == 'application/xml') + self.assertTrue("allow-access-from domain" in f.read()) + + def test_options(self): + pass + # not sure if OPTIONS is supported by urllib2... + # f = self.query('') # TODO: call with OPTIONS. + # self.assertTrue(f.headers.getheader('Content-Type') == 'text/html') + # self.assertTrue(f.headers.getheader('Allow') == 'GET,POST,PUT,OPTIONS') + # self.assertTrue(f.headers.getheader('Content-Length') == '0') + # self.assertTrue(f.headers.getheader('Access-Control-Allow-Origin') == '*') + + +class TestJSON(TestWebdis): + + def test_set(self): + "success type (+OK)" + self.query('DEL/hello') + f = self.query('SET/hello/world') + self.assertTrue(f.headers.getheader('Content-Type') == 'application/json') + self.assertTrue(f.headers.getheader('ETag') == '"0db1124cf79ffeb80aff6d199d5822f8"') + self.assertTrue(f.read() == '{"SET":[true,"OK"]}') + + def test_get(self): + "string type" + self.query('SET/hello/world') + f = self.query('GET/hello') + self.assertTrue(f.headers.getheader('Content-Type') == 'application/json') + self.assertTrue(f.headers.getheader('ETag') == '"8cf38afc245b7a6a88696566483d1390"') + self.assertTrue(f.read() == '{"GET":"world"}') + + def test_incr(self): + "integer type" + self.query('DEL/hello') + f = self.query('INCR/hello') + self.assertTrue(f.headers.getheader('Content-Type') == 'application/json') + self.assertTrue(f.headers.getheader('ETag') == '"500e9bcdcbb1e98f25c1fbb880a96c99"') + self.assertTrue(f.read() == '{"INCR":1}') + + def test_list(self): + "list type" + self.query('DEL/hello') + self.query('RPUSH/hello/abc') + self.query('RPUSH/hello/def') + f = self.query('LRANGE/hello/0/-1') + self.assertTrue(f.headers.getheader('Content-Type') == 'application/json') + self.assertTrue(f.headers.getheader('ETag') == '"622e51f547a480bef7cf5452fb7782db"') + self.assertTrue(f.read() == '{"LRANGE":["abc","def"]}') + + def test_error(self): + "error return type" + f = self.query('UNKNOWN/COMMAND') + self.assertTrue(f.headers.getheader('Content-Type') == 'application/json') + try: + obj = json.loads(f.read()) + except: + self.assertTrue(False) + return + + self.assertTrue(len(obj) == 1) + self.assertTrue('UNKNOWN' in obj) + self.assertTrue(isinstance(obj['UNKNOWN'], list)) + self.assertTrue(obj['UNKNOWN'][0] == False) + self.assertTrue(isinstance(obj['UNKNOWN'][1], unicode)) + +class TestCustom(TestWebdis): + def test_list(self): + "List responses with custom format" + self.query('DEL/hello') + self.query('RPUSH/hello/a/b/c') + f = self.query('LRANGE/hello/0/-1.txt') + self.assertTrue(f.headers.getheader('Content-Type') == 'text/plain') + self.assertTrue(f.read() == "abc") + + def test_separator(self): + "Separator in list responses with custom format" + self.query('DEL/hello') + self.query('RPUSH/hello/a/b/c') + f = self.query('LRANGE/hello/0/-1.txt?sep=--') + self.assertTrue(f.headers.getheader('Content-Type') == 'text/plain') + self.assertTrue(f.read() == "a--b--c") + +class TestRaw(TestWebdis): + + def test_set(self): + "success type (+OK)" + self.query('DEL/hello') + f = self.query('SET/hello/world.raw') + self.assertTrue(f.headers.getheader('Content-Type') == 'binary/octet-stream') + self.assertTrue(f.read() == "+OK\r\n") + + def test_get(self): + "string type" + self.query('SET/hello/world') + f = self.query('GET/hello.raw') + self.assertTrue(f.read() == '$5\r\nworld\r\n') + + def test_incr(self): + "integer type" + self.query('DEL/hello') + f = self.query('INCR/hello.raw') + self.assertTrue(f.read() == ':1\r\n') + + def test_list(self): + "list type" + self.query('DEL/hello') + self.query('RPUSH/hello/abc') + self.query('RPUSH/hello/def') + f = self.query('LRANGE/hello/0/-1.raw') + self.assertTrue(f.read() == "*2\r\n$3\r\nabc\r\n$3\r\ndef\r\n") + + def test_error(self): + "error return type" + f = self.query('UNKNOWN/COMMAND.raw') + self.assertTrue(f.read().startswith("-ERR ")) + +def need_msgpack(fn): + def wrapper(self): + if msgpack: + fn(self) + return wrapper + +class TestMsgPack(TestWebdis): + + @need_msgpack + def test_set(self): + "success type (+OK)" + self.query('DEL/hello') + f = self.query('SET/hello/world.msg') + self.assertTrue(f.headers.getheader('Content-Type') == 'application/x-msgpack') + obj = msgpack.loads(f.read()) + self.assertTrue(obj == {'SET': (True, 'OK')}) + + @need_msgpack + def test_get(self): + "string type" + self.query('SET/hello/world') + f = self.query('GET/hello.msg') + obj = msgpack.loads(f.read()) + self.assertTrue(obj == {'GET': 'world'}) + + @need_msgpack + def test_incr(self): + "integer type" + self.query('DEL/hello') + f = self.query('INCR/hello.msg') + obj = msgpack.loads(f.read()) + self.assertTrue(obj == {'INCR': 1}) + + @need_msgpack + def test_list(self): + "list type" + self.query('DEL/hello') + self.query('RPUSH/hello/abc') + self.query('RPUSH/hello/def') + f = self.query('LRANGE/hello/0/-1.msg') + obj = msgpack.loads(f.read()) + self.assertTrue(obj == {'LRANGE': ('abc', 'def')}) + + @need_msgpack + def test_error(self): + "error return type" + f = self.query('UNKNOWN/COMMAND.msg') + obj = msgpack.loads(f.read()) + self.assertTrue('UNKNOWN' in obj) + self.assertTrue(isinstance(obj, dict)) + self.assertTrue(isinstance(obj['UNKNOWN'], tuple)) + self.assertTrue(obj['UNKNOWN'][0] == False) + self.assertTrue(isinstance(obj['UNKNOWN'][1], str)) + +class TestETag(TestWebdis): + + def test_etag_match(self): + self.query('SET/hello/world') + h = hashlib.md5("world").hexdigest() # match Etag + try: + f = self.query('GET/hello.txt', None, {'If-None-Match': '"'+ h +'"'}) + except urllib2.HTTPError as e: + self.assertTrue(e.code == 304) + return + self.assertTrue(False) # we should have received a 304. + + def test_etag_fail(self): + self.query('SET/hello/world') + h = hashlib.md5("nonsense").hexdigest() # non-matching Etag + f = self.query('GET/hello.txt', None, {'If-None-Match': '"'+ h +'"'}) + self.assertTrue(f.read() == 'world') + +class TestDbSwitch(TestWebdis): + def test_db(self): + "Test database change" + self.query('0/SET/key/val0') + self.query('1/SET/key/val1') + f = self.query('0/GET/key.txt') + self.assertTrue(f.read() == "val0") + f = self.query('1/GET/key.txt') + self.assertTrue(f.read() == "val1") + f = self.query('GET/key.txt') + self.assertTrue(f.read() == "val0") + +if __name__ == '__main__': + unittest.main() diff --git a/tests/bench.sh b/tests/bench.sh new file mode 100755 index 0000000..0761696 --- /dev/null +++ b/tests/bench.sh @@ -0,0 +1,71 @@ +#!/bin/bash +CLIENTS=100 +REQUESTS=100000 + +HOST=$WEBDIS_HOST +PORT=$WEBDIS_PORT + +[ -n $HOST ] && HOST=127.0.0.1 +[ -n $PORT ] && PORT=7379 + +info() { + echo "Testing on $HOST:$PORT with $CLIENTS clients in parallel, for a total of $REQUESTS requests per benchmark." +} + +once() { + curl -q http://$HOST:$PORT/$1 1> /dev/null 2> /dev/null +} + +bench() { + NUM=`ab -k -c $CLIENTS -n $REQUESTS http://$HOST:$PORT/$1 2>/dev/null | grep "#/sec" | sed -e "s/[^0-9.]//g"` + echo -ne $NUM +} + +test_ping() { + echo -en "PING: " + bench "PING" + echo " requests/sec." +} + +test_set() { + echo -en "SET(hello,world): " + bench "SET/hello/world" + echo " requests/sec." +} + +test_get() { + echo -en "GET(hello): " + bench "GET/hello" + echo " requests/sec." +} + +test_incr() { + once "DEL/hello" + + echo -en "INCR(hello): " + bench "INCR/hello" + echo " requests/sec." +} + +test_lpush() { + once "DEL/hello" + + echo -en "LPUSH(hello,abc): " + bench "LPUSH/hello/abc" + echo " requests/sec." +} + +test_lrange() { + echo -en "LRANGE(hello,$1,$2): " + bench "LRANGE/hello/$1/$2" + echo " requests/sec." +} + +info +test_ping +test_set +test_get +test_incr +test_lpush +test_lrange 0 10 +test_lrange 0 100 diff --git a/tests/limits.py b/tests/limits.py new file mode 100755 index 0000000..1fdecc8 --- /dev/null +++ b/tests/limits.py @@ -0,0 +1,105 @@ +#!/usr/bin/python +import socket +import unittest + +import os +HOST = os.getenv('WEBDIS_HOST', '127.0.0.1') +PORT = int(os.getenv('WEBDIS_PORT', 7379)) + +class BlockingSocket: + + def __init__(self): + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.s.setblocking(True) + self.s.connect((HOST, PORT)) + + def recv(self): + out = "" + while True: + try: + ret = self.s.recv(4096) + except: + return out + if len(ret) == 0: + return out + out += ret + + def recv_until(self, limit): + out = "" + while not limit in out: + try: + out += self.s.recv(4096) + except: + return False + return out + + def send(self, buf): + sz = len(buf) + done = 0 + while done < sz: + try: + ret = self.s.send(buf[done:4096]) + except: + return False + done = done + ret + # print "Sent %d/%d so far (%s just now)" % (done, sz, ret) + if ret < 0: + return False + return True + +class LargeString: + + def __init__(self, c, n): + self.char = c + self.len = n + + def __len__(self): + return self.len + + def __getitem__(self, chunk): + if chunk.start > self.len: + return "" + if chunk.start + chunk.stop > self.len: + return self.char * (self.len - chunk.start) + return self.char * chunk.stop + +class TestSocket(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super(TestSocket, self).__init__(*args, **kwargs) + self.s = BlockingSocket() + + +class TestHugeUrl(TestSocket): + + def test_huge_url(self): + n = 1024*1024*1024 # 1GB query-string + + start = "GET /GET/x" + end = " HTTP/1.0\r\n\r\n" + + ok = self.s.send(start) + fail1 = self.s.send(LargeString("A", n)) + fail2 = self.s.send(end) + out = self.s.recv() + + self.assertTrue(ok) + self.assertTrue("400 Bad Request" in out) + + def test_huge_upload(self): + n = 1024*1024*1024 # upload 1GB + + start = "PUT /SET/x HTTP/1.0\r\n"\ + + ("Content-Length: %d\r\n" % (n))\ + + "Expect: 100-continue\r\n\r\n" + + ok = self.s.send(start) + cont = self.s.recv_until("\r\n") + fail = self.s.send(LargeString("A", n)) + + self.assertTrue(ok) + self.assertTrue("HTTP/1.1 100 Continue" in cont) + self.assertFalse(fail) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/pubsub.c b/tests/pubsub.c new file mode 100644 index 0000000..f4a3451 --- /dev/null +++ b/tests/pubsub.c @@ -0,0 +1,339 @@ +#include +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + + +/* + * Connection object. + */ +struct cx { + int fd; + + int *counter; /* shared counter of the number of messages received. */ + int total; /* total number of messages to send */ + + char *http_request; + + void (*read_fun)(int,short,void*); /* called when able to read fd */ + void (*write_fun)(int,short,void*); /* called when able to write fd */ + + /* libevent data structures */ + struct event evr, evw; + struct event_base *base; +}; + +int +webdis_connect(const char *host, short port) { + + int ret; + int fd; + struct sockaddr_in addr; + + /* connect socket */ + fd = socket(AF_INET, SOCK_STREAM, 0); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + memset(&(addr.sin_addr), 0, sizeof(addr.sin_addr)); + addr.sin_addr.s_addr = inet_addr(host); + + ret = connect(fd, (struct sockaddr*)&addr, sizeof(struct sockaddr)); + if(ret != 0) { + fprintf(stderr, "connect: ret=%d: %s\n", ret, strerror(errno)); + return -1; + } + + return fd; +} + +/** + * Send request and read until the delimiter string is reached. blocking. + */ +void +reader_http_request(struct cx *c, const char* buffer, const char *limit) { + + char resp[2048]; + int pos = 0; + + int r = write(c->fd, buffer, strlen(buffer)); + (void)r; + + memset(resp, 0, sizeof(resp)); + while(1) { + int ret = read(c->fd, resp+pos, sizeof(resp)-pos); + if(ret <= 0) { + return; + } + pos += ret; + + if(strstr(resp, limit) != NULL) { + break; + } + } +} + +/** + * (re)install connection in the event loop. + */ +void +cx_install(struct cx *c) { + + if(c->read_fun) { /* attach callback for read. */ + event_set(&c->evr, c->fd, EV_READ, c->read_fun, c); + event_base_set(c->base, &c->evr); + event_add(&c->evr, NULL); + } + if(c->write_fun) { /* attach callback for write. */ + event_set(&c->evw, c->fd, EV_WRITE, c->write_fun, c); + event_base_set(c->base, &c->evw); + event_add(&c->evw, NULL); + } + +} + +/** + * Called when a reader has received data. + */ +void +reader_can_read(int fd, short event, void *ptr) { + + char buffer[1024]; + struct cx *c = ptr; + const char *p; + + (void)event; + + int ret = read(fd, buffer, sizeof(buffer)); + if(ret > 0) { + + /* count messages, each message starts with '{' */ + p = buffer; + do { + /* look for the start of a message */ + p = memchr(p, '{', buffer + ret - p); + + if(!p) break; /* none left. */ + p++; + + (*c->counter)++; /* increment the global message counter. */ + if(((*c->counter * 100) % c->total) == 0) { /* show progress. */ + printf("\r%d %%", 100 * *c->counter / c->total); + fflush(stdout); + } + if(*c->counter > c->total) { + /* halt event loop. */ + event_base_loopbreak(c->base); + } + } while(1); + } + + cx_install(c); +} + + +/** + * create a new reader object. + */ +void +reader_new(struct event_base *base, const char *host, short port, int total, int *counter, int chan) { + + struct cx *c = calloc(1, sizeof(struct cx)); + c->base = base; + c->counter = counter; + c->total = total; + c->fd = webdis_connect(host, port); + c->read_fun = reader_can_read; + + /* send subscription request. */ + c->http_request = malloc(100); + sprintf(c->http_request, "GET /SUBSCRIBE/chan:%d HTTP/1.1\r\n\r\n", chan); + reader_http_request(c, c->http_request, "{\"SUBSCRIBE\":[\"subscribe\""); + + /* add to the event loop. */ + cx_install(c); +} + +/** + * Called when a writer has received data back. read and ignore. + */ +void +writer_can_read(int fd, short event, void *ptr) { + char buffer[1024]; + struct cx *c = ptr; + int r; + + (void)event; + + r = read(fd, buffer, sizeof(buffer)); /* discard */ + (void)r; + + /* re-install in the event loop. */ + cx_install(c); +} + +/* send request */ +void +writer_can_write(int fd, short event, void *ptr) { + + struct cx *c = ptr; + + (void)fd; + (void)event; + + reader_http_request(c, c->http_request, "{\"PUBLISH\":"); + + cx_install(c); +} + +void +writer_new(struct event_base *base, const char *host, short port, int chan) { + + struct cx *c = malloc(sizeof(struct cx)); + c->base = base; + c->fd = webdis_connect(host, port); + c->read_fun = writer_can_read; + c->write_fun = writer_can_write; + + /* send request. */ + c->http_request = malloc(100); + sprintf(c->http_request, "GET /PUBLISH/chan:%d/hi HTTP/1.1\r\n\r\n", chan); + reader_http_request(c, c->http_request, "{\"PUBLISH\":"); + + cx_install(c); +} + + +void +usage(const char* argv0, char *host_default, short port_default, + int r_default, int w_default, int n_default, int c_default) { + + printf("Usage: %s [options]\n" + "Options are:\n" + "\t-h host\t\t(default = \"%s\")\n" + "\t-p port\t\t(default = %d)\n" + "\t-r readers\t(default = %d)\n" + "\t-w writers\t(default = %d)\n" + "\t-c channels\t(default = %d)\n" + "\t-n messages\t(number of messages to read in total, default = %d)\n", + argv0, host_default, (int)port_default, + r_default, w_default, + c_default, n_default); +} + +static struct event_base *__base; +void on_sigint(int s) { + (void)s; + event_base_loopbreak(__base); +} + + +int +main(int argc, char *argv[]) { + + /* Create R readers and W writers, send N messages in total. */ + + struct timespec t0, t1; + + struct event_base *base = event_base_new(); + int i, count = 0; + + /* getopt vars */ + int opt; + char *colon; + + /* default values */ + short port_default = 7379; + char *host_default = "127.0.0.1"; + int r_default = 450, w_default = 10, n_default = 100000, c_default = 1; + + + /* real values */ + int r = r_default, w = w_default, chans = c_default, n = n_default; + char *host = host_default; + short port = port_default; + + /* getopt */ + while ((opt = getopt(argc, argv, "h:p:r:w:c:n:")) != -1) { + switch (opt) { + case 'h': + colon = strchr(optarg, ':'); + if(!colon) { + size_t sz = strlen(optarg); + host = calloc(1 + sz, 1); + strncpy(host, optarg, sz); + } else { + host = calloc(1+colon-optarg, 1); + strncpy(host, optarg, colon-optarg); + port = (short)atol(colon+1); + } + break; + + case 'p': + port = (short)atol(optarg); + break; + + case 'r': + r = atoi(optarg); + break; + + case 'w': + w = atoi(optarg); + break; + + case 'c': + chans = atoi(optarg); + break; + + case 'n': + n = atoi(optarg); + break; + + default: + usage(argv[0], host_default, port_default, + r_default, w_default, + n_default, c_default); + exit(EXIT_FAILURE); + } + } + + for(i = 0; i < r; ++i) { + reader_new(base, host, port, n, &count, i % chans); + } + + for(i = 0; i < w; ++i) { + writer_new(base, host, port, i % chans); + } + + /* install signal handler */ + __base = base; + signal(SIGINT, on_sigint); + + /* save time now */ + clock_gettime(CLOCK_MONOTONIC, &t0); + + /* run test */ + event_base_dispatch(base); + + /* timing */ + clock_gettime(CLOCK_MONOTONIC, &t1); + float mili0 = t0.tv_sec * 1000 + t0.tv_nsec / 1000000; + float mili1 = t1.tv_sec * 1000 + t1.tv_nsec / 1000000; + + printf("\rReceived %d messages from %d writers to %d readers through %d channels in %0.2f sec: received %0.2f msg/sec\n", + count, w, r, chans, + (mili1-mili0)/1000.0, + 1000*count/(mili1-mili0)); + + return EXIT_SUCCESS; +} + diff --git a/tests/websocket.c b/tests/websocket.c new file mode 100644 index 0000000..1bce69d --- /dev/null +++ b/tests/websocket.c @@ -0,0 +1,326 @@ +/* http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 */ + +#include +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +struct host_info { + char *host; + short port; +}; + +/* worker_thread, with counter of remaining messages */ +struct worker_thread { + struct host_info *hi; + struct event_base *base; + + int msg_target; + int msg_received; + int msg_sent; + int byte_count; + pthread_t thread; + + struct evbuffer *buffer; + int got_header; + + int verbose; + struct event ev_w; +}; + +void +process_message(struct worker_thread *wt, size_t sz) { + + // printf("process_message\n"); + if(wt->msg_received % 10000 == 0) { + printf("thread %u: %8d messages left (got %9d bytes so far).\n", + (unsigned int)wt->thread, + wt->msg_target - wt->msg_received, wt->byte_count); + } + wt->byte_count += sz; + + /* decrement read count, and stop receiving when we reach zero. */ + wt->msg_received++; + if(wt->msg_received == wt->msg_target) { + event_base_loopexit(wt->base, NULL); + } +} + +void +websocket_write(int fd, short event, void *ptr) { + int ret; + struct worker_thread *wt = ptr; + + if(event != EV_WRITE) { + return; + } + + char message[] = "\x00[\"SET\",\"key\",\"value\"]\xff\x00[\"GET\",\"key\"]\xff"; + ret = write(fd, message, sizeof(message)-1); + if(ret != sizeof(message)-1) { + fprintf(stderr, "write on %d failed: %s\n", fd, strerror(errno)); + close(fd); + } + + wt->msg_sent += 2; + if(wt->msg_sent < wt->msg_target) { + event_set(&wt->ev_w, fd, EV_WRITE, websocket_write, wt); + event_base_set(wt->base, &wt->ev_w); + ret = event_add(&wt->ev_w, NULL); + } +} + +static void +websocket_read(int fd, short event, void *ptr) { + char packet[2048], *pos; + int ret, success = 1; + + struct worker_thread *wt = ptr; + + if(event != EV_READ) { + return; + } + + /* read message */ + ret = read(fd, packet, sizeof(packet)); + pos = packet; + if(ret > 0) { + char *data, *last; + int sz, msg_sz; + + if(wt->got_header == 0) { /* first response */ + char *frame_start = strstr(packet, "MH"); /* end of the handshake */ + if(frame_start == NULL) { + return; /* not yet */ + } else { /* start monitoring possible writes */ + printf("start monitoring possible writes\n"); + evbuffer_add(wt->buffer, frame_start + 2, ret - (frame_start + 2 - packet)); + + wt->got_header = 1; + event_set(&wt->ev_w, fd, EV_WRITE, + websocket_write, wt); + event_base_set(wt->base, &wt->ev_w); + ret = event_add(&wt->ev_w, NULL); + } + } else { + /* we've had the header already, now bufffer data. */ + evbuffer_add(wt->buffer, packet, ret); + } + + while(1) { + data = (char*)EVBUFFER_DATA(wt->buffer); + sz = EVBUFFER_LENGTH(wt->buffer); + + if(sz == 0) { /* no data */ + break; + } + if(*data != 0) { /* missing frame start */ + success = 0; + break; + } + last = memchr(data, 0xff, sz); /* look for frame end */ + if(!last) { + /* no end of frame in sight. */ + break; + } + msg_sz = last - data - 1; + process_message(ptr, msg_sz); /* record packet */ + + /* drain including frame delimiters (+2 bytes) */ + evbuffer_drain(wt->buffer, msg_sz + 2); + } + } else { + printf("ret=%d\n", ret); + success = 0; + } + if(success == 0) { + shutdown(fd, SHUT_RDWR); + close(fd); + event_base_loopexit(wt->base, NULL); + } +} + +void* +worker_main(void *ptr) { + + char ws_template[] = "GET /.json HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Connection: Upgrade\r\n" + "Upgrade: WebSocket\r\n" + "Origin: http://%s:%d\r\n" + "Sec-WebSocket-Key1: 18x 6]8vM;54 *(5: { U1]8 z [ 8\r\n" + "Sec-WebSocket-Key2: 1_ tx7X d < nw 334J702) 7]o}` 0\r\n" + "\r\n" + "Tm[K T2u"; + + struct worker_thread *wt = ptr; + + int ret; + int fd; + struct sockaddr_in addr; + char *ws_handshake; + size_t ws_handshake_sz; + + /* connect socket */ + fd = socket(AF_INET, SOCK_STREAM, 0); + addr.sin_family = AF_INET; + addr.sin_port = htons(wt->hi->port); + memset(&(addr.sin_addr), 0, sizeof(addr.sin_addr)); + addr.sin_addr.s_addr = inet_addr(wt->hi->host); + + ret = connect(fd, (struct sockaddr*)&addr, sizeof(struct sockaddr)); + if(ret != 0) { + fprintf(stderr, "connect: ret=%d: %s\n", ret, strerror(errno)); + return NULL; + } + + /* initialize worker thread */ + wt->base = event_base_new(); + wt->buffer = evbuffer_new(); + wt->byte_count = 0; + wt->got_header = 0; + + /* send handshake */ + ws_handshake_sz = sizeof(ws_handshake) + + 2*strlen(wt->hi->host) + 500; + ws_handshake = calloc(ws_handshake_sz, 1); + ws_handshake_sz = (size_t)sprintf(ws_handshake, ws_template, + wt->hi->host, wt->hi->port, + wt->hi->host, wt->hi->port); + ret = write(fd, ws_handshake, ws_handshake_sz); + + struct event ev_r; + event_set(&ev_r, fd, EV_READ | EV_PERSIST, websocket_read, wt); + event_base_set(wt->base, &ev_r); + event_add(&ev_r, NULL); + + /* go! */ + event_base_dispatch(wt->base); + event_base_free(wt->base); + free(ws_handshake); + return NULL; +} + +void +usage(const char* argv0, char *host_default, short port_default, + int thread_count_default, int messages_default) { + + printf("Usage: %s [options]\n" + "Options are:\n" + "\t-h host\t\t(default = \"%s\")\n" + "\t-p port\t\t(default = %d)\n" + "\t-c threads\t(default = %d)\n" + "\t-n count\t(number of messages per thread, default = %d)\n" + "\t-v\t\t(verbose)\n", + argv0, host_default, (int)port_default, + thread_count_default, messages_default); +} + +int +main(int argc, char *argv[]) { + + struct timespec t0, t1; + + int messages_default = 100000; + int thread_count_default = 4; + short port_default = 7379; + char *host_default = "127.0.0.1"; + + int msg_target = messages_default; + int thread_count = thread_count_default; + int i, opt; + char *colon; + double total = 0, total_bytes = 0; + int verbose = 0; + + struct host_info hi = {host_default, port_default}; + + struct worker_thread *workers; + + /* getopt */ + while ((opt = getopt(argc, argv, "h:p:c:n:v")) != -1) { + switch (opt) { + case 'h': + colon = strchr(optarg, ':'); + if(!colon) { + size_t sz = strlen(optarg); + hi.host = calloc(1 + sz, 1); + strncpy(hi.host, optarg, sz); + } else { + hi.host = calloc(1+colon-optarg, 1); + strncpy(hi.host, optarg, colon-optarg); + hi.port = (short)atol(colon+1); + } + break; + + case 'p': + hi.port = (short)atol(optarg); + break; + + case 'c': + thread_count = atoi(optarg); + break; + + case 'n': + msg_target = atoi(optarg); + break; + + case 'v': + verbose = 1; + break; + default: /* '?' */ + usage(argv[0], host_default, port_default, + thread_count_default, + messages_default); + exit(EXIT_FAILURE); + } + } + + /* run threads */ + workers = calloc(sizeof(struct worker_thread), thread_count); + + clock_gettime(CLOCK_MONOTONIC, &t0); + for(i = 0; i < thread_count; ++i) { + workers[i].msg_target = msg_target; + workers[i].hi = &hi; + workers[i].verbose = verbose; + + pthread_create(&workers[i].thread, NULL, + worker_main, &workers[i]); + } + + /* wait for threads to finish */ + for(i = 0; i < thread_count; ++i) { + pthread_join(workers[i].thread, NULL); + total += workers[i].msg_received; + total_bytes += workers[i].byte_count; + } + + /* timing */ + clock_gettime(CLOCK_MONOTONIC, &t1); + float mili0 = t0.tv_sec * 1000 + t0.tv_nsec / 1000000; + float mili1 = t1.tv_sec * 1000 + t1.tv_nsec / 1000000; + + if(total != 0) { + printf("Read %ld messages in %0.2f sec: %0.2f msg/sec (%d MB/sec, %d KB/sec)\n", + (long)total, + (mili1-mili0)/1000.0, + 1000*total/(mili1-mili0), + (int)(total_bytes / (1000*(mili1-mili0))), + (int)(total_bytes / (mili1-mili0))); + return EXIT_SUCCESS; + } else { + printf("No message was read.\n"); + return EXIT_FAILURE; + } +} + diff --git a/tests/websocket.html b/tests/websocket.html new file mode 100644 index 0000000..a30f936 --- /dev/null +++ b/tests/websocket.html @@ -0,0 +1,153 @@ + + + + WebSocket example + + + + + +
Webdis with HTML5 WebSockets
+ +
+

JSON

+
+ Connecting... +
+
+
+

Raw

+
+ Connecting... +
+
+ + + + + diff --git a/version.h b/version.h new file mode 100644 index 0000000..8022b9a --- /dev/null +++ b/version.h @@ -0,0 +1,8 @@ +#ifndef VERSION_H +#define VERSION_H + +#ifndef WEBDIS_VERSION +#define WEBDIS_VERSION "0.1.1" +#endif + +#endif /* VERSION_H */ diff --git a/webdis.c b/webdis.c new file mode 100644 index 0000000..34e88e3 --- /dev/null +++ b/webdis.c @@ -0,0 +1,20 @@ +#include "server.h" + +#include + +int +main(int argc, char *argv[]) { + + struct server *s; + + if(argc > 1) { + s = server_new(argv[1]); + } else { + s = server_new("webdis.json"); + } + + server_start(s); + + return EXIT_SUCCESS; +} + diff --git a/webdis.json b/webdis.json new file mode 100644 index 0000000..f4b0714 --- /dev/null +++ b/webdis.json @@ -0,0 +1,31 @@ +{ + "redis_host": "127.0.0.1", + + "redis_port": 6379, + "redis_auth": null, + + "http_host": "0.0.0.0", + "http_port": 7379, + + "threads": 5, + "pool_size": 20, + + "daemonize": false, + "websockets": false, + + "database": 0, + + "acl": [ + { + "disabled": ["DEBUG"] + }, + + { + "http_basic_auth": "user:password", + "enabled": ["DEBUG"] + } + ], + + "verbosity": 6, + "logfile": "webdis.log" +} diff --git a/webdis.prod.json b/webdis.prod.json new file mode 100644 index 0000000..de35ca8 --- /dev/null +++ b/webdis.prod.json @@ -0,0 +1,28 @@ +{ + "redis_host": "127.0.0.1", + + "redis_port": 6379, + "redis_auth": null, + + "http_host": "0.0.0.0", + "http_port": 7379, + "threads": 4, + + "daemonize": true, + + "database": 0, + + "acl": [ + { + "disabled": ["DEBUG"] + }, + + { + "http_basic_auth": "user:password", + "enabled": ["DEBUG"] + } + ], + + "verbosity": 3, + "logfile": "/var/log/webdis.log" +} diff --git a/websocket.c b/websocket.c new file mode 100644 index 0000000..9b0223c --- /dev/null +++ b/websocket.c @@ -0,0 +1,380 @@ +#include "sha1/sha1.h" +#include +#include "websocket.h" +#include "client.h" +#include "cmd.h" +#include "worker.h" +#include "pool.h" +#include "http.h" + +/* message parsers */ +#include "formats/json.h" +#include "formats/raw.h" + +#include +#include +#include +#include +#include +#include + +/** + * This code uses the WebSocket specification from RFC 6455. + * A copy is available at http://www.rfc-editor.org/rfc/rfc6455.txt + */ + +/* custom 64-bit encoding functions to avoid portability issues */ +#define webdis_ntohl64(p) \ + ((((uint64_t)((p)[0])) << 0) + (((uint64_t)((p)[1])) << 8) +\ + (((uint64_t)((p)[2])) << 16) + (((uint64_t)((p)[3])) << 24) +\ + (((uint64_t)((p)[4])) << 32) + (((uint64_t)((p)[5])) << 40) +\ + (((uint64_t)((p)[6])) << 48) + (((uint64_t)((p)[7])) << 56)) + +#define webdis_htonl64(p) {\ + (char)(((p & ((uint64_t)0xff << 0)) >> 0) & 0xff), (char)(((p & ((uint64_t)0xff << 8)) >> 8) & 0xff), \ + (char)(((p & ((uint64_t)0xff << 16)) >> 16) & 0xff), (char)(((p & ((uint64_t)0xff << 24)) >> 24) & 0xff), \ + (char)(((p & ((uint64_t)0xff << 32)) >> 32) & 0xff), (char)(((p & ((uint64_t)0xff << 40)) >> 40) & 0xff), \ + (char)(((p & ((uint64_t)0xff << 48)) >> 48) & 0xff), (char)(((p & ((uint64_t)0xff << 56)) >> 56) & 0xff) } +static int +ws_compute_handshake(struct http_client *c, char *out, size_t *out_sz) { + + unsigned char *buffer, sha1_output[20]; + char magic[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + SHA1Context ctx; + base64_encodestate b64_ctx; + int pos, i; + + // websocket handshake + const char *key = client_get_header(c, "Sec-WebSocket-Key"); + size_t key_sz = key?strlen(key):0, buffer_sz = key_sz + sizeof(magic) - 1; + buffer = calloc(buffer_sz, 1); + + // concatenate key and guid in buffer + memcpy(buffer, key, key_sz); + memcpy(buffer+key_sz, magic, sizeof(magic)-1); + + // compute sha-1 + SHA1Reset(&ctx); + SHA1Input(&ctx, buffer, buffer_sz); + SHA1Result(&ctx); + for(i = 0; i < 5; ++i) { // put in correct byte order before memcpy. + ctx.Message_Digest[i] = ntohl(ctx.Message_Digest[i]); + } + memcpy(sha1_output, (unsigned char*)ctx.Message_Digest, 20); + + // encode `sha1_output' in base 64, into `out'. + base64_init_encodestate(&b64_ctx); + pos = base64_encode_block((const char*)sha1_output, 20, out, &b64_ctx); + base64_encode_blockend(out + pos, &b64_ctx); + + // compute length, without \n + *out_sz = strlen(out); + if(out[*out_sz-1] == '\n') + (*out_sz)--; + + free(buffer); + + return 0; +} + +int +ws_handshake_reply(struct http_client *c) { + + int ret; + char sha1_handshake[40]; + char *buffer = NULL, *p; + const char *origin = NULL, *host = NULL; + size_t origin_sz = 0, host_sz = 0, handshake_sz = 0, sz; + + char template0[] = "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Origin: "; /* %s */ + char template1[] = "\r\n" + "Sec-WebSocket-Location: ws://"; /* %s%s */ + char template2[] = "\r\n" + "Origin: http://"; /* %s */ + char template3[] = "\r\n" + "Sec-WebSocket-Accept: "; /* %s */ + char template4[] = "\r\n\r\n"; + + if((origin = client_get_header(c, "Origin"))) { + origin_sz = strlen(origin); + } else if((origin = client_get_header(c, "Sec-WebSocket-Origin"))) { + origin_sz = strlen(origin); + } + if((host = client_get_header(c, "Host"))) { + host_sz = strlen(host); + } + + /* need those headers */ + if(!origin || !origin_sz || !host || !host_sz || !c->path || !c->path_sz) { + return -1; + } + + memset(sha1_handshake, 0, sizeof(sha1_handshake)); + if(ws_compute_handshake(c, &sha1_handshake[0], &handshake_sz) != 0) { + /* failed to compute handshake. */ + return -1; + } + + sz = sizeof(template0)-1 + origin_sz + + sizeof(template1)-1 + host_sz + c->path_sz + + sizeof(template2)-1 + host_sz + + sizeof(template3)-1 + handshake_sz + + sizeof(template4)-1; + + p = buffer = malloc(sz); + + /* Concat all */ + + /* template0 */ + memcpy(p, template0, sizeof(template0)-1); + p += sizeof(template0)-1; + memcpy(p, origin, origin_sz); + p += origin_sz; + + /* template1 */ + memcpy(p, template1, sizeof(template1)-1); + p += sizeof(template1)-1; + memcpy(p, host, host_sz); + p += host_sz; + memcpy(p, c->path, c->path_sz); + p += c->path_sz; + + /* template2 */ + memcpy(p, template2, sizeof(template2)-1); + p += sizeof(template2)-1; + memcpy(p, host, host_sz); + p += host_sz; + + /* template3 */ + memcpy(p, template3, sizeof(template3)-1); + p += sizeof(template3)-1; + memcpy(p, &sha1_handshake[0], handshake_sz); + p += handshake_sz; + + /* template4 */ + memcpy(p, template4, sizeof(template4)-1); + p += sizeof(template4)-1; + + /* send data to client */ + ret = write(c->fd, buffer, sz); + (void)ret; + free(buffer); + + return 0; +} + + +static int +ws_execute(struct http_client *c, const char *frame, size_t frame_len) { + + struct cmd*(*fun_extract)(struct http_client *, const char *, size_t) = NULL; + formatting_fun fun_reply = NULL; + + if((c->path_sz == 1 && strncmp(c->path, "/", 1) == 0) || + strncmp(c->path, "/.json", 6) == 0) { + fun_extract = json_ws_extract; + fun_reply = json_reply; + } else if(strncmp(c->path, "/.raw", 5) == 0) { + fun_extract = raw_ws_extract; + fun_reply = raw_reply; + } + + if(fun_extract) { + + /* Parse websocket frame into a cmd object. */ + struct cmd *cmd = fun_extract(c, frame, frame_len); + + if(cmd) { + /* copy client info into cmd. */ + cmd_setup(cmd, c); + cmd->is_websocket = 1; + + if (c->pub_sub != NULL) { + /* This client already has its own connection + * to Redis due to a subscription; use it from + * now on. */ + cmd->ac = c->pub_sub->ac; + } else if (cmd_is_subscribe(cmd)) { + /* New subscribe command; make new Redis context + * for this client */ + cmd->ac = pool_connect(c->w->pool, cmd->database, 0); + c->pub_sub = cmd; + cmd->pub_sub_client = c; + } else { + /* get Redis connection from pool */ + cmd->ac = (redisAsyncContext*)pool_get_context(c->w->pool); + } + + /* send it off */ + cmd_send(cmd, fun_reply); + + return 0; + } + } + + return -1; +} + +static struct ws_msg * +ws_msg_new() { + return calloc(1, sizeof(struct ws_msg)); +} + +static void +ws_msg_add(struct ws_msg *m, const char *p, size_t psz, const unsigned char *mask) { + + /* add data to frame */ + size_t i; + m->payload = realloc(m->payload, m->payload_sz + psz); + memcpy(m->payload + m->payload_sz, p, psz); + + /* apply mask */ + for(i = 0; i < psz && mask; ++i) { + m->payload[m->payload_sz + i] = (unsigned char)p[i] ^ mask[i%4]; + } + + /* save new size */ + m->payload_sz += psz; +} + +static void +ws_msg_free(struct ws_msg **m) { + + free((*m)->payload); + free(*m); + *m = NULL; +} + +static enum ws_state +ws_parse_data(const char *frame, size_t sz, struct ws_msg **msg) { + + int has_mask; + uint64_t len; + const char *p; + unsigned char mask[4]; + + /* parse frame and extract contents */ + if(sz < 8) { + return WS_READING; + } + + has_mask = frame[1] & 0x80 ? 1:0; + + /* get payload length */ + len = frame[1] & 0x7f; /* remove leftmost bit */ + if(len <= 125) { /* data starts right after the mask */ + p = frame + 2 + (has_mask ? 4 : 0); + if(has_mask) memcpy(&mask, frame + 2, sizeof(mask)); + } else if(len == 126) { + uint16_t sz16; + memcpy(&sz16, frame + 2, sizeof(uint16_t)); + len = ntohs(sz16); + p = frame + 4 + (has_mask ? 4 : 0); + if(has_mask) memcpy(&mask, frame + 4, sizeof(mask)); + } else if(len == 127) { + len = webdis_ntohl64(frame+2); + p = frame + 10 + (has_mask ? 4 : 0); + if(has_mask) memcpy(&mask, frame + 10, sizeof(mask)); + } else { + return WS_ERROR; + } + + /* we now have the (possibly masked) data starting in p, and its length. */ + if(len > sz - (p - frame)) { /* not enough data */ + return WS_READING; + } + + if(!*msg) + *msg = ws_msg_new(); + ws_msg_add(*msg, p, len, has_mask ? mask : NULL); + (*msg)->total_sz += len + (p - frame); + + if(frame[0] & 0x80) { /* FIN bit set */ + return WS_MSG_COMPLETE; + } else { + return WS_READING; /* need more data */ + } +} + + +/** + * Process some data just received on the socket. + */ +enum ws_state +ws_add_data(struct http_client *c) { + + enum ws_state state; + + state = ws_parse_data(c->buffer, c->sz, &c->frame); + + if(state == WS_MSG_COMPLETE) { + int ret = ws_execute(c, c->frame->payload, c->frame->payload_sz); + + /* remove frame from client buffer */ + http_client_remove_data(c, c->frame->total_sz); + + /* free frame and set back to NULL */ + ws_msg_free(&c->frame); + + if(ret != 0) { + /* can't process frame. */ + return WS_ERROR; + } + } + return state; +} + +int +ws_reply(struct cmd *cmd, const char *p, size_t sz) { + + char *frame = malloc(sz + 8); /* create frame by prepending header */ + size_t frame_sz = 0; + struct http_response *r; + if (frame == NULL) + return -1; + + /* + The length of the "Payload data", in bytes: if 0-125, that is the + payload length. If 126, the following 2 bytes interpreted as a + 16-bit unsigned integer are the payload length. If 127, the + following 8 bytes interpreted as a 64-bit unsigned integer (the + most significant bit MUST be 0) are the payload length. + */ + frame[0] = '\x81'; + if(sz <= 125) { + frame[1] = sz; + memcpy(frame + 2, p, sz); + frame_sz = sz + 2; + } else if (sz > 125 && sz <= 65536) { + uint16_t sz16 = htons(sz); + frame[1] = 126; + memcpy(frame + 2, &sz16, 2); + memcpy(frame + 4, p, sz); + frame_sz = sz + 4; + } else if (sz > 65536) { + char sz64[8] = webdis_htonl64(sz); + frame[1] = 127; + memcpy(frame + 2, sz64, 8); + memcpy(frame + 10, p, sz); + frame_sz = sz + 10; + } + + /* send WS frame */ + r = http_response_init(cmd->w, 0, NULL); + if (cmd_is_subscribe(cmd)) { + r->keep_alive = 1; + } + + if (r == NULL) + return -1; + + r->out = frame; + r->out_sz = frame_sz; + r->sent = 0; + http_schedule_write(cmd->fd, r); + + return 0; +} diff --git a/websocket.h b/websocket.h new file mode 100644 index 0000000..dc9d764 --- /dev/null +++ b/websocket.h @@ -0,0 +1,30 @@ +#ifndef WEBSOCKET_H +#define WEBSOCKET_H + +#include +#include + +struct http_client; +struct cmd; + +enum ws_state { + WS_ERROR, + WS_READING, + WS_MSG_COMPLETE}; + +struct ws_msg { + char *payload; + size_t payload_sz; + size_t total_sz; +}; + +int +ws_handshake_reply(struct http_client *c); + +enum ws_state +ws_add_data(struct http_client *c); + +int +ws_reply(struct cmd *cmd, const char *p, size_t sz); + +#endif diff --git a/worker.c b/worker.c new file mode 100644 index 0000000..ceec0f9 --- /dev/null +++ b/worker.c @@ -0,0 +1,234 @@ +#include "worker.h" +#include "client.h" +#include "http.h" +#include "cmd.h" +#include "pool.h" +#include "slog.h" +#include "websocket.h" +#include "conf.h" +#include "server.h" + +#include +#include +#include +#include +#include + + +struct worker * +worker_new(struct server *s) { + + int ret; + struct worker *w = calloc(1, sizeof(struct worker)); + w->s = s; + + /* setup communication link */ + ret = pipe(w->link); + (void)ret; + + /* Redis connection pool */ + w->pool = pool_new(w, s->cfg->pool_size_per_thread); + + return w; + +} + +void +worker_can_read(int fd, short event, void *p) { + + struct http_client *c = p; + int ret, nparsed; + + (void)fd; + (void)event; + + ret = http_client_read(c); + if(ret <= 0) { + if((client_error_t)ret == CLIENT_DISCONNECTED) { + return; + } else if (c->failed_alloc || (client_error_t)ret == CLIENT_OOM) { + slog(c->w->s, WEBDIS_DEBUG, "503", 3); + http_send_error(c, 503, "Service Unavailable"); + return; + } + } + + if(c->is_websocket) { + /* Got websocket data */ + ws_add_data(c); + } else { + /* run parser */ + nparsed = http_client_execute(c); + + if(c->failed_alloc) { + slog(c->w->s, WEBDIS_DEBUG, "503", 3); + http_send_error(c, 503, "Service Unavailable"); + } else if(c->is_websocket) { + /* we need to use the remaining (unparsed) data as the body. */ + if(nparsed < ret) { + http_client_add_to_body(c, c->buffer + nparsed + 1, c->sz - nparsed - 1); + ws_handshake_reply(c); + } else { + c->broken = 1; + } + free(c->buffer); + c->buffer = NULL; + c->sz = 0; + } else if(nparsed != ret) { + slog(c->w->s, WEBDIS_DEBUG, "400", 3); + http_send_error(c, 400, "Bad Request"); + } else if(c->request_sz > c->s->cfg->http_max_request_size) { + slog(c->w->s, WEBDIS_DEBUG, "413", 3); + http_send_error(c, 413, "Request Entity Too Large"); + } + } + + if(c->broken) { /* terminate client */ + http_client_free(c); + } else { + /* start monitoring input again */ + worker_monitor_input(c); + } +} + +/** + * Monitor client FD for possible reads. + */ +void +worker_monitor_input(struct http_client *c) { + + event_set(&c->ev, c->fd, EV_READ, worker_can_read, c); + event_base_set(c->w->base, &c->ev); + event_add(&c->ev, NULL); +} + +/** + * Called when a client is sent to this worker. + */ +static void +worker_on_new_client(int pipefd, short event, void *ptr) { + + struct http_client *c; + unsigned long addr; + + (void)event; + (void)ptr; + + /* Get client from messaging pipe */ + int ret = read(pipefd, &addr, sizeof(addr)); + if(ret == sizeof(addr)) { + c = (struct http_client*)addr; + + /* monitor client for input */ + worker_monitor_input(c); + } +} + +static void +worker_pool_connect(struct worker *w) { + + int i; + /* create connections */ + for(i = 0; i < w->pool->count; ++i) { + pool_connect(w->pool, w->s->cfg->database, 1); + } + +} + +static void* +worker_main(void *p) { + + struct worker *w = p; + struct event ev; + + /* setup libevent */ + w->base = event_base_new(); + + /* monitor pipe link */ + event_set(&ev, w->link[0], EV_READ | EV_PERSIST, worker_on_new_client, w); + event_base_set(w->base, &ev); + event_add(&ev, NULL); + + /* connect to Redis */ + worker_pool_connect(w); + + /* loop */ + event_base_dispatch(w->base); + + return NULL; +} + +void +worker_start(struct worker *w) { + + pthread_create(&w->thread, NULL, worker_main, w); +} + +/** + * Queue new client to process + */ +void +worker_add_client(struct worker *w, struct http_client *c) { + + /* write into pipe link */ + unsigned long addr = (unsigned long)c; + int ret = write(w->link[1], &addr, sizeof(addr)); + (void)ret; +} + +/** + * Called when a client has finished reading input and can create a cmd + */ +void +worker_process_client(struct http_client *c) { + + /* check that the command can be executed */ + struct worker *w = c->w; + cmd_response_t ret = CMD_PARAM_ERROR; + switch(c->parser.method) { + case HTTP_GET: + if(c->path_sz == 16 && memcmp(c->path, "/crossdomain.xml", 16) == 0) { + http_crossdomain(c); + return; + } + slog(w->s, WEBDIS_DEBUG, c->path, c->path_sz); + ret = cmd_run(c->w, c, 1+c->path, c->path_sz-1, NULL, 0); + break; + + case HTTP_POST: + slog(w->s, WEBDIS_DEBUG, c->path, c->path_sz); + ret = cmd_run(c->w, c, c->body, c->body_sz, NULL, 0); + break; + + case HTTP_PUT: + slog(w->s, WEBDIS_DEBUG, c->path, c->path_sz); + ret = cmd_run(c->w, c, 1+c->path, c->path_sz-1, + c->body, c->body_sz); + break; + + case HTTP_OPTIONS: + http_send_options(c); + + default: + slog(w->s, WEBDIS_DEBUG, "405", 3); + http_send_error(c, 405, "Method Not Allowed"); + return; + } + + switch(ret) { + case CMD_ACL_FAIL: + case CMD_PARAM_ERROR: + slog(w->s, WEBDIS_DEBUG, "403", 3); + http_send_error(c, 403, "Forbidden"); + break; + + case CMD_REDIS_UNAVAIL: + slog(w->s, WEBDIS_DEBUG, "503", 3); + http_send_error(c, 503, "Service Unavailable"); + break; + default: + break; + } + +} + diff --git a/worker.h b/worker.h new file mode 100644 index 0000000..b4e9cde --- /dev/null +++ b/worker.h @@ -0,0 +1,41 @@ +#ifndef WORKER_H +#define WORKER_H + +#include + +struct http_client; +struct pool; + +struct worker { + + /* self */ + pthread_t thread; + struct event_base *base; + + /* connection dispatcher */ + struct server *s; + int link[2]; + + /* Redis connection pool */ + struct pool *pool; +}; + +struct worker * +worker_new(struct server *s); + +void +worker_start(struct worker *w); + +void +worker_add_client(struct worker *w, struct http_client *c); + +void +worker_monitor_input(struct http_client *c); + +void +worker_can_read(int fd, short event, void *p); + +void +worker_process_client(struct http_client *c); + +#endif