diff --git a/CMakeLists.txt b/CMakeLists.txt index 100d770357..8ada925c80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -820,6 +820,7 @@ set(HDR_PRIVATE util-internal.h openssl-compat.h evconfig-private.h + sha1.h compat/sys/queue.h) set(HDR_COMPAT @@ -854,6 +855,7 @@ set(HDR_PUBLIC include/event2/tag_compat.h include/event2/thread.h include/event2/util.h + include/event2/ws.h include/event2/visibility.h ${PROJECT_BINARY_DIR}/include/event2/event-config.h) @@ -966,8 +968,12 @@ set(SRC_EXTRA event_tagging.c http.c evdns.c + ws.c + sha1.c evrpc.c) +set_source_files_properties(sha1.c PROPERTIES COMPILE_FLAGS + -D${CMAKE_C_BYTE_ORDER}=1) add_definitions(-DHAVE_CONFIG_H) # We use BEFORE here so we don't accidentally look in system directories @@ -1134,6 +1140,7 @@ if (NOT EVENT__DISABLE_SAMPLES) set(SAMPLES_WOPT dns-example + ws-chat-server http-server ) foreach (SAMPLE ${SAMPLES_WOPT}) @@ -1217,6 +1224,7 @@ if (NOT EVENT__DISABLE_TESTS) test/regress_et.c test/regress_finalize.c test/regress_http.c + test/regress_http.h test/regress_listener.c test/regress_main.c test/regress_minheap.c @@ -1225,6 +1233,8 @@ if (NOT EVENT__DISABLE_TESTS) test/regress_testutils.h test/regress_util.c test/regress_watch.c + test/regress_ws.c + test/regress_ws.h test/tinytest.c) if (WIN32) diff --git a/Doxyfile b/Doxyfile index 486ab66900..3e4e96aec4 100644 --- a/Doxyfile +++ b/Doxyfile @@ -81,6 +81,7 @@ INPUT = \ $(SRCDIR)/include/event2/tag.h \ $(SRCDIR)/include/event2/tag_compat.h \ $(SRCDIR)/include/event2/thread.h \ + $(SRCDIR)/include/event2/ws.h \ $(SRCDIR)/include/event2/util.h \ $(SRCDIR)/include/event2/watch.h diff --git a/Makefile.am b/Makefile.am index 5c97c375ac..2007bedaff 100644 --- a/Makefile.am +++ b/Makefile.am @@ -259,6 +259,8 @@ EXTRAS_SRC = \ evdns.c \ event_tagging.c \ evrpc.c \ + sha1.c \ + ws.c \ http.c if BUILD_WITH_NO_UNDEFINED @@ -338,6 +340,7 @@ noinst_HEADERS += \ util-internal.h \ openssl-compat.h \ mbedtls-compat.h \ + sha1.h \ ssl-compat.h \ wepoll.h diff --git a/configure.ac b/configure.ac index 0d7e098ce1..b0a37fe85e 100644 --- a/configure.ac +++ b/configure.ac @@ -857,6 +857,8 @@ AC_SUBST([LIBEVENT_GC_SECTIONS]) AM_CONDITIONAL([INSTALL_LIBEVENT], [test "$enable_libevent_install" = "yes"]) +AC_C_BIGENDIAN([CFLAGS="$CFLAGS -DBIG_ENDIAN"], [CFLAGS="$CFLAGS -DLITTLE_ENDIAN"]) + dnl Doxygen support DX_HTML_FEATURE(ON) DX_MAN_FEATURE(OFF) diff --git a/http-internal.h b/http-internal.h index 705daba29f..976a119845 100644 --- a/http-internal.h +++ b/http-internal.h @@ -124,6 +124,9 @@ struct evhttp_cb { /* both the http server as well as the rpc system need to queue connections */ TAILQ_HEAD(evconq, evhttp_connection); +/* WebSockets connections */ +TAILQ_HEAD(evwsq, evws_connection); + /* each bound socket is stored in one of these */ struct evhttp_bound_socket { TAILQ_ENTRY(evhttp_bound_socket) next; @@ -151,8 +154,10 @@ struct evhttp { TAILQ_HEAD(httpcbq, evhttp_cb) callbacks; - /* All live connections on this host. */ + /* All live HTTP connections on this host. */ struct evconq connections; + /* All live WebSockets sessions on this host. */ + struct evwsq ws_sessions; int connection_max; int connection_cnt; @@ -221,6 +226,8 @@ void evhttp_start_write_(struct evhttp_connection *); void evhttp_response_code_(struct evhttp_request *, int, const char *); void evhttp_send_page_(struct evhttp_request *, struct evbuffer *); +struct bufferevent * evhttp_start_ws_(struct evhttp_request *req); + /* [] has been stripped */ #define _EVHTTP_URI_HOST_HAS_BRACKETS 0x02 diff --git a/http.c b/http.c index 1421a8e628..bd97565689 100644 --- a/http.c +++ b/http.c @@ -107,6 +107,7 @@ #include "event2/http_struct.h" #include "event2/http_compat.h" #include "event2/util.h" +#include "event2/ws.h" #include "event2/listener.h" #include "log-internal.h" #include "util-internal.h" @@ -3191,6 +3192,31 @@ evhttp_send_reply_chunk_with_cb(struct evhttp_request *req, struct evbuffer *dat evhttp_write_buffer(evcon, cb, arg); } +struct bufferevent * +evhttp_start_ws_(struct evhttp_request *req) +{ + struct evhttp_connection *evcon = req->evcon; + struct bufferevent *bufev; + + evhttp_response_code_(req, HTTP_SWITCH_PROTOCOLS, "Switching Protocols"); + + if (req->evcon == NULL) + return NULL; + + evhttp_make_header(req->evcon, req); + evhttp_write_buffer(req->evcon, NULL, NULL); + + TAILQ_REMOVE(&evcon->requests, req, next); + + bufev = evcon->bufev; + evcon->bufev = NULL; + evcon->closecb = NULL; + + evhttp_request_free(req); + evhttp_connection_free(evcon); + return bufev; +} + void evhttp_send_reply_chunk(struct evhttp_request *req, struct evbuffer *databuf) { @@ -3961,6 +3987,7 @@ evhttp_new_object(void) TAILQ_INIT(&http->sockets); TAILQ_INIT(&http->callbacks); TAILQ_INIT(&http->connections); + TAILQ_INIT(&http->ws_sessions); TAILQ_INIT(&http->virtualhosts); TAILQ_INIT(&http->aliases); @@ -4005,6 +4032,7 @@ evhttp_free(struct evhttp* http) { struct evhttp_cb *http_cb; struct evhttp_connection *evcon; + struct evws_connection *evws; struct evhttp_bound_socket *bound; struct evhttp* vhost; struct evhttp_server_alias *alias; @@ -4023,6 +4051,10 @@ evhttp_free(struct evhttp* http) evhttp_connection_free(evcon); } + while ((evws = TAILQ_FIRST(&http->ws_sessions)) != NULL) { + evws_connection_free(evws); + } + while ((http_cb = TAILQ_FIRST(&http->callbacks)) != NULL) { TAILQ_REMOVE(&http->callbacks, http_cb, next); mm_free(http_cb->what); diff --git a/include/event2/ws.h b/include/event2/ws.h new file mode 100644 index 0000000000..0816a1266e --- /dev/null +++ b/include/event2/ws.h @@ -0,0 +1,58 @@ +#ifndef EVENT2_WS_H_INCLUDED_ +#define EVENT2_WS_H_INCLUDED_ + +struct evws_connection; + +#define WS_CR_NONE 0 +#define WS_CR_NORMAL 1000 +#define WS_CR_PROTO_ERR 1002 +#define WS_CR_DATA_TOO_BIG 1009 + +#define WS_TEXT_FRAME 0x1 +#define WS_BINARY_FRAME 0x2 + +typedef void (*ws_on_msg_cb)( + struct evws_connection *, int type, const unsigned char *, size_t, void *); +typedef void (*ws_on_close_cb)(struct evws_connection *, void *); + +/** Opens new WebSocket session from HTTP request. + @param req a request object + @param cb the callback function that gets invoked on receiving message + with len bytes length. In case of receiving text messages user is responsible + to make a string with terminating \0 (with copying-out data) or use text data + other way in which \0 is not required + @param arg an additional context argument for the callback + @return a pointer to a newly initialized WebSocket connection or NULL + on error + @see evws_close() + */ +EVENT2_EXPORT_SYMBOL +struct evws_connection *evws_new_session( + struct evhttp_request *req, ws_on_msg_cb, void *arg); + +/** Sends data over WebSocket connection */ +EVENT2_EXPORT_SYMBOL +void evws_send( + struct evws_connection *evws, const char *packet_str, size_t str_len); + +/** Closes a WebSocket connection with reason code */ +EVENT2_EXPORT_SYMBOL +void evws_close(struct evws_connection *evws, uint16_t reason); + +/** Sets a callback for connection close. */ +EVENT2_EXPORT_SYMBOL +void evws_connection_set_closecb( + struct evws_connection *evws, ws_on_close_cb, void *); + +/** Frees a WebSocket connection */ +EVENT2_EXPORT_SYMBOL +void evws_connection_free(struct evws_connection *evws); + +/** + * Return the bufferevent that an evws_connection is using. + */ +EVENT2_EXPORT_SYMBOL +struct bufferevent *evws_connection_get_bufferevent( + struct evws_connection *evws); + +#endif diff --git a/include/include.am b/include/include.am index 8a5bd026a3..d73771df13 100644 --- a/include/include.am +++ b/include/include.am @@ -31,6 +31,7 @@ EVENT2_EXPORT = \ include/event2/tag_compat.h \ include/event2/thread.h \ include/event2/util.h \ + include/event2/ws.h \ include/event2/visibility.h if OPENSSL diff --git a/sample/include.am b/sample/include.am index b8dd400ee4..6c0e7b5ebe 100644 --- a/sample/include.am +++ b/sample/include.am @@ -12,6 +12,7 @@ SAMPLES = \ sample/http-connect \ sample/signal-test \ sample/time-test \ + sample/ws-chat-server \ sample/watch-timing if OPENSSL @@ -74,3 +75,6 @@ sample_http_connect_SOURCES = sample/http-connect.c sample_http_connect_LDADD = $(LIBEVENT_GC_SECTIONS) libevent.la sample_watch_timing_SOURCES = sample/watch-timing.c sample_watch_timing_LDADD = $(LIBEVENT_GC_SECTIONS) libevent.la -lm +sample_ws_chat_server_SOURCES = sample/ws-chat-server.c +sample_ws_chat_server_LDADD = $(LIBEVENT_GC_SECTIONS) libevent.la -lm +EXTRA_DIST+=sample/ws-chat.html diff --git a/sample/ws-chat-server.c b/sample/ws-chat-server.c new file mode 100644 index 0000000000..8761df3185 --- /dev/null +++ b/sample/ws-chat-server.c @@ -0,0 +1,244 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include +#include + +#ifndef stat +#define stat _stat +#endif +#ifndef fstat +#define fstat _fstat +#endif +#ifndef open +#define open _open +#endif +#ifndef close +#define close _close +#endif +#ifndef O_RDONLY +#define O_RDONLY _O_RDONLY +#endif + +#else /* !_WIN32 */ + +#ifdef EVENT__HAVE_ARPA_INET_H +#include +#endif +#ifdef EVENT__HAVE_NETINET_IN_H +#include +#endif +#ifdef EVENT__HAVE_NETINET_IN6_H +#include +#endif +#include + +#endif /* _WIN32 */ + +#define log_d(...) fprintf(stderr, __VA_ARGS__) + +typedef struct client { + struct evws_connection *evws; + char name[INET6_ADDRSTRLEN]; + TAILQ_ENTRY(client) next; +} client_t; +typedef TAILQ_HEAD(clients_s, client) clients_t; +static clients_t clients; + +static void +broadcast_msg(char *msg) +{ + struct client *client; + + TAILQ_FOREACH (client, &clients, next) { + evws_send(client->evws, msg, strlen(msg)); + } + log_d("%s\n", msg); +} + +static void +on_msg_cb(struct evws_connection *evws, int type, const unsigned char *data, + size_t len, void *arg) +{ + struct client *self = arg; + char buf[4096]; + const char *msg = (const char *)data; + + snprintf(buf, sizeof(buf), "%.*s", (int)len, msg); + if (len == 5 && memcmp(buf, "/quit", 5) == 0) { + evws_close(evws, WS_CR_NORMAL); + snprintf(buf, sizeof(buf), "'%s' left the chat", self->name); + } else if (len > 6 && strncmp(msg, "/name ", 6) == 0) { + const char *new_name = (const char *)msg + 6; + int name_len = len - 6; + + snprintf(buf, sizeof(buf), "'%s' renamed itself to '%.*s'", self->name, + name_len, new_name); + snprintf( + self->name, sizeof(self->name) - 1, "%.*s", name_len, new_name); + } else { + snprintf(buf, sizeof(buf), "[%s] %.*s", self->name, (int)len, msg); + } + + broadcast_msg(buf); +} + +static void +on_close_cb(struct evws_connection *evws, void *arg) +{ + client_t *client = arg; + log_d("'%s' disconnected\n", client->name); + TAILQ_REMOVE(&clients, client, next); + free(arg); +} + +static const char * +nice_addr(const char *addr) +{ + if (strncmp(addr, "::ffff:", 7) == 0) + addr += 7; + + return addr; +} + +static void +addr2str(struct sockaddr *sa, char *addr, size_t len) +{ + const char *nice; + unsigned short port; + size_t adlen; + + if (sa->sa_family == AF_INET) { + struct sockaddr_in *s = (struct sockaddr_in *)sa; + port = ntohs(s->sin_port); + evutil_inet_ntop(AF_INET, &s->sin_addr, addr, len); + } else { // AF_INET6 + struct sockaddr_in6 *s = (struct sockaddr_in6 *)sa; + port = ntohs(s->sin6_port); + evutil_inet_ntop(AF_INET6, &s->sin6_addr, addr, len); + nice = nice_addr(addr); + if (nice != addr) { + size_t len = strlen(addr) - (nice - addr); + memmove(addr, nice, len); + addr[len] = 0; + } + } + adlen = strlen(addr); + snprintf(addr + adlen, len - adlen, ":%d", port); +} + + +static void +on_ws(struct evhttp_request *req, void *arg) +{ + struct client *client; + evutil_socket_t fd; + struct sockaddr_storage addr; + socklen_t len; + + client = calloc(sizeof(*client), 1); + client->evws = evws_new_session(req, on_msg_cb, client); + fd = bufferevent_getfd(evws_connection_get_bufferevent(client->evws)); + + len = sizeof(addr); + getpeername(fd, (struct sockaddr *)&addr, &len); + + addr2str((struct sockaddr *)&addr, client->name, sizeof(client->name)); + log_d("New client joined from %s\n", client->name); + + evws_connection_set_closecb(client->evws, on_close_cb, client); + TAILQ_INSERT_TAIL(&clients, client, next); +} + +static void +on_html(struct evhttp_request *req, void *arg) +{ + int fd = -1; + struct evbuffer *evb; + struct stat st; + + evhttp_add_header( + evhttp_request_get_output_headers(req), "Content-Type", "text/html"); + if ((fd = open("ws-chat.html", O_RDONLY)) < 0) { + perror("open"); + goto err; + } + + if (fstat(fd, &st) < 0) { + /* Make sure the length still matches, now that we + * opened the file :/ */ + perror("fstat"); + goto err; + } + + + evb = evbuffer_new(); + evbuffer_add_file(evb, fd, 0, st.st_size); + evhttp_send_reply(req, HTTP_OK, NULL, evb); + evbuffer_free(evb); + return; + +err: + evhttp_send_error(req, HTTP_NOTFOUND, NULL); +} + +#ifndef EVENT__HAVE_STRSIGNAL +static inline const char * +strsignal(evutil_socket_t sig) +{ + return "Signal"; +} +#endif + +static void +signal_cb(evutil_socket_t fd, short event, void *arg) +{ + printf("%s signal received\n", strsignal(fd)); + event_base_loopbreak(arg); +} + +int +main(int argc, char **argv) +{ + struct event_base *base; + struct event *sig_int; + struct evhttp *http_server; + + TAILQ_INIT(&clients); + + base = event_base_new(); + + sig_int = evsignal_new(base, SIGINT, signal_cb, base); + event_add(sig_int, NULL); + + http_server = evhttp_new(base); + evhttp_bind_socket_with_handle(http_server, "0.0.0.0", 8080); + + evhttp_set_cb(http_server, "/", on_html, NULL); + evhttp_set_cb(http_server, "/ws", on_ws, NULL); + + log_d("Server runs\n"); + event_base_dispatch(base); + + log_d("Active connections: %d\n", evhttp_get_connection_count(http_server)); + evhttp_free(http_server); + + event_free(sig_int); + event_base_free(base); + libevent_global_shutdown(); +} diff --git a/sample/ws-chat.html b/sample/ws-chat.html new file mode 100644 index 0000000000..f4083f8046 --- /dev/null +++ b/sample/ws-chat.html @@ -0,0 +1,98 @@ + + + +Chat Example + + + + +
+
+ + +
+ + diff --git a/sha1.c b/sha1.c new file mode 100644 index 0000000000..33ca1dfbe2 --- /dev/null +++ b/sha1.c @@ -0,0 +1,278 @@ +/* +SHA-1 in C +By Steve Reid +100% Public Domain + +Test Vectors (from FIPS PUB 180-1) +"abc" + A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D +"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" + 84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1 +A million repetitions of "a" + 34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F +*/ + +/* #define LITTLE_ENDIAN * This should be #define'd already, if true. */ +/* #define SHA1HANDSOFF * Copies data before messing with it. */ + +#define SHA1HANDSOFF + +#include +#include + +/* for uint32_t */ +#include + +#include "sha1.h" + +#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits)))) + +/* blk0() and blk() perform the initial expand. */ +/* I got the idea of expanding during the round function from SSLeay */ +#if defined(LITTLE_ENDIAN) +#define blk0(i) \ + (block->l[i] = (rol(block->l[i], 24) & 0xFF00FF00) | \ + (rol(block->l[i], 8) & 0x00FF00FF)) +#elif defined(BIG_ENDIAN) +#define blk0(i) block->l[i] +#else +#error "Endianness not defined!" +#endif +#define blk(i) \ + (block->l[i & 15] = rol(block->l[(i + 13) & 15] ^ block->l[(i + 8) & 15] ^ \ + block->l[(i + 2) & 15] ^ block->l[i & 15], \ + 1)) + +/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */ +#define R0(v, w, x, y, z, i) \ + z += ((w & (x ^ y)) ^ y) + blk0(i) + 0x5A827999 + rol(v, 5); \ + w = rol(w, 30); +#define R1(v, w, x, y, z, i) \ + z += ((w & (x ^ y)) ^ y) + blk(i) + 0x5A827999 + rol(v, 5); \ + w = rol(w, 30); +#define R2(v, w, x, y, z, i) \ + z += (w ^ x ^ y) + blk(i) + 0x6ED9EBA1 + rol(v, 5); \ + w = rol(w, 30); +#define R3(v, w, x, y, z, i) \ + z += (((w | x) & y) | (w & x)) + blk(i) + 0x8F1BBCDC + rol(v, 5); \ + w = rol(w, 30); +#define R4(v, w, x, y, z, i) \ + z += (w ^ x ^ y) + blk(i) + 0xCA62C1D6 + rol(v, 5); \ + w = rol(w, 30); + +/* Hash a single 512-bit block. This is the core of the algorithm. */ + +void SHA1Transform(uint32_t state[5], const unsigned char buffer[64]) { + uint32_t a, b, c, d, e; + + typedef union { + unsigned char c[64]; + uint32_t l[16]; + } CHAR64LONG16; + +#ifdef SHA1HANDSOFF + CHAR64LONG16 block[1]; /* use array to appear as a pointer */ + + memcpy(block, buffer, 64); +#else + /* The following had better never be used because it causes the + * pointer-to-const buffer to be cast into a pointer to non-const. + * And the result is written through. I threw a "const" in, hoping + * this will cause a diagnostic. + */ + CHAR64LONG16 *block = (const CHAR64LONG16 *)buffer; +#endif + /* Copy context->state[] to working vars */ + a = state[0]; + b = state[1]; + c = state[2]; + d = state[3]; + e = state[4]; + /* 4 rounds of 20 operations each. Loop unrolled. */ + R0(a, b, c, d, e, 0); + R0(e, a, b, c, d, 1); + R0(d, e, a, b, c, 2); + R0(c, d, e, a, b, 3); + R0(b, c, d, e, a, 4); + R0(a, b, c, d, e, 5); + R0(e, a, b, c, d, 6); + R0(d, e, a, b, c, 7); + R0(c, d, e, a, b, 8); + R0(b, c, d, e, a, 9); + R0(a, b, c, d, e, 10); + R0(e, a, b, c, d, 11); + R0(d, e, a, b, c, 12); + R0(c, d, e, a, b, 13); + R0(b, c, d, e, a, 14); + R0(a, b, c, d, e, 15); + R1(e, a, b, c, d, 16); + R1(d, e, a, b, c, 17); + R1(c, d, e, a, b, 18); + R1(b, c, d, e, a, 19); + R2(a, b, c, d, e, 20); + R2(e, a, b, c, d, 21); + R2(d, e, a, b, c, 22); + R2(c, d, e, a, b, 23); + R2(b, c, d, e, a, 24); + R2(a, b, c, d, e, 25); + R2(e, a, b, c, d, 26); + R2(d, e, a, b, c, 27); + R2(c, d, e, a, b, 28); + R2(b, c, d, e, a, 29); + R2(a, b, c, d, e, 30); + R2(e, a, b, c, d, 31); + R2(d, e, a, b, c, 32); + R2(c, d, e, a, b, 33); + R2(b, c, d, e, a, 34); + R2(a, b, c, d, e, 35); + R2(e, a, b, c, d, 36); + R2(d, e, a, b, c, 37); + R2(c, d, e, a, b, 38); + R2(b, c, d, e, a, 39); + R3(a, b, c, d, e, 40); + R3(e, a, b, c, d, 41); + R3(d, e, a, b, c, 42); + R3(c, d, e, a, b, 43); + R3(b, c, d, e, a, 44); + R3(a, b, c, d, e, 45); + R3(e, a, b, c, d, 46); + R3(d, e, a, b, c, 47); + R3(c, d, e, a, b, 48); + R3(b, c, d, e, a, 49); + R3(a, b, c, d, e, 50); + R3(e, a, b, c, d, 51); + R3(d, e, a, b, c, 52); + R3(c, d, e, a, b, 53); + R3(b, c, d, e, a, 54); + R3(a, b, c, d, e, 55); + R3(e, a, b, c, d, 56); + R3(d, e, a, b, c, 57); + R3(c, d, e, a, b, 58); + R3(b, c, d, e, a, 59); + R4(a, b, c, d, e, 60); + R4(e, a, b, c, d, 61); + R4(d, e, a, b, c, 62); + R4(c, d, e, a, b, 63); + R4(b, c, d, e, a, 64); + R4(a, b, c, d, e, 65); + R4(e, a, b, c, d, 66); + R4(d, e, a, b, c, 67); + R4(c, d, e, a, b, 68); + R4(b, c, d, e, a, 69); + R4(a, b, c, d, e, 70); + R4(e, a, b, c, d, 71); + R4(d, e, a, b, c, 72); + R4(c, d, e, a, b, 73); + R4(b, c, d, e, a, 74); + R4(a, b, c, d, e, 75); + R4(e, a, b, c, d, 76); + R4(d, e, a, b, c, 77); + R4(c, d, e, a, b, 78); + R4(b, c, d, e, a, 79); + /* Add the working vars back into context.state[] */ + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + state[4] += e; + /* Wipe variables */ + a = b = c = d = e = 0; +#ifdef SHA1HANDSOFF + memset(block, '\0', sizeof(block)); +#endif +} + +/* SHA1Init - Initialize new context */ + +void SHA1Init(SHA1_CTX *context) { + /* SHA1 initialization constants */ + context->state[0] = 0x67452301; + context->state[1] = 0xEFCDAB89; + context->state[2] = 0x98BADCFE; + context->state[3] = 0x10325476; + context->state[4] = 0xC3D2E1F0; + context->count[0] = context->count[1] = 0; +} + +/* Run your data through this. */ + +void SHA1Update(SHA1_CTX *context, const unsigned char *data, uint32_t len) { + uint32_t i; + + uint32_t j; + + j = context->count[0]; + if ((context->count[0] += len << 3) < j) + context->count[1]++; + context->count[1] += (len >> 29); + j = (j >> 3) & 63; + if ((j + len) > 63) { + memcpy(&context->buffer[j], data, (i = 64 - j)); + SHA1Transform(context->state, context->buffer); + for (; i + 63 < len; i += 64) { + SHA1Transform(context->state, &data[i]); + } + j = 0; + } else + i = 0; + memcpy(&context->buffer[j], &data[i], len - i); +} + +/* Add padding and return the message digest. */ + +void SHA1Final(unsigned char digest[20], SHA1_CTX *context) { + unsigned i; + + unsigned char finalcount[8]; + + unsigned char c; + +#if 0 /* untested "improvement" by DHR */ + /* Convert context->count to a sequence of bytes + * in finalcount. Second element first, but + * big-endian order within element. + * But we do it all backwards. + */ + unsigned char *fcp = &finalcount[8]; + + for (i = 0; i < 2; i++) + { + uint32_t t = context->count[i]; + + int j; + + for (j = 0; j < 4; t >>= 8, j++) + *--fcp = (unsigned char) t} +#else + for (i = 0; i < 8; i++) { + finalcount[i] = (unsigned char)((context->count[(i >= 4 ? 0 : 1)] >> + ((3 - (i & 3)) * 8)) & + 255); /* Endian independent */ + } +#endif + c = 0200; + SHA1Update(context, &c, 1); + while ((context->count[0] & 504) != 448) { + c = 0000; + SHA1Update(context, &c, 1); + } + SHA1Update(context, finalcount, 8); /* Should cause a SHA1Transform() */ + for (i = 0; i < 20; i++) { + digest[i] = + (unsigned char)((context->state[i >> 2] >> ((3 - (i & 3)) * 8)) & + 255); + } + /* Wipe variables */ + memset(context, '\0', sizeof(*context)); + memset(&finalcount, '\0', sizeof(finalcount)); +} + +void SHA1(char *hash_out, const char *str, int len) { + SHA1_CTX ctx; + int ii; + + SHA1Init(&ctx); + for (ii = 0; ii < len; ii += 1) + SHA1Update(&ctx, (const unsigned char *)str + ii, 1); + SHA1Final((unsigned char *)hash_out, &ctx); +} diff --git a/sha1.h b/sha1.h new file mode 100644 index 0000000000..1accbc779f --- /dev/null +++ b/sha1.h @@ -0,0 +1,28 @@ +#ifndef SHA1_H +#define SHA1_H + +/* + SHA-1 in C + By Steve Reid + 100% Public Domain + */ + +#include "stdint.h" + +typedef struct { + uint32_t state[5]; + uint32_t count[2]; + unsigned char buffer[64]; +} SHA1_CTX; + +void SHA1Transform(uint32_t state[5], const unsigned char buffer[64]); + +void SHA1Init(SHA1_CTX *context); + +void SHA1Update(SHA1_CTX *context, const unsigned char *data, uint32_t len); + +void SHA1Final(unsigned char digest[20], SHA1_CTX *context); + +void SHA1(char *hash_out, const char *str, int len); + +#endif /* SHA1_H */ diff --git a/test/include.am b/test/include.am index 8ec8d5348f..53a682232c 100644 --- a/test/include.am +++ b/test/include.am @@ -116,6 +116,7 @@ test_regress_SOURCES = \ test/regress_et.c \ test/regress_finalize.c \ test/regress_http.c \ + test/regress_http.h \ test/regress_listener.c \ test/regress_main.c \ test/regress_minheap.c \ @@ -124,6 +125,8 @@ test_regress_SOURCES = \ test/regress_testutils.h \ test/regress_util.c \ test/regress_watch.c \ + test/regress_ws.c \ + test/regress_ws.h \ test/tinytest.c \ $(regress_thread_SOURCES) \ $(regress_zlib_SOURCES) diff --git a/test/regress_http.c b/test/regress_http.c index 3f6b71b1a8..0e971c0a66 100644 --- a/test/regress_http.c +++ b/test/regress_http.c @@ -64,6 +64,8 @@ #include "log-internal.h" #include "http-internal.h" #include "regress.h" +#include "regress_http.h" +#include "regress_ws.h" #include "regress_testutils.h" #define ARRAY_SIZE(x) (sizeof(x)/sizeof((x)[0])) @@ -223,10 +225,11 @@ http_setup_gencb(ev_uint16_t *pport, struct event_base *base, int mask, evhttp_set_cb(myhttp, "/largedelay", http_large_delay_cb, base); evhttp_set_cb(myhttp, "/badrequest", http_badreq_cb, base); evhttp_set_cb(myhttp, "/oncomplete", http_on_complete_cb, base); + evhttp_set_cb(myhttp, "/ws", http_on_ws_cb, base); evhttp_set_cb(myhttp, "/", http_dispatcher_cb, base); return (myhttp); } -static struct evhttp * +struct evhttp * http_setup(ev_uint16_t *pport, struct event_base *base, int mask) { return http_setup_gencb(pport, base, mask, NULL, NULL); } @@ -234,7 +237,7 @@ http_setup(ev_uint16_t *pport, struct event_base *base, int mask) #define NI_MAXSERV 1024 #endif -static evutil_socket_t +evutil_socket_t http_connect(const char *address, ev_uint16_t port) { /* Stupid code for connecting */ @@ -349,7 +352,7 @@ http_readcb(struct bufferevent *bev, void *arg) } } -static void +void http_writecb(struct bufferevent *bev, void *arg) { if (evbuffer_get_length(bufferevent_get_output(bev)) == 0) { @@ -531,7 +534,7 @@ http_chunked_input_cb(struct evhttp_request *req, void *arg) evbuffer_free(buf); } -static struct bufferevent * +struct bufferevent * create_bev(struct event_base *base, evutil_socket_t fd, int ssl_mask, int flags_) { int flags = BEV_OPT_DEFER_CALLBACKS | flags_; @@ -5943,6 +5946,7 @@ struct testcase_t http_testcases[] = { HTTP(terminate_chunked), HTTP(terminate_chunked_oneshot), HTTP(on_complete), + HTTP(ws), HTTP(highport), HTTP(dispatcher), diff --git a/test/regress_http.h b/test/regress_http.h new file mode 100644 index 0000000000..31c2f58dfe --- /dev/null +++ b/test/regress_http.h @@ -0,0 +1,11 @@ +#ifndef REGRESS_HTTP_H +#define REGRESS_HTTP_H + +struct evhttp *http_setup( + ev_uint16_t *pport, struct event_base *base, int mask); +evutil_socket_t http_connect(const char *address, ev_uint16_t port); +struct bufferevent *create_bev( + struct event_base *base, evutil_socket_t fd, int ssl_mask, int flags_); +void http_writecb(struct bufferevent *bev, void *arg); + +#endif /* REGRESS_HTTP_H */ diff --git a/test/regress_ws.c b/test/regress_ws.c new file mode 100644 index 0000000000..06c3f5f066 --- /dev/null +++ b/test/regress_ws.c @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2003-2007 Niels Provos + * Copyright (c) 2007-2012 Niels Provos and Nick Mathewson + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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 "util-internal.h" + +#ifdef _WIN32 +#include +#include +#include +#endif + +#include "event2/event-config.h" + +#include +#include +#include +#ifdef EVENT__HAVE_SYS_TIME_H +#include +#endif +#include +#ifndef _WIN32 +#include +#include +#include +#include +#endif +#include + +#include "event2/event.h" +#include "event2/http.h" +#include "event2/buffer.h" +#include "event2/bufferevent_ssl.h" +#include "event2/ws.h" +#include "regress.h" +#include "regress_http.h" +#include "regress_ws.h" + +#define htonll(x) \ + ((1 == htonl(1)) \ + ? (x) \ + : ((uint64_t)htonl((x)&0xFFFFFFFF) << 32) | htonl((x) >> 32)) +#define ntohll(x) htonll(x) + + +static struct event_base *exit_base; + +static void +on_ws_msg_cb(struct evws_connection *evws, int type, const unsigned char *data, + size_t len, void *arg) +{ + ev_uintptr_t val = (ev_uintptr_t)arg; + char msg[4096]; + + if (val != 0xDEADBEEF) { + fprintf(stdout, "FAILED on_complete_cb argument\n"); + exit(1); + } + + + snprintf(msg, sizeof(msg), "%.*s", (int)len, data); + if (!strcmp(msg, "Send echo")) { + const char *reply = "Reply echo"; + + evws_send(evws, reply, strlen(reply)); + test_ok++; + } else if (!strcmp(msg, "Client: hello")) { + test_ok++; + } else if (!strcmp(msg, "Close")) { + evws_close(evws, 0); + test_ok++; + } else { + /* unexpected test message */ + event_base_loopexit(arg, NULL); + } +} + +static void +on_ws_close_cb(struct evws_connection *evws, void *arg) +{ + ev_uintptr_t val = (ev_uintptr_t)arg; + + if (val != 0xDEADBEEF) { + fprintf(stdout, "FAILED on_complete_cb argument\n"); + exit(1); + } + test_ok++; +} + +void +http_on_ws_cb(struct evhttp_request *req, void *arg) +{ + struct evws_connection *evws; + const char *hello = "Server: hello"; + + evws = evws_new_session(req, on_ws_msg_cb, (void *)0xDEADBEEF); + if (!evws) + return; + test_ok++; + + evws_connection_set_closecb(evws, on_ws_close_cb, (void *)0xDEADBEEF); + evws_send(evws, hello, strlen(hello)); +} + +static void +http_ws_errorcb(struct bufferevent *bev, short what, void *arg) +{ + /** For ssl */ + if (what & BEV_EVENT_CONNECTED) + return; + test_ok++; + event_base_loopexit(arg, NULL); +} + +static char * +receive_ws_msg(struct evbuffer *buf, size_t *out_len, unsigned *options) +{ + unsigned char *data; + int fin, opcode, mask; + uint64_t payload_len; + size_t header_len; + const unsigned char *mask_key; + char *out_buf = NULL; + size_t data_len = evbuffer_get_length(buf); + + data = evbuffer_pullup(buf, data_len); + + fin = !!(*data & 0x80); + opcode = *data & 0x0F; + mask = !!(*(data + 1) & 0x80); + payload_len = *(data + 1) & 0x7F; + + header_len = 2 + (mask ? 4 : 0); + + if (payload_len < 126) { + if (header_len > data_len) + return NULL; + + } else if (payload_len == 126) { + header_len += 2; + if (header_len > data_len) + return NULL; + + payload_len = ntohs(*(uint16_t *)(data + 2)); + + } else if (payload_len == 127) { + header_len += 8; + if (header_len > data_len) + return NULL; + + payload_len = ntohll(*(uint64_t *)(data + 2)); + } + + if (header_len + payload_len > data_len) + return NULL; + + mask_key = data + header_len - 4; + for (size_t i = 0; mask && i < payload_len; i++) + data[header_len + i] ^= mask_key[i % 4]; + + *out_len = payload_len; + + /* text */ + if (opcode == 0x01) { + out_buf = calloc(payload_len + 1, 1); + } else { /* binary */ + out_buf = malloc(payload_len); + } + memcpy(out_buf, (const char *)data + header_len, payload_len); + + if (!fin) { + *options = 1; + } + + evbuffer_drain(buf, header_len + payload_len); + return out_buf; +} + +static void +send_ws_msg(struct evbuffer *buf, const char *msg, bool final) +{ + size_t len = strlen(msg); + uint8_t a = 0, b = 0, c = 0, d = 0; + uint8_t mask_key[4] = {1, 2, 3, 4}; /* should be random */ + uint8_t m; + + if (final) + a |= 1 << 7; /* fin */ + a |= 1; /* text frame */ + + b |= 1 << 7; /* mask */ + + /* payload len */ + if (len < 126) { + b |= len; + } else if (len < (1 << 16)) { + b |= 126; + c = htons(len); + } else { + b |= 127; + d = htonll(len); + } + + evbuffer_add(buf, &a, 1); + evbuffer_add(buf, &b, 1); + + if (c) + evbuffer_add(buf, &c, sizeof(c)); + else if (d) + evbuffer_add(buf, &d, sizeof(d)); + + evbuffer_add(buf, &mask_key, 4); + + for (size_t i = 0; i < len; i++) { + m = msg[i] ^ mask_key[i % 4]; + evbuffer_add(buf, &m, 1); + } +} + +static void +http_ws_readcb_phase2(struct bufferevent *bev, void *arg) +{ + struct evbuffer *input = bufferevent_get_input(bev); + struct evbuffer *output = bufferevent_get_output(bev); + + while (evbuffer_get_length(input) >= 2) { + size_t len = 0; + unsigned options = 0; + char *msg; + + msg = receive_ws_msg(input, &len, &options); + if (msg) { + if (!strcmp(msg, "Server: hello")) { + send_ws_msg(output, "Send ", false); + send_ws_msg(output, "echo", true); + test_ok++; + } else if (!strcmp(msg, "Reply echo")) { + send_ws_msg(output, "Close", true); + test_ok++; + } else { + test_ok--; + } + free(msg); + } + } +} + +static void +http_ws_readcb_hdr(struct bufferevent *bev, void *arg) +{ + struct evbuffer *input = bufferevent_get_input(bev); + struct evbuffer *output = bufferevent_get_output(bev); + size_t nread = 0, n = 0; + char *line; + + while ((line = evbuffer_readln(input, &nread, EVBUFFER_EOL_CRLF))) { + if (n == 0 && + !strncmp(line, "HTTP/1.1 101 ", strlen("HTTP/1.1 101 "))) { + test_ok++; + } else if (!strcmp(line, + "Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=")) { + test_ok++; + } else if (strlen(line) == 0) { + free(line); + bufferevent_setcb( + bev, http_ws_readcb_phase2, http_writecb, http_ws_errorcb, arg); + send_ws_msg(output, "Client:", false); + send_ws_msg(output, " ", false); + send_ws_msg(output, "hello", true); + test_ok++; + if (evbuffer_get_length(input) > 0) { + http_ws_readcb_phase2(bev, arg); + } + return; + } + free(line); + n++; + }; +} + +static void +http_ws_readcb_bad(struct bufferevent *bev, void *arg) +{ + struct evbuffer *input = bufferevent_get_input(bev); + size_t nread; + char *line; + + line = evbuffer_readln(input, &nread, EVBUFFER_EOL_CRLF); + if (!strncmp(line, "HTTP/1.1 401 ", strlen("HTTP/1.1 401 "))) { + test_ok++; + } + if (line) + free(line); +} + +void +http_ws_test(void *arg) +{ + struct basic_test_data *data = arg; + struct bufferevent *bev = NULL; + evutil_socket_t fd; + ev_uint16_t port = 0; + int ssl = 0; + struct evhttp *http = http_setup(&port, data->base, ssl); + struct evbuffer *out; + + exit_base = data->base; + + /* Send HTTP-only request to WS endpoint */ + fd = http_connect("127.0.0.1", port); + bev = create_bev(data->base, fd, ssl, BEV_OPT_CLOSE_ON_FREE); + bufferevent_setcb( + bev, http_ws_readcb_bad, http_writecb, http_ws_errorcb, data->base); + out = bufferevent_get_output(bev); + + evbuffer_add_printf(out, "GET /ws HTTP/1.1\r\n" + "Host: somehost\r\n" + "Connection: close\r\n" + "\r\n"); + + test_ok = 0; + event_base_dispatch(data->base); + tt_int_op(test_ok, ==, 2); + + bufferevent_free(bev); + + /* Check for WS handshake and Sec-WebSocket-Accept correctness */ + fd = http_connect("127.0.0.1", port); + bev = create_bev(data->base, fd, ssl, BEV_OPT_CLOSE_ON_FREE); + bufferevent_setcb( + bev, http_ws_readcb_hdr, http_writecb, http_ws_errorcb, data->base); + out = bufferevent_get_output(bev); + + evbuffer_add_printf(out, "GET /ws HTTP/1.1\r\n" + "Host: somehost\r\n" + "Connection: Upgrade\r\n" + "Upgrade: websocket\r\n" + "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n" + "\r\n"); + + test_ok = 0; + event_base_dispatch(data->base); + tt_int_op(test_ok, ==, 13); + + evhttp_free(http); +end: + if (bev) + bufferevent_free(bev); +} diff --git a/test/regress_ws.h b/test/regress_ws.h new file mode 100644 index 0000000000..c1f5518ecb --- /dev/null +++ b/test/regress_ws.h @@ -0,0 +1,7 @@ +#ifndef REGRESS_WS_H +#define REGRESS_WS_H + +void http_on_ws_cb(struct evhttp_request *req, void *arg); +void http_ws_test(void *arg); + +#endif /* REGRESS_WS_H */ diff --git a/ws.c b/ws.c new file mode 100644 index 0000000000..fa8891565b --- /dev/null +++ b/ws.c @@ -0,0 +1,439 @@ +#include "event2/event-config.h" +#include "evconfig-private.h" + +#include "event2/buffer.h" +#include "event2/bufferevent.h" +#include "event2/event.h" +#include "event2/http.h" +#include "event2/ws.h" +#include "util-internal.h" +#include "mm-internal.h" +#include "sha1.h" +#include "event2/bufferevent.h" +#include "sys/queue.h" +#include "http-internal.h" + +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#else /* _WIN32 */ +#include +#include +#endif /* _WIN32 */ + +#ifdef EVENT__HAVE_ARPA_INET_H +#include +#endif +#ifdef EVENT__HAVE_NETINET_IN_H +#include +#endif +#ifdef EVENT__HAVE_NETINET_IN6_H +#include +#endif + +#define WS_UUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + +struct evws_connection { + TAILQ_ENTRY(evws_connection) next; + + struct bufferevent *bufev; + + ws_on_msg_cb cb; + void *cb_arg; + + ws_on_close_cb cbclose; + void *cbclose_arg; + + /* for server connections, the http server they are connected with */ + struct evhttp *http_server; + + struct evbuffer *incomplete_frames; + bool closed; +}; + +enum WebSocketFrameType { + ERROR_FRAME = 0xFF, + INCOMPLETE_DATA = 0xFE, + + CLOSING_FRAME = 0x8, + + INCOMPLETE_FRAME = 0x81, + + TEXT_FRAME = 0x1, + BINARY_FRAME = 0x2, + + PING_FRAME = 0x9, + PONG_FRAME = 0xA +}; + +/* + * Clean up a WebSockets connection object + */ + +void +evws_connection_free(struct evws_connection *evws) +{ + /* notify interested parties that this connection is going down */ + if (evws->cbclose != NULL) + (*evws->cbclose)(evws, evws->cbclose_arg); + + if (evws->http_server != NULL) { + struct evhttp *http = evws->http_server; + TAILQ_REMOVE(&http->ws_sessions, evws, next); + http->connection_cnt--; + } + + if (evws->bufev != NULL) { + bufferevent_free(evws->bufev); + } + if (evws->incomplete_frames != NULL) { + evbuffer_free(evws->incomplete_frames); + } + + mm_free(evws); +} + +static const char basis_64[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static int +Base64encode(char *encoded, const char *string, int len) +{ + int i; + char *p; + + p = encoded; + for (i = 0; i < len - 2; i += 3) { + *p++ = basis_64[(string[i] >> 2) & 0x3F]; + *p++ = basis_64[((string[i] & 0x3) << 4) | + ((int)(string[i + 1] & 0xF0) >> 4)]; + *p++ = basis_64[((string[i + 1] & 0xF) << 2) | + ((int)(string[i + 2] & 0xC0) >> 6)]; + *p++ = basis_64[string[i + 2] & 0x3F]; + } + if (i < len) { + *p++ = basis_64[(string[i] >> 2) & 0x3F]; + if (i == (len - 1)) { + *p++ = basis_64[((string[i] & 0x3) << 4)]; + *p++ = '='; + } else { + *p++ = basis_64[((string[i] & 0x3) << 4) | + ((int)(string[i + 1] & 0xF0) >> 4)]; + *p++ = basis_64[((string[i + 1] & 0xF) << 2)]; + } + *p++ = '='; + } + + *p++ = '\0'; + return p - encoded; +} + +static char * +ws_gen_accept_key(const char *ws_key, char out[32]) +{ + char buf[1024]; + char digest[20]; + + snprintf(buf, sizeof(buf), "%s" WS_UUID, ws_key); + + SHA1(digest, buf, strlen(buf)); + Base64encode(out, digest, sizeof(digest)); + return out; +} + +static void +close_after_write_cb(struct bufferevent *bev, void *ctx) +{ + if (evbuffer_get_length(bufferevent_get_output(bev)) == 0) { + evws_connection_free(ctx); + } +} + +static void +close_event_cb(struct bufferevent *bev, short what, void *ctx) +{ + evws_connection_free(ctx); +} + +void +evws_close(struct evws_connection *evws, uint16_t reason) +{ + uint8_t fr[4] = {0x8 | 0x80, 2, 0}; + struct evbuffer *output; + uint16_t *u16; + + if (evws->closed) + return; + evws->closed = true; + + u16 = (uint16_t *)&fr[2]; + *u16 = htons((int16_t)reason); + output = bufferevent_get_output(evws->bufev); + evbuffer_add(output, fr, 4); + + /* wait for close frame writing complete and close connection */ + bufferevent_setcb( + evws->bufev, NULL, close_after_write_cb, close_event_cb, evws); +} + +static void +evws_force_disconnect_(struct evws_connection *evws) +{ + evws_close(evws, WS_CR_NONE); +} + +/* parse base frame according to + * https://www.rfc-editor.org/rfc/rfc6455#section-5.2 + */ +static enum WebSocketFrameType +get_ws_frame(unsigned char *in_buffer, int buf_len, unsigned char **payload_ptr, + int *out_len) +{ + unsigned char opcode; + unsigned char fin; + unsigned char masked; + int payload_len; + int pos; + int length_field; + unsigned int mask; + + if (buf_len < 2) { + return INCOMPLETE_DATA; + } + + opcode = in_buffer[0] & 0x0F; + fin = (in_buffer[0] >> 7) & 0x01; + masked = (in_buffer[1] >> 7) & 0x01; + + payload_len = 0; + pos = 2; + length_field = in_buffer[1] & (~0x80); + + if (length_field <= 125) { + payload_len = length_field; + } else if (length_field == 126) { /* msglen is 16bit */ + if (buf_len < 4) + return INCOMPLETE_DATA; + payload_len = ntohs(*(uint16_t *)(in_buffer + 2)); + pos += 2; + } else if (length_field == 127) { /* msglen is 64bit */ + if (buf_len < 10) + return INCOMPLETE_DATA; + payload_len = ntohs(*(uint64_t *)(in_buffer + 2)); + pos += 8; + } + if (buf_len < payload_len + pos + (masked ? 4 : 0)) { + return INCOMPLETE_DATA; + } + + /* According to RFC it seems that unmasked data should be prohibited + * but we support it for nonconformant clients + */ + if (masked) { + unsigned char *c; + + mask = *((unsigned int *)(in_buffer + pos)); + pos += 4; + + /* unmask data */ + c = in_buffer + pos; + for (int i = 0; i < payload_len; i++) { + c[i] = c[i] ^ ((unsigned char *)(&mask))[i % 4]; + } + } + + *payload_ptr = in_buffer + pos; + *out_len = payload_len; + + /* are reserved for further frames */ + if ((opcode >= 3 && opcode <= 7) || (opcode >= 0xb)) + return ERROR_FRAME; + + if (opcode <= 0x3 && !fin) { + return INCOMPLETE_FRAME; + } + return opcode; +} + + +static void +ws_evhttp_read_cb(struct bufferevent *bufev, void *arg) +{ + struct evws_connection *evws = arg; + unsigned char *payload; + enum WebSocketFrameType type; + int msg_len, in_len, header_sz; + struct evbuffer *input = bufferevent_get_input(evws->bufev); + + while ((in_len = evbuffer_get_length(input))) { + unsigned char *data = evbuffer_pullup(input, in_len); + if (data == NULL) { + return; + } + + type = get_ws_frame(data, in_len, &payload, &msg_len); + if (type == INCOMPLETE_DATA) { + /* incomplete data received, wait for next chunk */ + return; + } + header_sz = payload - data; + evbuffer_drain(input, header_sz); + data = evbuffer_pullup(input, -1); + + switch (type) { + case TEXT_FRAME: + case BINARY_FRAME: + if (evws->incomplete_frames != NULL) { + /* we already have incomplete frames in internal buffer + * and need to concatenate them with final one */ + evbuffer_add(evws->incomplete_frames, data, msg_len); + + data = evbuffer_pullup(evws->incomplete_frames, -1); + + evws->cb(evws, type, data, + evbuffer_get_length(evws->incomplete_frames), evws->cb_arg); + evbuffer_free(evws->incomplete_frames); + evws->incomplete_frames = NULL; + } else { + evws->cb(evws, type, data, msg_len, evws->cb_arg); + } + break; + case INCOMPLETE_FRAME: + /* we received full frame until get fin and need to + * postpone callback until all data arrives */ + if (evws->incomplete_frames == NULL) { + evws->incomplete_frames = evbuffer_new(); + } + evbuffer_remove_buffer(input, evws->incomplete_frames, msg_len); + continue; + case CLOSING_FRAME: + case ERROR_FRAME: + evws_force_disconnect_(evws); + break; + case PING_FRAME: + case PONG_FRAME: + /* ping or pong frame */ + break; + default: + event_warn("%s: unexpected frame type %d\n", __func__, type); + evws_force_disconnect_(evws); + } + evbuffer_drain(input, msg_len); + } +} + +static void +ws_evhttp_error_cb(struct bufferevent *bufev, short what, void *arg) +{ + /* when client just disappears after connection (wscat closed by Cmd+Q) */ + if (what & BEV_EVENT_EOF) { + close_after_write_cb(bufev, arg); + } +} + +struct evws_connection * +evws_new_session(struct evhttp_request *req, ws_on_msg_cb cb, void *arg) +{ + struct evws_connection *evws = NULL; + struct evkeyvalq *in_hdrs; + const char *upgrade, *connection, *ws_key, *ws_protocol; + struct evkeyvalq *out_hdrs; + struct evhttp_connection *evcon; + + in_hdrs = evhttp_request_get_input_headers(req); + upgrade = evhttp_find_header(in_hdrs, "Upgrade"); + if (upgrade == NULL || strcmp(upgrade, "websocket")) + goto error; + + connection = evhttp_find_header(in_hdrs, "Connection"); + if (connection == NULL || strcmp(connection, "Upgrade")) + goto error; + + ws_key = evhttp_find_header(in_hdrs, "Sec-WebSocket-Key"); + if (ws_key == NULL) + goto error; + + out_hdrs = evhttp_request_get_output_headers(req); + evhttp_add_header(out_hdrs, "Upgrade", "websocket"); + evhttp_add_header(out_hdrs, "Connection", "Upgrade"); + + evhttp_add_header(out_hdrs, "Sec-WebSocket-Accept", + ws_gen_accept_key(ws_key, (char[32]){0})); + + ws_protocol = evhttp_find_header(in_hdrs, "Sec-WebSocket-Protocol"); + if (ws_protocol != NULL) + evhttp_add_header(out_hdrs, "Sec-WebSocket-Protocol", ws_protocol); + + if ((evws = mm_calloc(1, sizeof(struct evws_connection))) == NULL) { + event_warn("%s: calloc failed", __func__); + goto error; + } + + evws->cb = cb; + evws->cb_arg = arg; + + evcon = evhttp_request_get_connection(req); + evws->http_server = evcon->http_server; + + evws->bufev = evhttp_start_ws_(req); + bufferevent_setcb( + evws->bufev, ws_evhttp_read_cb, NULL, ws_evhttp_error_cb, evws); + + TAILQ_INSERT_TAIL(&evws->http_server->ws_sessions, evws, next); + evws->http_server->connection_cnt++; + + return evws; + +error: + evhttp_send_reply(req, HTTP_BADREQUEST, NULL, NULL); + return NULL; +} + +static void +make_ws_frame(struct evbuffer *output, enum WebSocketFrameType frame_type, + unsigned char *msg, int len) +{ + int pos = 0; + unsigned char header[16] = {0}; + + header[pos++] = (unsigned char)frame_type | 0x80; /* fin */ + if (len <= 125) { + header[pos++] = len; + } else if (len <= 65535) { + header[pos++] = 126; /* 16 bit length */ + header[pos++] = (len >> 8) & 0xFF; /* rightmost first */ + header[pos++] = len & 0xFF; + } else { /* >2^16-1 */ + header[pos++] = 127; /* 64 bit length */ + + pos += 8; + } + evbuffer_add(output, header, pos); + evbuffer_add(output, msg, len); +} + +void +evws_send(struct evws_connection *evws, const char *packet_str, size_t str_len) +{ + struct evbuffer *output = bufferevent_get_output(evws->bufev); + + make_ws_frame(output, TEXT_FRAME, (unsigned char *)packet_str, str_len); +} + +void +evws_connection_set_closecb( + struct evws_connection *evws, ws_on_close_cb cb, void *cbarg) +{ + evws->cbclose = cb; + evws->cbclose_arg = cbarg; +} + +struct bufferevent * +evws_connection_get_bufferevent(struct evws_connection *evws) +{ + return evws->bufev; +}