IXWebSocket/ixwebsocket/IXWebSocketTransport.cpp

817 lines
28 KiB
C++
Raw Normal View History

/*
* The MIT License (MIT)
*
* Copyright (c) 2012, 2013 <dhbaird@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
2018-09-27 23:56:48 +02:00
/*
* IXWebSocketTransport.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved.
2018-09-27 23:56:48 +02:00
*/
//
// Adapted from https://github.com/dhbaird/easywsclient
//
#include "IXWebSocketTransport.h"
#include "IXWebSocketHandshake.h"
#include "IXWebSocketHttpHeaders.h"
Feature/http (#16) * add skeleton and broken http client code. GET returns "Resource temporarily unavailable" errors... * linux compile fix * can GET some pages * Update formatting in README.md * unittest for sending large messages * document bug * Feature/send large message (#14) * introduce send fragment * can pass a fin frame * can send messages which are a perfect multiple of the chunk size * set fin only for last fragment * cleanup * last fragment should be of type CONTINUATION * Add simple send and receive programs * speedups receiving + better way to wait for thing * receive speedup by using linked list of chunks instead of large array * document bug * use chunks to receive data * trailing spaces * Update README.md Add note about message fragmentation. * Feature/ws cli (#15) * New command line tool for transfering files / still very beta. * add readme * use cli11 for argument parsing * json -> msgpack * stop using base64 and use binary which can be stored in message pack * add target for building with homebrew * all CMakeLists are referenced by the top level one * add ws_chat and ws_connect sub commands to ws * cleanup * add echo and broadcast server as ws sub-commands * add gitignore * comments * ping pong added to ws * mv cobra_publisher under ws folder * Update README.md * linux build fix * linux build fix * move http_client to a ws sub-command * simple HTTP post support (urlencode parameters) * can specify extra headers * chunk encoding / simple redirect support / -I option * follow redirects is optional * make README vim markdown plugin friendly * cleanup argument parsing + add socket creation factory * add missing file * http gzip compression * cleanup * doc * Feature/send large message (#14) * introduce send fragment * can pass a fin frame * can send messages which are a perfect multiple of the chunk size * set fin only for last fragment * cleanup * last fragment should be of type CONTINUATION * Add simple send and receive programs * speedups receiving + better way to wait for thing * receive speedup by using linked list of chunks instead of large array * document bug * use chunks to receive data * trailing spaces
2019-03-01 06:54:03 +01:00
#include "IXUrlParser.h"
#include "IXSocketFactory.h"
2018-09-27 23:56:48 +02:00
#include <string.h>
2018-10-09 06:42:45 +02:00
#include <stdlib.h>
2018-09-27 23:56:48 +02:00
#include <cstdlib>
#include <vector>
#include <string>
#include <cstdarg>
#include <iostream>
#include <sstream>
#include <chrono>
#include <thread>
2018-09-27 23:56:48 +02:00
namespace ix
{
2019-03-20 05:31:43 +01:00
const std::string WebSocketTransport::kHeartBeatPingMessage("ixwebsocket::heartbeat");
2019-01-24 21:42:49 +01:00
const int WebSocketTransport::kDefaultHeartBeatPeriod(-1);
constexpr size_t WebSocketTransport::kChunkSize;
2019-01-24 21:42:49 +01:00
2018-09-27 23:56:48 +02:00
WebSocketTransport::WebSocketTransport() :
_useMask(true),
_readyState(CLOSED),
2018-12-15 01:28:11 +01:00
_closeCode(0),
_closeWireSize(0),
_enablePerMessageDeflate(false),
2019-01-24 21:42:49 +01:00
_requestInitCancellation(false),
2019-01-26 01:11:39 +01:00
_heartBeatPeriod(kDefaultHeartBeatPeriod),
_lastSendTimePoint(std::chrono::steady_clock::now())
2018-09-27 23:56:48 +02:00
{
_readbuf.resize(kChunkSize);
2018-09-27 23:56:48 +02:00
}
WebSocketTransport::~WebSocketTransport()
{
;
}
2019-01-24 21:42:49 +01:00
void WebSocketTransport::configure(const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions,
2019-03-20 05:31:43 +01:00
int heartBeatPeriod)
2018-09-27 23:56:48 +02:00
{
_perMessageDeflateOptions = perMessageDeflateOptions;
_enablePerMessageDeflate = _perMessageDeflateOptions.enabled();
2019-03-20 05:31:43 +01:00
_heartBeatPeriod = heartBeatPeriod;
2018-09-27 23:56:48 +02:00
}
2018-12-30 08:15:27 +01:00
// Client
2019-01-04 03:33:08 +01:00
WebSocketInitResult WebSocketTransport::connectToUrl(const std::string& url,
int timeoutSecs)
2018-09-27 23:56:48 +02:00
{
std::string protocol, host, path, query;
int port;
Feature/http (#16) * add skeleton and broken http client code. GET returns "Resource temporarily unavailable" errors... * linux compile fix * can GET some pages * Update formatting in README.md * unittest for sending large messages * document bug * Feature/send large message (#14) * introduce send fragment * can pass a fin frame * can send messages which are a perfect multiple of the chunk size * set fin only for last fragment * cleanup * last fragment should be of type CONTINUATION * Add simple send and receive programs * speedups receiving + better way to wait for thing * receive speedup by using linked list of chunks instead of large array * document bug * use chunks to receive data * trailing spaces * Update README.md Add note about message fragmentation. * Feature/ws cli (#15) * New command line tool for transfering files / still very beta. * add readme * use cli11 for argument parsing * json -> msgpack * stop using base64 and use binary which can be stored in message pack * add target for building with homebrew * all CMakeLists are referenced by the top level one * add ws_chat and ws_connect sub commands to ws * cleanup * add echo and broadcast server as ws sub-commands * add gitignore * comments * ping pong added to ws * mv cobra_publisher under ws folder * Update README.md * linux build fix * linux build fix * move http_client to a ws sub-command * simple HTTP post support (urlencode parameters) * can specify extra headers * chunk encoding / simple redirect support / -I option * follow redirects is optional * make README vim markdown plugin friendly * cleanup argument parsing + add socket creation factory * add missing file * http gzip compression * cleanup * doc * Feature/send large message (#14) * introduce send fragment * can pass a fin frame * can send messages which are a perfect multiple of the chunk size * set fin only for last fragment * cleanup * last fragment should be of type CONTINUATION * Add simple send and receive programs * speedups receiving + better way to wait for thing * receive speedup by using linked list of chunks instead of large array * document bug * use chunks to receive data * trailing spaces
2019-03-01 06:54:03 +01:00
bool websocket = true;
2018-09-27 23:56:48 +02:00
Feature/http (#16) * add skeleton and broken http client code. GET returns "Resource temporarily unavailable" errors... * linux compile fix * can GET some pages * Update formatting in README.md * unittest for sending large messages * document bug * Feature/send large message (#14) * introduce send fragment * can pass a fin frame * can send messages which are a perfect multiple of the chunk size * set fin only for last fragment * cleanup * last fragment should be of type CONTINUATION * Add simple send and receive programs * speedups receiving + better way to wait for thing * receive speedup by using linked list of chunks instead of large array * document bug * use chunks to receive data * trailing spaces * Update README.md Add note about message fragmentation. * Feature/ws cli (#15) * New command line tool for transfering files / still very beta. * add readme * use cli11 for argument parsing * json -> msgpack * stop using base64 and use binary which can be stored in message pack * add target for building with homebrew * all CMakeLists are referenced by the top level one * add ws_chat and ws_connect sub commands to ws * cleanup * add echo and broadcast server as ws sub-commands * add gitignore * comments * ping pong added to ws * mv cobra_publisher under ws folder * Update README.md * linux build fix * linux build fix * move http_client to a ws sub-command * simple HTTP post support (urlencode parameters) * can specify extra headers * chunk encoding / simple redirect support / -I option * follow redirects is optional * make README vim markdown plugin friendly * cleanup argument parsing + add socket creation factory * add missing file * http gzip compression * cleanup * doc * Feature/send large message (#14) * introduce send fragment * can pass a fin frame * can send messages which are a perfect multiple of the chunk size * set fin only for last fragment * cleanup * last fragment should be of type CONTINUATION * Add simple send and receive programs * speedups receiving + better way to wait for thing * receive speedup by using linked list of chunks instead of large array * document bug * use chunks to receive data * trailing spaces
2019-03-01 06:54:03 +01:00
if (!UrlParser::parse(url, protocol, host, path, query, port, websocket))
2018-09-27 23:56:48 +02:00
{
return WebSocketInitResult(false, 0,
std::string("Could not parse URL ") + url);
2018-09-27 23:56:48 +02:00
}
Feature/http (#16) * add skeleton and broken http client code. GET returns "Resource temporarily unavailable" errors... * linux compile fix * can GET some pages * Update formatting in README.md * unittest for sending large messages * document bug * Feature/send large message (#14) * introduce send fragment * can pass a fin frame * can send messages which are a perfect multiple of the chunk size * set fin only for last fragment * cleanup * last fragment should be of type CONTINUATION * Add simple send and receive programs * speedups receiving + better way to wait for thing * receive speedup by using linked list of chunks instead of large array * document bug * use chunks to receive data * trailing spaces * Update README.md Add note about message fragmentation. * Feature/ws cli (#15) * New command line tool for transfering files / still very beta. * add readme * use cli11 for argument parsing * json -> msgpack * stop using base64 and use binary which can be stored in message pack * add target for building with homebrew * all CMakeLists are referenced by the top level one * add ws_chat and ws_connect sub commands to ws * cleanup * add echo and broadcast server as ws sub-commands * add gitignore * comments * ping pong added to ws * mv cobra_publisher under ws folder * Update README.md * linux build fix * linux build fix * move http_client to a ws sub-command * simple HTTP post support (urlencode parameters) * can specify extra headers * chunk encoding / simple redirect support / -I option * follow redirects is optional * make README vim markdown plugin friendly * cleanup argument parsing + add socket creation factory * add missing file * http gzip compression * cleanup * doc * Feature/send large message (#14) * introduce send fragment * can pass a fin frame * can send messages which are a perfect multiple of the chunk size * set fin only for last fragment * cleanup * last fragment should be of type CONTINUATION * Add simple send and receive programs * speedups receiving + better way to wait for thing * receive speedup by using linked list of chunks instead of large array * document bug * use chunks to receive data * trailing spaces
2019-03-01 06:54:03 +01:00
bool tls = protocol == "wss";
std::string errorMsg;
_socket = createSocket(tls, errorMsg);
if (!_socket)
2018-09-27 23:56:48 +02:00
{
Feature/http (#16) * add skeleton and broken http client code. GET returns "Resource temporarily unavailable" errors... * linux compile fix * can GET some pages * Update formatting in README.md * unittest for sending large messages * document bug * Feature/send large message (#14) * introduce send fragment * can pass a fin frame * can send messages which are a perfect multiple of the chunk size * set fin only for last fragment * cleanup * last fragment should be of type CONTINUATION * Add simple send and receive programs * speedups receiving + better way to wait for thing * receive speedup by using linked list of chunks instead of large array * document bug * use chunks to receive data * trailing spaces * Update README.md Add note about message fragmentation. * Feature/ws cli (#15) * New command line tool for transfering files / still very beta. * add readme * use cli11 for argument parsing * json -> msgpack * stop using base64 and use binary which can be stored in message pack * add target for building with homebrew * all CMakeLists are referenced by the top level one * add ws_chat and ws_connect sub commands to ws * cleanup * add echo and broadcast server as ws sub-commands * add gitignore * comments * ping pong added to ws * mv cobra_publisher under ws folder * Update README.md * linux build fix * linux build fix * move http_client to a ws sub-command * simple HTTP post support (urlencode parameters) * can specify extra headers * chunk encoding / simple redirect support / -I option * follow redirects is optional * make README vim markdown plugin friendly * cleanup argument parsing + add socket creation factory * add missing file * http gzip compression * cleanup * doc * Feature/send large message (#14) * introduce send fragment * can pass a fin frame * can send messages which are a perfect multiple of the chunk size * set fin only for last fragment * cleanup * last fragment should be of type CONTINUATION * Add simple send and receive programs * speedups receiving + better way to wait for thing * receive speedup by using linked list of chunks instead of large array * document bug * use chunks to receive data * trailing spaces
2019-03-01 06:54:03 +01:00
return WebSocketInitResult(false, 0, errorMsg);
2018-09-27 23:56:48 +02:00
}
WebSocketHandshake webSocketHandshake(_requestInitCancellation,
_socket,
_perMessageDeflate,
_perMessageDeflateOptions,
_enablePerMessageDeflate);
2019-01-04 03:33:08 +01:00
auto result = webSocketHandshake.clientHandshake(url, host, path, port,
timeoutSecs);
if (result.success)
{
setReadyState(OPEN);
}
return result;
}
2018-12-30 08:15:27 +01:00
// Server
2019-01-04 03:33:08 +01:00
WebSocketInitResult WebSocketTransport::connectToSocket(int fd, int timeoutSecs)
2018-12-30 06:53:33 +01:00
{
2019-03-22 23:33:04 +01:00
// Server should not mask the data it sends to the client
_useMask = false;
2019-03-15 02:37:38 +01:00
std::string errorMsg;
_socket = createSocket(fd, errorMsg);
if (!_socket)
{
return WebSocketInitResult(false, 0, errorMsg);
}
2018-12-30 06:53:33 +01:00
WebSocketHandshake webSocketHandshake(_requestInitCancellation,
_socket,
_perMessageDeflate,
_perMessageDeflateOptions,
_enablePerMessageDeflate);
2018-12-30 08:15:27 +01:00
2019-01-04 03:33:08 +01:00
auto result = webSocketHandshake.serverHandshake(fd, timeoutSecs);
if (result.success)
2018-12-30 08:15:27 +01:00
{
setReadyState(OPEN);
2018-12-30 08:15:27 +01:00
}
return result;
2018-12-30 06:53:33 +01:00
}
WebSocketTransport::ReadyStateValues WebSocketTransport::getReadyState() const
2018-09-27 23:56:48 +02:00
{
return _readyState;
}
void WebSocketTransport::setReadyState(ReadyStateValues readyStateValue)
{
// No state change, return
if (_readyState == readyStateValue) return;
if (readyStateValue == CLOSED)
{
std::lock_guard<std::mutex> lock(_closeDataMutex);
_onCloseCallback(_closeCode, _closeReason, _closeWireSize);
_closeCode = 0;
_closeReason = std::string();
2018-12-15 01:28:11 +01:00
_closeWireSize = 0;
}
2018-09-27 23:56:48 +02:00
_readyState = readyStateValue;
}
void WebSocketTransport::setOnCloseCallback(const OnCloseCallback& onCloseCallback)
2018-09-27 23:56:48 +02:00
{
_onCloseCallback = onCloseCallback;
2018-09-27 23:56:48 +02:00
}
// Only consider send time points for that computation.
// The receive time points is taken into account in Socket::poll (second parameter).
bool WebSocketTransport::heartBeatPeriodExceeded()
2019-01-26 01:11:39 +01:00
{
std::lock_guard<std::mutex> lock(_lastSendTimePointMutex);
auto now = std::chrono::steady_clock::now();
return now - _lastSendTimePoint > std::chrono::seconds(_heartBeatPeriod);
}
2018-09-27 23:56:48 +02:00
void WebSocketTransport::poll()
{
_socket->poll(
2019-01-24 21:42:49 +01:00
[this](PollResultType pollResult)
2018-09-27 23:56:48 +02:00
{
2019-01-26 01:11:39 +01:00
// If (1) heartbeat is enabled, and (2) no data was received or
// send for a duration exceeding our heart-beat period, send a
// ping to the server.
2019-03-19 17:29:57 +01:00
if (pollResult == PollResultType::Timeout &&
heartBeatPeriodExceeded())
2019-01-24 21:42:49 +01:00
{
2019-01-25 06:16:32 +01:00
std::stringstream ss;
ss << kHeartBeatPingMessage << "::" << _heartBeatPeriod << "s";
sendPing(ss.str());
2019-01-24 21:42:49 +01:00
}
// Make sure we send all the buffered data
// there can be a lot of it for large messages.
2019-03-19 17:29:57 +01:00
else if (pollResult == PollResultType::SendRequest)
2018-09-27 23:56:48 +02:00
{
while (!isSendBufferEmpty() && !_requestInitCancellation)
2018-09-27 23:56:48 +02:00
{
// Wait with a 10ms timeout until the socket is ready to write.
// This way we are not busy looping
2019-03-19 01:54:51 +01:00
PollResultType result = _socket->isReadyToWrite(10);
2019-03-19 06:00:08 +01:00
2019-03-19 17:29:57 +01:00
if (result == PollResultType::Error)
{
_socket->close();
setReadyState(CLOSED);
break;
}
2019-03-19 17:29:57 +01:00
else if (result == PollResultType::ReadyForWrite)
{
sendOnSocket();
}
2018-09-27 23:56:48 +02:00
}
}
2019-03-19 17:29:57 +01:00
else if (pollResult == PollResultType::ReadyForRead)
{
while (true)
2018-09-27 23:56:48 +02:00
{
ssize_t ret = _socket->recv((char*)&_readbuf[0], _readbuf.size());
if (ret < 0 && (_socket->getErrno() == EWOULDBLOCK ||
_socket->getErrno() == EAGAIN))
{
break;
}
else if (ret <= 0)
{
_rxbuf.clear();
_socket->close();
setReadyState(CLOSED);
break;
}
else
{
_rxbuf.insert(_rxbuf.end(),
_readbuf.begin(),
_readbuf.begin() + ret);
}
2018-09-27 23:56:48 +02:00
}
}
2019-03-19 17:29:57 +01:00
else if (pollResult == PollResultType::Error)
2018-09-27 23:56:48 +02:00
{
_socket->close();
}
2019-03-19 17:29:57 +01:00
else if (pollResult == PollResultType::CloseRequest)
{
2019-03-16 19:38:23 +01:00
_socket->close();
}
2019-03-16 19:38:23 +01:00
// Avoid a race condition where we get stuck in select
// while closing.
if (_readyState == CLOSING)
{
_socket->close();
}
2019-01-24 21:42:49 +01:00
},
_heartBeatPeriod);
2018-09-27 23:56:48 +02:00
}
bool WebSocketTransport::isSendBufferEmpty() const
{
std::lock_guard<std::mutex> lock(_txbufMutex);
return _txbuf.empty();
}
void WebSocketTransport::appendToSendBuffer(const std::vector<uint8_t>& header,
std::string::const_iterator begin,
std::string::const_iterator end,
uint64_t message_size,
uint8_t masking_key[4])
{
std::lock_guard<std::mutex> lock(_txbufMutex);
_txbuf.insert(_txbuf.end(), header.begin(), header.end());
_txbuf.insert(_txbuf.end(), begin, end);
if (_useMask)
2018-09-27 23:56:48 +02:00
{
for (size_t i = 0; i != (size_t) message_size; ++i)
{
*(_txbuf.end() - (size_t) message_size + i) ^= masking_key[i&0x3];
}
2018-09-27 23:56:48 +02:00
}
}
void WebSocketTransport::unmaskReceiveBuffer(const wsheader_type& ws)
{
if (ws.mask)
{
for (size_t j = 0; j != ws.N; ++j)
{
_rxbuf[j+ws.header_size] ^= ws.masking_key[j&0x3];
}
}
}
2018-09-27 23:56:48 +02:00
//
// http://tools.ietf.org/html/rfc6455#section-5.2 Base Framing Protocol
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-------+-+-------------+-------------------------------+
// |F|R|R|R| opcode|M| Payload len | Extended payload length |
// |I|S|S|S| (4) |A| (7) | (16/64) |
// |N|V|V|V| |S| | (if payload len==126/127) |
// | |1|2|3| |K| | |
// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
// | Extended payload length continued, if payload len == 127 |
// + - - - - - - - - - - - - - - - +-------------------------------+
// | |Masking-key, if MASK set to 1 |
// +-------------------------------+-------------------------------+
// | Masking-key (continued) | Payload Data |
// +-------------------------------- - - - - - - - - - - - - - - - +
// : Payload Data continued ... :
// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
// | Payload Data continued ... |
// +---------------------------------------------------------------+
//
void WebSocketTransport::dispatch(const OnMessageCallback& onMessageCallback)
{
while (true)
{
2018-09-27 23:56:48 +02:00
wsheader_type ws;
if (_rxbuf.size() < 2) return; /* Need at least 2 */
const uint8_t * data = (uint8_t *) &_rxbuf[0]; // peek, but don't consume
ws.fin = (data[0] & 0x80) == 0x80;
2018-11-07 20:45:17 +01:00
ws.rsv1 = (data[0] & 0x40) == 0x40;
2018-09-27 23:56:48 +02:00
ws.opcode = (wsheader_type::opcode_type) (data[0] & 0x0f);
ws.mask = (data[1] & 0x80) == 0x80;
ws.N0 = (data[1] & 0x7f);
ws.header_size = 2 + (ws.N0 == 126? 2 : 0) + (ws.N0 == 127? 8 : 0) + (ws.mask? 4 : 0);
if (_rxbuf.size() < ws.header_size) return; /* Need: ws.header_size - _rxbuf.size() */
2018-09-27 23:56:48 +02:00
//
// Calculate payload length:
// 0-125 mean the payload is that long.
// 126 means that the following two bytes indicate the length,
// 127 means the next 8 bytes indicate the length.
//
int i = 0;
if (ws.N0 < 126)
{
ws.N = ws.N0;
i = 2;
}
else if (ws.N0 == 126)
{
ws.N = 0;
ws.N |= ((uint64_t) data[2]) << 8;
ws.N |= ((uint64_t) data[3]) << 0;
i = 4;
}
else if (ws.N0 == 127)
{
ws.N = 0;
ws.N |= ((uint64_t) data[2]) << 56;
ws.N |= ((uint64_t) data[3]) << 48;
ws.N |= ((uint64_t) data[4]) << 40;
ws.N |= ((uint64_t) data[5]) << 32;
ws.N |= ((uint64_t) data[6]) << 24;
ws.N |= ((uint64_t) data[7]) << 16;
ws.N |= ((uint64_t) data[8]) << 8;
ws.N |= ((uint64_t) data[9]) << 0;
i = 10;
}
else
{
// invalid payload length according to the spec. bail out
return;
}
2018-09-27 23:56:48 +02:00
if (ws.mask)
{
ws.masking_key[0] = ((uint8_t) data[i+0]) << 0;
ws.masking_key[1] = ((uint8_t) data[i+1]) << 0;
ws.masking_key[2] = ((uint8_t) data[i+2]) << 0;
ws.masking_key[3] = ((uint8_t) data[i+3]) << 0;
}
else
{
ws.masking_key[0] = 0;
ws.masking_key[1] = 0;
ws.masking_key[2] = 0;
ws.masking_key[3] = 0;
}
if (_rxbuf.size() < ws.header_size+ws.N)
{
return; /* Need: ws.header_size+ws.N - _rxbuf.size() */
}
// We got a whole message, now do something with it:
if (
ws.opcode == wsheader_type::TEXT_FRAME
2018-09-27 23:56:48 +02:00
|| ws.opcode == wsheader_type::BINARY_FRAME
|| ws.opcode == wsheader_type::CONTINUATION
) {
unmaskReceiveBuffer(ws);
//
// Usual case. Small unfragmented messages
//
if (ws.fin && _chunks.empty())
{
emitMessage(MSG,
std::string(_rxbuf.begin()+ws.header_size,
_rxbuf.begin()+ws.header_size+(size_t) ws.N),
ws,
onMessageCallback);
}
else
{
//
// Add intermediary message to our chunk list.
// We use a chunk list instead of a big buffer because resizing
// large buffer can be very costly when we need to re-allocate
// the internal buffer which is slow and can let the internal OS
// receive buffer fill out.
//
_chunks.emplace_back(
std::vector<uint8_t>(_rxbuf.begin()+ws.header_size,
_rxbuf.begin()+ws.header_size+(size_t)ws.N));
if (ws.fin)
{
emitMessage(MSG, getMergedChunks(), ws, onMessageCallback);
_chunks.clear();
}
else
{
emitMessage(FRAGMENT, std::string(), ws, onMessageCallback);
}
2018-09-27 23:56:48 +02:00
}
}
else if (ws.opcode == wsheader_type::PING)
{
unmaskReceiveBuffer(ws);
2018-09-27 23:56:48 +02:00
std::string pingData(_rxbuf.begin()+ws.header_size,
_rxbuf.begin()+ws.header_size + (size_t) ws.N);
2018-10-25 21:01:47 +02:00
// Reply back right away
2018-11-14 02:46:05 +01:00
bool compress = false;
sendData(wsheader_type::PONG, pingData, compress);
2018-10-25 21:01:47 +02:00
emitMessage(PING, pingData, ws, onMessageCallback);
2018-10-25 21:01:47 +02:00
}
else if (ws.opcode == wsheader_type::PONG)
{
unmaskReceiveBuffer(ws);
2018-10-25 21:01:47 +02:00
std::string pongData(_rxbuf.begin()+ws.header_size,
_rxbuf.begin()+ws.header_size + (size_t) ws.N);
emitMessage(PONG, pongData, ws, onMessageCallback);
2018-09-27 23:56:48 +02:00
}
else if (ws.opcode == wsheader_type::CLOSE)
{
unmaskReceiveBuffer(ws);
// Extract the close code first, available as the first 2 bytes
uint16_t code = 0;
code |= ((uint64_t) _rxbuf[ws.header_size]) << 8;
code |= ((uint64_t) _rxbuf[ws.header_size+1]) << 0;
// Get the reason.
std::string reason(_rxbuf.begin()+ws.header_size + 2,
_rxbuf.begin()+ws.header_size + 2 + (size_t) ws.N);
{
std::lock_guard<std::mutex> lock(_closeDataMutex);
_closeCode = code;
_closeReason = reason;
_closeWireSize = _rxbuf.size();
}
close();
}
else
{
close();
}
2018-09-27 23:56:48 +02:00
// Erase the message that has been processed from the input/read buffer
2018-09-27 23:56:48 +02:00
_rxbuf.erase(_rxbuf.begin(),
_rxbuf.begin() + ws.header_size + (size_t) ws.N);
}
}
std::string WebSocketTransport::getMergedChunks() const
{
size_t length = 0;
for (auto&& chunk : _chunks)
{
length += chunk.size();
}
std::string msg;
msg.reserve(length);
for (auto&& chunk : _chunks)
{
std::string str(chunk.begin(), chunk.end());
msg += str;
}
return msg;
}
void WebSocketTransport::emitMessage(MessageKind messageKind,
const std::string& message,
const wsheader_type& ws,
const OnMessageCallback& onMessageCallback)
{
2018-11-14 02:46:05 +01:00
size_t wireSize = message.size();
2018-11-14 02:46:05 +01:00
// When the RSV1 bit is 1 it means the message is compressed
if (_enablePerMessageDeflate && ws.rsv1 && messageKind != FRAGMENT)
{
2018-11-14 02:46:05 +01:00
std::string decompressedMessage;
2018-11-15 00:52:28 +01:00
bool success = _perMessageDeflate.decompress(message, decompressedMessage);
2019-01-05 02:28:13 +01:00
onMessageCallback(decompressedMessage, wireSize, !success, messageKind);
}
else
{
2018-11-15 00:52:28 +01:00
onMessageCallback(message, wireSize, false, messageKind);
}
}
2018-09-27 23:56:48 +02:00
unsigned WebSocketTransport::getRandomUnsigned()
{
auto now = std::chrono::system_clock::now();
auto seconds =
2018-09-27 23:56:48 +02:00
std::chrono::duration_cast<std::chrono::seconds>(
now.time_since_epoch()).count();
return static_cast<unsigned>(seconds);
}
WebSocketSendInfo WebSocketTransport::sendData(
wsheader_type::opcode_type type,
const std::string& message,
bool compress,
const OnProgressCallback& onProgressCallback)
2018-09-27 23:56:48 +02:00
{
if (_readyState == CLOSING || _readyState == CLOSED)
{
return WebSocketSendInfo();
}
size_t payloadSize = message.size();
size_t wireSize = message.size();
std::string compressedMessage;
2018-11-15 00:52:28 +01:00
bool compressionError = false;
std::string::const_iterator message_begin = message.begin();
std::string::const_iterator message_end = message.end();
if (compress)
{
if (!_perMessageDeflate.compress(message, compressedMessage))
{
bool success = false;
compressionError = true;
payloadSize = 0;
wireSize = 0;
return WebSocketSendInfo(success, compressionError, payloadSize, wireSize);
}
compressionError = false;
wireSize = compressedMessage.size();
message_begin = compressedMessage.begin();
message_end = compressedMessage.end();
2018-09-27 23:56:48 +02:00
}
// Common case for most message. No fragmentation required.
if (wireSize < kChunkSize)
{
sendFragment(type, true, message_begin, message_end, compress);
}
else
{
//
// Large messages need to be fragmented
//
// Rules:
// First message needs to specify a proper type (BINARY or TEXT)
// Intermediary and last messages need to be of type CONTINUATION
// Last message must set the fin byte.
//
auto steps = wireSize / kChunkSize;
std::string::const_iterator begin = message_begin;
std::string::const_iterator end = message_end;
for (uint64_t i = 0 ; i < steps; ++i)
{
bool firstStep = i == 0;
bool lastStep = (i+1) == steps;
bool fin = lastStep;
end = begin + kChunkSize;
if (lastStep)
{
end = message_end;
}
auto opcodeType = type;
if (!firstStep)
{
opcodeType = wsheader_type::CONTINUATION;
}
// Send message
sendFragment(opcodeType, fin, begin, end, compress);
if (onProgressCallback && !onProgressCallback((int)i, (int) steps))
{
break;
}
begin += kChunkSize;
}
}
// Request to flush the send buffer on the background thread if it isn't empty
if (!isSendBufferEmpty())
{
_socket->wakeUpFromPoll(Socket::kSendRequest);
}
return WebSocketSendInfo(true, compressionError, payloadSize, wireSize);
}
void WebSocketTransport::sendFragment(wsheader_type::opcode_type type,
bool fin,
std::string::const_iterator message_begin,
std::string::const_iterator message_end,
bool compress)
{
auto message_size = message_end - message_begin;
2018-09-27 23:56:48 +02:00
unsigned x = getRandomUnsigned();
uint8_t masking_key[4] = {};
masking_key[0] = (x >> 24);
masking_key[1] = (x >> 16) & 0xff;
masking_key[2] = (x >> 8) & 0xff;
masking_key[3] = (x) & 0xff;
std::vector<uint8_t> header;
header.assign(2 +
(message_size >= 126 ? 2 : 0) +
2019-03-22 23:33:04 +01:00
(message_size >= 65536 ? 6 : 0) +
(_useMask ? 4 : 0), 0);
header[0] = type;
// The fin bit indicate that this is the last fragment. Fin is French for end.
if (fin)
{
header[0] |= 0x80;
}
// This bit indicate that the frame is compressed
if (compress)
{
header[0] |= 0x40;
}
2018-09-27 23:56:48 +02:00
if (message_size < 126)
{
header[1] = (message_size & 0xff) | (_useMask ? 0x80 : 0);
2018-09-27 23:56:48 +02:00
if (_useMask)
{
header[2] = masking_key[0];
header[3] = masking_key[1];
header[4] = masking_key[2];
header[5] = masking_key[3];
}
2018-09-27 23:56:48 +02:00
}
else if (message_size < 65536)
2018-09-27 23:56:48 +02:00
{
header[1] = 126 | (_useMask ? 0x80 : 0);
2018-09-27 23:56:48 +02:00
header[2] = (message_size >> 8) & 0xff;
header[3] = (message_size >> 0) & 0xff;
if (_useMask)
{
header[4] = masking_key[0];
header[5] = masking_key[1];
header[6] = masking_key[2];
header[7] = masking_key[3];
}
2018-09-27 23:56:48 +02:00
}
else
{ // TODO: run coverage testing here
header[1] = 127 | (_useMask ? 0x80 : 0);
2018-09-27 23:56:48 +02:00
header[2] = (message_size >> 56) & 0xff;
header[3] = (message_size >> 48) & 0xff;
header[4] = (message_size >> 40) & 0xff;
header[5] = (message_size >> 32) & 0xff;
header[6] = (message_size >> 24) & 0xff;
header[7] = (message_size >> 16) & 0xff;
header[8] = (message_size >> 8) & 0xff;
header[9] = (message_size >> 0) & 0xff;
if (_useMask)
{
header[10] = masking_key[0];
header[11] = masking_key[1];
header[12] = masking_key[2];
header[13] = masking_key[3];
}
2018-09-27 23:56:48 +02:00
}
// _txbuf will keep growing until it can be transmitted over the socket:
appendToSendBuffer(header, message_begin, message_end,
message_size, masking_key);
// Now actually send this data
sendOnSocket();
}
WebSocketSendInfo WebSocketTransport::sendPing(const std::string& message)
2018-09-27 23:56:48 +02:00
{
2018-11-14 02:46:05 +01:00
bool compress = false;
return sendData(wsheader_type::PING, message, compress);
2018-09-27 23:56:48 +02:00
}
WebSocketSendInfo WebSocketTransport::sendBinary(
const std::string& message,
const OnProgressCallback& onProgressCallback)
2018-09-27 23:56:48 +02:00
{
return sendData(wsheader_type::BINARY_FRAME, message,
_enablePerMessageDeflate, onProgressCallback);
2018-09-27 23:56:48 +02:00
}
WebSocketSendInfo WebSocketTransport::sendText(
const std::string& message,
const OnProgressCallback& onProgressCallback)
{
return sendData(wsheader_type::TEXT_FRAME, message,
_enablePerMessageDeflate, onProgressCallback);
}
2018-09-27 23:56:48 +02:00
void WebSocketTransport::sendOnSocket()
{
std::lock_guard<std::mutex> lock(_txbufMutex);
while (_txbuf.size())
{
ssize_t ret = _socket->send((char*)&_txbuf[0], _txbuf.size());
2018-09-27 23:56:48 +02:00
if (ret < 0 && (_socket->getErrno() == EWOULDBLOCK ||
2018-10-09 06:42:45 +02:00
_socket->getErrno() == EAGAIN))
2018-09-27 23:56:48 +02:00
{
break;
}
2018-11-15 00:52:28 +01:00
else if (ret <= 0)
2018-09-27 23:56:48 +02:00
{
_socket->close();
setReadyState(CLOSED);
break;
}
else
{
_txbuf.erase(_txbuf.begin(), _txbuf.begin() + ret);
}
}
2019-01-26 01:11:39 +01:00
std::lock_guard<std::mutex> lck(_lastSendTimePointMutex);
_lastSendTimePoint = std::chrono::steady_clock::now();
2018-09-27 23:56:48 +02:00
}
void WebSocketTransport::close()
{
2018-12-15 01:28:11 +01:00
_requestInitCancellation = true;
2018-09-27 23:56:48 +02:00
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'
//
2018-11-15 00:52:28 +01:00
const std::string normalClosure = std::string("\x03\xe8");
bool compress = false;
sendData(wsheader_type::CLOSE, normalClosure, compress);
2018-09-27 23:56:48 +02:00
setReadyState(CLOSING);
_socket->wakeUpFromPoll(Socket::kCloseRequest);
_socket->close();
_closeCode = 1000;
2019-03-14 22:52:51 +01:00
_closeReason = "Normal Closure";
setReadyState(CLOSED);
}
size_t WebSocketTransport::bufferedAmount() const
{
std::lock_guard<std::mutex> lock(_txbufMutex);
return _txbuf.size();
2018-09-27 23:56:48 +02:00
}
} // namespace ix