From 9891c74872a62b430bcbbffb79900ef958e26caf Mon Sep 17 00:00:00 2001 From: Richard Aas Date: Thu, 10 Apr 2014 11:50:57 +0000 Subject: [PATCH] websock: added WebSocket client/server module --- Makefile | 2 +- docs/README | 2 + include/re.h | 1 + include/re_http.h | 2 + include/re_websock.h | 74 +++++ src/http/client.c | 18 +- src/websock/mod.mk | 7 + src/websock/websock.c | 729 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 832 insertions(+), 3 deletions(-) create mode 100644 include/re_websock.h create mode 100644 src/websock/mod.mk create mode 100644 src/websock/websock.c diff --git a/Makefile b/Makefile index aad52d9..0770e3c 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ include $(MK) # List of modules MODULES += sip sipevent sipreg sipsess -MODULES += uri http httpauth msg +MODULES += uri http httpauth msg websock MODULES += stun turn ice MODULES += natbd MODULES += rtp sdp jbuf telev diff --git a/docs/README b/docs/README index 24f0cef..98f0795 100644 --- a/docs/README +++ b/docs/README @@ -61,6 +61,7 @@ Modules: * turn testing Obtaining Relay Addresses from STUN (TURN) * udp testing UDP transport * uri testing Generic URI library +* websock unstable WebSocket Client and Server legend: "stable" - Code complete; Stable code and stable API @@ -111,6 +112,7 @@ Features: * RFC 5780 - NAT Behaviour Discovery Using STUN * RFC 6026 - Correct Transaction Handling for 2xx Resp. to SIP INVITE Requests * RFC 6156 - TURN Extension for IPv6 +* RFC 6455 - The WebSocket Protocol * Symmetric RTP * ITU-T G.711 Appendix I and Appendix II * draft-ietf-bfcpbis-rfc4582bis-08 diff --git a/include/re.h b/include/re.h index a506fff..ddc441a 100644 --- a/include/re.h +++ b/include/re.h @@ -55,6 +55,7 @@ extern "C" { #include "re_tls.h" #include "re_turn.h" #include "re_udp.h" +#include "re_websock.h" #ifdef __cplusplus } diff --git a/include/re_http.h b/include/re_http.h index 2cdcd0d..e9a9cc2 100644 --- a/include/re_http.h +++ b/include/re_http.h @@ -122,6 +122,8 @@ int http_client_alloc(struct http_cli **clip, struct dnsc *dnsc); int http_request(struct http_req **reqp, struct http_cli *cli, const char *met, const char *uri, http_resp_h *resph, http_data_h *datah, void *arg, const char *fmt, ...); +struct tcp_conn *http_req_tcp(struct http_req *req); +struct tls_conn *http_req_tls(struct http_req *req); /* Server */ diff --git a/include/re_websock.h b/include/re_websock.h new file mode 100644 index 0000000..78551ff --- /dev/null +++ b/include/re_websock.h @@ -0,0 +1,74 @@ +/** + * @file re_websock.h The WebSocket Protocol + * + * Copyright (C) 2010 Creytiv.com + */ + + +enum { + WEBSOCK_VERSION = 13, +}; + +enum websock_opcode { + /* Data frames */ + WEBSOCK_CONT = 0x0, + WEBSOCK_TEXT = 0x1, + WEBSOCK_BIN = 0x2, + /* Control frames */ + WEBSOCK_CLOSE = 0x8, + WEBSOCK_PING = 0x9, + WEBSOCK_PONG = 0xa, +}; + +enum websock_scode { + WEBSOCK_NORMAL_CLOSURE = 1000, + WEBSOCK_GOING_AWAY = 1001, + WEBSOCK_PROTOCOL_ERROR = 1002, + WEBSOCK_UNSUPPORTED_DATA = 1003, + WEBSOCK_INVALID_PAYLOAD = 1007, + WEBSOCK_POLICY_VIOLATION = 1008, + WEBSOCK_MESSAGE_TOO_BIG = 1009, + WEBSOCK_EXTENSION_ERROR = 1010, + WEBSOCK_INTERNAL_ERROR = 1011, +}; + +struct websock_hdr { + unsigned fin:1; + unsigned rsv1:1; + unsigned rsv2:1; + unsigned rsv3:1; + unsigned opcode:4; + unsigned mask:1; + uint64_t len; + uint8_t mkey[4]; +}; + +struct websock; +struct websock_conn; + +typedef void (websock_estab_h)(void *arg); +typedef void (websock_recv_h)(const struct websock_hdr *hdr, struct mbuf *mb, + void *arg); +typedef void (websock_close_h)(int err, void *arg); + + +int websock_connect(struct websock_conn **connp, struct websock *sock, + struct http_cli *cli, const char *uri, unsigned kaint, + websock_estab_h *estabh, websock_recv_h *recvh, + websock_close_h *closeh, void *arg, + const char *fmt, ...); +int websock_accept(struct websock_conn **connp, struct websock *sock, + struct http_conn *htconn, const struct http_msg *msg, + unsigned kaint, websock_recv_h *recvh, + websock_close_h *closeh, void *arg); +int websock_send(struct websock_conn *conn, enum websock_opcode opcode, + const char *fmt, ...); +int websock_close(struct websock_conn *conn, enum websock_scode scode, + const char *fmt, ...); +const struct sa *websock_peer(const struct websock_conn *conn); + +typedef void (websock_shutdown_h)(void *arg); + +int websock_alloc(struct websock **sockp, websock_shutdown_h *shuth, + void *arg); +void websock_shutdown(struct websock *sock); diff --git a/src/http/client.c b/src/http/client.c index 05dff16..2e1787d 100644 --- a/src/http/client.c +++ b/src/http/client.c @@ -338,12 +338,14 @@ int http_request(struct http_req **reqp, struct http_cli *cli, const char *met, &scheme, &host, NULL, &port, &path) || scheme.p != uri) return EINVAL; - if (!pl_strcasecmp(&scheme, "http")) { + if (!pl_strcasecmp(&scheme, "http") || + !pl_strcasecmp(&scheme, "ws")) { secure = false; defport = 80; } #ifdef USE_TLS - else if (!pl_strcasecmp(&scheme, "https")) { + else if (!pl_strcasecmp(&scheme, "https") || + !pl_strcasecmp(&scheme, "wss")) { secure = true; defport = 443; } @@ -455,3 +457,15 @@ int http_client_alloc(struct http_cli **clip, struct dnsc *dnsc) return err; } + + +struct tcp_conn *http_req_tcp(struct http_req *req) +{ + return req ? req->tc : NULL; +} + + +struct tls_conn *http_req_tls(struct http_req *req) +{ + return req ? req->sc : NULL; +} diff --git a/src/websock/mod.mk b/src/websock/mod.mk new file mode 100644 index 0000000..d9667a7 --- /dev/null +++ b/src/websock/mod.mk @@ -0,0 +1,7 @@ +# +# mod.mk +# +# Copyright (C) 2010 Creytiv.com +# + +SRCS += websock/websock.c diff --git a/src/websock/websock.c b/src/websock/websock.c new file mode 100644 index 0000000..5861f3b --- /dev/null +++ b/src/websock/websock.c @@ -0,0 +1,729 @@ +/** + * @file websock.c Implementation of The WebSocket Protocol + * + * Copyright (C) 2010 Creytiv.com + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +enum { + TIMEOUT_CLOSE = 10000, + BUFSIZE_MAX = 131072, +}; + +enum websock_state { + ACCEPTING = 0, + CONNECTING, + OPEN, + CLOSING, + CLOSED, +}; + +struct websock { + websock_shutdown_h *shuth; + void *arg; + bool shutdown; +}; + +struct websock_conn { + struct tmr tmr; + struct sa peer; + char nonce[24]; + struct websock *sock; + struct tcp_conn *tc; + struct tls_conn *sc; + struct mbuf *mb; + struct http_req *req; + websock_estab_h *estabh; + websock_recv_h *recvh; + websock_close_h *closeh; + void *arg; + enum websock_state state; + unsigned kaint; + bool active; +}; + + +static const char magic[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + +static void timeout_handler(void *arg); + + +static void dummy_recv_handler(const struct websock_hdr *hdr, struct mbuf *mb, + void *arg) +{ + (void)hdr; + (void)mb; + (void)arg; +} + + +static void internal_close_handler(int err, void *arg) +{ + struct websock_conn *conn = arg; + (void)err; + + mem_deref(conn); +} + + +static void sock_destructor(void *arg) +{ + struct websock *sock = arg; + + if (sock->shutdown) { + sock->shutdown = false; + mem_ref(sock); + if (sock->shuth) + sock->shuth(sock->arg); + return; + } +} + + +static void conn_destructor(void *arg) +{ + struct websock_conn *conn = arg; + + if (conn->state == OPEN) + (void)websock_close(conn, WEBSOCK_GOING_AWAY, "Going Away"); + + if (conn->state == CLOSING) { + + conn->recvh = dummy_recv_handler; + conn->closeh = internal_close_handler; + conn->arg = conn; + + tmr_start(&conn->tmr, TIMEOUT_CLOSE, timeout_handler, conn); + + /* important: the hack below depends on this */ + mem_ref(conn); + return; + } + + tmr_cancel(&conn->tmr); + mem_deref(conn->sc); + mem_deref(conn->tc); + mem_deref(conn->mb); + mem_deref(conn->req); + mem_deref(conn->sock); +} + + +static void conn_close(struct websock_conn *conn, int err) +{ + tmr_cancel(&conn->tmr); + conn->sc = mem_deref(conn->sc); + conn->tc = mem_deref(conn->tc); + conn->state = CLOSED; + + conn->closeh(err, conn->arg); +} + + +static void timeout_handler(void *arg) +{ + struct websock_conn *conn = arg; + + conn_close(conn, ETIMEDOUT); +} + + +static void keepalive_handler(void *arg) +{ + struct websock_conn *conn = arg; + + tmr_start(&conn->tmr, conn->kaint, keepalive_handler, conn); + + (void)websock_send(conn, WEBSOCK_PING, NULL); +} + + +static enum websock_scode websock_err2scode(int err) +{ + switch (err) { + + case EOVERFLOW: return WEBSOCK_MESSAGE_TOO_BIG; + case EPROTO: return WEBSOCK_PROTOCOL_ERROR; + case EBADMSG: return WEBSOCK_PROTOCOL_ERROR; + default: return WEBSOCK_INTERNAL_ERROR; + } +} + + +static int websock_decode(struct websock_hdr *hdr, struct mbuf *mb) +{ + uint8_t v, *p; + size_t i; + + if (mbuf_get_left(mb) < 2) + return ENODATA; + + v = mbuf_read_u8(mb); + hdr->fin = v>>7 & 0x1; + hdr->rsv1 = v>>6 & 0x1; + hdr->rsv2 = v>>5 & 0x1; + hdr->rsv3 = v>>4 & 0x1; + hdr->opcode = v & 0x0f; + + v = mbuf_read_u8(mb); + hdr->mask = v>>7 & 0x1; + hdr->len = v & 0x7f; + + if (hdr->len == 126) { + + if (mbuf_get_left(mb) < 2) + return ENODATA; + + hdr->len = ntohs(mbuf_read_u16(mb)); + } + else if (hdr->len == 127) { + + if (mbuf_get_left(mb) < 8) + return ENODATA; + + hdr->len = sys_ntohll(mbuf_read_u64(mb)); + } + + if (hdr->mask) { + + if (mbuf_get_left(mb) < (4 + hdr->len)) + return ENODATA; + + hdr->mkey[0] = mbuf_read_u8(mb); + hdr->mkey[1] = mbuf_read_u8(mb); + hdr->mkey[2] = mbuf_read_u8(mb); + hdr->mkey[3] = mbuf_read_u8(mb); + + for (i=0, p=mbuf_buf(mb); ilen; i++) + p[i] = p[i] ^ hdr->mkey[i%4]; + } + else { + if (mbuf_get_left(mb) < hdr->len) + return ENODATA; + } + + return 0; +} + + +static void recv_handler(struct mbuf *mb, void *arg) +{ + struct websock_conn *conn = arg; + int err = 0; + + if (conn->mb) { + + const size_t len = mbuf_get_left(mb), pos = conn->mb->pos; + + if ((mbuf_get_left(conn->mb) + len) > BUFSIZE_MAX) { + err = EOVERFLOW; + goto out; + } + + conn->mb->pos = conn->mb->end; + + err = mbuf_write_mem(conn->mb, mbuf_buf(mb), len); + if (err) + goto out; + + conn->mb->pos = pos; + } + else { + conn->mb = mem_ref(mb); + } + + while (conn->mb) { + + struct websock_hdr hdr; + size_t pos, end; + + pos = conn->mb->pos; + + err = websock_decode(&hdr, conn->mb); + if (err) { + if (err == ENODATA) { + conn->mb->pos = pos; + err = 0; + break; + } + + goto out; + } + + if (conn->active == hdr.mask) { + err = EPROTO; + goto out; + } + + if (hdr.rsv1 || hdr.rsv2 || hdr.rsv3) { + err = EPROTO; + goto out; + } + + mb = conn->mb; + + end = mb->end; + mb->end = mb->pos + (size_t)hdr.len; + + if (end > mb->end) { + struct mbuf *mbn = mbuf_alloc(end - mb->end); + if (!mbn) { + err = ENOMEM; + goto out; + } + + (void)mbuf_write_mem(mbn, mb->buf + mb->end, + end - mb->end); + mbn->pos = 0; + + conn->mb = mbn; + } + else { + conn->mb = NULL; + } + + switch (hdr.opcode) { + + case WEBSOCK_CONT: + case WEBSOCK_TEXT: + case WEBSOCK_BIN: + mem_ref(conn); + conn->recvh(&hdr, mb, conn->arg); + + if (mem_nrefs(conn) == 1) { + + if (conn->state == OPEN) + (void)websock_close(conn, + WEBSOCK_GOING_AWAY, + "Going Away"); + + /* + * This is a hack. We enforce CLOSING + * state so we know the connection will + * continue to live. + */ + conn->state = CLOSING; + } + mem_deref(conn); + break; + + case WEBSOCK_CLOSE: + if (conn->state == OPEN) + (void)websock_send(conn, WEBSOCK_CLOSE, "%b", + mbuf_buf(mb), mbuf_get_left(mb)); + conn_close(conn, 0); + mem_deref(mb); + return; + + case WEBSOCK_PING: + (void)websock_send(conn, WEBSOCK_PONG, "%b", + mbuf_buf(mb), mbuf_get_left(mb)); + break; + + case WEBSOCK_PONG: + break; + + default: + mem_deref(mb); + err = EPROTO; + goto out; + } + + mem_deref(mb); + } + + out: + if (err) { + (void)websock_close(conn, websock_err2scode(err), NULL); + conn_close(conn, err); + } +} + + +static void close_handler(int err, void *arg) +{ + struct websock_conn *conn = arg; + + conn_close(conn, err); +} + + +static int accept_print(struct re_printf *pf, const struct pl *key) +{ + uint8_t digest[SHA_DIGEST_LENGTH]; + SHA_CTX ctx; + + SHA1_Init(&ctx); + SHA1_Update(&ctx, key->p, key->l); + SHA1_Update(&ctx, magic, sizeof(magic)-1); + SHA1_Final(digest, &ctx); + + return base64_print(pf, digest, sizeof(digest)); +} + + +static void http_resp_handler(int err, const struct http_msg *msg, void *arg) +{ + struct websock_conn *conn = arg; + const struct http_hdr *hdr; + struct pl key; + char buf[32]; + + if (err || msg->scode != 101) + goto fail; + + if (!http_msg_hdr_has_value(msg, HTTP_HDR_UPGRADE, "websocket")) + goto fail; + + if (!http_msg_hdr_has_value(msg, HTTP_HDR_CONNECTION, "Upgrade")) + goto fail; + + hdr = http_msg_hdr(msg, HTTP_HDR_SEC_WEBSOCKET_ACCEPT); + if (!hdr) + goto fail; + + key.p = conn->nonce; + key.l = sizeof(conn->nonce); + + if (re_snprintf(buf, sizeof(buf), "%H", accept_print, &key) < 0) + goto fail; + + if (pl_strcmp(&hdr->val, buf)) + goto fail; + + /* here we are ok */ + + conn->state = OPEN; + conn->tc = mem_ref(http_req_tcp(conn->req)); + conn->sc = mem_ref(http_req_tls(conn->req)); + (void)tcp_conn_peer_get(conn->tc, &conn->peer); + + tcp_set_handlers(conn->tc, NULL, recv_handler, close_handler, conn); + conn->req = mem_deref(conn->req); + + if (conn->kaint) + tmr_start(&conn->tmr, conn->kaint, keepalive_handler, conn); + + conn->estabh(conn->arg); + return; + + fail: + conn_close(conn, err ? err : EPROTO); +} + + +/* dummy HTTP data handler, this must be here so that HTTP client + * is not closing the underlying TCP-connection (which we need ..) + */ +static void http_data_handler(struct mbuf *mb, void *arg) +{ + (void)mb; + (void)arg; +} + + +int websock_connect(struct websock_conn **connp, struct websock *sock, + struct http_cli *cli, const char *uri, unsigned kaint, + websock_estab_h *estabh, websock_recv_h *recvh, + websock_close_h *closeh, void *arg, + const char *fmt, ...) +{ + struct websock_conn *conn; + uint8_t nonce[16]; + va_list ap; + size_t len; + int err; + + if (!connp || !sock || !cli || !uri || !estabh || !recvh || !closeh) + return EINVAL; + + conn = mem_zalloc(sizeof(*conn), conn_destructor); + if (!conn) + return ENOMEM; + + /* The nonce MUST be selected randomly for each connection */ + rand_bytes(nonce, sizeof(nonce)); + + len = sizeof(conn->nonce); + + err = base64_encode(nonce, sizeof(nonce), conn->nonce, &len); + if (err) + goto out; + + conn->sock = mem_ref(sock); + conn->kaint = kaint; + conn->estabh = estabh; + conn->recvh = recvh; + conn->closeh = closeh; + conn->arg = arg; + conn->state = CONNECTING; + conn->active = true; + + /* Protocol Handshake */ + va_start(ap, fmt); + err = http_request(&conn->req, cli, "GET", uri, + http_resp_handler, http_data_handler, conn, + "Upgrade: websocket\r\n" + "Connection: upgrade\r\n" + "Sec-WebSocket-Key: %b\r\n" + "Sec-WebSocket-Version: 13\r\n" + "%v" + "\r\n", + conn->nonce, sizeof(conn->nonce), + fmt, &ap); + va_end(ap); + if (err) + goto out; + + out: + if (err) + mem_deref(conn); + else + *connp = conn; + + return err; +} + + +int websock_accept(struct websock_conn **connp, struct websock *sock, + struct http_conn *htconn, const struct http_msg *msg, + unsigned kaint, websock_recv_h *recvh, + websock_close_h *closeh, void *arg) +{ + const struct http_hdr *key; + struct websock_conn *conn; + int err; + + if (!connp || !sock || !htconn || !msg || !recvh || !closeh) + return EINVAL; + + if (!http_msg_hdr_has_value(msg, HTTP_HDR_UPGRADE, "websocket")) + return EBADMSG; + + if (!http_msg_hdr_has_value(msg, HTTP_HDR_CONNECTION, "Upgrade")) + return EBADMSG; + + if (!http_msg_hdr_has_value(msg, HTTP_HDR_SEC_WEBSOCKET_VERSION, "13")) + return EBADMSG; + + key = http_msg_hdr(msg, HTTP_HDR_SEC_WEBSOCKET_KEY); + if (!key) + return EBADMSG; + + conn = mem_zalloc(sizeof(*conn), conn_destructor); + if (!conn) + return ENOMEM; + + err = http_reply(htconn, 101, "Switching Protocols", + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: %H\r\n" + "\r\n", + accept_print, &key->val); + if (err) + goto out; + + sa_cpy(&conn->peer, http_conn_peer(htconn)); + conn->sock = mem_ref(sock); + conn->tc = mem_ref(http_conn_tcp(htconn)); + conn->sc = mem_ref(http_conn_tls(htconn)); + conn->kaint = kaint; + conn->recvh = recvh; + conn->closeh = closeh; + conn->arg = arg; + conn->state = OPEN; + conn->active = false; + + tcp_set_handlers(conn->tc, NULL, recv_handler, close_handler, conn); + http_conn_close(htconn); + + if (conn->kaint) + tmr_start(&conn->tmr, conn->kaint, keepalive_handler, conn); + + out: + if (err) + mem_deref(conn); + else + *connp = conn; + + return err; +} + + +static int websock_encode(struct mbuf *mb, bool fin, + enum websock_opcode opcode, bool mask, size_t len) +{ + int err; + + err = mbuf_write_u8(mb, (fin<<7) | (opcode & 0x0f)); + + if (len > 0xffff) { + err |= mbuf_write_u8(mb, (mask<<7) | 127); + err |= mbuf_write_u64(mb, sys_htonll(len)); + } + else if (len > 125) { + err |= mbuf_write_u8(mb, (mask<<7) | 126); + err |= mbuf_write_u16(mb, htons(len)); + } + else { + err |= mbuf_write_u8(mb, (mask<<7) | len); + } + + if (mask) { + uint8_t mkey[4]; + uint8_t *p; + size_t i; + + rand_bytes(mkey, sizeof(mkey)); + + err |= mbuf_write_mem(mb, mkey, sizeof(mkey)); + + for (i=0, p=mbuf_buf(mb); iactive ? 14 : 10; + size_t len, start; + struct mbuf *mb; + int err = 0; + + if (conn->state != OPEN) + return ENOTCONN; + + mb = mbuf_alloc(2048); + if (!mb) + return ENOMEM; + + mb->pos = hsz; + + if (scode) + err |= mbuf_write_u16(mb, htons(scode)); + if (fmt) + err |= mbuf_vprintf(mb, fmt, ap); + if (err) + goto out; + + len = mb->pos - hsz; + + if (len > 0xffff) + start = mb->pos = 0; + else if (len > 125) + start = mb->pos = 6; + else + start = mb->pos = 8; + + err = websock_encode(mb, true, opcode, conn->active, len); + if (err) + goto out; + + mb->pos = start; + + err = tcp_send(conn->tc, mb); + if (err) + goto out; + + out: + mem_deref(mb); + + return err; +} + + +int websock_send(struct websock_conn *conn, enum websock_opcode opcode, + const char *fmt, ...) +{ + va_list ap; + int err; + + if (!conn) + return EINVAL; + + va_start(ap, fmt); + err = websock_vsend(conn, opcode, 0, fmt, ap); + va_end(ap); + + return err; +} + + +int websock_close(struct websock_conn *conn, enum websock_scode scode, + const char *fmt, ...) +{ + va_list ap; + int err; + + if (!conn) + return EINVAL; + + if (!scode) + fmt = NULL; + + va_start(ap, fmt); + err = websock_vsend(conn, WEBSOCK_CLOSE, scode, fmt, ap); + va_end(ap); + + if (!err) + conn->state = CLOSING; + + return err; +} + + +const struct sa *websock_peer(const struct websock_conn *conn) +{ + return conn ? &conn->peer : NULL; +} + + +int websock_alloc(struct websock **sockp, websock_shutdown_h *shuth, void *arg) +{ + struct websock *sock; + + if (!sockp) + return EINVAL; + + sock = mem_zalloc(sizeof(*sock), sock_destructor); + if (!sock) + return ENOMEM; + + sock->shuth = shuth; + sock->arg = arg; + + *sockp = sock; + + return 0; +} + + +void websock_shutdown(struct websock *sock) +{ + if (!sock || sock->shutdown) + return; + + sock->shutdown = true; + mem_deref(sock); +}