diff --git a/CMakeLists.txt b/CMakeLists.txt index a8eac880..d748e00e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,4 @@ # -# cmd_websocket_chat.cpp # Author: Benjamin Sergeant # Copyright (c) 2018 Machine Zone, Inc. All rights reserved. # diff --git a/examples/satori_publisher/satori_publisher.cpp b/examples/satori_publisher/satori_publisher.cpp index 7b180403..8ecfae4c 100644 --- a/examples/satori_publisher/satori_publisher.cpp +++ b/examples/satori_publisher/satori_publisher.cpp @@ -51,7 +51,7 @@ int main(int argc, char* argv[]) bool done = false; ix::SatoriConnection satoriConnection; ix::WebSocketPerMessageDeflateOptions webSocketPerMessageDeflateOptions( - false, false, false, 15, 15); + true, false, false, 15, 15); satoriConnection.configure(appkey, endpoint, rolename, rolesecret, webSocketPerMessageDeflateOptions); satoriConnection.connect(); diff --git a/examples/ws_connect/ws_connect.cpp b/examples/ws_connect/ws_connect.cpp index d57e0d8d..89a70714 100644 --- a/examples/ws_connect/ws_connect.cpp +++ b/examples/ws_connect/ws_connect.cpp @@ -49,6 +49,10 @@ namespace { _webSocket.setUrl(_url); + ix::WebSocketPerMessageDeflateOptions webSocketPerMessageDeflateOptions( + true, false, false, 15, 15); + _webSocket.setPerMessageDeflateOptions(webSocketPerMessageDeflateOptions); + std::stringstream ss; log(std::string("Connecting to url: ") + _url); @@ -64,6 +68,11 @@ namespace if (messageType == ix::WebSocket_MessageType_Open) { log("ws_connect: connected"); + std::cout << "Handshake Headers:" << std::endl; + for (auto it : headers) + { + std::cout << it.first << ": " << it.second << std::endl; + } } else if (messageType == ix::WebSocket_MessageType_Close) { diff --git a/ixwebsocket/IXWebSocketPerMessageDeflate.cpp b/ixwebsocket/IXWebSocketPerMessageDeflate.cpp index 3452387c..20eeb182 100644 --- a/ixwebsocket/IXWebSocketPerMessageDeflate.cpp +++ b/ixwebsocket/IXWebSocketPerMessageDeflate.cpp @@ -1,11 +1,48 @@ /* - * IXWebSocketPerMessageDeflate.cpp + * Copyright (c) 2015, Peter Thorson. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of the WebSocket++ Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 PETER THORSON 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. + */ + +/* * Author: Benjamin Sergeant * Copyright (c) 2018 Machine Zone, Inc. All rights reserved. * - * Per message Deflate RFC: https://tools.ietf.org/html/rfc7692 + * Adapted from websocketpp/extensions/permessage_deflate/enabled.hpp + * (same license as MZ: https://opensource.org/licenses/BSD-3-Clause) * + * - Reused zlib compression + decompression bits. + * - Refactored to have 2 class for compression and decompression, to allow multi-threading + * and make sure that _compressBuffer is not shared between threads. + * - Original code wasn't working for some reason, I had to add checks + * for the presence of the kEmptyUncompressedBlock at the end of buffer so that servers + * would start accepting receiving/decoding compressed messages. Original code was probably + * modifying the passed in buffers before processing in enabled.hpp ? + * - Added more documentation. + * + * Per message Deflate RFC: https://tools.ietf.org/html/rfc7692 * Chrome websocket -> https://github.com/chromium/chromium/tree/2ca8c5037021c9d2ecc00b787d58a31ed8fc8bcb/net/websockets + * */ #include "IXWebSocketPerMessageDeflate.h" @@ -43,7 +80,7 @@ namespace ix } bool WebSocketPerMessageDeflateCompressor::init(uint8_t deflateBits, - bool client_no_context_takeover) + bool clientNoContextTakeOver) { int ret = deflateInit2( &_deflateState, @@ -57,14 +94,9 @@ namespace ix if (ret != Z_OK) return false; _compressBuffer.reset(new unsigned char[_compressBufferSize]); - if (client_no_context_takeover) - { - _flush = Z_FULL_FLUSH; - } - else - { - _flush = Z_SYNC_FLUSH; - } + _flush = (clientNoContextTakeOver) + ? Z_FULL_FLUSH + : Z_SYNC_FLUSH; return true; } @@ -79,6 +111,23 @@ namespace ix bool WebSocketPerMessageDeflateCompressor::compress(const std::string& in, std::string& out) { + // + // 7.2.1. Compression + // + // An endpoint uses the following algorithm to compress a message. + // + // 1. Compress all the octets of the payload of the message using + // DEFLATE. + // + // 2. If the resulting data does not end with an empty DEFLATE block + // with no compression (the "BTYPE" bits are set to 00), append an + // empty DEFLATE block with no compression to the tail end. + // + // 3. Remove 4 octets (that are 0x00 0x00 0xff 0xff) from the tail end. + // After this step, the last octet of the compressed data contains + // (possibly part of) the DEFLATE header bits with the "BTYPE" bits + // set to 00. + // size_t output; if (in.empty()) @@ -131,7 +180,7 @@ namespace ix } bool WebSocketPerMessageDeflateDecompressor::init(uint8_t inflateBits, - bool client_no_context_takeover) + bool clientNoContextTakeOver) { int ret = inflateInit2( &_inflateState, @@ -141,14 +190,9 @@ namespace ix if (ret != Z_OK) return false; _compressBuffer.reset(new unsigned char[_compressBufferSize]); - if (client_no_context_takeover) - { - _flush = Z_FULL_FLUSH; - } - else - { - _flush = Z_SYNC_FLUSH; - } + _flush = (clientNoContextTakeOver) + ? Z_FULL_FLUSH + : Z_SYNC_FLUSH; return true; } @@ -156,6 +200,16 @@ namespace ix bool WebSocketPerMessageDeflateDecompressor::decompress(const std::string& in, std::string& out) { + // + // 7.2.2. Decompression + // + // An endpoint uses the following algorithm to decompress a message. + // + // 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the + // payload of the message. + // + // 2. Decompress the resulting data using DEFLATE. + // std::string inFixed(in); inFixed += kEmptyUncompressedBlock; diff --git a/ixwebsocket/IXWebSocketPerMessageDeflate.h b/ixwebsocket/IXWebSocketPerMessageDeflate.h index cbc56022..b8e91ecb 100644 --- a/ixwebsocket/IXWebSocketPerMessageDeflate.h +++ b/ixwebsocket/IXWebSocketPerMessageDeflate.h @@ -1,7 +1,35 @@ /* - * IXWebSocketPerMessageDeflate.h + * Copyright (c) 2015, Peter Thorson. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of the WebSocket++ Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 PETER THORSON 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. + */ + +/* * Author: Benjamin Sergeant * Copyright (c) 2018 Machine Zone, Inc. All rights reserved. + * + * Adapted from websocketpp/extensions/permessage_deflate/enabled.hpp + * (same license as MZ: https://opensource.org/licenses/BSD-3-Clause) */ #pragma once @@ -18,7 +46,8 @@ namespace ix public: WebSocketPerMessageDeflateCompressor(); ~WebSocketPerMessageDeflateCompressor(); - bool init(uint8_t deflate_bits, bool client_no_context_takeover); + + bool init(uint8_t deflateBits, bool clientNoContextTakeOver); bool compress(const std::string& in, std::string& out); private: @@ -27,7 +56,6 @@ namespace ix int _flush; size_t _compressBufferSize; std::unique_ptr _compressBuffer; - z_stream _deflateState; }; @@ -36,14 +64,14 @@ namespace ix public: WebSocketPerMessageDeflateDecompressor(); ~WebSocketPerMessageDeflateDecompressor(); - bool init(uint8_t inflate_bits, bool client_no_context_takeover); + + bool init(uint8_t inflateBits, bool clientNoContextTakeOver); bool decompress(const std::string& in, std::string& out); private: int _flush; size_t _compressBufferSize; std::unique_ptr _compressBuffer; - z_stream _inflateState; }; @@ -51,17 +79,13 @@ namespace ix { public: WebSocketPerMessageDeflate(); - virtual ~WebSocketPerMessageDeflate(); + ~WebSocketPerMessageDeflate(); bool init(const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions); - bool compress(const std::string& in, std::string& out); bool decompress(const std::string& in, std::string& out); private: - // mode::value m_server_max_window_bits_mode; - // mode::value m_client_max_window_bits_mode; - std::shared_ptr _compressor; std::shared_ptr _decompressor; }; diff --git a/ixwebsocket/IXWebSocketTransport.cpp b/ixwebsocket/IXWebSocketTransport.cpp index 15cd28b8..c76f10c8 100644 --- a/ixwebsocket/IXWebSocketTransport.cpp +++ b/ixwebsocket/IXWebSocketTransport.cpp @@ -578,7 +578,7 @@ namespace ix _rxbuf.begin()+ws.header_size + (size_t) ws.N); // Reply back right away - sendData(wsheader_type::PONG, pingData); + sendData(wsheader_type::PONG, pingData, _enablePerMessageDeflate); emitMessage(PING, pingData, ws, onMessageCallback); } @@ -658,7 +658,8 @@ namespace ix } WebSocketSendInfo WebSocketTransport::sendData(wsheader_type::opcode_type type, - const std::string& message) + const std::string& message, + bool compress) { if (_readyState == CLOSING || _readyState == CLOSED) { @@ -672,7 +673,7 @@ namespace ix std::string::const_iterator message_begin = message.begin(); std::string::const_iterator message_end = message.end(); - if (_enablePerMessageDeflate) + if (compress) { _perMessageDeflate.compress(message, compressedMessage); wireSize = compressedMessage.size(); @@ -697,7 +698,7 @@ namespace ix header[0] = 0x80 | type; // This bit indicate that the frame is compressed - if (_enablePerMessageDeflate) + if (compress) { header[0] |= 0x40; } @@ -752,12 +753,12 @@ namespace ix WebSocketSendInfo WebSocketTransport::sendPing(const std::string& message) { - return sendData(wsheader_type::PING, message); + return sendData(wsheader_type::PING, message, _enablePerMessageDeflate); } WebSocketSendInfo WebSocketTransport::sendBinary(const std::string& message) { - return sendData(wsheader_type::BINARY_FRAME, message); + return sendData(wsheader_type::BINARY_FRAME, message, _enablePerMessageDeflate); } void WebSocketTransport::sendOnSocket() @@ -791,12 +792,17 @@ namespace ix { if (_readyState == CLOSING || _readyState == CLOSED) return; + // See list of close events here: + // https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent + // We use 1000: normal closure. + // + // >>> struct.pack('!H', 1000) + // b'\x03\xe8' + // + const std::string normalClosure = std::string("\x03\xe9"); + bool compress = false; + sendData(wsheader_type::CLOSE, normalClosure, compress); setReadyState(CLOSING); - uint8_t closeFrame[6] = {0x88, 0x80, 0x00, 0x00, 0x00, 0x00}; // last 4 bytes are a masking key - std::vector header(closeFrame, closeFrame+6); - appendToSendBuffer(header); - - sendOnSocket(); _socket->wakeUpFromPoll(); _socket->close(); diff --git a/ixwebsocket/IXWebSocketTransport.h b/ixwebsocket/IXWebSocketTransport.h index f19a79a1..7c52184d 100644 --- a/ixwebsocket/IXWebSocketTransport.h +++ b/ixwebsocket/IXWebSocketTransport.h @@ -146,7 +146,8 @@ namespace ix void sendOnSocket(); WebSocketSendInfo sendData(wsheader_type::opcode_type type, - const std::string& message); + const std::string& message, + bool compress); void emitMessage(MessageKind messageKind, const std::string& message,