From e8313084f9e8b064433cb10eb9a79bf87407fab6 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin Date: Mon, 12 Sep 2022 22:16:56 +0300 Subject: [PATCH] Add minimal WebSocket server implementation for evhttp (#1322) This adds few functions to use evhttp-based webserver to handle incoming WebSockets connections. We've tried to use both libevent and libwebsockets in our application, but found that we need to have different ports at the same time to handle standard HTTP and WebSockets traffic. This change can help to stick only with libevent library. Implementation was inspired by modified Libevent source code in ipush project [1]. [1]: https://github.com/sqfasd/ipush/tree/master/deps/libevent-2.0.21-stable Also, WebSocket-based chat server was added as a sample. --- CMakeLists.txt | 10 + Doxyfile | 1 + Makefile.am | 3 + configure.ac | 2 + http-internal.h | 9 +- http.c | 32 +++ include/event2/ws.h | 58 ++++++ include/include.am | 1 + sample/include.am | 4 + sample/ws-chat-server.c | 244 ++++++++++++++++++++++ sample/ws-chat.html | 98 +++++++++ sha1.c | 278 +++++++++++++++++++++++++ sha1.h | 28 +++ test/include.am | 3 + test/regress_http.c | 12 +- test/regress_http.h | 11 + test/regress_ws.c | 371 +++++++++++++++++++++++++++++++++ test/regress_ws.h | 7 + ws.c | 439 ++++++++++++++++++++++++++++++++++++++++ 19 files changed, 1606 insertions(+), 5 deletions(-) create mode 100644 include/event2/ws.h create mode 100644 sample/ws-chat-server.c create mode 100644 sample/ws-chat.html create mode 100644 sha1.c create mode 100644 sha1.h create mode 100644 test/regress_http.h create mode 100644 test/regress_ws.c create mode 100644 test/regress_ws.h create mode 100644 ws.c 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; +}