From aec5c5186fe5ee6d3ce5d122a38bd626f9b04a8a Mon Sep 17 00:00:00 2001 From: Oliver Goodman Date: Mon, 26 Sep 2016 22:04:53 +0000 Subject: [PATCH] Adds a C++ API WebSocket example. --- lib/atscppapi/examples/Makefile.am | 3 + lib/atscppapi/examples/websocket/README.txt | 14 + lib/atscppapi/examples/websocket/WSBuffer.cc | 247 ++++++++++++++++++ lib/atscppapi/examples/websocket/WSBuffer.h | 93 +++++++ lib/atscppapi/examples/websocket/WebSocket.cc | 150 +++++++++++ lib/atscppapi/examples/websocket/WebSocket.h | 69 +++++ 6 files changed, 576 insertions(+) create mode 100644 lib/atscppapi/examples/websocket/README.txt create mode 100644 lib/atscppapi/examples/websocket/WSBuffer.cc create mode 100644 lib/atscppapi/examples/websocket/WSBuffer.h create mode 100644 lib/atscppapi/examples/websocket/WebSocket.cc create mode 100644 lib/atscppapi/examples/websocket/WebSocket.h diff --git a/lib/atscppapi/examples/Makefile.am b/lib/atscppapi/examples/Makefile.am index a0f4be5596f..47064fb323c 100644 --- a/lib/atscppapi/examples/Makefile.am +++ b/lib/atscppapi/examples/Makefile.am @@ -39,6 +39,7 @@ plugins = \ StatExample.la \ TimeoutExamplePlugin.la \ TransactionHookPlugin.la \ + WebSocket.la \ boom.la \ intercept.la @@ -74,6 +75,7 @@ ServerResponse_la_SOURCES = serverresponse/ServerResponse.cc StatExample_la_SOURCES = stat_example/StatExample.cc TimeoutExamplePlugin_la_SOURCES = timeout_example/TimeoutExamplePlugin.cc TransactionHookPlugin_la_SOURCES = transactionhook/TransactionHookPlugin.cc +WebSocket_la_SOURCES = websocket/WebSocket.cc websocket/WSBuffer.cc boom_la_SOURCES = boom/boom.cc intercept_la_SOURCES = intercept/intercept.cc @@ -97,6 +99,7 @@ ServerResponse_la_LIBADD = $(libatscppai) StatExample_la_LIBADD = $(libatscppai) TimeoutExamplePlugin_la_LIBADD = $(libatscppai) TransactionHookPlugin_la_LIBADD = $(libatscppai) +WebSocket_la_LIBADD = $(libatscppai) boom_la_LIBADD = $(libatscppai) intercept_la_LIBADD = $(libatscppai) diff --git a/lib/atscppapi/examples/websocket/README.txt b/lib/atscppapi/examples/websocket/README.txt new file mode 100644 index 00000000000..f12c00294bc --- /dev/null +++ b/lib/atscppapi/examples/websocket/README.txt @@ -0,0 +1,14 @@ +To test this plugin, add WebSocket.so to plugin.config, start +Traffic Server, then in a browser JavaScript console enter the +following: + + ws = new WebSocket('ws://some.host:8080/'); + ws.onmessage = function(e) { console.log(e.data); }; + ws.send('hello'); + +The host name 'some.host' must resolve to the server where Traffic +Server is running. You should get a response from the plugin. + +It appears to be necessary that the host name be a valid DNS name on +the server where Traffic Server is running. If not, the WebSocket +connection will fail with an error of 502 "Cannot find server." diff --git a/lib/atscppapi/examples/websocket/WSBuffer.cc b/lib/atscppapi/examples/websocket/WSBuffer.cc new file mode 100644 index 00000000000..b6aaf4138d7 --- /dev/null +++ b/lib/atscppapi/examples/websocket/WSBuffer.cc @@ -0,0 +1,247 @@ +/** @file + + WebSocket termination example. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include "WSBuffer.h" + +#include +#include "openssl/evp.h" +#include +#include + +#if defined(__linux__) +#include +#elif defined(__APPLE__) +#include +#define be64toh(x) OSSwapBigToHostInt64(x) +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__FreeBSD__) +#include +#elif defined(__DragonFly__) +#include +#if BYTE_ORDER == LITTLE_ENDIAN +#define be64toh(x) __bswap64(x) +#elif BYTE_ORDER == BIG_ENDIAN +#define be64toh(x) (x) +#endif +#endif + +#define BASE64_ENCODE_DSTLEN(_length) ((_length * 8) / 6 + 4) +#define WS_DIGEST_MAX BASE64_ENCODE_DSTLEN(20) + +static const std::string magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + +WSBuffer::WSBuffer() +{ +} + +void +WSBuffer::buffer(std::string const &data) +{ + ws_buf_ += data; +} + +bool +WSBuffer::read_buffered_message(std::string &message, int &code) +{ + // There are two basic states depending on whether or + // not we have parsed a message length. If we're looking + // for a message length, we don't advance pos_ until we + // have read one (as well as control bytes and mask). + // + // Once we have a message length, we don't advance + // pos_ until we have a complete message. (If the message + // length is 0 we will produce the message immediately + // and revert to looking for the next message length.) + // + // When incoming data is fragmented we may be called several + // times before we get a length or a complete message. + + char mask[4]; + + size_t avail = ws_buf_.size(); + + // Check if there is a mask (there should be). + if (avail < 2) + return false; + size_t mask_len = (ws_buf_[1] & WS_MASKED) ? 4 : 0; + + int frame = ws_buf_[0] & WS_OPCODE; + bool first = frame != WS_FRAME_CONTINUATION; + auto final = ws_buf_[0] & WS_FIN; + + // Save/restore frame type on first/continuation. + if (first) { + frame_ = frame; + msg_buf_.clear(); + } else + frame = frame_; + + // Read the msg_length if we have enough data. + if (avail < 2 + mask_len) + return false; + + size_t msg_len = ws_buf_[1] & WS_LENGTH; + size_t pos; + if (msg_len == WS_16BIT_LEN) { + if (avail < 4 + mask_len) { // 2 + 2 + length bytes + mask. + return false; + } + msg_len = ntohs(*(uint16_t *)(ws_buf_.data() + 2)); + pos = 4; + } else if (msg_len == WS_64BIT_LEN) { + if (avail < 10 + mask_len) { // 2 + 8 length bytes + mask. + return false; + } + msg_len = be64toh(*(uint64_t *)(ws_buf_.data() + 2)); + pos = 10; + } else { + pos = 2; + } + + // Check if we have enough data to read the message. + if (ws_buf_.size() < pos + msg_len) + return false; // not enough data. + + // Copy any mask. + for (size_t i = 0; i < mask_len; ++i, ++pos) { + mask[i] = ws_buf_[pos]; + } + + // Apply any mask. + if (mask_len) { + for (size_t i = 0, p = pos; i < msg_len; ++i, ++p) { + ws_buf_[p] ^= mask[i & 3]; + } + } + + // Copy the message out. + if (final) { + message = msg_buf_; + message += ws_buf_.substr(pos, msg_len); + code = frame; + } else { + msg_buf_ += ws_buf_.substr(pos, msg_len); + } + + // Discard consumed data. + ws_buf_.erase(0, pos + msg_len); + + return true; +} + +std::string +WSBuffer::ws_digest(std::string const &key) +{ + EVP_MD_CTX digest; + EVP_MD_CTX_init(&digest); + + if (!EVP_DigestInit_ex(&digest, EVP_sha1(), NULL)) { + EVP_MD_CTX_cleanup(&digest); + return "init-failed"; + } + if (!EVP_DigestUpdate(&digest, key.data(), key.length())) { + EVP_MD_CTX_cleanup(&digest); + return "update1-failed"; + } + if (!EVP_DigestUpdate(&digest, magic.data(), magic.length())) { + EVP_MD_CTX_cleanup(&digest); + return "update2-failed"; + } + + unsigned char hash_buf[EVP_MAX_MD_SIZE]; + unsigned int hash_len = 0; + if (!EVP_DigestFinal_ex(&digest, hash_buf, &hash_len)) { + EVP_MD_CTX_cleanup(&digest); + return "final-failed"; + } + EVP_MD_CTX_cleanup(&digest); + if (hash_len != 20) { + return "bad-hash-length"; + } + + char digest_buf[WS_DIGEST_MAX]; + size_t digest_len = 0; + + TSBase64Encode((char *)hash_buf, hash_len, digest_buf, WS_DIGEST_MAX, &digest_len); + + return std::string((char *)digest_buf, digest_len); +} + +std::string +WSBuffer::get_handshake(std::string const &ws_key) +{ + std::string digest = ws_digest(ws_key); + + // NOTE: a real server might be expecting a Sec-WebSocket-Protocol + // header and wish to respond accordingly. In that case you must + // call ws_digest() and construct the headers yourself. + + std::string headers = "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: " + + digest + "\r\n\r\n"; + return headers; +} + +std::string +WSBuffer::get_frame(size_t len, int code) +{ + std::string frame; + frame.reserve(10); + frame += char(code); + + int len_len; + if (len <= 125) { + frame += char(len); + len_len = 0; + } else if (len <= UINT16_MAX) { + frame += char(WS_16BIT_LEN); + len_len = 2; + } else { + frame += char(WS_64BIT_LEN); + len_len = 8; + } + // Convert length to big-endian bytes. + while (--len_len >= 0) { + frame += char((len >> (8 * len_len)) & 0xFF); + } + + return frame; +} + +uint16_t +WSBuffer::get_closing_code(std::string const &message, std::string *desc) +{ + uint16_t code = 0; + if (message.size() >= 2) { + code = (unsigned char)message[0]; + code <<= 8; + code += (unsigned char)message[1]; + if (desc) + *desc = message.substr(2); + } else { + if (desc) + *desc = ""; + } + return code; +} diff --git a/lib/atscppapi/examples/websocket/WSBuffer.h b/lib/atscppapi/examples/websocket/WSBuffer.h new file mode 100644 index 00000000000..3608efe7d53 --- /dev/null +++ b/lib/atscppapi/examples/websocket/WSBuffer.h @@ -0,0 +1,93 @@ +/** @file + + WebSocket termination example. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#pragma once +#ifndef WSBUFFER_H_20F6C829_0736_47D3_A5D2_7130D4D99CC0 +#define WSBUFFER_H_20F6C829_0736_47D3_A5D2_7130D4D99CC0 + +#include + +enum ws_frametype { + WS_FRAME_CONTINUATION = 0x0, + WS_FRAME_TEXT = 0x1, + WS_FRAME_BINARY = 0x2, + WS_FRAME_CLOSE = 0x8, + WS_FRAME_PING = 0x9, + WS_FRAME_PONG = 0xA +}; +typedef enum ws_frametype WS_FRAMETYPE; + +#define WS_RSV1 0x40 +#define WS_RSV2 0x20 +#define WS_RSV3 0x10 +#define WS_MASKED 0x80 +#define WS_OPCODE 0x0F +#define WS_FIN 0x80 +#define WS_LENGTH 0x7F +#define WS_16BIT_LEN 126 +#define WS_64BIT_LEN 127 + +class WSBuffer +{ +public: + WSBuffer(); + + /** + * Adds incoming websocket data to the buffer for decoding. + */ + void buffer(std::string const &data); + + /** + * Returns a decoded message if there is sufficient data buffered. + */ + bool read_buffered_message(std::string &message, int &code); + + /** + * Calculates the Sec-WebSocket-Accept digest value for a given key. + */ + static std::string ws_digest(std::string const &ws_key); + + /** + * Convenience method returning a complete upgrade response. + */ + static std::string get_handshake(std::string const &ws_key); + + /** + * Gets the frame prefix for sending a message to the client. + * + * The complete message is: get_frame(msg.size(), code) + msg. + */ + static std::string get_frame(size_t len, int code = WS_FIN + WS_FRAME_TEXT); + + /** + * Gets the closing code and message if any. + */ + static uint16_t get_closing_code(std::string const &message, std::string *desc = nullptr); + +private: + std::string ws_buf_; // incoming data. + int frame_; // frame type of current message + std::string msg_buf_; // decoded message data +}; + +#endif /* WSBUFFER_H_20F6C829_0736_47D3_A5D2_7130D4D99CC0 */ diff --git a/lib/atscppapi/examples/websocket/WebSocket.cc b/lib/atscppapi/examples/websocket/WebSocket.cc new file mode 100644 index 00000000000..c41352f1a20 --- /dev/null +++ b/lib/atscppapi/examples/websocket/WebSocket.cc @@ -0,0 +1,150 @@ +/** @file + + WebSocket termination example. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include "WebSocket.h" + +#include + +// DISCLAIMER: this is intended for demonstration purposes only and +// does not pretend to implement a complete (or useful) server. + +using namespace atscppapi; + +void +TSPluginInit(int argc, const char *argv[]) +{ + RegisterGlobalPlugin("CPP_Example_WebSocket", "apache", "dev@trafficserver.apache.org"); + new WebSocketInstaller(); +} + +// WebSocketInstaller + +WebSocketInstaller::WebSocketInstaller() : GlobalPlugin(true /* ignore internal transactions */) +{ + GlobalPlugin::registerHook(Plugin::HOOK_READ_REQUEST_HEADERS_PRE_REMAP); +} + +void +WebSocketInstaller::handleReadRequestHeadersPreRemap(Transaction &transaction) +{ + TS_DEBUG("websocket", "Incoming request."); + transaction.addPlugin(new WebSocket(transaction)); + transaction.resume(); +} + +// WebSocket implementation. + +WebSocket::WebSocket(Transaction &transaction) : InterceptPlugin(transaction, InterceptPlugin::SERVER_INTERCEPT) +{ + if (isWebsocket()) { + TS_DEBUG("websocket", "WebSocket connection started."); + ws_key_ = transaction.getClientRequest().getHeaders().values("sec-websocket-key"); + TS_DEBUG("websocket", "ws_key_ obtained"); + } +} + +WebSocket::~WebSocket() +{ + TS_DEBUG("websocket", "WebSocket finished."); +} + +void +WebSocket::consume(const std::string &data, InterceptPlugin::RequestDataType type) +{ + TS_DEBUG("websocket", "WebSocket consuming data"); + if (ws_key_.size()) { + produce(WSBuffer::get_handshake(ws_key_)); + ws_key_ = ""; + } + + if (type == InterceptPlugin::REQUEST_HEADER) { + headers_ += data; + } else if (isWebsocket()) { + int code; + std::string message; + ws_buf_.buffer(data); + while (ws_buf_.read_buffered_message(message, code)) { + ws_receive(message, code); + if (code == WS_FRAME_CLOSE) + break; + } + } else { + body_ += data; + } +} + +void +WebSocket::ws_send(std::string const &msg, int code) +{ + produce(WSBuffer::get_frame(msg.size(), code) + msg); +} + +void +WebSocket::ws_receive(std::string const &message, int code) +{ + switch (code) { + case WS_FRAME_CLOSE: + // NOTE: first two bytes (if sent) are a reason code + // which we are expected to echo. + if (message.size() > 2) { + ws_send(message.substr(0, 2), WS_FIN + WS_FRAME_CLOSE); + } else { + ws_send("", WS_FIN + WS_FRAME_CLOSE); + } + setOutputComplete(); + break; + case WS_FRAME_TEXT: + TS_DEBUG("websocket", "WS client: %s", message.c_str()); + ws_send("got: " + message, WS_FIN + WS_FRAME_TEXT); + break; + case WS_FRAME_BINARY: + TS_DEBUG("websocket", "WS client sent %d bytes", (int)message.size()); + ws_send("got binary data", WS_FIN + WS_FRAME_TEXT); + break; + case WS_FRAME_PING: + TS_DEBUG("websocket", "WS client ping"); + ws_send(message, WS_FRAME_PONG); + break; + case WS_FRAME_CONTINUATION: + // WSBuffer should not pass these on. + case WS_FRAME_PONG: + // We should not get these so just ignore. + default: + // Ignoring unrecognized opcodes. + break; + } +} + +void +WebSocket::handleInputComplete() +{ + TS_DEBUG("websocket", "Request data complete (not a WebSocket connection)."); + + std::string out = "HTTP/1.1 200 Ok\r\n" + "Content-type: text/plain\r\n" + "Content-length: 10\r\n" + "\r\n" + "Hi there!\n"; + produce(out); + setOutputComplete(); +} diff --git a/lib/atscppapi/examples/websocket/WebSocket.h b/lib/atscppapi/examples/websocket/WebSocket.h new file mode 100644 index 00000000000..2c65c4a9a22 --- /dev/null +++ b/lib/atscppapi/examples/websocket/WebSocket.h @@ -0,0 +1,69 @@ +/** @file + + WebSocket termination example. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#ifndef WEBSOCKET_H_3AE11C09_90DC_4BC6_A297_B38C3B8AEFBF +#define WEBSOCKET_H_3AE11C09_90DC_4BC6_A297_B38C3B8AEFBF + +#include +#include + +#include +#include + +#include "WSBuffer.h" + +// WebSocket InterceptPlugin + +using atscppapi::InterceptPlugin; +using atscppapi::Transaction; +using atscppapi::GlobalPlugin; + +class WebSocket : public InterceptPlugin +{ +public: + WebSocket(Transaction &transaction); + ~WebSocket(); + + void consume(const std::string &data, InterceptPlugin::RequestDataType type); + void handleInputComplete(); + + void ws_send(std::string const &data, int code); + void ws_receive(std::string const &data, int code); + +private: + std::string headers_; + std::string body_; + + std::string ws_key_; // value of sec-websocket-key header + WSBuffer ws_buf_; // incoming data. +}; + +class WebSocketInstaller : public GlobalPlugin +{ +public: + WebSocketInstaller(); + + void handleReadRequestHeadersPreRemap(Transaction &transaction); +}; + +#endif /* WEBSOCKET_H_3AE11C09_90DC_4BC6_A297_B38C3B8AEFBF */