trafficserver-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jpe...@apache.org
Subject [trafficserver] branch master updated: Adds a C++ API WebSocket example.
Date Tue, 27 Sep 2016 17:52:30 GMT
This is an automated email from the ASF dual-hosted git repository.

jpeach pushed a commit to branch master
in repository https://git-dual.apache.org/repos/asf/trafficserver.git

The following commit(s) were added to refs/heads/master by this push:
       new  ff15279   Adds a C++ API WebSocket example.
ff15279 is described below

commit ff152791d6aeb10ce8ea1ff6b58ea30341a9f33d
Author: Oliver Goodman <oag@optusnet.com.au>
AuthorDate: Mon Sep 26 22:04:53 2016 +0000

    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(+)

diff --git a/lib/atscppapi/examples/Makefile.am b/lib/atscppapi/examples/Makefile.am
index a0f4be5..47064fb 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 0000000..f12c002
--- /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 0000000..b6aaf41
--- /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 <ts/ts.h>
+#include "openssl/evp.h"
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#if defined(__linux__)
+#include <endian.h>
+#elif defined(__APPLE__)
+#include <libkern/OSByteOrder.h>
+#define be64toh(x) OSSwapBigToHostInt64(x)
+#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__FreeBSD__)
+#include <sys/endian.h>
+#elif defined(__DragonFly__)
+#include <machine/endian.h>
+#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 0000000..3608efe
--- /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 <string>
+
+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 0000000..c41352f
--- /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 <atscppapi/Logger.h>
+
+// 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 0000000..2c65c4a
--- /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 <atscppapi/GlobalPlugin.h>
+#include <atscppapi/InterceptPlugin.h>
+
+#include <string>
+#include <stddef.h>
+
+#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 */

-- 
To stop receiving notification emails like this one, please contact
['"commits@trafficserver.apache.org" <commits@trafficserver.apache.org>'].

Mime
View raw message