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.
This commit is contained in:
Dmitry Ilyin 2022-09-12 22:16:56 +03:00 committed by GitHub
parent bb41229ff4
commit e8313084f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1606 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

32
http.c
View File

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

58
include/event2/ws.h Normal file
View File

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

View File

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

View File

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

244
sample/ws-chat-server.c Normal file
View File

@ -0,0 +1,244 @@
#include <event2/buffer.h>
#include <event2/bufferevent.h>
#include <event2/event.h>
#include <event2/http.h>
#include <event2/ws.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/queue.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <getopt.h>
#include <io.h>
#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 <arpa/inet.h>
#endif
#ifdef EVENT__HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif
#ifdef EVENT__HAVE_NETINET_IN6_H
#include <netinet/in6.h>
#endif
#include <unistd.h>
#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();
}

98
sample/ws-chat.html Normal file
View File

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Chat Example</title>
<script type="text/javascript">
window.onload = function () {
var conn;
var msg = document.getElementById("msg");
var log = document.getElementById("log");
function appendLog(item) {
var doScroll = log.scrollTop > log.scrollHeight - log.clientHeight - 1;
log.appendChild(item);
if (doScroll) {
log.scrollTop = log.scrollHeight - log.clientHeight;
}
}
document.getElementById("form").onsubmit = function () {
if (!conn) {
return false;
}
if (!msg.value) {
return false;
}
conn.send(msg.value);
msg.value = "";
return false;
};
if (window["WebSocket"]) {
conn = new WebSocket("ws://" + document.location.host + ":8080/ws");
conn.onclose = function (evt) {
var item = document.createElement("div");
item.innerHTML = "<b>Connection closed.</b>";
appendLog(item);
};
conn.onmessage = function (evt) {
var messages = evt.data.split('\n');
for (var i = 0; i < messages.length; i++) {
var item = document.createElement("div");
item.innerText = messages[i];
appendLog(item);
}
};
} else {
var item = document.createElement("div");
item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
appendLog(item);
}
};
</script>
<style type="text/css">
html {
overflow: hidden;
}
body {
overflow: hidden;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
background: gray;
}
#log {
background: white;
margin: 0;
padding: 0.5em 0.5em 0.5em 0.5em;
position: absolute;
top: 0.5em;
left: 0.5em;
right: 0.5em;
bottom: 3em;
overflow: auto;
}
#form {
padding: 0 0.5em 0 0.5em;
margin: 0;
position: absolute;
bottom: 1em;
left: 0px;
width: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<div id="log"></div>
<form id="form">
<input type="submit" value="Send" />
<input type="text" id="msg" size="64" autofocus />
</form>
</body>
</html>

278
sha1.c Normal file
View File

@ -0,0 +1,278 @@
/*
SHA-1 in C
By Steve Reid <steve@edmweb.com>
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 <stdio.h>
#include <string.h>
/* for uint32_t */
#include <stdint.h>
#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);
}

28
sha1.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef SHA1_H
#define SHA1_H
/*
SHA-1 in C
By Steve Reid <steve@edmweb.com>
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 */

View File

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

View File

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

11
test/regress_http.h Normal file
View File

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

371
test/regress_ws.c Normal file
View File

@ -0,0 +1,371 @@
/*
* Copyright (c) 2003-2007 Niels Provos <provos@citi.umich.edu>
* 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 <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#endif
#include "event2/event-config.h"
#include <stdbool.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifdef EVENT__HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#include <sys/queue.h>
#ifndef _WIN32
#include <sys/socket.h>
#include <signal.h>
#include <unistd.h>
#include <netdb.h>
#endif
#include <string.h>
#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);
}

7
test/regress_ws.h Normal file
View File

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

439
ws.c Normal file
View File

@ -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 <assert.h>
#include <string.h>
#include <stdbool.h>
#ifndef _WIN32
#include <sys/socket.h>
#include <sys/stat.h>
#else /* _WIN32 */
#include <winsock2.h>
#include <ws2tcpip.h>
#endif /* _WIN32 */
#ifdef EVENT__HAVE_ARPA_INET_H
#include <arpa/inet.h>
#endif
#ifdef EVENT__HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif
#ifdef EVENT__HAVE_NETINET_IN6_H
#include <netinet/in6.h>
#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;
}