Compare commits

..

43 Commits

Author SHA1 Message Date
b1c1e6e28d websocket server: closed connection threads are joined properly 2019-04-17 16:23:24 -07:00
66440e2330 less buggy server connection per thread system 2019-04-16 22:14:30 -07:00
792610d44f use threads instead of std::async, need to cleanup threads 2019-04-16 18:21:52 -07:00
bcf2fc1812 make closeWireSize a default parameter of WebSocketTransport::close 2019-04-16 09:55:12 -07:00
935e6791a3 close method change and fix code (#28)
* close method change and fix code

* missing mutex
2019-04-16 08:58:34 -07:00
fbb7c012a3 fix windows build (#29)
* fix windows build

* fix for Unix

* Fix build if TLS is OFF

* add OpenSSL req to ws

* Fix build on Mac

* fix tests for windows
2019-04-16 08:51:57 -07:00
dac18fcabf move security framework linking to ixwebsocket (#26) 2019-04-14 17:13:24 -07:00
d8e83caffc fix warning 2019-04-13 21:16:04 -07:00
fbf80b9f50 ws: new command to subscribe to a cobra server and send an event to a sentry server 2019-04-11 16:03:05 -07:00
c2a9139d41 (ws) add subcommands: cobra subscribe, and cobra subscribe to statsd bridge 2019-04-08 21:56:01 -07:00
6e3dff149a linux ci tentative fix 2019-04-03 22:02:10 -07:00
1bacbe38f4 better unittest runner / can run through lldb and produce a junit XML artifact 2019-03-29 15:54:05 -07:00
2e9c610ac9 Bump sleep time in test shell script 2019-03-29 09:36:56 -07:00
eb063ec60a (redis_subscribe) in verbose mode, received message gets printed with a 'received: ' header 2019-03-29 09:35:19 -07:00
37fb14646d Add clarification notice about third party modules 2019-03-29 09:34:17 -07:00
ae543518d3 offline version of remark-latest 2019-03-28 16:06:43 -07:00
c865d64608 redis conf slides 2019-03-28 14:17:19 -07:00
3004422cb6 slides 2019-03-27 16:27:52 -07:00
0c46a17443 add redis-conf slides 2019-03-27 15:53:55 -07:00
497373d976 ws redis command improvements + test script 2019-03-27 13:41:46 -07:00
91198aca0d (ws) redis_subscribe and redis_publish can take a password + display subscribe response 2019-03-26 09:33:22 -07:00
b17a5e5f0b update doc 2019-03-24 21:48:14 -07:00
3f0ef59f65 remove Formula folder
Homebrew stuff is at https://github.com/bsergean/homebrew-IXWebSocket
2019-03-24 21:43:38 -07:00
1e96edc293 (server) fix masking bug 2019-03-22 15:33:04 -07:00
0afb77393b can send TEXT message (we only support BINARY messages now) 2019-03-22 14:24:22 -07:00
7614b642bb unmasked code is broken 2019-03-22 14:24:15 -07:00
bc89580dfe remove printf + unittest fix 2019-03-22 09:56:28 -07:00
358ae13a88 (server) server should not mask data when sending to client (some python client libraries enforce that and assert) 2019-03-22 09:53:56 -07:00
ccf9dcba70 (server) HTTP response is malformed 2019-03-22 09:52:19 -07:00
94604fad61 minor cleanup 2019-03-21 13:51:25 -07:00
5c4cc7c50d HTTP/1.1 response should contains a reason (websocket server)
Fix compatibility problem with websockets python library, where the response does not contains a reason

File "/.../lib/python3.7/site-packages/websockets/http.py", line 126, in read_response
version, status_code, reason = status_line[:-2].split(b' ', 2)
ValueError: not enough values to unpack (expected 3, got 2)

The above exception was the direct cause of the following exception:

websockets.exceptions.InvalidMessage: Malformed HTTP message
2019-03-21 13:43:47 -07:00
9ed961ec06 cleanup, remove dead method 2019-03-21 10:06:59 -07:00
e6bd8cc8c4 (cmake) add a warning about 32/64 conversion problems. 2019-03-20 21:51:38 -07:00
ee25bd0f92 Feature/connection state (#25)
* (cmake) add a warning about 32/64 conversion problems.

* fix typo

* New connection state for server code + fix OpenSSL double init bug

* update README
2019-03-20 18:34:24 -07:00
e77b9176f3 Feature/redis (#23)
* Fix warning

* (cmake) add a warning about 32/64 conversion problems.

* simple redis clients

* can publish to redis

* redis subscribe

* display messages received per second

* verbose flag

* (cmake) use clang only compile option -Wshorten-64-to-32 when compiling with clang
2019-03-20 14:29:02 -07:00
afe8b966ad Fixed heartbeat typos (#22) 2019-03-19 21:31:43 -07:00
310724c961 make PollResultType an enum class 2019-03-19 09:29:57 -07:00
ceba8ae620 fix bug with isReadyToWrite 2019-03-18 22:05:04 -07:00
fead661ab7 workaround bug in Socket::isReadyToWrite 2019-03-18 20:37:33 -07:00
9c8c17f577 use milliseconds 2019-03-18 20:17:44 -07:00
a04f83930f ws / log subcommand name 2019-03-18 17:54:06 -07:00
c421d19800 disable sigpipe on osx when writing/reading into a dead pipe 2019-03-18 17:52:01 -07:00
521f02c90e edit homebrew install steps 2019-03-18 15:45:33 -07:00
86 changed files with 3066 additions and 8576 deletions

View File

@ -15,6 +15,10 @@ if (NOT WIN32)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")
endif()
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshorten-64-to-32")
endif()
set( IXWEBSOCKET_SOURCES
ixwebsocket/IXSocket.cpp
ixwebsocket/IXSocketServer.cpp
@ -33,8 +37,8 @@ set( IXWEBSOCKET_SOURCES
ixwebsocket/IXHttpClient.cpp
ixwebsocket/IXUrlParser.cpp
ixwebsocket/IXSelectInterrupt.cpp
ixwebsocket/IXSelectInterruptPipe.cpp
ixwebsocket/IXSelectInterruptFactory.cpp
ixwebsocket/IXConnectionState.cpp
)
set( IXWEBSOCKET_HEADERS
@ -60,10 +64,16 @@ set( IXWEBSOCKET_HEADERS
ixwebsocket/IXHttpClient.h
ixwebsocket/IXUrlParser.h
ixwebsocket/IXSelectInterrupt.h
ixwebsocket/IXSelectInterruptPipe.h
ixwebsocket/IXSelectInterruptFactory.h
ixwebsocket/IXConnectionState.h
)
if (UNIX)
# Linux, Mac, iOS, Android
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/IXSelectInterruptPipe.cpp )
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/IXSelectInterruptPipe.h )
endif()
# Platform specific code
if (APPLE)
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/apple/IXSetThreadName_apple.cpp)
@ -75,6 +85,7 @@ else()
list( APPEND IXWEBSOCKET_HEADERS ixwebsocket/IXSelectInterruptEventFd.h)
endif()
set(USE_OPEN_SSL FALSE)
if (USE_TLS)
add_definitions(-DIXWEBSOCKET_USE_TLS)
@ -85,6 +96,7 @@ if (USE_TLS)
list( APPEND IXWEBSOCKET_HEADERS ixwebsocket/IXSocketSChannel.h)
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/IXSocketSChannel.cpp)
else()
set(USE_OPEN_SSL TRUE)
list( APPEND IXWEBSOCKET_HEADERS ixwebsocket/IXSocketOpenSSL.h)
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/IXSocketOpenSSL.cpp)
endif()
@ -95,14 +107,16 @@ add_library( ixwebsocket STATIC
${IXWEBSOCKET_HEADERS}
)
# gcc/Linux needs -pthread
find_package(Threads)
if (APPLE AND USE_TLS)
target_link_libraries(ixwebsocket "-framework foundation" "-framework security")
endif()
if(UNIX AND NOT APPLE)
if(USE_OPEN_SSL)
find_package(OpenSSL REQUIRED)
add_definitions(${OPENSSL_DEFINITIONS})
message(STATUS "OpenSSL: " ${OPENSSL_VERSION})
include_directories(${OPENSSL_INCLUDE_DIR})
target_link_libraries(ixwebsocket ${OPENSSL_LIBRARIES})
endif()
if (WIN32)
@ -117,15 +131,21 @@ if (WIN32)
target_link_libraries(ixwebsocket libz wsock32 ws2_32)
add_definitions(-D_CRT_SECURE_NO_WARNINGS)
else()
# gcc/Linux needs -pthread
find_package(Threads)
target_link_libraries(ixwebsocket
z ${OPENSSL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
z ${CMAKE_THREAD_LIBS_INIT})
endif()
set( IXWEBSOCKET_INCLUDE_DIRS
.
../../shared/OpenSSL/include)
)
target_include_directories( ixwebsocket PUBLIC ${IXWEBSOCKET_INCLUDE_DIRS} )
add_subdirectory(ws)
if (NOT WIN32)
add_subdirectory(ws)
endif()

1
DOCKER_VERSION Normal file
View File

@ -0,0 +1 @@
1.4.0

View File

@ -1,31 +1,47 @@
FROM debian:stretch
# Build time
FROM debian:buster as build
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get -y install wget
RUN mkdir -p /tmp/cmake
WORKDIR /tmp/cmake
RUN wget https://github.com/Kitware/CMake/releases/download/v3.14.0/cmake-3.14.0-Linux-x86_64.tar.gz
RUN tar zxf cmake-3.14.0-Linux-x86_64.tar.gz
RUN apt-get -y install g++
RUN apt-get -y install libssl-dev
RUN apt-get -y install gdb
RUN apt-get -y install screen
RUN apt-get -y install procps
RUN apt-get -y install lsof
RUN apt-get -y install libz-dev
RUN apt-get -y install vim
RUN apt-get -y install make
RUN apt-get -y install cmake
RUN apt-get -y install curl
RUN apt-get -y install python
RUN apt-get -y install netcat
# debian strech cmake is too old for building with Docker
COPY makefile .
RUN ["make", "install_cmake_for_linux"]
COPY . .
ARG CMAKE_BIN_PATH=/tmp/cmake/cmake-3.14.0-rc4-Linux-x86_64/bin
ARG CMAKE_BIN_PATH=/tmp/cmake/cmake-3.14.0-Linux-x86_64/bin
ENV PATH="${CMAKE_BIN_PATH}:${PATH}"
# RUN ["make"]
RUN ["make"]
EXPOSE 8765
CMD ["/ws/ws", "transfer", "--port", "8765", "--host", "0.0.0.0"]
# Runtime
FROM debian:buster as runtime
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
# Runtime
RUN apt-get install -y libssl1.1
# Debugging
RUN apt-get install -y strace
RUN apt-get install -y gdb
RUN apt-get install -y procps
RUN apt-get install -y htop
RUN adduser --disabled-password --gecos '' app
COPY --chown=app:app --from=build /usr/local/bin/ws /usr/local/bin/ws
RUN chmod +x /usr/local/bin/ws
RUN ldd /usr/local/bin/ws
# Now run in usermode
USER app
WORKDIR /home/app
CMD ["ws"]

View File

@ -4,13 +4,14 @@
## Introduction
[*WebSocket*](https://en.wikipedia.org/wiki/WebSocket) is a computer communications protocol, providing full-duplex
communication channels over a single TCP connection. *IXWebSocket* is a C++ library for client and server Websocket communication, and for client HTTP communication. The code is derived from [easywsclient](https://github.com/dhbaird/easywsclient) and from the [Satori C SDK](https://github.com/satori-com/satori-rtm-sdk-c). It has been tested on the following platforms.
[*WebSocket*](https://en.wikipedia.org/wiki/WebSocket) is a computer communications protocol, providing full-duplex and bi-directionnal communication channels over a single TCP connection. *IXWebSocket* is a C++ library for client and server Websocket communication, and for client HTTP communication. The code is derived from [easywsclient](https://github.com/dhbaird/easywsclient) and from the [Satori C SDK](https://github.com/satori-com/satori-rtm-sdk-c). It has been tested on the following platforms.
* macOS
* iOS
* Linux
* Android
* Android
The code was made to compile once on Windows but support is currently broken on this platform.
## Examples
@ -46,9 +47,12 @@ webSocket.setOnMessageCallback(
// Now that our callback is setup, we can start our background thread and receive messages
webSocket.start();
// Send a message to the server
// Send a message to the server (default to BINARY mode)
webSocket.send("hello world");
// The message can be sent in TEXT mode
webSocket.sendText("hello again");
// ... finally ...
// Stop the connection
@ -63,10 +67,11 @@ Here is what the server API looks like. Note that server support is very recent
ix::WebSocketServer server(port);
server.setOnConnectionCallback(
[&server](std::shared_ptr<ix::WebSocket> webSocket)
[&server](std::shared_ptr<WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState)
{
webSocket->setOnMessageCallback(
[webSocket, &server](ix::WebSocketMessageType messageType,
[webSocket, connectionState, &server](ix::WebSocketMessageType messageType,
const std::string& str,
size_t wireSize,
const ix::WebSocketErrorInfo& error,
@ -77,6 +82,12 @@ server.setOnConnectionCallback(
{
std::cerr << "New connection" << std::endl;
// A connection state object is available, and has a default id
// You can subclass ConnectionState and pass an alternate factory
// to override it. It is useful if you want to store custom
// attributes per connection (authenticated bool flag, attributes, etc...)
std::cerr << "id: " << connectionState->getId() << std::endl;
// The uri the client did connect to.
std::cerr << "Uri: " << openInfo.uri << std::endl;
@ -183,7 +194,7 @@ There is a Dockerfile for running some code on Linux, and a unittest which can b
You can build and install the ws command line tool with Homebrew.
```
brew create --cmake https://github.com/machinezone/IXWebSocket/archive/v1.1.0.tar.gz
brew tap bsergean/IXWebSocket
brew install IXWebSocket
```
@ -207,11 +218,11 @@ If the remote end (server) breaks the connection, the code will try to perpetual
### Large messages
Large frames are broken up into smaller chunks or messages to avoid filling up the os tcp buffers, which is permitted thanks to WebSocket [fragmentation](https://tools.ietf.org/html/rfc6455#section-5.4). Messages up to 500M were sent and received succesfully.
Large frames are broken up into smaller chunks or messages to avoid filling up the os tcp buffers, which is permitted thanks to WebSocket [fragmentation](https://tools.ietf.org/html/rfc6455#section-5.4). Messages up to 1G were sent and received succesfully.
## Limitations
* There is no text support for sending data, only the binary protocol is supported. Sending json or text over the binary protocol works well.
* No utf-8 validation is made when sending TEXT message with sendText()
* Automatic reconnection works at the TCP socket level, and will detect remote end disconnects. However, if the device/computer network become unreachable (by turning off wifi), it is quite hard to reliably and timely detect it at the socket level using `recv` and `send` error codes. [Here](https://stackoverflow.com/questions/14782143/linux-socket-how-to-detect-disconnected-network-in-a-client-program) is a good discussion on the subject. This behavior is consistent with other runtimes such as node.js. One way to detect a disconnected device with low level C code is to do a name resolution with DNS but this can be expensive. Mobile devices have good and reliable API to do that.
* The server code is using select to detect incoming data, and creates one OS thread per connection. This is not as scalable as strategies using epoll or kqueue.
@ -223,13 +234,13 @@ Here is a simplistic diagram which explains how the code is structured in term o
+-----------------------+ --- Public
| | Start the receiving Background thread. Auto reconnection. Simple websocket Ping.
| IXWebSocket | Interface used by C++ test clients. No IX dependencies.
| |
| |
+-----------------------+
| |
| IXWebSocketServer | Run a server and give each connections its own WebSocket object.
| | Each connection is handled in a new OS thread.
| |
+-----------------------+ --- Private
+-----------------------+ --- Private
| |
| IXWebSocketTransport | Low level websocket code, framing, managing raw socket. Adapted from easywsclient.
| |

21
docker-compose.yml Normal file
View File

@ -0,0 +1,21 @@
version: "3"
services:
ws:
stdin_open: true
tty: true
image: bsergean/ws:build
ports:
- "8765:8765"
entrypoint: bash
networks:
- ws-net
depends_on:
- redis1
redis1:
image: redis:alpine
networks:
- ws-net
networks:
ws-net:

View File

@ -0,0 +1,43 @@
/*
* IXConnectionState.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
*/
#include "IXConnectionState.h"
namespace ix
{
std::atomic<uint64_t> ConnectionState::_globalId(0);
ConnectionState::ConnectionState() : _terminated(false)
{
computeId();
}
void ConnectionState::computeId()
{
_id = std::to_string(_globalId++);
}
const std::string& ConnectionState::getId() const
{
return _id;
}
std::shared_ptr<ConnectionState> ConnectionState::createConnectionState()
{
return std::make_shared<ConnectionState>();
}
bool ConnectionState::isTerminated() const
{
return _terminated;
}
bool ConnectionState::setTerminated()
{
_terminated = true;
}
}

View File

@ -0,0 +1,37 @@
/*
* IXConnectionState.h
* Author: Benjamin Sergeant
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
*/
#pragma once
#include <stdint.h>
#include <string>
#include <atomic>
#include <memory>
namespace ix
{
class ConnectionState {
public:
ConnectionState();
virtual ~ConnectionState() = default;
virtual void computeId();
virtual const std::string& getId() const;
bool setTerminated();
bool isTerminated() const;
static std::shared_ptr<ConnectionState> createConnectionState();
protected:
std::atomic<bool> _terminated;
std::string _id;
static std::atomic<uint64_t> _globalId;
};
}

View File

@ -73,7 +73,7 @@ namespace ix
errMsg = "no error";
// Maybe a cancellation request got in before the background thread terminated ?
if (isCancellationRequested())
if (isCancellationRequested && isCancellationRequested())
{
errMsg = "cancellation requested";
return nullptr;
@ -121,7 +121,7 @@ namespace ix
}
// Were we cancelled ?
if (isCancellationRequested())
if (isCancellationRequested && isCancellationRequested())
{
errMsg = "cancellation requested";
return nullptr;
@ -129,7 +129,7 @@ namespace ix
}
// Maybe a cancellation request got in before the bg terminated ?
if (isCancellationRequested())
if (isCancellationRequested && isCancellationRequested())
{
errMsg = "cancellation requested";
return nullptr;

View File

@ -56,7 +56,7 @@ namespace ix
if (fcntl(_fildes[kPipeReadIndex], F_SETFL, O_NONBLOCK) == -1)
{
std::stringstream ss;
ss << "SelectInterruptPipe::init() failed in fcntl() call"
ss << "SelectInterruptPipe::init() failed in fcntl(..., O_NONBLOCK) call"
<< " : " << strerror(errno);
errorMsg = ss.str();
@ -68,7 +68,7 @@ namespace ix
if (fcntl(_fildes[kPipeWriteIndex], F_SETFL, O_NONBLOCK) == -1)
{
std::stringstream ss;
ss << "SelectInterruptPipe::init() failed in fcntl() call"
ss << "SelectInterruptPipe::init() failed in fcntl(..., O_NONBLOCK) call"
<< " : " << strerror(errno);
errorMsg = ss.str();
@ -77,13 +77,31 @@ namespace ix
return false;
}
//
// FIXME: on macOS we should configure the pipe to not trigger SIGPIPE
// on reads/writes to a closed fd
//
// The generation of the SIGPIPE signal can be suppressed using the
// F_SETNOSIGPIPE fcntl command.
//
#ifdef F_SETNOSIGPIPE
if (fcntl(_fildes[kPipeWriteIndex], F_SETNOSIGPIPE, 1) == -1)
{
std::stringstream ss;
ss << "SelectInterruptPipe::init() failed in fcntl(.... F_SETNOSIGPIPE) call"
<< " : " << strerror(errno);
errorMsg = ss.str();
_fildes[kPipeReadIndex] = -1;
_fildes[kPipeWriteIndex] = -1;
return false;
}
if (fcntl(_fildes[kPipeWriteIndex], F_SETNOSIGPIPE, 1) == -1)
{
std::stringstream ss;
ss << "SelectInterruptPipe::init() failed in fcntl(..., F_SETNOSIGPIPE) call"
<< " : " << strerror(errno);
errorMsg = ss.str();
_fildes[kPipeReadIndex] = -1;
_fildes[kPipeWriteIndex] = -1;
return false;
}
#endif
return true;
}
@ -104,6 +122,7 @@ namespace ix
uint64_t value = 0;
::read(fd, &value, sizeof(value));
return value;
}

View File

@ -21,6 +21,10 @@
#include <algorithm>
#include <iostream>
#ifdef min
#undef min
#endif
namespace ix
{
const int Socket::kDefaultPollNoTimeout = -1; // No poll timeout by default
@ -45,16 +49,16 @@ namespace ix
{
if (_sockfd == -1)
{
if (onPollCallback) onPollCallback(PollResultType_Error);
if (onPollCallback) onPollCallback(PollResultType::Error);
return;
}
PollResultType pollResult = isReadyToRead(timeoutSecs, 0);
PollResultType pollResult = isReadyToRead(1000 * timeoutSecs);
if (onPollCallback) onPollCallback(pollResult);
}
PollResultType Socket::select(bool readyToRead, int timeoutSecs, int timeoutMs)
PollResultType Socket::select(bool readyToRead, int timeoutMs)
{
fd_set rfds;
fd_set wfds;
@ -62,7 +66,6 @@ namespace ix
FD_ZERO(&wfds);
fd_set* fds = (readyToRead) ? &rfds : & wfds;
FD_SET(_sockfd, fds);
// File descriptor used to interrupt select when needed
@ -73,64 +76,60 @@ namespace ix
}
struct timeval timeout;
timeout.tv_sec = timeoutSecs;
timeout.tv_usec = 1000 * timeoutMs;
timeout.tv_sec = timeoutMs / 1000;
timeout.tv_usec = (timeoutMs < 1000) ? 0 : 1000 * (timeoutMs % 1000);
// Compute the highest fd.
int sockfd = _sockfd;
int nfds = (std::max)(sockfd, interruptFd);
int ret = ::select(nfds + 1, &rfds, &wfds, nullptr,
(timeoutSecs < 0) ? nullptr : &timeout);
(timeoutMs < 0) ? nullptr : &timeout);
PollResultType pollResult = PollResultType_ReadyForRead;
PollResultType pollResult = PollResultType::ReadyForRead;
if (ret < 0)
{
pollResult = PollResultType_Error;
pollResult = PollResultType::Error;
}
else if (ret == 0)
{
pollResult = PollResultType_Timeout;
pollResult = PollResultType::Timeout;
}
else if (interruptFd != -1 && FD_ISSET(interruptFd, fds))
else if (interruptFd != -1 && FD_ISSET(interruptFd, &rfds))
{
uint64_t value = _selectInterrupt->read();
if (value == kSendRequest)
{
pollResult = PollResultType_SendRequest;
pollResult = PollResultType::SendRequest;
}
else if (value == kCloseRequest)
{
pollResult = PollResultType_CloseRequest;
pollResult = PollResultType::CloseRequest;
}
}
else if (sockfd != -1 && FD_ISSET(sockfd, fds))
else if (sockfd != -1 && readyToRead && FD_ISSET(sockfd, &rfds))
{
if (readyToRead)
{
pollResult = PollResultType_ReadyForRead;
}
else
{
pollResult = PollResultType_ReadyForWrite;
}
pollResult = PollResultType::ReadyForRead;
}
else if (sockfd != -1 && !readyToRead && FD_ISSET(sockfd, &wfds))
{
pollResult = PollResultType::ReadyForWrite;
}
return pollResult;
}
PollResultType Socket::isReadyToRead(int timeoutSecs, int timeoutMs)
PollResultType Socket::isReadyToRead(int timeoutMs)
{
bool readyToRead = true;
return select(readyToRead, timeoutSecs, timeoutMs);
return select(readyToRead, timeoutMs);
}
PollResultType Socket::isReadyToWrite(int timeoutSecs, int timeoutMs)
PollResultType Socket::isReadyToWrite(int timeoutMs)
{
bool readyToRead = false;
return select(readyToRead, timeoutSecs, timeoutMs);
return select(readyToRead, timeoutMs);
}
// Wake up from poll/select by writing to the pipe which is watched by select
@ -215,7 +214,7 @@ namespace ix
{
while (true)
{
if (isCancellationRequested()) return false;
if (isCancellationRequested && isCancellationRequested()) return false;
char* buffer = const_cast<char*>(str.c_str());
int len = (int) str.size();
@ -227,7 +226,7 @@ namespace ix
{
return ret == len;
}
// There is possibly something to be write, try again
// There is possibly something to be writen, try again
else if (ret < 0 && (getErrno() == EWOULDBLOCK ||
getErrno() == EAGAIN))
{
@ -246,7 +245,7 @@ namespace ix
{
while (true)
{
if (isCancellationRequested()) return false;
if (isCancellationRequested && isCancellationRequested()) return false;
ssize_t ret;
ret = recv(buffer, 1);
@ -262,7 +261,7 @@ namespace ix
{
// Wait with a 1ms timeout until the socket is ready to read.
// This way we are not busy looping
if (isReadyToRead(0, 1) == PollResultType_Error)
if (isReadyToRead(1) == PollResultType::Error)
{
return false;
}
@ -309,9 +308,12 @@ namespace ix
std::vector<uint8_t> output;
while (output.size() != length)
{
if (isCancellationRequested()) return std::make_pair(false, std::string());
if (isCancellationRequested && isCancellationRequested())
{
return std::make_pair(false, std::string());
}
int size = std::min(kChunkSize, length - output.size());
size_t size = std::min(kChunkSize, length - output.size());
ssize_t ret = recv((char*)&_readBuffer[0], size);
if (ret <= 0 && (getErrno() != EWOULDBLOCK &&
@ -331,7 +333,7 @@ namespace ix
// Wait with a 1ms timeout until the socket is ready to read.
// This way we are not busy looping
if (isReadyToRead(0, 1) == PollResultType_Error)
if (isReadyToRead(1) == PollResultType::Error)
{
return std::make_pair(false, std::string());
}

View File

@ -25,14 +25,14 @@ namespace ix
{
class SelectInterrupt;
enum PollResultType
enum class PollResultType
{
PollResultType_ReadyForRead = 0,
PollResultType_ReadyForWrite = 1,
PollResultType_Timeout = 2,
PollResultType_Error = 3,
PollResultType_SendRequest = 4,
PollResultType_CloseRequest = 5
ReadyForRead = 0,
ReadyForWrite = 1,
Timeout = 2,
Error = 3,
SendRequest = 4,
CloseRequest = 5
};
class Socket {
@ -50,9 +50,8 @@ namespace ix
int timeoutSecs = kDefaultPollTimeout);
bool wakeUpFromPoll(uint8_t wakeUpCode);
PollResultType select(bool readyToRead, int timeoutSecs, int timeoutMs);
PollResultType isReadyToWrite(int timeoutSecs, int timeoutMs);
PollResultType isReadyToRead(int timeoutSecs, int timeoutMs);
PollResultType isReadyToWrite(int timeoutMs);
PollResultType isReadyToRead(int timeoutMs);
// Virtual methods
virtual bool connect(const std::string& url,
@ -92,6 +91,8 @@ namespace ix
std::mutex _socketMutex;
private:
PollResultType select(bool readyToRead, int timeoutMs);
static const int kDefaultPollTimeout;
static const int kDefaultPollNoTimeout;

View File

@ -66,7 +66,7 @@ namespace ix
for (;;)
{
if (isCancellationRequested()) // Must handle timeout as well
if (isCancellationRequested && isCancellationRequested()) // Must handle timeout as well
{
closeSocket(fd);
errMsg = "Cancelled";

View File

@ -6,12 +6,20 @@
#include "IXSocketFactory.h"
#if defined(__APPLE__) or defined(__linux__)
#ifdef IXWEBSOCKET_USE_TLS
# ifdef __APPLE__
# include <ixwebsocket/IXSocketAppleSSL.h>
# elif defined(_WIN32)
# include <ixwebsocket/IXSocketSChannel.h>
# else
# include <ixwebsocket/IXSocketOpenSSL.h>
# endif
#else
#include <ixwebsocket/IXSocket.h>
#endif
namespace ix
@ -31,6 +39,8 @@ namespace ix
#ifdef IXWEBSOCKET_USE_TLS
# ifdef __APPLE__
socket = std::make_shared<SocketAppleSSL>();
# elif defined(_WIN32)
socket = std::make_shared<SocketSChannel>();
# else
socket = std::make_shared<SocketOpenSSL>();
# endif

View File

@ -8,6 +8,7 @@
#pragma once
#include <memory>
#include <string>
namespace ix
{

View File

@ -21,6 +21,7 @@
namespace ix
{
std::atomic<bool> SocketOpenSSL::_openSSLInitializationSuccessful(false);
std::once_flag SocketOpenSSL::_openSSLInitFlag;
SocketOpenSSL::SocketOpenSSL(int fd) : Socket(fd),
_ssl_connection(nullptr),

View File

@ -50,7 +50,7 @@ namespace ix
const SSL_METHOD* _ssl_method;
mutable std::mutex _mutex; // OpenSSL routines are not thread-safe
std::once_flag _openSSLInitFlag;
static std::once_flag _openSSLInitFlag;
static std::atomic<bool> _openSSLInitializationSuccessful;
};

View File

@ -18,7 +18,7 @@
# include <ws2def.h>
# include <WS2tcpip.h>
# include <schannel.h>
# include <sslsock.h>
//# include <sslsock.h>
# include <io.h>
#define WIN32_LEAN_AND_MEAN
@ -75,7 +75,7 @@ namespace ix
int port,
std::string& errMsg)
{
return Socket::connect(host, port, errMsg);
return Socket::connect(host, port, errMsg, nullptr);
}
@ -89,17 +89,17 @@ namespace ix
Socket::close();
}
int SocketSChannel::send(char* buf, size_t nbyte)
ssize_t SocketSChannel::send(char* buf, size_t nbyte)
{
return Socket::send(buf, nbyte);
}
int SocketSChannel::send(const std::string& buffer)
ssize_t SocketSChannel::send(const std::string& buffer)
{
return Socket::send(buffer);
}
int SocketSChannel::recv(void* buf, size_t nbyte)
ssize_t SocketSChannel::recv(void* buf, size_t nbyte)
{
return Socket::recv(buf, nbyte);
}

View File

@ -24,9 +24,9 @@ namespace ix
// The important override
virtual void secureSocket() final;
virtual int send(char* buffer, size_t length) final;
virtual int send(const std::string& buffer) final;
virtual int recv(void* buffer, size_t length) final;
virtual ssize_t send(char* buffer, size_t length) final;
virtual ssize_t send(const std::string& buffer) final;
virtual ssize_t recv(void* buffer, size_t length) final;
private:
};

View File

@ -29,7 +29,8 @@ namespace ix
_host(host),
_backlog(backlog),
_maxConnections(maxConnections),
_stop(false)
_stop(false),
_connectionStateFactory(&ConnectionState::createConnectionState)
{
}
@ -135,6 +136,9 @@ namespace ix
void SocketServer::stop()
{
closeTerminatedThreads();
assert(_connectionsThreads.empty());
if (!_thread.joinable()) return; // nothing to do
_stop = true;
@ -145,18 +149,50 @@ namespace ix
::close(_serverFd);
}
void SocketServer::setConnectionStateFactory(
const ConnectionStateFactory& connectionStateFactory)
{
_connectionStateFactory = connectionStateFactory;
}
// join the threads for connections that have been closed
void SocketServer::closeTerminatedThreads()
{
auto it = _connectionsThreads.begin();
auto itEnd = _connectionsThreads.end();
while (it != itEnd)
{
auto& connectionState = it->first;
auto& thread = it->second;
if (!connectionState->isTerminated() ||
!thread.joinable())
{
++it;
continue;
}
thread.join();
it = _connectionsThreads.erase(it);
}
}
void SocketServer::run()
{
// Set the socket to non blocking mode, so that accept calls are not blocking
SocketConnect::configure(_serverFd);
// Return value of std::async, ignored
std::future<void> f;
for (;;)
{
if (_stop) return;
// Garbage collection to shutdown/join threads for closed connections.
// We could run this in its own thread, so that we dont need to accept
// a new connection to close a thread.
// We could also use a condition variable to be notify when we need to do this
closeTerminatedThreads();
// Use select to check whether a new connection is in progress
fd_set rfds;
struct timeval timeout;
@ -214,14 +250,19 @@ namespace ix
continue;
}
std::shared_ptr<ConnectionState> connectionState;
if (_connectionStateFactory)
{
connectionState = _connectionStateFactory();
}
// Launch the handleConnection work asynchronously in its own thread.
//
// the destructor of a future returned by std::async blocks,
// so we need to declare it outside of this loop
f = std::async(std::launch::async,
&SocketServer::handleConnection,
this,
clientFd);
_connectionsThreads.push_back(std::make_pair(
connectionState,
std::thread(&SocketServer::handleConnection,
this,
clientFd,
connectionState)));
}
}
}

View File

@ -6,10 +6,13 @@
#pragma once
#include "IXConnectionState.h"
#include <utility> // pair
#include <string>
#include <set>
#include <thread>
#include <list>
#include <mutex>
#include <functional>
#include <memory>
@ -20,6 +23,12 @@ namespace ix
{
class SocketServer {
public:
using ConnectionStateFactory = std::function<std::shared_ptr<ConnectionState>()>;
// We use a list as we only care about remove and append operations.
using ConnectionThreads = std::list<std::pair<std::shared_ptr<ConnectionState>,
std::thread>>;
SocketServer(int port = SocketServer::kDefaultPort,
const std::string& host = SocketServer::kDefaultHost,
int backlog = SocketServer::kDefaultTcpBacklog,
@ -27,6 +36,8 @@ namespace ix
virtual ~SocketServer();
virtual void stop();
void setConnectionStateFactory(const ConnectionStateFactory& connectionStateFactory);
const static int kDefaultPort;
const static std::string kDefaultHost;
const static int kDefaultTcpBacklog;
@ -57,12 +68,20 @@ namespace ix
std::atomic<bool> _stop;
std::thread _thread;
ConnectionThreads _connectionsThreads;
std::condition_variable _conditionVariable;
std::mutex _conditionVariableMutex;
//
ConnectionStateFactory _connectionStateFactory;
// Methods
void run();
virtual void handleConnection(int fd) = 0;
virtual void handleConnection(int fd,
std::shared_ptr<ConnectionState> connectionState) = 0;
virtual size_t getConnectedClientsCount() = 0;
void closeTerminatedThreads();
};
}

View File

@ -79,10 +79,10 @@ namespace ix
return _perMessageDeflateOptions;
}
void WebSocket::setHeartBeatPeriod(int hearBeatPeriod)
void WebSocket::setHeartBeatPeriod(int heartBeatPeriod)
{
std::lock_guard<std::mutex> lock(_configMutex);
_heartBeatPeriod = hearBeatPeriod;
_heartBeatPeriod = heartBeatPeriod;
}
int WebSocket::getHeartBeatPeriod() const
@ -302,7 +302,13 @@ namespace ix
WebSocketSendInfo WebSocket::send(const std::string& text,
const OnProgressCallback& onProgressCallback)
{
return sendMessage(text, false, onProgressCallback);
return sendMessage(text, SendMessageKind::Binary, onProgressCallback);
}
WebSocketSendInfo WebSocket::sendText(const std::string& text,
const OnProgressCallback& onProgressCallback)
{
return sendMessage(text, SendMessageKind::Text, onProgressCallback);
}
WebSocketSendInfo WebSocket::ping(const std::string& text)
@ -311,11 +317,11 @@ namespace ix
constexpr size_t pingMaxPayloadSize = 125;
if (text.size() > pingMaxPayloadSize) return WebSocketSendInfo(false);
return sendMessage(text, true);
return sendMessage(text, SendMessageKind::Ping);
}
WebSocketSendInfo WebSocket::sendMessage(const std::string& text,
bool ping,
SendMessageKind sendMessageKind,
const OnProgressCallback& onProgressCallback)
{
if (!isConnected()) return WebSocketSendInfo(false);
@ -332,13 +338,22 @@ namespace ix
std::lock_guard<std::mutex> lock(_writeMutex);
WebSocketSendInfo webSocketSendInfo;
if (ping)
switch (sendMessageKind)
{
webSocketSendInfo = _ws.sendPing(text);
}
else
{
webSocketSendInfo = _ws.sendBinary(text, onProgressCallback);
case SendMessageKind::Text:
{
webSocketSendInfo = _ws.sendText(text, onProgressCallback);
} break;
case SendMessageKind::Binary:
{
webSocketSendInfo = _ws.sendBinary(text, onProgressCallback);
} break;
case SendMessageKind::Ping:
{
webSocketSendInfo = _ws.sendPing(text);
} break;
}
WebSocket::invokeTrafficTrackerCallback(webSocketSendInfo.wireSize, false);

View File

@ -89,7 +89,7 @@ namespace ix
void setUrl(const std::string& url);
void setPerMessageDeflateOptions(const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions);
void setHandshakeTimeout(int handshakeTimeoutSecs);
void setHeartBeatPeriod(int hearBeatPeriod);
void setHeartBeatPeriod(int heartBeatPeriod);
// Run asynchronously, by calling start and stop.
void start();
@ -101,6 +101,8 @@ namespace ix
WebSocketSendInfo send(const std::string& text,
const OnProgressCallback& onProgressCallback = nullptr);
WebSocketSendInfo sendText(const std::string& text,
const OnProgressCallback& onProgressCallback = nullptr);
WebSocketSendInfo ping(const std::string& text);
void close();
@ -120,7 +122,7 @@ namespace ix
private:
WebSocketSendInfo sendMessage(const std::string& text,
bool ping,
SendMessageKind sendMessageKind,
const OnProgressCallback& callback = nullptr);
bool isConnected() const;

View File

@ -114,7 +114,7 @@ namespace ix
std::stringstream ss;
ss << "HTTP/1.1 ";
ss << code;
ss << "\r\n";
ss << " ";
ss << reason;
ss << "\r\n";
@ -353,7 +353,7 @@ namespace ix
WebSocketHandshakeKeyGen::generate(headers["sec-websocket-key"].c_str(), output);
std::stringstream ss;
ss << "HTTP/1.1 101\r\n";
ss << "HTTP/1.1 101 Switching Protocols\r\n";
ss << "Sec-WebSocket-Accept: " << std::string(output) << "\r\n";
ss << "Upgrade: websocket\r\n";
ss << "Connection: Upgrade\r\n";

View File

@ -6,12 +6,28 @@
#include "IXWebSocketHttpHeaders.h"
#include "IXSocket.h"
#include <string>
#include <unordered_map>
#include <algorithm>
#include <locale>
namespace ix
{
bool CaseInsensitiveLess::NocaseCompare::operator()(const unsigned char & c1, const unsigned char & c2) const
{
#ifdef _WIN32
return std::tolower(c1, std::locale()) < std::tolower(c2, std::locale());
#else
return std::tolower(c1) < std::tolower(c2);
#endif
}
bool CaseInsensitiveLess::operator()(const std::string & s1, const std::string & s2) const
{
return std::lexicographical_compare
(s1.begin(), s1.end(), // source range
s2.begin(), s2.end(), // dest range
NocaseCompare()); // comparison
}
std::pair<bool, WebSocketHttpHeaders> parseHttpHeaders(
std::shared_ptr<Socket> socket,
const CancellationRequest& isCancellationRequested)

View File

@ -11,7 +11,6 @@
#include <string>
#include <map>
#include <memory>
#include <algorithm>
namespace ix
{
@ -22,19 +21,10 @@ namespace ix
// Case Insensitive compare_less binary function
struct NocaseCompare
{
bool operator() (const unsigned char& c1, const unsigned char& c2) const
{
return std::tolower(c1) < std::tolower(c2);
}
bool operator() (const unsigned char& c1, const unsigned char& c2) const;
};
bool operator() (const std::string & s1, const std::string & s2) const
{
return std::lexicographical_compare
(s1.begin(), s1.end(), // source range
s2.begin(), s2.end(), // dest range
NocaseCompare()); // comparison
}
bool operator() (const std::string & s1, const std::string & s2) const;
};
using WebSocketHttpHeaders = std::map<std::string, std::string, CaseInsensitiveLess>;

View File

@ -49,10 +49,12 @@ namespace ix
_onConnectionCallback = callback;
}
void WebSocketServer::handleConnection(int fd)
void WebSocketServer::handleConnection(
int fd,
std::shared_ptr<ConnectionState> connectionState)
{
auto webSocket = std::make_shared<WebSocket>();
_onConnectionCallback(webSocket);
_onConnectionCallback(webSocket, connectionState);
webSocket->disableAutomaticReconnection();
@ -89,6 +91,7 @@ namespace ix
}
logInfo("WebSocketServer::handleConnection() done");
connectionState->setTerminated();
}
std::set<std::shared_ptr<WebSocket>> WebSocketServer::getClients()

View File

@ -20,7 +20,8 @@
namespace ix
{
using OnConnectionCallback = std::function<void(std::shared_ptr<WebSocket>)>;
using OnConnectionCallback = std::function<void(std::shared_ptr<WebSocket>,
std::shared_ptr<ConnectionState>)>;
class WebSocketServer : public SocketServer {
public:
@ -49,7 +50,8 @@ namespace ix
const static int kDefaultHandShakeTimeoutSecs;
// Methods
virtual void handleConnection(int fd) final;
virtual void handleConnection(int fd,
std::shared_ptr<ConnectionState> connectionState) final;
virtual size_t getConnectedClientsCount() final;
};
}

View File

@ -53,11 +53,12 @@
namespace ix
{
const std::string WebSocketTransport::kHeartBeatPingMessage("ixwebsocket::hearbeat");
const std::string WebSocketTransport::kHeartBeatPingMessage("ixwebsocket::heartbeat");
const int WebSocketTransport::kDefaultHeartBeatPeriod(-1);
constexpr size_t WebSocketTransport::kChunkSize;
WebSocketTransport::WebSocketTransport() :
_useMask(true),
_readyState(CLOSED),
_closeCode(0),
_closeWireSize(0),
@ -75,11 +76,11 @@ namespace ix
}
void WebSocketTransport::configure(const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions,
int hearBeatPeriod)
int heartBeatPeriod)
{
_perMessageDeflateOptions = perMessageDeflateOptions;
_enablePerMessageDeflate = _perMessageDeflateOptions.enabled();
_heartBeatPeriod = hearBeatPeriod;
_heartBeatPeriod = heartBeatPeriod;
}
// Client
@ -123,6 +124,9 @@ namespace ix
// Server
WebSocketInitResult WebSocketTransport::connectToSocket(int fd, int timeoutSecs)
{
// Server should not mask the data it sends to the client
_useMask = false;
std::string errorMsg;
_socket = createSocket(fd, errorMsg);
@ -189,7 +193,7 @@ namespace ix
// 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.
if (pollResult == PollResultType_Timeout &&
if (pollResult == PollResultType::Timeout &&
heartBeatPeriodExceeded())
{
std::stringstream ss;
@ -198,26 +202,27 @@ namespace ix
}
// Make sure we send all the buffered data
// there can be a lot of it for large messages.
else if (pollResult == PollResultType_SendRequest)
else if (pollResult == PollResultType::SendRequest)
{
while (!isSendBufferEmpty() && !_requestInitCancellation)
{
// Wait with a 10ms timeout until the socket is ready to write.
// This way we are not busy looping
PollResultType result = _socket->isReadyToWrite(0, 10);
if (result == PollResultType_Error)
PollResultType result = _socket->isReadyToWrite(10);
if (result == PollResultType::Error)
{
_socket->close();
setReadyState(CLOSED);
break;
}
else if (result == PollResultType_ReadyForWrite)
else if (result == PollResultType::ReadyForWrite)
{
sendOnSocket();
}
}
}
else if (pollResult == PollResultType_ReadyForRead)
else if (pollResult == PollResultType::ReadyForRead)
{
while (true)
{
@ -243,11 +248,11 @@ namespace ix
}
}
}
else if (pollResult == PollResultType_Error)
else if (pollResult == PollResultType::Error)
{
_socket->close();
}
else if (pollResult == PollResultType_CloseRequest)
else if (pollResult == PollResultType::CloseRequest)
{
_socket->close();
}
@ -279,19 +284,15 @@ namespace ix
_txbuf.insert(_txbuf.end(), header.begin(), header.end());
_txbuf.insert(_txbuf.end(), begin, end);
// Masking
for (size_t i = 0; i != (size_t) message_size; ++i)
if (_useMask)
{
*(_txbuf.end() - (size_t) message_size + i) ^= masking_key[i&0x3];
for (size_t i = 0; i != (size_t) message_size; ++i)
{
*(_txbuf.end() - (size_t) message_size + i) ^= masking_key[i&0x3];
}
}
}
void WebSocketTransport::appendToSendBuffer(const std::vector<uint8_t>& buffer)
{
std::lock_guard<std::mutex> lock(_txbufMutex);
_txbuf.insert(_txbuf.end(), buffer.begin(), buffer.end());
}
void WebSocketTransport::unmaskReceiveBuffer(const wsheader_type& ws)
{
if (ws.mask)
@ -473,14 +474,8 @@ namespace ix
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();
close(code, reason, _rxbuf.size());
}
else
{
@ -643,7 +638,7 @@ namespace ix
std::string::const_iterator message_end,
bool compress)
{
auto message_size = message_end - message_begin;
uint64_t message_size = static_cast<uint64_t>(message_end - message_begin);
unsigned x = getRandomUnsigned();
uint8_t masking_key[4] = {};
@ -655,7 +650,8 @@ namespace ix
std::vector<uint8_t> header;
header.assign(2 +
(message_size >= 126 ? 2 : 0) +
(message_size >= 65536 ? 6 : 0) + 4, 0);
(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.
@ -672,27 +668,33 @@ namespace ix
if (message_size < 126)
{
header[1] = (message_size & 0xff) | 0x80;
header[1] = (message_size & 0xff) | (_useMask ? 0x80 : 0);
header[2] = masking_key[0];
header[3] = masking_key[1];
header[4] = masking_key[2];
header[5] = masking_key[3];
if (_useMask)
{
header[2] = masking_key[0];
header[3] = masking_key[1];
header[4] = masking_key[2];
header[5] = masking_key[3];
}
}
else if (message_size < 65536)
{
header[1] = 126 | 0x80;
header[1] = 126 | (_useMask ? 0x80 : 0);
header[2] = (message_size >> 8) & 0xff;
header[3] = (message_size >> 0) & 0xff;
header[4] = masking_key[0];
header[5] = masking_key[1];
header[6] = masking_key[2];
header[7] = masking_key[3];
if (_useMask)
{
header[4] = masking_key[0];
header[5] = masking_key[1];
header[6] = masking_key[2];
header[7] = masking_key[3];
}
}
else
{ // TODO: run coverage testing here
header[1] = 127 | 0x80;
header[1] = 127 | (_useMask ? 0x80 : 0);
header[2] = (message_size >> 56) & 0xff;
header[3] = (message_size >> 48) & 0xff;
header[4] = (message_size >> 40) & 0xff;
@ -702,10 +704,13 @@ namespace ix
header[8] = (message_size >> 8) & 0xff;
header[9] = (message_size >> 0) & 0xff;
header[10] = masking_key[0];
header[11] = masking_key[1];
header[12] = masking_key[2];
header[13] = masking_key[3];
if (_useMask)
{
header[10] = masking_key[0];
header[11] = masking_key[1];
header[12] = masking_key[2];
header[13] = masking_key[3];
}
}
// _txbuf will keep growing until it can be transmitted over the socket:
@ -731,6 +736,15 @@ namespace ix
_enablePerMessageDeflate, onProgressCallback);
}
WebSocketSendInfo WebSocketTransport::sendText(
const std::string& message,
const OnProgressCallback& onProgressCallback)
{
return sendData(wsheader_type::TEXT_FRAME, message,
_enablePerMessageDeflate, onProgressCallback);
}
void WebSocketTransport::sendOnSocket()
{
std::lock_guard<std::mutex> lock(_txbufMutex);
@ -761,7 +775,7 @@ namespace ix
_lastSendTimePoint = std::chrono::steady_clock::now();
}
void WebSocketTransport::close()
void WebSocketTransport::close(uint16_t code, const std::string& reason, size_t closeWireSize)
{
_requestInitCancellation = true;
@ -769,21 +783,22 @@ namespace ix
// 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\xe8");
const std::string closure{(char)(code >> 8), (char)(code & 0xff)};
bool compress = false;
sendData(wsheader_type::CLOSE, normalClosure, compress);
sendData(wsheader_type::CLOSE, closure, compress);
setReadyState(CLOSING);
_socket->wakeUpFromPoll(Socket::kCloseRequest);
_socket->close();
_closeCode = 1000;
_closeReason = "Normal Closure";
{
std::lock_guard<std::mutex> lock(_closeDataMutex);
_closeCode = code;
_closeReason = reason;
_closeWireSize = closeWireSize;
}
setReadyState(CLOSED);
}

View File

@ -30,6 +30,13 @@ namespace ix
{
class Socket;
enum class SendMessageKind
{
Text,
Binary,
Ping
};
class WebSocketTransport
{
public:
@ -61,7 +68,7 @@ namespace ix
~WebSocketTransport();
void configure(const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions,
int hearBeatPeriod);
int heartBeatPeriod);
WebSocketInitResult connectToUrl(const std::string& url, // Client
int timeoutSecs);
@ -71,8 +78,14 @@ namespace ix
void poll();
WebSocketSendInfo sendBinary(const std::string& message,
const OnProgressCallback& onProgressCallback);
WebSocketSendInfo sendText(const std::string& message,
const OnProgressCallback& onProgressCallback);
WebSocketSendInfo sendPing(const std::string& message);
void close();
void close(uint16_t code = 1000,
const std::string& reason = "Normal closure",
size_t closeWireSize = 0);
ReadyStateValues getReadyState() const;
void setReadyState(ReadyStateValues readyStateValue);
void setOnCloseCallback(const OnCloseCallback& onCloseCallback);
@ -100,6 +113,10 @@ namespace ix
uint8_t masking_key[4];
};
// Tells whether we should mask the data we send.
// client should mask but server should not
bool _useMask;
// Buffer for reading from our socket. That buffer is never resized.
std::vector<uint8_t> _readbuf;
@ -148,7 +165,7 @@ namespace ix
mutable std::mutex _lastSendTimePointMutex;
std::chrono::time_point<std::chrono::steady_clock> _lastSendTimePoint;
// No data was send through the socket for longer that the hearbeat period
// No data was send through the socket for longer than the heartbeat period
bool heartBeatPeriodExceeded();
void sendOnSocket();
@ -174,7 +191,6 @@ namespace ix
std::string::const_iterator end,
uint64_t message_size,
uint8_t masking_key[4]);
void appendToSendBuffer(const std::vector<uint8_t>& buffer);
unsigned getRandomUnsigned();
void unmaskReceiveBuffer(const wsheader_type& ws);

View File

@ -9,8 +9,20 @@ brew:
mkdir -p build && (cd build ; cmake .. ; make -j install)
.PHONY: docker
NAME := bsergean/ws
TAG := $(shell cat DOCKER_VERSION)
IMG := ${NAME}:${TAG}
LATEST := ${NAME}:latest
BUILD := ${NAME}:build
docker:
docker build -t ws:latest .
docker build -t ${IMG} .
docker tag ${IMG} ${BUILD}
docker_push:
docker tag ${IMG} ${LATEST}
docker push ${LATEST}
run:
docker run --cap-add sys_ptrace -it ws:latest
@ -36,10 +48,10 @@ test_server:
# env TEST=Websocket_chat make test
# env TEST=heartbeat make test
test:
python test/run.py
(cd test ; python2.7 run.py)
ws_test: all
(cd ws ; sh test_ws.sh)
(cd ws ; bash test_ws.sh)
# For the fork that is configured with appveyor
rebase_upstream:

View File

@ -29,6 +29,7 @@ set (SOURCES
IXDNSLookupTest.cpp
IXSocketTest.cpp
IXSocketConnectTest.cpp
)
# Some unittest don't work on windows yet

View File

@ -0,0 +1,43 @@
/*
* IXSocketConnectTest.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2018 Machine Zone. All rights reserved.
*/
#include "catch.hpp"
#include "IXTest.h"
#include <ixwebsocket/IXSocketConnect.h>
#include <iostream>
using namespace ix;
TEST_CASE("socket_connect", "[net]")
{
SECTION("Test connecting to a known hostname")
{
std::string errMsg;
int fd = SocketConnect::connect("www.google.com", 80, errMsg, [] { return false; });
std::cerr << "Error message: " << errMsg << std::endl;
REQUIRE(fd != -1);
}
SECTION("Test connecting to a non-existing hostname")
{
std::string errMsg;
std::string hostname("12313lhjlkjhopiupoijlkasdckljqwehrlkqjwehraospidcuaposidcasdc");
int fd = SocketConnect::connect(hostname, 80, errMsg, [] { return false; });
std::cerr << "Error message: " << errMsg << std::endl;
REQUIRE(fd == -1);
}
SECTION("Test connecting to a good hostname, with cancellation")
{
std::string errMsg;
// The callback returning true means we are requesting cancellation
int fd = SocketConnect::connect("www.google.com", 80, errMsg, [] { return true; });
std::cerr << "Error message: " << errMsg << std::endl;
REQUIRE(fd == -1);
}
}

View File

@ -73,7 +73,7 @@ TEST_CASE("socket", "[socket]")
testSocket(host, port, request, socket, expectedStatus, timeoutSecs);
}
#if defined(__APPLE__) or defined(__linux__)
#if defined(__APPLE__) || defined(__linux__)
SECTION("Connect to google HTTPS server. Send GET request without header. Should return 200")
{
std::string errMsg;

View File

@ -16,6 +16,7 @@
#include <iostream>
#include <stdlib.h>
#include <stack>
#include <iomanip>
namespace ix
{
@ -108,7 +109,7 @@ namespace ix
}
struct sockaddr_in sa; // server address information
unsigned int len;
socklen_t len;
if (getsockname(sockfd, (struct sockaddr *) &sa, &len) < 0)
{
log("Cannot compute a free port. getsockname error.");
@ -148,4 +149,21 @@ namespace ix
return -1;
}
void hexDump(const std::string& prefix,
const std::string& s)
{
std::ostringstream ss;
bool upper_case = false;
for (std::string::size_type i = 0; i < s.length(); ++i)
{
ss << std::hex
<< std::setfill('0')
<< std::setw(2)
<< (upper_case ? std::uppercase : std::nouppercase) << (int)s[i];
}
std::cout << prefix << ": " << s << " => " << ss.str() << std::endl;
}
}

View File

@ -65,7 +65,7 @@ namespace
_webSocket.setUrl(url);
// The important bit for this test.
// Set a 1 second hearbeat ; if no traffic is present on the connection for 1 second
// Set a 1 second heartbeat ; if no traffic is present on the connection for 1 second
// a ping message will be sent by the client.
_webSocket.setHeartBeatPeriod(1);
@ -128,10 +128,11 @@ namespace
{
// A dev/null server
server.setOnConnectionCallback(
[&server, &receivedPingMessages](std::shared_ptr<ix::WebSocket> webSocket)
[&server, &receivedPingMessages](std::shared_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState)
{
webSocket->setOnMessageCallback(
[webSocket, &server, &receivedPingMessages](ix::WebSocketMessageType messageType,
[webSocket, connectionState, &server, &receivedPingMessages](ix::WebSocketMessageType messageType,
const std::string& str,
size_t wireSize,
const ix::WebSocketErrorInfo& error,
@ -141,6 +142,7 @@ namespace
if (messageType == ix::WebSocket_MessageType_Open)
{
Logger() << "New server connection";
Logger() << "id: " << connectionState->getId();
Logger() << "Uri: " << openInfo.uri;
Logger() << "Headers:";
for (auto it : openInfo.headers)
@ -210,6 +212,10 @@ TEST_CASE("Websocket_heartbeat", "[heartbeat]")
webSocketClientA.stop();
webSocketClientB.stop();
// Here we test heart beat period exceeded for clientA
// but it should not be exceeded for clientB which has sent data.
// -> expected ping messages == 2, but add a small buffer to make this more reliable.
REQUIRE(serverReceivedPingMessages >= 2);
REQUIRE(serverReceivedPingMessages <= 4);

View File

@ -18,13 +18,32 @@ using namespace ix;
namespace ix
{
bool startServer(ix::WebSocketServer& server)
// Test that we can override the connectionState impl to provide our own
class ConnectionStateCustom : public ConnectionState
{
void computeId()
{
// a very boring invariant id that we can test against in the unittest
_id = "foobarConnectionId";
}
};
bool startServer(ix::WebSocketServer& server,
std::string& connectionId)
{
auto factory = []() -> std::shared_ptr<ConnectionState>
{
return std::make_shared<ConnectionStateCustom>();
};
server.setConnectionStateFactory(factory);
server.setOnConnectionCallback(
[&server](std::shared_ptr<ix::WebSocket> webSocket)
[&server, &connectionId](std::shared_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState)
{
webSocket->setOnMessageCallback(
[webSocket, &server](ix::WebSocketMessageType messageType,
[webSocket, connectionState,
&connectionId, &server](ix::WebSocketMessageType messageType,
const std::string& str,
size_t wireSize,
const ix::WebSocketErrorInfo& error,
@ -34,12 +53,16 @@ namespace ix
if (messageType == ix::WebSocket_MessageType_Open)
{
Logger() << "New connection";
connectionState->computeId();
Logger() << "id: " << connectionState->getId();
Logger() << "Uri: " << openInfo.uri;
Logger() << "Headers:";
for (auto it : openInfo.headers)
{
Logger() << it.first << ": " << it.second;
}
connectionId = connectionState->getId();
}
else if (messageType == ix::WebSocket_MessageType_Close)
{
@ -78,7 +101,8 @@ TEST_CASE("Websocket_server", "[websocket_server]")
{
int port = getFreePort();
ix::WebSocketServer server(port);
REQUIRE(startServer(server));
std::string connectionId;
REQUIRE(startServer(server, connectionId));
std::string errMsg;
bool tls = false;
@ -111,7 +135,8 @@ TEST_CASE("Websocket_server", "[websocket_server]")
{
int port = getFreePort();
ix::WebSocketServer server(port);
REQUIRE(startServer(server));
std::string connectionId;
REQUIRE(startServer(server, connectionId));
std::string errMsg;
bool tls = false;
@ -147,7 +172,8 @@ TEST_CASE("Websocket_server", "[websocket_server]")
{
int port = getFreePort();
ix::WebSocketServer server(port);
REQUIRE(startServer(server));
std::string connectionId;
REQUIRE(startServer(server, connectionId));
std::string errMsg;
bool tls = false;
@ -178,6 +204,8 @@ TEST_CASE("Websocket_server", "[websocket_server]")
// Give us 500ms for the server to notice that clients went away
ix::msleep(500);
REQUIRE(connectionId == "foobarConnectionId");
server.stop();
REQUIRE(server.getClients().size() == 0);
}

View File

@ -217,10 +217,11 @@ namespace
bool startServer(ix::WebSocketServer& server)
{
server.setOnConnectionCallback(
[&server](std::shared_ptr<ix::WebSocket> webSocket)
[&server](std::shared_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState)
{
webSocket->setOnMessageCallback(
[webSocket, &server](ix::WebSocketMessageType messageType,
[webSocket, connectionState, &server](ix::WebSocketMessageType messageType,
const std::string& str,
size_t wireSize,
const ix::WebSocketErrorInfo& error,
@ -230,6 +231,7 @@ namespace
if (messageType == ix::WebSocket_MessageType_Open)
{
Logger() << "New connection";
Logger() << "id: " << connectionState->getId();
Logger() << "Uri: " << openInfo.uri;
Logger() << "Headers:";
for (auto it : openInfo.headers)

507
test/run.py Normal file → Executable file
View File

@ -1,9 +1,31 @@
import os
import platform
import shutil
#!/usr/bin/env python2.7
'''
'''
import subprocess
from __future__ import print_function
import os
import sys
import platform
import argparse
import multiprocessing
import tempfile
import time
import datetime
import threading
import subprocess
import re
import xml.etree.ElementTree as ET
from xml.dom import minidom
hasClick = True
try:
import click
except ImportError:
hasClick = False
DEFAULT_EXE = 'ixwebsocket_unittest'
class Command(object):
@ -16,18 +38,22 @@ class Command(object):
self.cmd = cmd
self.process = None
def run_command(self, capture = False):
def run_command(self):
self.process = subprocess.Popen(self.cmd, shell=True)
self.process.communicate()
def run(self, timeout = 5 * 60):
def run(self, timeout=None):
'''5 minutes default timeout'''
if timeout is None:
timeout = 5 * 60
thread = threading.Thread(target=self.run_command, args=())
thread.start()
thread.join(timeout)
if thread.is_alive():
print 'Command timeout, kill it: ' + self.cmd
print('Command timeout, kill it: ' + self.cmd)
self.process.terminate()
thread.join()
return False, 255
@ -35,85 +61,424 @@ class Command(object):
return True, self.process.returncode
osName = platform.system()
print('os name = {}'.format(osName))
def runCommand(cmd, assertOnFailure=True, timeout=None):
'''Small wrapper to run a command and make sure it succeed'''
root = os.path.dirname(os.path.realpath(__file__))
buildDir = os.path.join(root, 'build', osName)
if timeout is None:
timeout = 30 * 60 # 30 minute default timeout
if not os.path.exists(buildDir):
os.makedirs(buildDir)
print('\nRunning', cmd)
command = Command(cmd)
timedout, ret = command.run(timeout)
os.chdir(buildDir)
if timedout:
print('Unittest timed out')
if osName == 'Windows':
generator = '-G"NMake Makefiles"'
make = 'nmake'
testBinary ='ixwebsocket_unittest.exe'
else:
generator = ''
make = 'make -j6'
testBinary ='./ixwebsocket_unittest'
msg = 'cmd {} failed with error code {}'.format(cmd, ret)
if ret != 0:
print(msg)
if assertOnFailure:
assert False
sanitizersFlags = {
'asan': '-DSANITIZE_ADDRESS=On',
'ubsan': '-DSANITIZE_UNDEFINED=On',
'tsan': '-DSANITIZE_THREAD=On',
'none': ''
}
sanitizer = 'tsan'
if osName == 'Linux':
sanitizer = 'none'
sanitizerFlags = sanitizersFlags[sanitizer]
def runCMake(sanitizer, buildDir):
'''Generate a makefile from CMake.
We do an out of dir build, so that cleaning up is easy
(remove build sub-folder).
'''
# if osName == 'Windows':
# os.environ['CC'] = 'clang-cl'
# os.environ['CXX'] = 'clang-cl'
# CMake installed via Self Service ends up here.
cmake_executable = '/Applications/CMake.app/Contents/bin/cmake'
cmakeCmd = 'cmake -DCMAKE_BUILD_TYPE=Debug {} {} ../..'.format(generator, sanitizerFlags)
print(cmakeCmd)
ret = os.system(cmakeCmd)
assert ret == 0, 'CMake failed, exiting'
if not os.path.exists(cmake_executable):
cmake_executable = 'cmake'
ret = os.system(make)
assert ret == 0, 'Make failed, exiting'
sanitizersFlags = {
'asan': '-DSANITIZE_ADDRESS=On',
'ubsan': '-DSANITIZE_UNDEFINED=On',
'tsan': '-DSANITIZE_THREAD=On',
'none': ''
}
sanitizerFlag = sanitizersFlags[sanitizer]
def findFiles(prefix):
'''Find all files under a given directory'''
# CMake installed via Self Service ends up here.
cmakeExecutable = '/Applications/CMake.app/Contents/bin/cmake'
if not os.path.exists(cmakeExecutable):
cmakeExecutable = 'cmake'
paths = []
fmt = '''
{cmakeExecutable} -H. \
{sanitizerFlag} \
-B{buildDir} \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
'''
cmakeCmd = fmt.format(**locals())
runCommand(cmakeCmd)
for root, _, files in os.walk(prefix):
for path in files:
fullPath = os.path.join(root, path)
if os.path.islink(fullPath):
continue
def runTest(args, buildDir, xmlOutput, testRunName):
'''Execute the unittest.
'''
if args is None:
args = ''
paths.append(fullPath)
fmt = '{buildDir}/{DEFAULT_EXE} -o {xmlOutput} -n "{testRunName}" -r junit "{args}"'
testCommand = fmt.format(**locals())
runCommand(testCommand,
assertOnFailure=False)
return paths
#for path in findFiles('.'):
# print(path)
def validateTestSuite(xmlOutput):
'''
Parse the output XML file to validate that all tests passed.
# We need to copy the zlib DLL in the current work directory
shutil.copy(os.path.join(
'..',
'..',
'..',
'third_party',
'ZLIB-Windows',
'zlib-1.2.11_deploy_v140',
'release_dynamic',
'x64',
'bin',
'zlib.dll'), '.')
Assume that the XML file contains only one testsuite.
(which is true when generate by catch2)
'''
tree = ET.parse(xmlOutput)
root = tree.getroot()
testSuite = root[0]
testSuiteAttributes = testSuite.attrib
# lldb = "lldb --batch -o 'run' -k 'thread backtrace all' -k 'quit 1'"
lldb = "" # Disabled for now
testCommand = '{} {} {}'.format(lldb, testBinary, os.getenv('TEST', ''))
command = Command(testCommand)
timedout, ret = command.run()
assert ret == 0, 'Test command failed'
tests = testSuiteAttributes['tests']
success = True
for testcase in testSuite:
if testcase.tag != 'testcase':
continue
testName = testcase.attrib['name']
systemOutput = None
for child in testcase:
if child.tag == 'system-out':
systemOutput = child.text
if child.tag == 'failure':
success = False
print("Testcase '{}' failed".format(testName))
print(' ', systemOutput)
return success, tests
def log(msg, color):
if hasClick:
click.secho(msg, fg=color)
else:
print(msg)
def isSuccessFullRun(output):
'''When being run from lldb, we cannot capture the exit code
so we have to parse the output which is produced in a
consistent way. Whenever we'll be on a recent enough version of lldb we
won't have to do this.
'''
pid = None
matchingPids = False
exitCode = -1
# 'Process 279 exited with status = 1 (0x00000001) ',
exitPattern = re.compile('^Process (?P<pid>[0-9]+) exited with status = (?P<exitCode>[0-9]+)')
# "Process 99232 launched: '/Users/bse...
launchedPattern = re.compile('^Process (?P<pid>[0-9]+) launched: ')
for line in output:
match = exitPattern.match(line)
if match:
exitCode = int(match.group('exitCode'))
pid = match.group('pid')
match = launchedPattern.match(line)
if match:
matchingPids = (pid == match.group('pid'))
return exitCode == 0 and matchingPids
def testLLDBOutput():
failedOutputWithCrashLines = [
' frame #15: 0x00007fff73f4d305 libsystem_pthread.dylib`_pthread_body + 126',
' frame #16: 0x00007fff73f5026f libsystem_pthread.dylib`_pthread_start + 70',
' frame #17: 0x00007fff73f4c415 libsystem_pthread.dylib`thread_start + 13',
'(lldb) quit 1'
]
failedOutputWithFailedUnittest = [
'===============================================================================',
'test cases: 1 | 0 passed | 1 failed', 'assertions: 15 | 14 passed | 1 failed',
'',
'Process 279 exited with status = 1 (0x00000001) ',
'',
"Process 279 launched: '/Users/bsergeant/src/foss/ixwebsocket/test/build/Darwin/ixwebsocket_unittest' (x86_64)"
]
successLines = [
'...',
'...',
'All tests passed (16 assertions in 1 test case)',
'',
'Process 99232 exited with status = 0 (0x00000000) ',
'',
"Process 99232 launched: '/Users/bsergeant/src/foss/ixwebsocket/test/build/Darwin/ixwebsocket_unittest' (x86_64)"
]
assert not isSuccessFullRun(failedOutputWithCrashLines)
assert not isSuccessFullRun(failedOutputWithFailedUnittest)
assert isSuccessFullRun(successLines)
def executeJob(job):
'''Execute a unittest and capture info about it (runtime, success, etc...)'''
start = time.time()
sys.stderr.write('.')
# 2 minutes of timeout for a single test
timeout = 2 * 60
command = Command(job['cmd'])
timedout, ret = command.run(timeout)
job['exit_code'] = ret
job['success'] = ret == 0
job['runtime'] = time.time() - start
# Record unittest console output
job['output'] = ''
path = job['output_path']
if os.path.exists(path):
with open(path) as f:
output = f.read()
job['output'] = output
outputLines = output.splitlines()
if job['use_lldb']:
job['success'] = isSuccessFullRun(outputLines)
# Cleanup tmp file now that its content was read
os.unlink(path)
return job
def executeJobs(jobs):
'''Execute a list of job concurrently on multiple CPU/cores'''
poolSize = multiprocessing.cpu_count()
pool = multiprocessing.Pool(poolSize)
results = pool.map(executeJob, jobs)
pool.close()
pool.join()
return results
def computeAllTestNames(buildDir):
'''Compute all test case names, by executing the unittest in a custom mode'''
executable = os.path.join(buildDir, DEFAULT_EXE)
cmd = '"{}" --list-test-names-only'.format(executable)
names = os.popen(cmd).read().splitlines()
names.sort() # Sort test names for execution determinism
return names
def prettyPrintXML(root):
'''Pretty print an XML file. Default writer write it on a single line
which makes it hard for human to inspect.'''
serializedXml = ET.tostring(root, encoding='utf-8')
reparsed = minidom.parseString(serializedXml)
prettyPrinted = reparsed.toprettyxml(indent=" ")
return prettyPrinted
def generateXmlOutput(results, xmlOutput, testRunName, runTime):
'''Generate a junit compatible XML file
We prefer doing this ourself instead of letting Catch2 do it.
When the test is crashing (as has happened on Jenkins), an invalid file
with no trailer can be created which trigger an XML reading error in validateTestSuite.
Something like that:
```
<testsuite>
<foo>
```
'''
root = ET.Element('testsuites')
testSuite = ET.Element('testsuite', {
'name': testRunName,
'tests': str(len(results)),
'failures': str(sum(1 for result in results if not result['success'])),
'time': str(runTime),
'timestamp': datetime.datetime.utcnow().isoformat(),
})
root.append(testSuite)
for result in results:
testCase = ET.Element('testcase', {
'name': result['name'],
'time': str(result['runtime'])
})
systemOut = ET.Element('system-out')
systemOut.text = result['output'].decode('utf-8')
testCase.append(systemOut)
if not result['success']:
failure = ET.Element('failure')
testCase.append(failure)
testSuite.append(testCase)
with open(xmlOutput, 'w') as f:
content = prettyPrintXML(root)
f.write(content.encode('utf-8'))
def run(testName, buildDir, sanitizer, xmlOutput, testRunName, buildOnly, useLLDB):
'''Main driver. Run cmake, compiles, execute and validate the testsuite.'''
runCMake(sanitizer, buildDir)
runCommand('make -C {} -j8'.format(buildDir))
if buildOnly:
return
# A specific test case can be provided on the command line
if testName:
testNames = [testName]
else:
# Default case
testNames = computeAllTestNames(buildDir)
# This should be empty. It is useful to have a blacklist during transitions
# We could add something for asan as well.
blackLists = {
'ubsan': []
}
blackList = blackLists.get(sanitizer, [])
# Run through LLDB to capture crashes
lldb = ''
if useLLDB:
lldb = "lldb --batch -o 'run' -k 'thread backtrace all' -k 'quit 1'"
# Jobs is a list of python dicts
jobs = []
for testName in testNames:
outputPath = tempfile.mktemp(suffix=testName + '.log')
if testName in blackList:
log('Skipping blacklisted test {}'.format(testName), 'yellow')
continue
# testName can contains spaces, so we enclose them in double quotes
executable = os.path.join(buildDir, DEFAULT_EXE)
cmd = '{} "{}" "{}" >& "{}"'.format(lldb, executable, testName, outputPath)
jobs.append({
'name': testName,
'cmd': cmd,
'output_path': outputPath,
'use_lldb': useLLDB
})
start = time.time()
results = executeJobs(jobs)
runTime = time.time() - start
generateXmlOutput(results, xmlOutput, testRunName, runTime)
# Validate and report results
print('\nParsing junit test result file: {}'.format(xmlOutput))
log('## Results', 'blue')
success, tests = validateTestSuite(xmlOutput)
if success:
label = 'tests' if int(tests) > 1 else 'test'
msg = 'All test passed (#{} {})'.format(tests, label)
color = 'green'
else:
msg = 'unittest failed'
color = 'red'
log(msg, color)
log('Execution time: %.2fs' % (runTime), 'blue')
sys.exit(0 if success else 1)
def main():
buildDir = 'build/' + platform.system()
if not os.path.exists(buildDir):
os.makedirs(buildDir)
defaultOutput = DEFAULT_EXE + '.xml'
parser = argparse.ArgumentParser(description='Build and Run the engine unittest')
sanitizers = ['tsan', 'asan', 'ubsan', 'none']
parser.add_argument('--sanitizer', choices=sanitizers,
help='Run a clang sanitizer.')
parser.add_argument('--test', '-t', help='Test name.')
parser.add_argument('--list', '-l', action='store_true',
help='Print test names and exit.')
parser.add_argument('--no_sanitizer', action='store_true',
help='Do not execute a clang sanitizer.')
parser.add_argument('--validate', action='store_true',
help='Validate XML output.')
parser.add_argument('--build_only', '-b', action='store_true',
help='Stop after building. Do not run the unittest.')
parser.add_argument('--output', '-o', help='Output XML file.')
parser.add_argument('--lldb', action='store_true',
help='Run the test through lldb.')
parser.add_argument('--run_name', '-n',
help='Name of the test run.')
args = parser.parse_args()
# Default sanitizer is tsan
sanitizer = args.sanitizer
if args.sanitizer is None:
sanitizer = 'tsan'
defaultRunName = 'ixengine_{}_{}'.format(platform.system(), sanitizer)
xmlOutput = args.output or defaultOutput
testRunName = args.run_name or os.getenv('IXENGINE_TEST_RUN_NAME') or defaultRunName
if args.list:
# catch2 exit with a different error code when requesting the list of files
try:
runTest('--list-test-names-only', buildDir, xmlOutput, testRunName)
except AssertionError:
pass
return
if args.validate:
validateTestSuite(xmlOutput)
return
if platform.system() != 'Darwin' and args.lldb:
print('LLDB is only supported on Apple at this point')
args.lldb = False
return run(args.test, buildDir, sanitizer, xmlOutput,
testRunName, args.build_only, args.lldb)
if __name__ == '__main__':
main()

View File

@ -7,8 +7,6 @@
#define CATCH_CONFIG_RUNNER
#include "catch.hpp"
#include <ixwebsocket/IXSocket.h>
int main(int argc, char* argv[])
{
int result = Catch::Session().run(argc, argv);

1
third_party/README.md vendored Normal file
View File

@ -0,0 +1 @@
Except ZLIB on Windows (whose port is currently broken...) all dependencies here are for the ws command line tools, not for the IXWebSockets library which is standalone.

View File

@ -1,20 +0,0 @@
class Ixwebsocket < Formula
desc "WebSocket client and server, and HTTP client command-line tool"
homepage "https://github.com/machinezone/IXWebSocket"
url "https://github.com/machinezone/IXWebSocket/archive/v1.1.0.tar.gz"
sha256 "52592ce3d0a67ad0f90ac9e8a458f61724175d95a01a38d1bad3fcdc5c7b6666"
depends_on "cmake" => :build
def install
system "cmake", ".", *std_cmake_args
system "make", "install"
end
test do
system "#{bin}/ws", "--help"
system "#{bin}/ws", "send", "--help"
system "#{bin}/ws", "receive", "--help"
system "#{bin}/ws", "transfer", "--help"
system "#{bin}/ws", "curl", "--help"
end
end

View File

@ -1,2 +1,3 @@
find . -type f -name '*.cpp' -exec sed -i '' 's/[[:space:]]*$//' {} \+
find . -type f -name '*.h' -exec sed -i '' 's/[[:space:]]*$//' {} \+
find . -type f -name '*.md' -exec sed -i '' 's/[[:space:]]*$//' {} \+

View File

@ -0,0 +1,13 @@
# Compiled Object files
*.slo
*.lo
*.o
# Compiled Dynamic libraries
*.so
*.dylib
# Compiled Static libraries
*.lai
*.la
*.a

View File

@ -0,0 +1,18 @@
cmake_minimum_required(VERSION 3.1)
project(helloCLion)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
include_directories(
src
)
add_library(statsdcppclient STATIC src/statsd_client.cpp)
add_definitions("-fPIC")
target_link_libraries(statsdcppclient pthread)
add_executable(system_monitor demo/system_monitor.cpp)
target_link_libraries(system_monitor statsdcppclient)
add_executable(test_client demo/test_client.cpp)
target_link_libraries(test_client statsdcppclient)

27
third_party/statsd-client-cpp/LICENSE vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2014, Rex
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 {organization} 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 THE COPYRIGHT HOLDER OR CONTRIBUTORS 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.

34
third_party/statsd-client-cpp/README.md vendored Normal file
View File

@ -0,0 +1,34 @@
# a client sdk for StatsD, written in C++
## API
See [header file](src/statsd_client.h) for more api detail.
** Notice: this client is not thread-safe **
## Demo
### test\_client
This simple demo shows how the use this client.
### system\_monitor
This is a daemon for monitoring a Linux system.
It'll wake up every minute and monitor the following:
* load
* cpu
* free memory
* free swap (disabled)
* received bytes
* transmitted bytes
* procs
* uptime
The stats sent to statsd will be in "host.MACAddress" namespace.
Usage:
system_monitor statsd-host interface-to-monitor
e.g.
`system_monitor 172.16.42.1 eth0`

View File

@ -0,0 +1,164 @@
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <netdb.h>
#include <sys/sysinfo.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <net/if.h>
#include <string>
#include <vector>
#include "statsd_client.h"
using namespace std;
static int running = 1;
void sigterm(int sig)
{
running = 0;
}
string localhost() {
struct addrinfo hints, *info, *p;
string hostname(1024, '\0');
gethostname((char*)hostname.data(), hostname.capacity());
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; /*either IPV4 or IPV6*/
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_CANONNAME;
if ( getaddrinfo(hostname.c_str(), "http", &hints, &info) == 0) {
for(p = info; p != NULL; p = p->ai_next) {
hostname = p->ai_canonname;
}
freeaddrinfo(info);
}
string::size_type pos = hostname.find(".");
while ( pos != string::npos )
{
hostname[pos] = '_';
pos = hostname.find(".", pos);
}
return hostname;
}
vector<string>& StringSplitTrim(const string& sData,
const string& sDelim, vector<string>& vItems)
{
vItems.clear();
string::size_type bpos = 0;
string::size_type epos = 0;
string::size_type nlen = sDelim.size();
while(sData.substr(epos,nlen) == sDelim)
{
epos += nlen;
}
bpos = epos;
while ((epos=sData.find(sDelim, epos)) != string::npos)
{
vItems.push_back(sData.substr(bpos, epos-bpos));
epos += nlen;
while(sData.substr(epos,nlen) == sDelim)
{
epos += nlen;
}
bpos = epos;
}
if(bpos != sData.size())
{
vItems.push_back(sData.substr(bpos, sData.size()-bpos));
}
return vItems;
}
int main(int argc, char *argv[])
{
FILE *net, *stat;
struct sysinfo si;
char line[256];
unsigned int user, nice, sys, idle, total, busy, old_total=0, old_busy=0;
if (argc != 3) {
printf( "Usage: %s host port\n"
"Eg: %s 127.0.0.1 8125\n",
argv[0], argv[0]);
exit(1);
}
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN); /* will save one syscall per sleep */
signal(SIGTERM, sigterm);
if ( (net = fopen("/proc/net/dev", "r")) == NULL) {
perror("fopen");
exit(-1);
}
if ( (stat = fopen("/proc/stat", "r")) == NULL) {
perror("fopen");
exit(-1);
}
string ns = string("host.") + localhost().c_str() + ".";
statsd::StatsdClient client(argv[1], atoi(argv[2]), ns);
daemon(0,0);
printf("running in background.\n");
while(running) {
rewind(net);
vector<string> items;
while(!feof(net)) {
fgets(line, sizeof(line), net);
StringSplitTrim(line, " ", items);
if ( items.size() < 17 ) continue;
if ( items[0].find(":") == string::npos ) continue;
if ( items[1] == "0" and items[9] == "0" ) continue;
string netface = "network."+items[0].erase( items[0].find(":") );
client.count( netface+".receive.bytes", atoll(items[1].c_str()) );
client.count( netface+".receive.packets", atoll(items[2].c_str()) );
client.count( netface+".transmit.bytes", atoll(items[9].c_str()) );
client.count( netface+".transmit.packets", atoll(items[10].c_str()) );
}
sysinfo(&si);
client.gauge("system.load", 100*si.loads[0]/0x10000);
client.gauge("system.freemem", si.freeram/1024);
client.gauge("system.procs", si.procs);
client.count("system.uptime", si.uptime);
/* rewind doesn't do the trick for /proc/stat */
freopen("/proc/stat", "r", stat);
fgets(line, sizeof(line), stat);
sscanf(line, "cpu %u %u %u %u", &user, &nice, &sys, &idle);
total = user + sys + idle;
busy = user + sys;
client.send("system.cpu", 100 * (busy - old_busy)/(total - old_total), "g", 1.0);
old_total = total;
old_busy = busy;
sleep(6);
}
fclose(net);
fclose(stat);
exit(0);
}

View File

@ -0,0 +1,28 @@
#include <iostream>
#include <unistd.h>
#include "statsd_client.h"
int main(void)
{
std::cout << "running..." << std::endl;
statsd::StatsdClient client;
statsd::StatsdClient client2("127.0.0.1", 8125, "myproject.abx.", true);
client.count("count1", 123, 1.0);
client.count("count2", 125, 1.0);
client.gauge("speed", 10);
int i;
for (i=0; i<1000; i++)
client2.timing("request", i);
sleep(1);
client.inc("count1", 1.0);
client2.dec("count2", 1.0);
// for(i=0; i<1000; i++) {
// client2.count("count3", i, 0.8);
// }
std::cout << "done" << std::endl;
return 0;
}

View File

@ -0,0 +1,246 @@
#include <math.h>
#include <netdb.h>
#include <time.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <netinet/in.h>
#include "statsd_client.h"
using namespace std;
namespace statsd {
inline bool fequal(float a, float b)
{
const float epsilon = 0.0001;
return ( fabs(a - b) < epsilon );
}
inline bool should_send(float sample_rate)
{
if ( fequal(sample_rate, 1.0) )
{
return true;
}
float p = ((float)random() / RAND_MAX);
return sample_rate > p;
}
struct _StatsdClientData {
int sock;
struct sockaddr_in server;
string ns;
string host;
short port;
bool init;
char errmsg[1024];
};
StatsdClient::StatsdClient(const string& host,
int port,
const string& ns,
const bool batching)
: batching_(batching), exit_(false)
{
d = new _StatsdClientData;
d->sock = -1;
config(host, port, ns);
srandom(time(NULL));
if (batching_) {
pthread_mutex_init(&batching_mutex_lock_, nullptr);
batching_thread_ = std::thread([this] {
while (!exit_) {
std::deque<std::string> staged_message_queue;
pthread_mutex_lock(&batching_mutex_lock_);
batching_message_queue_.swap(staged_message_queue);
pthread_mutex_unlock(&batching_mutex_lock_);
while(!staged_message_queue.empty()) {
send_to_daemon(staged_message_queue.front());
staged_message_queue.pop_front();
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
});
}
}
StatsdClient::~StatsdClient()
{
if (batching_) {
exit_ = true;
batching_thread_.join();
pthread_mutex_destroy(&batching_mutex_lock_);
}
// close socket
if (d->sock >= 0) {
close(d->sock);
d->sock = -1;
delete d;
d = NULL;
}
}
void StatsdClient::config(const string& host, int port, const string& ns)
{
d->ns = ns;
d->host = host;
d->port = port;
d->init = false;
if ( d->sock >= 0 ) {
close(d->sock);
}
d->sock = -1;
}
int StatsdClient::init()
{
if ( d->init ) return 0;
d->sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if ( d->sock == -1 ) {
snprintf(d->errmsg, sizeof(d->errmsg), "could not create socket, err=%m");
return -1;
}
memset(&d->server, 0, sizeof(d->server));
d->server.sin_family = AF_INET;
d->server.sin_port = htons(d->port);
int ret = inet_aton(d->host.c_str(), &d->server.sin_addr);
if ( ret == 0 )
{
// host must be a domain, get it from internet
struct addrinfo hints, *result = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
ret = getaddrinfo(d->host.c_str(), NULL, &hints, &result);
if ( ret ) {
close(d->sock);
d->sock = -1;
snprintf(d->errmsg, sizeof(d->errmsg),
"getaddrinfo fail, error=%d, msg=%s", ret, gai_strerror(ret) );
return -2;
}
struct sockaddr_in* host_addr = (struct sockaddr_in*)result->ai_addr;
memcpy(&d->server.sin_addr, &host_addr->sin_addr, sizeof(struct in_addr));
freeaddrinfo(result);
}
d->init = true;
return 0;
}
/* will change the original string */
void StatsdClient::cleanup(string& key)
{
size_t pos = key.find_first_of(":|@");
while ( pos != string::npos )
{
key[pos] = '_';
pos = key.find_first_of(":|@");
}
}
int StatsdClient::dec(const string& key, float sample_rate)
{
return count(key, -1, sample_rate);
}
int StatsdClient::inc(const string& key, float sample_rate)
{
return count(key, 1, sample_rate);
}
int StatsdClient::count(const string& key, size_t value, float sample_rate)
{
return send(key, value, "c", sample_rate);
}
int StatsdClient::gauge(const string& key, size_t value, float sample_rate)
{
return send(key, value, "g", sample_rate);
}
int StatsdClient::timing(const string& key, size_t ms, float sample_rate)
{
return send(key, ms, "ms", sample_rate);
}
int StatsdClient::send(string key, size_t value, const string &type, float sample_rate)
{
if (!should_send(sample_rate)) {
return 0;
}
cleanup(key);
char buf[256];
if ( fequal( sample_rate, 1.0 ) )
{
snprintf(buf, sizeof(buf), "%s%s:%zd|%s",
d->ns.c_str(), key.c_str(), value, type.c_str());
}
else
{
snprintf(buf, sizeof(buf), "%s%s:%zd|%s|@%.2f",
d->ns.c_str(), key.c_str(), value, type.c_str(), sample_rate);
}
return send(buf);
}
int StatsdClient::send(const string &message)
{
if (batching_) {
pthread_mutex_lock(&batching_mutex_lock_);
if (batching_message_queue_.empty() ||
batching_message_queue_.back().length() > max_batching_size) {
batching_message_queue_.push_back(message);
} else {
(*batching_message_queue_.rbegin()).append("\n").append(message);
}
pthread_mutex_unlock(&batching_mutex_lock_);
return 0;
} else {
return send_to_daemon(message);
}
}
int StatsdClient::send_to_daemon(const string &message) {
std::cout << "send_to_daemon: " << message.length() << " B" << std::endl;
int ret = init();
if ( ret )
{
return ret;
}
ret = sendto(d->sock, message.data(), message.size(), 0, (struct sockaddr *) &d->server, sizeof(d->server));
if ( ret == -1) {
snprintf(d->errmsg, sizeof(d->errmsg),
"sendto server fail, host=%s:%d, err=%m", d->host.c_str(), d->port);
return -1;
}
return 0;
}
const char* StatsdClient::errmsg()
{
return d->errmsg;
}
}

View File

@ -0,0 +1,66 @@
#ifndef STATSD_CLIENT_H
#define STATSD_CLIENT_H
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#include <string>
#include <thread>
#include <deque>
#include <iostream>
namespace statsd {
struct _StatsdClientData;
class StatsdClient {
public:
StatsdClient(const std::string& host="127.0.0.1", int port=8125, const std::string& ns = "", const bool batching = false);
~StatsdClient();
public:
// you can config at anytime; client will use new address (useful for Singleton)
void config(const std::string& host, int port, const std::string& ns = "");
const char* errmsg();
int send_to_daemon(const std::string &);
public:
int inc(const std::string& key, float sample_rate = 1.0);
int dec(const std::string& key, float sample_rate = 1.0);
int count(const std::string& key, size_t value, float sample_rate = 1.0);
int gauge(const std::string& key, size_t value, float sample_rate = 1.0);
int timing(const std::string& key, size_t ms, float sample_rate = 1.0);
public:
/**
* (Low Level Api) manually send a message
* which might be composed of several lines.
*/
int send(const std::string& message);
/* (Low Level Api) manually send a message
* type = "c", "g" or "ms"
*/
int send(std::string key, size_t value,
const std::string& type, float sample_rate);
protected:
int init();
void cleanup(std::string& key);
protected:
struct _StatsdClientData* d;
bool batching_;
bool exit_;
pthread_mutex_t batching_mutex_lock_;
std::thread batching_thread_;
std::deque<std::string> batching_message_queue_;
const uint64_t max_batching_size = 32768;
};
}; // end namespace
#endif

1
ws/.gitignore vendored
View File

@ -1 +1,2 @@
build
node_modules

View File

@ -7,7 +7,12 @@ cmake_minimum_required (VERSION 3.4.1)
project (ws)
# There's -Weverything too for clang
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")
if (NOT WIN32)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")
endif()
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread")
#set(CMAKE_LD_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread")
set (CMAKE_CXX_STANDARD 14)
@ -16,12 +21,20 @@ option(USE_TLS "Add TLS support" ON)
include_directories(ws .)
include_directories(ws ..)
include_directories(ws ../third_party)
include_directories(ws ../third_party/statsd-client-cpp/src)
add_executable(ws
../third_party/msgpack11/msgpack11.cpp
../third_party/jsoncpp/jsoncpp.cpp
../third_party/statsd-client-cpp/src/statsd_client.cpp
ixcrypto/IXBase64.cpp
ixcrypto/IXHash.cpp
ixcrypto/IXUuid.cpp
ixcrypto/IXHMac.cpp
IXRedisClient.cpp
IXSentryClient.cpp
IXCobraConnection.cpp
ws_http_client.cpp
ws_ping_pong.cpp
@ -32,11 +45,21 @@ add_executable(ws
ws_transfer.cpp
ws_send.cpp
ws_receive.cpp
ws_redis_publish.cpp
ws_redis_subscribe.cpp
ws_cobra_subscribe.cpp
ws_cobra_to_statsd.cpp
ws_cobra_to_sentry.cpp
ws.cpp)
if (APPLE AND USE_TLS)
target_link_libraries(ws "-framework foundation" "-framework security")
target_link_libraries(ws ixwebsocket)
if(NOT APPLE)
find_package(OpenSSL REQUIRED)
add_definitions(${OPENSSL_DEFINITIONS})
message(STATUS "OpenSSL: " ${OPENSSL_VERSION})
include_directories(${OPENSSL_INCLUDE_DIR})
target_link_libraries(ws ${OPENSSL_LIBRARIES})
endif()
target_link_libraries(ws ixwebsocket)
install(TARGETS ws RUNTIME DESTINATION bin)

View File

@ -6,6 +6,7 @@
#include "IXCobraConnection.h"
#include <ixcrypto/IXHMac.h>
#include <ixwebsocket/IXWebSocket.h>
#include <algorithm>
#include <stdexcept>
@ -20,9 +21,10 @@ namespace ix
constexpr size_t CobraConnection::kQueueMaxSize;
CobraConnection::CobraConnection() :
_webSocket(new WebSocket()),
_publishMode(CobraConnection_PublishMode_Immediate),
_authenticated(false),
_eventCallback(nullptr),
_publishMode(CobraConnection_PublishMode_Immediate)
_eventCallback(nullptr)
{
_pdu["action"] = "rtm/publish";
@ -32,6 +34,7 @@ namespace ix
CobraConnection::~CobraConnection()
{
disconnect();
setEventCallback(nullptr);
}
void CobraConnection::setTrafficTrackerCallback(const TrafficTrackerCallback& callback)
@ -59,36 +62,40 @@ namespace ix
}
void CobraConnection::invokeEventCallback(ix::CobraConnectionEventType eventType,
const std::string& errorMsg,
const WebSocketHttpHeaders& headers)
const std::string& errorMsg,
const WebSocketHttpHeaders& headers,
const std::string& subscriptionId)
{
std::lock_guard<std::mutex> lock(_eventCallbackMutex);
if (_eventCallback)
{
_eventCallback(eventType, errorMsg, headers);
_eventCallback(eventType, errorMsg, headers, subscriptionId);
}
}
void CobraConnection::invokeErrorCallback(const std::string& errorMsg)
void CobraConnection::invokeErrorCallback(const std::string& errorMsg,
const std::string& serializedPdu)
{
invokeEventCallback(ix::CobraConnection_EventType_Error, errorMsg);
std::stringstream ss;
ss << errorMsg << " : received pdu => " << serializedPdu;
invokeEventCallback(ix::CobraConnection_EventType_Error, ss.str());
}
void CobraConnection::disconnect()
{
_authenticated = false;
_webSocket.stop();
_webSocket->stop();
}
void CobraConnection::initWebSocketOnMessageCallback()
{
_webSocket.setOnMessageCallback(
_webSocket->setOnMessageCallback(
[this](ix::WebSocketMessageType messageType,
const std::string& str,
size_t wireSize,
const ix::WebSocketErrorInfo& error,
const ix::WebSocketCloseInfo& closeInfo,
const ix::WebSocketHttpHeaders& headers)
const ix::WebSocketOpenInfo& openInfo,
const ix::WebSocketCloseInfo& closeInfo)
{
CobraConnection::invokeTrafficTrackerCallback(wireSize, true);
@ -97,7 +104,7 @@ namespace ix
{
invokeEventCallback(ix::CobraConnection_EventType_Open,
std::string(),
headers);
openInfo.headers);
sendHandshakeMessage();
}
else if (messageType == ix::WebSocket_MessageType_Close)
@ -116,13 +123,13 @@ namespace ix
Json::Reader reader;
if (!reader.parse(str, data))
{
invokeErrorCallback(std::string("Invalid json: ") + str);
invokeErrorCallback("Invalid json", str);
return;
}
if (!data.isMember("action"))
{
invokeErrorCallback("Missing action");
invokeErrorCallback("Missing action", str);
return;
}
@ -132,12 +139,12 @@ namespace ix
{
if (!handleHandshakeResponse(data))
{
invokeErrorCallback("Error extracting nonce from handshake response");
invokeErrorCallback("Error extracting nonce from handshake response", str);
}
}
else if (action == "auth/handshake/error")
{
invokeErrorCallback("Handshake error."); // print full message ?
invokeErrorCallback("Handshake error", str);
}
else if (action == "auth/authenticate/ok")
{
@ -147,15 +154,37 @@ namespace ix
}
else if (action == "auth/authenticate/error")
{
invokeErrorCallback("Authentication error."); // print full message ?
invokeErrorCallback("Authentication error", str);
}
else if (action == "rtm/subscription/data")
{
handleSubscriptionData(data);
}
else if (action == "rtm/subscribe/ok")
{
if (!handleSubscriptionResponse(data))
{
invokeErrorCallback("Error processing subscribe response", str);
}
}
else if (action == "rtm/subscribe/error")
{
invokeErrorCallback("Subscription error", str);
}
else if (action == "rtm/unsubscribe/ok")
{
if (!handleUnsubscriptionResponse(data))
{
invokeErrorCallback("Error processing subscribe response", str);
}
}
else if (action == "rtm/unsubscribe/error")
{
invokeErrorCallback("Unsubscription error", str);
}
else
{
invokeErrorCallback(std::string("Un-handled message type: ") + action);
invokeErrorCallback("Un-handled message type", str);
}
}
else if (messageType == ix::WebSocket_MessageType_Error)
@ -165,7 +194,7 @@ namespace ix
ss << "#retries: " << error.retries << std::endl;
ss << "Wait time(ms): " << error.wait_time << std::endl;
ss << "HTTP Status: " << error.http_status << std::endl;
invokeErrorCallback(ss.str());
invokeErrorCallback(ss.str(), std::string());
}
});
}
@ -176,10 +205,10 @@ namespace ix
}
void CobraConnection::configure(const std::string& appkey,
const std::string& endpoint,
const std::string& rolename,
const std::string& rolesecret,
WebSocketPerMessageDeflateOptions webSocketPerMessageDeflateOptions)
const std::string& endpoint,
const std::string& rolename,
const std::string& rolesecret,
WebSocketPerMessageDeflateOptions webSocketPerMessageDeflateOptions)
{
_appkey = appkey;
_endpoint = endpoint;
@ -192,8 +221,8 @@ namespace ix
ss << _appkey;
std::string url = ss.str();
_webSocket.setUrl(url);
_webSocket.setPerMessageDeflateOptions(webSocketPerMessageDeflateOptions);
_webSocket->setUrl(url);
_webSocket->setPerMessageDeflateOptions(webSocketPerMessageDeflateOptions);
}
//
@ -226,10 +255,10 @@ namespace ix
std::string serializedJson = serializeJson(pdu);
CobraConnection::invokeTrafficTrackerCallback(serializedJson.size(), false);
return _webSocket.send(serializedJson).success;
return _webSocket->send(serializedJson).success;
}
//
//
// Extract the nonce from the handshake response
// use it to compute a hash during authentication
//
@ -288,16 +317,47 @@ namespace ix
std::string serializedJson = serializeJson(pdu);
CobraConnection::invokeTrafficTrackerCallback(serializedJson.size(), false);
return _webSocket.send(serializedJson).success;
return _webSocket->send(serializedJson).success;
}
bool CobraConnection::handleSubscriptionResponse(const Json::Value& pdu)
{
if (!pdu.isMember("body")) return false;
Json::Value body = pdu["body"];
if (!body.isMember("subscription_id")) return false;
Json::Value subscriptionId = body["subscription_id"];
if (!subscriptionId.isString()) return false;
invokeEventCallback(ix::CobraConnection_EventType_Subscribed,
std::string(), WebSocketHttpHeaders(),
subscriptionId.asString());
return true;
}
bool CobraConnection::handleUnsubscriptionResponse(const Json::Value& pdu)
{
if (!pdu.isMember("body")) return false;
Json::Value body = pdu["body"];
if (!body.isMember("subscription_id")) return false;
Json::Value subscriptionId = body["subscription_id"];
if (!subscriptionId.isString()) return false;
invokeEventCallback(ix::CobraConnection_EventType_UnSubscribed,
std::string(), WebSocketHttpHeaders(),
subscriptionId.asString());
return true;
}
bool CobraConnection::handleSubscriptionData(const Json::Value& pdu)
{
if (!pdu.isMember("body")) return false;
Json::Value body = pdu["body"];
// Identify subscription_id, so that we can find
// Identify subscription_id, so that we can find
// which callback to execute
if (!body.isMember("subscription_id")) return false;
Json::Value subscriptionId = body["subscription_id"];
@ -320,13 +380,13 @@ namespace ix
bool CobraConnection::connect()
{
_webSocket.start();
_webSocket->start();
return true;
}
bool CobraConnection::isConnected() const
{
return _webSocket.getReadyState() == ix::WebSocket_ReadyState_Open;
return _webSocket->getReadyState() == ix::WebSocket_ReadyState_Open;
}
std::string CobraConnection::serializeJson(const Json::Value& value)
@ -339,7 +399,7 @@ namespace ix
// publish is not thread safe as we are trying to reuse some Json objects.
//
bool CobraConnection::publish(const Json::Value& channels,
const Json::Value& msg)
const Json::Value& msg)
{
_body["channels"] = channels;
_body["message"] = msg;
@ -371,7 +431,7 @@ namespace ix
}
void CobraConnection::subscribe(const std::string& channel,
SubscriptionCallback cb)
SubscriptionCallback cb)
{
// Create and send a subscribe pdu
Json::Value body;
@ -381,7 +441,7 @@ namespace ix
pdu["action"] = "rtm/subscribe";
pdu["body"] = body;
_webSocket.send(pdu.toStyledString());
_webSocket->send(pdu.toStyledString());
// Set the callback
std::lock_guard<std::mutex> lock(_cbsMutex);
@ -400,13 +460,13 @@ namespace ix
// Create and send an unsubscribe pdu
Json::Value body;
body["channel"] = channel;
body["subscription_id"] = channel;
Json::Value pdu;
pdu["action"] = "rtm/unsubscribe";
pdu["body"] = body;
_webSocket.send(pdu.toStyledString());
_webSocket->send(pdu.toStyledString());
}
//
@ -456,7 +516,7 @@ namespace ix
bool CobraConnection::publishMessage(const std::string& serializedJson)
{
auto webSocketSendInfo = _webSocket.send(serializedJson);
auto webSocketSendInfo = _webSocket->send(serializedJson);
CobraConnection::invokeTrafficTrackerCallback(webSocketSendInfo.wireSize,
false);
return webSocketSendInfo.success;
@ -471,5 +531,5 @@ namespace ix
{
connect();
}
} // namespace ix

View File

@ -11,19 +11,24 @@
#include <string>
#include <thread>
#include <unordered_map>
#include <memory>
#include <jsoncpp/json/json.h>
#include <ixwebsocket/IXWebSocket.h>
#include <ixwebsocket/IXWebSocketHttpHeaders.h>
#include <ixwebsocket/IXWebSocketPerMessageDeflateOptions.h>
namespace ix
{
class WebSocket;
enum CobraConnectionEventType
{
CobraConnection_EventType_Authenticated = 0,
CobraConnection_EventType_Error = 1,
CobraConnection_EventType_Open = 2,
CobraConnection_EventType_Closed = 3
CobraConnection_EventType_Closed = 3,
CobraConnection_EventType_Subscribed = 4,
CobraConnection_EventType_UnSubscribed = 5
};
enum CobraConnectionPublishMode
@ -35,7 +40,8 @@ namespace ix
using SubscriptionCallback = std::function<void(const Json::Value&)>;
using EventCallback = std::function<void(CobraConnectionEventType,
const std::string&,
const WebSocketHttpHeaders&)>;
const WebSocketHttpHeaders&,
const std::string&)>;
using TrafficTrackerCallback = std::function<void(size_t size, bool incoming)>;
class CobraConnection
@ -84,7 +90,7 @@ namespace ix
/// Returns true only if we're connected
bool isConnected() const;
/// Flush the publish queue
bool flushQueue();
@ -100,6 +106,8 @@ namespace ix
bool handleHandshakeResponse(const Json::Value& data);
bool sendAuthMessage(const std::string& nonce);
bool handleSubscriptionData(const Json::Value& pdu);
bool handleSubscriptionResponse(const Json::Value& pdu);
bool handleUnsubscriptionResponse(const Json::Value& pdu);
void initWebSocketOnMessageCallback();
@ -113,13 +121,15 @@ namespace ix
/// Invoke event callbacks
void invokeEventCallback(CobraConnectionEventType eventType,
const std::string& errorMsg = std::string(),
const WebSocketHttpHeaders& headers = WebSocketHttpHeaders());
void invokeErrorCallback(const std::string& errorMsg);
const WebSocketHttpHeaders& headers = WebSocketHttpHeaders(),
const std::string& subscriptionId = std::string());
void invokeErrorCallback(const std::string& errorMsg,
const std::string& serializedPdu);
///
/// Member variables
///
WebSocket _webSocket;
///
std::unique_ptr<WebSocket> _webSocket;
/// Configuration data
std::string _appkey;
@ -148,10 +158,10 @@ namespace ix
std::unordered_map<std::string, SubscriptionCallback> _cbs;
mutable std::mutex _cbsMutex;
// Message Queue can be touched on control+background thread,
// Message Queue can be touched on control+background thread,
// protecting with a mutex.
//
// Message queue is used when there are problems sending messages so
// Message queue is used when there are problems sending messages so
// that sending can be retried later.
std::deque<std::string> _messageQueue;
mutable std::mutex _queueMutex;
@ -159,5 +169,5 @@ namespace ix
// Cap the queue size (100 elems so far -> ~100k)
static constexpr size_t kQueueMaxSize = 256;
};
} // namespace ix

207
ws/IXRedisClient.cpp Normal file
View File

@ -0,0 +1,207 @@
/*
* IXRedisClient.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
*/
#include "IXRedisClient.h"
#include <ixwebsocket/IXSocketFactory.h>
#include <ixwebsocket/IXSocket.h>
#include <sstream>
#include <iomanip>
#include <vector>
#include <cstring>
namespace ix
{
bool RedisClient::connect(const std::string& hostname, int port)
{
bool tls = false;
std::string errorMsg;
_socket = createSocket(tls, errorMsg);
if (!_socket)
{
return false;
}
std::string errMsg;
return _socket->connect(hostname, port, errMsg, nullptr);
}
bool RedisClient::auth(const std::string& password,
std::string& response)
{
response.clear();
if (!_socket) return false;
std::stringstream ss;
ss << "AUTH ";
ss << password;
ss << "\r\n";
bool sent = _socket->writeBytes(ss.str(), nullptr);
if (!sent)
{
return false;
}
auto pollResult = _socket->isReadyToRead(-1);
if (pollResult == PollResultType::Error)
{
return false;
}
auto lineResult = _socket->readLine(nullptr);
auto lineValid = lineResult.first;
auto line = lineResult.second;
response = line;
return lineValid;
}
bool RedisClient::publish(const std::string& channel,
const std::string& message)
{
if (!_socket) return false;
std::stringstream ss;
ss << "PUBLISH ";
ss << channel;
ss << " ";
ss << message;
ss << "\r\n";
bool sent = _socket->writeBytes(ss.str(), nullptr);
if (!sent)
{
return false;
}
auto pollResult = _socket->isReadyToRead(-1);
if (pollResult == PollResultType::Error)
{
return false;
}
auto lineResult = _socket->readLine(nullptr);
auto lineValid = lineResult.first;
auto line = lineResult.second;
return lineValid;
}
//
// FIXME: we assume that redis never return errors...
//
bool RedisClient::subscribe(const std::string& channel,
const OnRedisSubscribeResponseCallback& responseCallback,
const OnRedisSubscribeCallback& callback)
{
if (!_socket) return false;
std::stringstream ss;
ss << "SUBSCRIBE ";
ss << channel;
ss << "\r\n";
bool sent = _socket->writeBytes(ss.str(), nullptr);
if (!sent)
{
return false;
}
// Wait 1s for the response
auto pollResult = _socket->isReadyToRead(-1);
if (pollResult == PollResultType::Error)
{
return false;
}
// build the response as a single string
std::stringstream oss;
// Read the first line of the response
auto lineResult = _socket->readLine(nullptr);
auto lineValid = lineResult.first;
auto line = lineResult.second;
oss << line;
if (!lineValid) return false;
// There are 5 items for the subscribe repply
for (int i = 0; i < 5; ++i)
{
auto lineResult = _socket->readLine(nullptr);
auto lineValid = lineResult.first;
auto line = lineResult.second;
oss << line;
if (!lineValid) return false;
}
responseCallback(oss.str());
// Wait indefinitely for new messages
while (true)
{
// Wait until something is ready to read
auto pollResult = _socket->isReadyToRead(-1);
if (pollResult == PollResultType::Error)
{
return false;
}
// The first line of the response describe the return type,
// => *3 (an array of 3 elements)
auto lineResult = _socket->readLine(nullptr);
auto lineValid = lineResult.first;
auto line = lineResult.second;
if (!lineValid) return false;
int arraySize;
{
std::stringstream ss;
ss << line.substr(1, line.size()-1);
ss >> arraySize;
}
// There are 6 items for each received message
for (int i = 0; i < arraySize; ++i)
{
auto lineResult = _socket->readLine(nullptr);
auto lineValid = lineResult.first;
auto line = lineResult.second;
if (!lineValid) return false;
// Messages are string, which start with a string size
// => $7 (7 bytes)
int stringSize;
std::stringstream ss;
ss << line.substr(1, line.size()-1);
ss >> stringSize;
auto readResult = _socket->readBytes(stringSize, nullptr, nullptr);
if (!readResult.first) return false;
if (i == 2)
{
// The message is the 3rd element.
callback(readResult.second);
}
// read last 2 bytes (\r\n)
char c;
_socket->readByte(&c, nullptr);
_socket->readByte(&c, nullptr);
}
}
return true;
}
}

41
ws/IXRedisClient.h Normal file
View File

@ -0,0 +1,41 @@
/*
* IXRedisClient.h
* Author: Benjamin Sergeant
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
*/
#pragma once
#include <memory>
#include <functional>
namespace ix
{
class Socket;
class RedisClient {
public:
using OnRedisSubscribeResponseCallback = std::function<void(const std::string&)>;
using OnRedisSubscribeCallback = std::function<void(const std::string&)>;
RedisClient() = default;
~RedisClient() = default;
bool connect(const std::string& hostname,
int port);
bool auth(const std::string& password,
std::string& response);
bool publish(const std::string& channel,
const std::string& message);
bool subscribe(const std::string& channel,
const OnRedisSubscribeResponseCallback& responseCallback,
const OnRedisSubscribeCallback& callback);
private:
std::shared_ptr<Socket> _socket;
};
}

178
ws/IXSentryClient.cpp Normal file
View File

@ -0,0 +1,178 @@
/*
* IXSentryClient.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2019 Machine Zone. All rights reserved.
*/
#include "IXSentryClient.h"
#include <chrono>
#include <iostream>
#include <ixwebsocket/IXWebSocketHttpHeaders.h>
namespace ix
{
SentryClient::SentryClient(const std::string& dsn) :
_dsn(dsn),
_validDsn(false),
_luaFrameRegex("\t([^/]+):([0-9]+): in function '([^/]+)'")
{
const std::regex dsnRegex("(http[s]?)://([^:]+):([^@]+)@([^/]+)/([0-9]+)");
std::smatch group;
if (std::regex_match(dsn, group, dsnRegex) and group.size() == 6)
{
_validDsn = true;
const auto scheme = group.str(1);
const auto host = group.str(4);
const auto project_id = group.str(5);
_url = scheme + "://" + host + "/api/" + project_id + "/store/";
_publicKey = group.str(2);
_secretKey = group.str(3);
}
}
int64_t SentryClient::getTimestamp()
{
const auto tp = std::chrono::system_clock::now();
const auto dur = tp.time_since_epoch();
return std::chrono::duration_cast<std::chrono::seconds>(dur).count();
}
std::string SentryClient::getIso8601()
{
std::time_t now;
std::time(&now);
char buf[sizeof "2011-10-08T07:07:09Z"];
std::strftime(buf, sizeof buf, "%Y-%m-%dT%H:%M:%SZ", std::gmtime(&now));
return buf;
}
std::string SentryClient::computeAuthHeader()
{
std::string securityHeader("Sentry sentry_version=5");
securityHeader += ",sentry_client=ws/1.0.0";
securityHeader += ",sentry_timestamp=" + std::to_string(SentryClient::getTimestamp());
securityHeader += ",sentry_key=" + _publicKey;
securityHeader += ",sentry_secret=" + _secretKey;
return securityHeader;
}
Json::Value SentryClient::parseLuaStackTrace(const std::string& stack)
{
Json::Value frames;
// Split by lines
std::string line;
std::stringstream tokenStream(stack);
std::stringstream ss;
std::smatch group;
while (std::getline(tokenStream, line))
{
// MapScene.lua:2169: in function 'singleCB'
if (std::regex_match(line, group, _luaFrameRegex))
{
const auto fileName = group.str(1);
const auto linenoStr = group.str(2);
const auto function = group.str(3);
ss << linenoStr;
uint64_t lineno;
ss >> lineno;
Json::Value frame;
frame["lineno"] = lineno;
frame["filename"] = fileName;
frame["function"] = function;
frames.append(frame);
}
}
return frames;
}
std::string SentryClient::computePayload(const Json::Value& msg)
{
Json::Value payload;
payload["platform"] = "python";
payload["sdk"]["name"] = "ws";
payload["sdk"]["version"] = "1.0.0";
payload["timestamp"] = SentryClient::getIso8601();
Json::Value exception;
exception["value"] = msg["data"]["message"];
std::string stackTraceFieldName =
(msg["id"].asString() == "game_noisytypes_id") ? "traceback" : "stack";
exception["stacktrace"]["frames"] =
parseLuaStackTrace(msg["data"][stackTraceFieldName].asString());
payload["exception"].append(exception);
Json::Value extra;
extra["cobra_event"] = msg;
exception["extra"] = extra;
return _jsonWriter.write(payload);
}
bool SentryClient::send(const Json::Value& msg,
bool verbose)
{
HttpRequestArgs args;
args.extraHeaders["X-Sentry-Auth"] = SentryClient::computeAuthHeader();
args.connectTimeout = 60;
args.transferTimeout = 5 * 60;
args.followRedirects = true;
args.verbose = verbose;
args.logger = [](const std::string& msg)
{
std::cout << msg;
};
std::string body = computePayload(msg);
HttpResponse out = _httpClient.post(_url, body, args);
auto statusCode = std::get<0>(out);
auto errorCode = std::get<1>(out);
auto responseHeaders = std::get<2>(out);
auto payload = std::get<3>(out);
auto errorMsg = std::get<4>(out);
auto uploadSize = std::get<5>(out);
auto downloadSize = std::get<6>(out);
if (verbose)
{
for (auto it : responseHeaders)
{
std::cerr << it.first << ": " << it.second << std::endl;
}
std::cerr << "Upload size: " << uploadSize << std::endl;
std::cerr << "Download size: " << downloadSize << std::endl;
std::cerr << "Status: " << statusCode << std::endl;
if (errorCode != HttpErrorCode_Ok)
{
std::cerr << "error message: " << errorMsg << std::endl;
}
if (responseHeaders["Content-Type"] != "application/octet-stream")
{
std::cerr << "payload: " << payload << std::endl;
}
}
return statusCode == 200;
}
} // namespace ix

47
ws/IXSentryClient.h Normal file
View File

@ -0,0 +1,47 @@
/*
* IXSentryClient.h
* Author: Benjamin Sergeant
* Copyright (c) 2019 Machine Zone. All rights reserved.
*/
#pragma once
#include <jsoncpp/json/json.h>
#include <regex>
#include <ixwebsocket/IXHttpClient.h>
namespace ix
{
class SentryClient
{
public:
SentryClient(const std::string& dsn);
~SentryClient() = default;
bool send(const Json::Value& msg, bool verbose);
private:
int64_t getTimestamp();
std::string computeAuthHeader();
std::string getIso8601();
std::string computePayload(const Json::Value& msg);
Json::Value parseLuaStackTrace(const std::string& stack);
std::string _dsn;
bool _validDsn;
std::string _url;
// Used for authentication with a header
std::string _publicKey;
std::string _secretKey;
Json::FastWriter _jsonWriter;
std::regex _luaFrameRegex;
HttpClient _httpClient;
};
} // namespace ix

View File

@ -20,6 +20,8 @@ Subcommands:
broadcast_server Broadcasting server
ping Ping pong
curl HTTP Client
redis_publish Redis publisher
redis_subscribe Redis subscriber
```
## file transfer
@ -29,7 +31,7 @@ Subcommands:
ws transfer # running on port 8080.
# Start receiver first
ws receive ws://localhost:8080
ws receive ws://localhost:8080
# Then send a file. File will be received and written to disk by the receiver process
ws send ws://localhost:8080 /file/to/path

View File

@ -1,3 +0,0 @@
venv
build
node_modules

View File

@ -1,38 +0,0 @@
#
# Author: Benjamin Sergeant
# Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
#
cmake_minimum_required (VERSION 3.4.1)
project (cobra_publisher)
# There's -Weverything too for clang
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic -Wshorten-64-to-32")
set (OPENSSL_PREFIX /usr/local/opt/openssl) # Homebrew openssl
set (CMAKE_CXX_STANDARD 14)
option(USE_TLS "Add TLS support" ON)
include_directories(cobra_publisher ${OPENSSL_PREFIX}/include)
include_directories(cobra_publisher .)
add_executable(cobra_publisher
jsoncpp/jsoncpp.cpp
ixcrypto/IXHMac.cpp
ixcrypto/IXBase64.cpp
IXCobraConnection.cpp
cobra_publisher.cpp)
if (APPLE AND USE_TLS)
target_link_libraries(cobra_publisher "-framework foundation" "-framework security")
endif()
get_filename_component(crypto_lib_path ${OPENSSL_PREFIX}/lib/libcrypto.a ABSOLUTE)
add_library(lib_crypto STATIC IMPORTED)
set_target_properties(lib_crypto PROPERTIES IMPORTED_LOCATION ${crypto_lib_path})
link_directories(/usr/local/opt/openssl/lib)
target_link_libraries(cobra_publisher ixwebsocket lib_crypto)
install(TARGETS cobra_publisher DESTINATION bin)

View File

@ -1,6 +0,0 @@
```
mkdir build
cd build
cmake ..
make && (cd .. ; sh cobra_publisher.sh)
```

View File

@ -1,123 +0,0 @@
/*
* cobra_publisher.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
*/
#include <iostream>
#include <sstream>
#include <fstream>
#include <atomic>
#include <ixwebsocket/IXWebSocket.h>
#include "IXCobraConnection.h"
#include "jsoncpp/json/json.h"
void msleep(int ms)
{
std::chrono::duration<double, std::milli> duration(ms);
std::this_thread::sleep_for(duration);
}
int main(int argc, char* argv[])
{
if (argc != 7)
{
std::cerr << "Usage error: need 6 arguments." << std::endl;
}
std::string endpoint = argv[1];
std::string appkey = argv[2];
std::string channel = argv[3];
std::string rolename = argv[4];
std::string rolesecret = argv[5];
std::string path = argv[6];
std::atomic<size_t> incomingBytes(0);
std::atomic<size_t> outgoingBytes(0);
ix::CobraConnection::setTrafficTrackerCallback(
[&incomingBytes, &outgoingBytes](size_t size, bool incoming)
{
if (incoming)
{
incomingBytes += size;
}
else
{
outgoingBytes += size;
}
}
);
bool done = false;
ix::CobraConnection cobraConnection;
ix::WebSocketPerMessageDeflateOptions webSocketPerMessageDeflateOptions(
true, false, false, 15, 15);
cobraConnection.configure(appkey, endpoint, rolename, rolesecret,
webSocketPerMessageDeflateOptions);
cobraConnection.connect();
cobraConnection.setEventCallback(
[&cobraConnection, channel, path, &done]
(ix::CobraConnectionEventType eventType,
const std::string& errMsg,
const ix::WebSocketHttpHeaders& headers)
{
if (eventType == ix::CobraConnection_EventType_Open)
{
std::cout << "Handshake Headers:" << std::endl;
for (auto it : headers)
{
std::cout << it.first << ": " << it.second << std::endl;
}
}
else if (eventType == ix::CobraConnection_EventType_Authenticated)
{
std::cout << "Authenticated" << std::endl;
std::string line;
std::ifstream f(path);
if (!f.is_open())
{
std::cerr << "Error while opening file: " << path << std::endl;
}
int n = 0;
while (getline(f, line))
{
Json::Value value;
Json::Reader reader;
reader.parse(line, value);
cobraConnection.publish(channel, value);
n++;
}
std::cerr << "#published messages: " << n << std::endl;
if (f.bad())
{
std::cerr << "Error while opening file: " << path << std::endl;
}
done = true;
}
else if (eventType == ix::CobraConnection_EventType_Error)
{
std::cerr << "Cobra Error received: " << errMsg << std::endl;
done = true;
}
else if (eventType == ix::CobraConnection_EventType_Closed)
{
std::cerr << "Cobra connection closed" << std::endl;
}
}
);
while (!done)
{
msleep(1);
}
std::cout << "Incoming bytes: " << incomingBytes << std::endl;
std::cout << "Outgoing bytes: " << outgoingBytes << std::endl;
return 0;
}

View File

@ -1,11 +0,0 @@
#!/bin/sh
endpoint="ws://127.0.0.1:8765"
endpoint="ws://127.0.0.1:5678"
appkey="appkey"
channel="foo"
rolename="a_role"
rolesecret="a_secret"
filename=${FILENAME:=events.jsonl}
build/cobra_publisher $endpoint $appkey $channel $rolename $rolesecret $filename

View File

@ -1,45 +0,0 @@
/*
* devnull_server.js
* Author: Benjamin Sergeant
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
*/
const WebSocket = require('ws');
let wss = new WebSocket.Server({ port: 5678, perMessageDeflate: true })
wss.on('connection', (ws) => {
let handshake = false
let authenticated = false
ws.on('message', (data) => {
console.log(data.toString('utf-8'))
if (!handshake) {
let response = {
"action": "auth/handshake/ok",
"body": {
"data": {
"nonce": "MTI0Njg4NTAyMjYxMzgxMzgzMg==",
"version": "0.0.24"
}
},
"id": 1
}
ws.send(JSON.stringify(response))
handshake = true
} else if (!authenticated) {
let response = {
"action": "auth/authenticate/ok",
"body": {},
"id": 2
}
ws.send(JSON.stringify(response))
authenticated = true
} else {
console.log(data)
}
});
})

View File

@ -1,43 +0,0 @@
#!/usr/bin/env python
import os
import json
import asyncio
import websockets
async def echo(websocket, path):
handshake = False
authenticated = False
async for message in websocket:
print(message)
if not handshake:
response = {
"action": "auth/handshake/ok",
"body": {
"data": {
"nonce": "MTI0Njg4NTAyMjYxMzgxMzgzMg==",
"version": "0.0.24"
}
},
"id": 1
}
await websocket.send(json.dumps(response))
handshake = True
elif not authenticated:
response = {
"action": "auth/authenticate/ok",
"body": {},
"id": 2
}
await websocket.send(json.dumps(response))
authenticated = True
asyncio.get_event_loop().run_until_complete(
websockets.serve(echo, 'localhost', 5678))
asyncio.get_event_loop().run_forever()

View File

@ -1,3 +0,0 @@
{"array":[1,2,3],"boolean":true,"color":"#82b92c","null":null,"number":123,"object":{"a":"b","c":"d","e":"f"},"string":"Foo"}
{"array":[1,2,3],"boolean":true,"color":"#82b92c","null":null,"number":123,"object":{"a":"b","c":"d","e":"f"},"string":"Bar"}
{"array":[1,2,3],"boolean":true,"color":"#82b92c","null":null,"number":123,"object":{"a":"b","c":"d","e":"f"},"string":"Baz"}

View File

@ -1,333 +0,0 @@
/// Json-cpp amalgated forward header (http://jsoncpp.sourceforge.net/).
/// It is intended to be used with #include "json/json-forwards.h"
/// This header provides forward declaration for all JsonCpp types.
// //////////////////////////////////////////////////////////////////////
// Beginning of content of file: LICENSE
// //////////////////////////////////////////////////////////////////////
/*
The JsonCpp library's source code, including accompanying documentation,
tests and demonstration applications, are licensed under the following
conditions...
Baptiste Lepilleur and The JsonCpp Authors explicitly disclaim copyright in all
jurisdictions which recognize such a disclaimer. In such jurisdictions,
this software is released into the Public Domain.
In jurisdictions which do not recognize Public Domain property (e.g. Germany as of
2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur and
The JsonCpp Authors, and is released under the terms of the MIT License (see below).
In jurisdictions which recognize Public Domain property, the user of this
software may choose to accept it either as 1) Public Domain, 2) under the
conditions of the MIT License (see below), or 3) under the terms of dual
Public Domain/MIT License conditions described here, as they choose.
The MIT License is about as close to Public Domain as a license can get, and is
described in clear, concise terms at:
http://en.wikipedia.org/wiki/MIT_License
The full text of the MIT License follows:
========================================================================
Copyright (c) 2007-2010 Baptiste Lepilleur and The JsonCpp Authors
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.
========================================================================
(END LICENSE TEXT)
The MIT license is compatible with both the GPL and commercial
software, affording one all of the rights of Public Domain with the
minor nuisance of being required to keep the above copyright notice
and license text in the source code. Note also that by accepting the
Public Domain "license" you can re-license your copy using whatever
license you like.
*/
// //////////////////////////////////////////////////////////////////////
// End of content of file: LICENSE
// //////////////////////////////////////////////////////////////////////
#ifndef JSON_FORWARD_AMALGATED_H_INCLUDED
# define JSON_FORWARD_AMALGATED_H_INCLUDED
/// If defined, indicates that the source file is amalgated
/// to prevent private header inclusion.
#define JSON_IS_AMALGAMATION
// //////////////////////////////////////////////////////////////////////
// Beginning of content of file: include/json/config.h
// //////////////////////////////////////////////////////////////////////
// Copyright 2007-2010 Baptiste Lepilleur and The JsonCpp Authors
// Distributed under MIT license, or public domain if desired and
// recognized in your jurisdiction.
// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE
#ifndef JSON_CONFIG_H_INCLUDED
#define JSON_CONFIG_H_INCLUDED
#include <stddef.h>
#include <string> //typedef String
#include <stdint.h> //typedef int64_t, uint64_t
/// If defined, indicates that json library is embedded in CppTL library.
//# define JSON_IN_CPPTL 1
/// If defined, indicates that json may leverage CppTL library
//# define JSON_USE_CPPTL 1
/// If defined, indicates that cpptl vector based map should be used instead of
/// std::map
/// as Value container.
//# define JSON_USE_CPPTL_SMALLMAP 1
// If non-zero, the library uses exceptions to report bad input instead of C
// assertion macros. The default is to use exceptions.
#ifndef JSON_USE_EXCEPTION
#define JSON_USE_EXCEPTION 1
#endif
/// If defined, indicates that the source file is amalgated
/// to prevent private header inclusion.
/// Remarks: it is automatically defined in the generated amalgated header.
// #define JSON_IS_AMALGAMATION
#ifdef JSON_IN_CPPTL
#include <cpptl/config.h>
#ifndef JSON_USE_CPPTL
#define JSON_USE_CPPTL 1
#endif
#endif
#ifdef JSON_IN_CPPTL
#define JSON_API CPPTL_API
#elif defined(JSON_DLL_BUILD)
#if defined(_MSC_VER) || defined(__MINGW32__)
#define JSON_API __declspec(dllexport)
#define JSONCPP_DISABLE_DLL_INTERFACE_WARNING
#endif // if defined(_MSC_VER)
#elif defined(JSON_DLL)
#if defined(_MSC_VER) || defined(__MINGW32__)
#define JSON_API __declspec(dllimport)
#define JSONCPP_DISABLE_DLL_INTERFACE_WARNING
#endif // if defined(_MSC_VER)
#endif // ifdef JSON_IN_CPPTL
#if !defined(JSON_API)
#define JSON_API
#endif
// If JSON_NO_INT64 is defined, then Json only support C++ "int" type for
// integer
// Storages, and 64 bits integer support is disabled.
// #define JSON_NO_INT64 1
#if defined(_MSC_VER) // MSVC
# if _MSC_VER <= 1200 // MSVC 6
// Microsoft Visual Studio 6 only support conversion from __int64 to double
// (no conversion from unsigned __int64).
# define JSON_USE_INT64_DOUBLE_CONVERSION 1
// Disable warning 4786 for VS6 caused by STL (identifier was truncated to '255'
// characters in the debug information)
// All projects I've ever seen with VS6 were using this globally (not bothering
// with pragma push/pop).
# pragma warning(disable : 4786)
# endif // MSVC 6
# if _MSC_VER >= 1500 // MSVC 2008
/// Indicates that the following function is deprecated.
# define JSONCPP_DEPRECATED(message) __declspec(deprecated(message))
# endif
#endif // defined(_MSC_VER)
// In c++11 the override keyword allows you to explicity define that a function
// is intended to override the base-class version. This makes the code more
// managable and fixes a set of common hard-to-find bugs.
#if __cplusplus >= 201103L
# define JSONCPP_OVERRIDE override
# define JSONCPP_NOEXCEPT noexcept
#elif defined(_MSC_VER) && _MSC_VER > 1600 && _MSC_VER < 1900
# define JSONCPP_OVERRIDE override
# define JSONCPP_NOEXCEPT throw()
#elif defined(_MSC_VER) && _MSC_VER >= 1900
# define JSONCPP_OVERRIDE override
# define JSONCPP_NOEXCEPT noexcept
#else
# define JSONCPP_OVERRIDE
# define JSONCPP_NOEXCEPT throw()
#endif
#ifndef JSON_HAS_RVALUE_REFERENCES
#if defined(_MSC_VER) && _MSC_VER >= 1600 // MSVC >= 2010
#define JSON_HAS_RVALUE_REFERENCES 1
#endif // MSVC >= 2010
#ifdef __clang__
#if __has_feature(cxx_rvalue_references)
#define JSON_HAS_RVALUE_REFERENCES 1
#endif // has_feature
#elif defined __GNUC__ // not clang (gcc comes later since clang emulates gcc)
#if defined(__GXX_EXPERIMENTAL_CXX0X__) || (__cplusplus >= 201103L)
#define JSON_HAS_RVALUE_REFERENCES 1
#endif // GXX_EXPERIMENTAL
#endif // __clang__ || __GNUC__
#endif // not defined JSON_HAS_RVALUE_REFERENCES
#ifndef JSON_HAS_RVALUE_REFERENCES
#define JSON_HAS_RVALUE_REFERENCES 0
#endif
#ifdef __clang__
# if __has_extension(attribute_deprecated_with_message)
# define JSONCPP_DEPRECATED(message) __attribute__ ((deprecated(message)))
# endif
#elif defined __GNUC__ // not clang (gcc comes later since clang emulates gcc)
# if (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5))
# define JSONCPP_DEPRECATED(message) __attribute__ ((deprecated(message)))
# elif (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1))
# define JSONCPP_DEPRECATED(message) __attribute__((__deprecated__))
# endif // GNUC version
#endif // __clang__ || __GNUC__
#if !defined(JSONCPP_DEPRECATED)
#define JSONCPP_DEPRECATED(message)
#endif // if !defined(JSONCPP_DEPRECATED)
#if __GNUC__ >= 6
# define JSON_USE_INT64_DOUBLE_CONVERSION 1
#endif
#if !defined(JSON_IS_AMALGAMATION)
# include "version.h"
# if JSONCPP_USING_SECURE_MEMORY
# include "allocator.h" //typedef Allocator
# endif
#endif // if !defined(JSON_IS_AMALGAMATION)
namespace Json {
typedef int Int;
typedef unsigned int UInt;
#if defined(JSON_NO_INT64)
typedef int LargestInt;
typedef unsigned int LargestUInt;
#undef JSON_HAS_INT64
#else // if defined(JSON_NO_INT64)
// For Microsoft Visual use specific types as long long is not supported
#if defined(_MSC_VER) // Microsoft Visual Studio
typedef __int64 Int64;
typedef unsigned __int64 UInt64;
#else // if defined(_MSC_VER) // Other platforms, use long long
typedef int64_t Int64;
typedef uint64_t UInt64;
#endif // if defined(_MSC_VER)
typedef Int64 LargestInt;
typedef UInt64 LargestUInt;
#define JSON_HAS_INT64
#endif // if defined(JSON_NO_INT64)
#if JSONCPP_USING_SECURE_MEMORY
#define JSONCPP_STRING std::basic_string<char, std::char_traits<char>, Json::SecureAllocator<char> >
#define JSONCPP_OSTRINGSTREAM std::basic_ostringstream<char, std::char_traits<char>, Json::SecureAllocator<char> >
#define JSONCPP_OSTREAM std::basic_ostream<char, std::char_traits<char>>
#define JSONCPP_ISTRINGSTREAM std::basic_istringstream<char, std::char_traits<char>, Json::SecureAllocator<char> >
#define JSONCPP_ISTREAM std::istream
#else
#define JSONCPP_STRING std::string
#define JSONCPP_OSTRINGSTREAM std::ostringstream
#define JSONCPP_OSTREAM std::ostream
#define JSONCPP_ISTRINGSTREAM std::istringstream
#define JSONCPP_ISTREAM std::istream
#endif // if JSONCPP_USING_SECURE_MEMORY
} // end namespace Json
#endif // JSON_CONFIG_H_INCLUDED
// //////////////////////////////////////////////////////////////////////
// End of content of file: include/json/config.h
// //////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////
// Beginning of content of file: include/json/forwards.h
// //////////////////////////////////////////////////////////////////////
// Copyright 2007-2010 Baptiste Lepilleur and The JsonCpp Authors
// Distributed under MIT license, or public domain if desired and
// recognized in your jurisdiction.
// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE
#ifndef JSON_FORWARDS_H_INCLUDED
#define JSON_FORWARDS_H_INCLUDED
#if !defined(JSON_IS_AMALGAMATION)
#include "config.h"
#endif // if !defined(JSON_IS_AMALGAMATION)
namespace Json {
// writer.h
class FastWriter;
class StyledWriter;
// reader.h
class Reader;
// features.h
class Features;
// value.h
typedef unsigned int ArrayIndex;
class StaticString;
class Path;
class PathArgument;
class Value;
class ValueIteratorBase;
class ValueIterator;
class ValueConstIterator;
} // namespace Json
#endif // JSON_FORWARDS_H_INCLUDED
// //////////////////////////////////////////////////////////////////////
// End of content of file: include/json/forwards.h
// //////////////////////////////////////////////////////////////////////
#endif //ifndef JSON_FORWARD_AMALGATED_H_INCLUDED

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,11 @@
#include "IXHMac.h"
#include "IXBase64.h"
#include <openssl/hmac.h>
#ifdef __APPLE__
# include <CommonCrypto/CommonHMAC.h>
#else
# include <openssl/hmac.h>
#endif
namespace ix
{
@ -15,10 +19,17 @@ namespace ix
constexpr size_t hashSize = 16;
unsigned char hash[hashSize];
#ifdef __APPLE__
CCHmac(kCCHmacAlgMD5,
key.c_str(), key.size(),
data.c_str(), data.size(),
&hash);
#else
HMAC(EVP_md5(),
key.c_str(), (int) key.size(),
(unsigned char *) data.c_str(), (int) data.size(),
(unsigned char *) hash, nullptr);
#endif
std::string hashString(reinterpret_cast<char*>(hash), hashSize);

View File

@ -8,9 +8,9 @@
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
},
"ws": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.0.tgz",
"integrity": "sha512-H3dGVdGvW2H8bnYpIDc3u3LH8Wue3Qh+Zto6aXXFzvESkTVT6rAfKR6tR/+coaUvxs8yHtmNV0uioBF62ZGSTg==",
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.0.tgz",
"integrity": "sha512-deZYUNlt2O4buFCa3t5bKLf8A7FPP/TVjwOeVNpw818Ma5nk4MLXls2eoEGS39o8119QIYxTrTDoPQ5B/gTD6w==",
"requires": {
"async-limiter": "1.0.0"
}

View File

@ -1,11 +1,21 @@
#!/bin/sh
# Handle Ctrl-C by killing all sub-processing AND exiting
trap cleanup INT
function cleanup {
kill `cat /tmp/ws_test/pidfile.transfer`
kill `cat /tmp/ws_test/pidfile.receive`
kill `cat /tmp/ws_test/pidfile.send`
exit 1
}
rm -rf /tmp/ws_test
mkdir -p /tmp/ws_test
# Start a transport server
cd /tmp/ws_test
ws transfer --port 8090 --pidfile /tmp/ws_test/pidfile &
ws transfer --port 8090 --pidfile /tmp/ws_test/pidfile.transfer &
# Wait until the transfer server is up
while true
@ -14,21 +24,21 @@ do
echo "Transfer server up and running"
break
}
echo "sleep ..."
echo "sleep ... wait for transfer server"
sleep 0.1
done
# Start a receiver
mkdir -p /tmp/ws_test/receive
cd /tmp/ws_test/receive
ws receive --delay 5 ws://127.0.0.1:8090 &
ws receive --delay 10 ws://127.0.0.1:8090 --pidfile /tmp/ws_test/pidfile.receive &
mkdir /tmp/ws_test/send
cd /tmp/ws_test/send
dd if=/dev/urandom of=20M_file count=20000 bs=1024
# Start the sender job
ws send ws://127.0.0.1:8090 20M_file
ws send --pidfile /tmp/ws_test/pidfile.send ws://127.0.0.1:8090 20M_file
# Wait until the file has been written to disk
while true
@ -37,7 +47,7 @@ do
echo "Received file does exists, exiting loop"
break
fi
echo "sleep ..."
echo "sleep ... wait for output file"
sleep 0.1
done
@ -48,4 +58,7 @@ cksum /tmp/ws_test/receive/20M_file
sleep 2
# Cleanup
kill `cat /tmp/ws_test/pidfile`
kill `cat /tmp/ws_test/pidfile.transfer`
kill `cat /tmp/ws_test/pidfile.receive`
kill `cat /tmp/ws_test/pidfile.send`

25
ws/test_ws_redis.sh Normal file
View File

@ -0,0 +1,25 @@
#!/bin/sh
# Handle Ctrl-C by killing all sub-processing AND exiting
trap cleanup INT
function cleanup {
kill `cat /tmp/pidfile.subscribe`
exit 1
}
REDIS_HOST=${REDIS_HOST:=localhost}
ws redis_subscribe --pidfile /tmp/pidfile.subscribe --host $REDIS_HOST foo &
# Wait for the subscriber to be ready
sleep 0.5
# Now publish messages
ws redis_publish -c 100000 --host ${REDIS_HOST} foo bar
# Wait a little for all messages to be received
sleep 1.5
# Cleanup
cleanup

118
ws/ws.cpp
View File

@ -35,26 +35,43 @@ int main(int argc, char** argv)
std::string output;
std::string hostname("127.0.0.1");
std::string pidfile;
std::string channel;
std::string message;
std::string password;
std::string appkey;
std::string endpoint;
std::string rolename;
std::string rolesecret;
std::string prefix("ws.test.v0");
std::string fields;
std::string dsn;
bool headersOnly = false;
bool followRedirects = false;
bool verbose = false;
bool save = false;
bool compress = false;
bool strict = false;
int port = 8080;
int redisPort = 6379;
int statsdPort = 8125;
int connectTimeOut = 60;
int transferTimeout = 1800;
int maxRedirects = 5;
int delayMs = -1;
int count = 1;
int jobs = 4;
CLI::App* sendApp = app.add_subcommand("send", "Send a file");
sendApp->add_option("url", url, "Connection url")->required();
sendApp->add_option("path", path, "Path to the file to send")
->required()->check(CLI::ExistingPath);
sendApp->add_option("--pidfile", pidfile, "Pid file");
CLI::App* receiveApp = app.add_subcommand("receive", "Receive a file");
receiveApp->add_option("url", url, "Connection url")->required();
receiveApp->add_option("--delay", delayMs, "Delay (ms) to wait after receiving a fragment"
" to artificially slow down the receiver");
receiveApp->add_option("--pidfile", pidfile, "Pid file");
CLI::App* transferApp = app.add_subcommand("transfer", "Broadcasting server");
transferApp->add_option("--port", port, "Connection url");
@ -94,21 +111,71 @@ int main(int argc, char** argv)
httpClientApp->add_option("--connect-timeout", connectTimeOut, "Connection timeout");
httpClientApp->add_option("--transfer-timeout", transferTimeout, "Transfer timeout");
CLI::App* redisPublishApp = app.add_subcommand("redis_publish", "Redis publisher");
redisPublishApp->add_option("--port", redisPort, "Port");
redisPublishApp->add_option("--host", hostname, "Hostname");
redisPublishApp->add_option("--password", password, "Password");
redisPublishApp->add_option("channel", channel, "Channel")->required();
redisPublishApp->add_option("message", message, "Message")->required();
redisPublishApp->add_option("-c", count, "Count");
CLI::App* redisSubscribeApp = app.add_subcommand("redis_subscribe", "Redis subscriber");
redisSubscribeApp->add_option("--port", redisPort, "Port");
redisSubscribeApp->add_option("--host", hostname, "Hostname");
redisSubscribeApp->add_option("--password", password, "Password");
redisSubscribeApp->add_option("channel", channel, "Channel")->required();
redisSubscribeApp->add_flag("-v", verbose, "Verbose");
redisSubscribeApp->add_option("--pidfile", pidfile, "Pid file");
CLI::App* cobraSubscribeApp = app.add_subcommand("cobra_subscribe", "Cobra subscriber");
cobraSubscribeApp->add_option("--appkey", appkey, "Appkey");
cobraSubscribeApp->add_option("--endpoint", endpoint, "Endpoint");
cobraSubscribeApp->add_option("--rolename", rolename, "Role name");
cobraSubscribeApp->add_option("--rolesecret", rolesecret, "Role secret");
cobraSubscribeApp->add_option("channel", channel, "Channel")->required();
cobraSubscribeApp->add_flag("-v", verbose, "Verbose");
cobraSubscribeApp->add_option("--pidfile", pidfile, "Pid file");
CLI::App* cobra2statsd = app.add_subcommand("cobra_to_statsd", "Cobra to statsd");
cobra2statsd->add_option("--appkey", appkey, "Appkey");
cobra2statsd->add_option("--endpoint", endpoint, "Endpoint");
cobra2statsd->add_option("--rolename", rolename, "Role name");
cobra2statsd->add_option("--rolesecret", rolesecret, "Role secret");
cobra2statsd->add_option("--host", hostname, "Statsd host");
cobra2statsd->add_option("--port", statsdPort, "Statsd port");
cobra2statsd->add_option("--prefix", prefix, "Statsd prefix");
cobra2statsd->add_option("--fields", fields, "Extract fields for naming the event")->join();
cobra2statsd->add_option("channel", channel, "Channel")->required();
cobra2statsd->add_flag("-v", verbose, "Verbose");
cobra2statsd->add_option("--pidfile", pidfile, "Pid file");
CLI::App* cobra2sentry = app.add_subcommand("cobra_to_sentry", "Cobra to sentry");
cobra2sentry->add_option("--appkey", appkey, "Appkey");
cobra2sentry->add_option("--endpoint", endpoint, "Endpoint");
cobra2sentry->add_option("--rolename", rolename, "Role name");
cobra2sentry->add_option("--rolesecret", rolesecret, "Role secret");
cobra2sentry->add_option("--dsn", dsn, "Sentry DSN");
cobra2sentry->add_option("--jobs", jobs, "Number of thread sending events to Sentry");
cobra2sentry->add_option("channel", channel, "Channel")->required();
cobra2sentry->add_flag("-v", verbose, "Verbose");
cobra2sentry->add_flag("-s", strict, "Strict mode. Error out when sending to sentry fails");
cobra2sentry->add_option("--pidfile", pidfile, "Pid file");
CLI11_PARSE(app, argc, argv);
// pid file handling
if (!pidfile.empty())
{
unlink(pidfile.c_str());
std::ofstream f;
f.open(pidfile);
f << getpid();
f.close();
}
if (app.got_subcommand("transfer"))
{
// pid file handling
if (!pidfile.empty())
{
unlink(pidfile.c_str());
std::ofstream f;
f.open(pidfile);
f << getpid();
f.close();
}
return ix::ws_transfer_main(port, hostname);
}
else if (app.got_subcommand("send"))
@ -147,6 +214,35 @@ int main(int argc, char** argv)
followRedirects, maxRedirects, verbose,
save, output, compress);
}
else if (app.got_subcommand("redis_publish"))
{
return ix::ws_redis_publish_main(hostname, redisPort, password,
channel, message, count);
}
else if (app.got_subcommand("redis_subscribe"))
{
return ix::ws_redis_subscribe_main(hostname, redisPort, password, channel, verbose);
}
else if (app.got_subcommand("cobra_subscribe"))
{
return ix::ws_cobra_subscribe_main(appkey, endpoint,
rolename, rolesecret,
channel, verbose);
}
else if (app.got_subcommand("cobra_to_statsd"))
{
return ix::ws_cobra_to_statsd_main(appkey, endpoint,
rolename, rolesecret,
channel, hostname, statsdPort,
prefix, fields, verbose);
}
else if (app.got_subcommand("cobra_to_sentry"))
{
return ix::ws_cobra_to_sentry_main(appkey, endpoint,
rolename, rolesecret,
channel, dsn,
verbose, strict, jobs);
}
return 1;
}

41
ws/ws.h
View File

@ -39,4 +39,45 @@ namespace ix
int ws_send_main(const std::string& url,
const std::string& path);
int ws_redis_publish_main(const std::string& hostname,
int port,
const std::string& password,
const std::string& channel,
const std::string& message,
int count);
int ws_redis_subscribe_main(const std::string& hostname,
int port,
const std::string& password,
const std::string& channel,
bool verbose);
int ws_cobra_subscribe_main(const std::string& appkey,
const std::string& endpoint,
const std::string& rolename,
const std::string& rolesecret,
const std::string& channel,
bool verbose);
int ws_cobra_to_statsd_main(const std::string& appkey,
const std::string& endpoint,
const std::string& rolename,
const std::string& rolesecret,
const std::string& channel,
const std::string& host,
int port,
const std::string& prefix,
const std::string& fields,
bool verbose);
int ws_cobra_to_sentry_main(const std::string& appkey,
const std::string& endpoint,
const std::string& rolename,
const std::string& rolesecret,
const std::string& channel,
const std::string& dsn,
bool verbose,
bool strict,
int jobs);
}

View File

@ -17,10 +17,11 @@ namespace ix
ix::WebSocketServer server(port, hostname);
server.setOnConnectionCallback(
[&server](std::shared_ptr<ix::WebSocket> webSocket)
[&server](std::shared_ptr<WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState)
{
webSocket->setOnMessageCallback(
[webSocket, &server](ix::WebSocketMessageType messageType,
[webSocket, connectionState, &server](ix::WebSocketMessageType messageType,
const std::string& str,
size_t wireSize,
const ix::WebSocketErrorInfo& error,
@ -30,6 +31,7 @@ namespace ix
if (messageType == ix::WebSocket_MessageType_Open)
{
std::cerr << "New connection" << std::endl;
std::cerr << "id: " << connectionState->getId() << std::endl;
std::cerr << "Uri: " << openInfo.uri << std::endl;
std::cerr << "Headers:" << std::endl;
for (auto it : openInfo.headers)

76
ws/ws_cobra_subscribe.cpp Normal file
View File

@ -0,0 +1,76 @@
/*
* ws_cobra_subscribe.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
*/
#include <iostream>
#include <sstream>
#include <chrono>
#include <thread>
#include <atomic>
#include "IXCobraConnection.h"
namespace ix
{
int ws_cobra_subscribe_main(const std::string& appkey,
const std::string& endpoint,
const std::string& rolename,
const std::string& rolesecret,
const std::string& channel,
bool verbose)
{
ix::CobraConnection conn;
conn.configure(appkey, endpoint,
rolename, rolesecret,
ix::WebSocketPerMessageDeflateOptions(true));
conn.connect();
Json::FastWriter jsonWriter;
conn.setEventCallback(
[&conn, &channel, &jsonWriter]
(ix::CobraConnectionEventType eventType,
const std::string& errMsg,
const ix::WebSocketHttpHeaders& headers,
const std::string& subscriptionId)
{
if (eventType == ix::CobraConnection_EventType_Open)
{
std::cout << "Subscriber: connected" << std::endl;
}
else if (eventType == ix::CobraConnection_EventType_Authenticated)
{
std::cout << "Subscriber authenticated" << std::endl;
conn.subscribe(channel,
[&jsonWriter](const Json::Value& msg)
{
// std::cout << "Received message" << std::endl;
std::cout << jsonWriter.write(msg) << std::endl;
});
}
else if (eventType == ix::CobraConnection_EventType_Subscribed)
{
std::cout << "Subscriber: subscribed to channel " << subscriptionId << std::endl;
}
else if (eventType == ix::CobraConnection_EventType_UnSubscribed)
{
std::cout << "Subscriber: unsubscribed from channel " << subscriptionId << std::endl;
}
else if (eventType == ix::CobraConnection_EventType_Error)
{
std::cout << "Subscriber: error" << errMsg << std::endl;
}
}
);
while (true)
{
std::chrono::duration<double, std::milli> duration(10);
std::this_thread::sleep_for(duration);
}
return 0;
}
}

189
ws/ws_cobra_to_sentry.cpp Normal file
View File

@ -0,0 +1,189 @@
/*
* ws_cobra_to_sentry.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
*/
#include <iostream>
#include <sstream>
#include <chrono>
#include <thread>
#include <atomic>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include "IXCobraConnection.h"
#include "IXSentryClient.h"
namespace ix
{
int ws_cobra_to_sentry_main(const std::string& appkey,
const std::string& endpoint,
const std::string& rolename,
const std::string& rolesecret,
const std::string& channel,
const std::string& dsn,
bool verbose,
bool strict,
int jobs)
{
ix::CobraConnection conn;
conn.configure(appkey, endpoint,
rolename, rolesecret,
ix::WebSocketPerMessageDeflateOptions(true));
conn.connect();
Json::FastWriter jsonWriter;
std::atomic<uint64_t> sentCount(0);
std::atomic<uint64_t> receivedCount(0);
std::atomic<bool> errorSending(false);
std::atomic<bool> stop(false);
std::mutex conditionVariableMutex;
std::condition_variable condition;
std::condition_variable progressCondition;
std::queue<Json::Value> queue;
auto sentrySender = [&condition, &progressCondition, &conditionVariableMutex,
&queue, verbose, &errorSending, &sentCount,
&stop, &dsn]
{
while (true)
{
Json::Value msg;
{
std::unique_lock<std::mutex> lock(conditionVariableMutex);
condition.wait(lock, [&queue, &stop]{ return !queue.empty() && !stop; });
msg = queue.front();
queue.pop();
}
SentryClient sc(dsn);
if (!sc.send(msg, verbose))
{
errorSending = true;
}
else
{
++sentCount;
}
progressCondition.notify_one();
if (stop) return;
}
};
// Create a thread pool
std::cerr << "Starting " << jobs << " sentry sender jobs" << std::endl;
std::vector<std::thread> pool;
for (int i = 0; i < jobs; i++)
{
pool.push_back(std::thread(sentrySender));
}
conn.setEventCallback(
[&conn, &channel, &jsonWriter,
verbose, &receivedCount, &sentCount,
&condition, &conditionVariableMutex,
&progressCondition, &queue]
(ix::CobraConnectionEventType eventType,
const std::string& errMsg,
const ix::WebSocketHttpHeaders& headers,
const std::string& subscriptionId)
{
if (eventType == ix::CobraConnection_EventType_Open)
{
std::cerr << "Subscriber: connected" << std::endl;
for (auto it : headers)
{
std::cerr << it.first << ": " << it.second << std::endl;
}
}
if (eventType == ix::CobraConnection_EventType_Closed)
{
std::cerr << "Subscriber: closed" << std::endl;
}
else if (eventType == ix::CobraConnection_EventType_Authenticated)
{
std::cerr << "Subscriber authenticated" << std::endl;
conn.subscribe(channel,
[&jsonWriter, verbose,
&sentCount, &receivedCount,
&condition, &conditionVariableMutex,
&progressCondition, &queue]
(const Json::Value& msg)
{
if (verbose)
{
std::cerr << jsonWriter.write(msg) << std::endl;
}
// If we cannot send to sentry fast enough, drop the message
const uint64_t scaleFactor = 2;
if (sentCount != 0 &&
receivedCount != 0 &&
(sentCount * scaleFactor < receivedCount))
{
std::cerr << "message dropped: sending is backlogged !"
<< std::endl;
condition.notify_one();
progressCondition.notify_one();
return;
}
++receivedCount;
{
std::unique_lock<std::mutex> lock(conditionVariableMutex);
queue.push(msg);
}
condition.notify_one();
progressCondition.notify_one();
});
}
else if (eventType == ix::CobraConnection_EventType_Subscribed)
{
std::cerr << "Subscriber: subscribed to channel " << subscriptionId << std::endl;
}
else if (eventType == ix::CobraConnection_EventType_UnSubscribed)
{
std::cerr << "Subscriber: unsubscribed from channel " << subscriptionId << std::endl;
}
else if (eventType == ix::CobraConnection_EventType_Error)
{
std::cerr << "Subscriber: error" << errMsg << std::endl;
}
}
);
std::mutex progressConditionVariableMutex;
while (true)
{
std::unique_lock<std::mutex> lock(progressConditionVariableMutex);
progressCondition.wait(lock);
std::cout << "messages"
<< " received " << receivedCount
<< " sent " << sentCount
<< std::endl;
if (strict && errorSending) break;
}
conn.disconnect();
// FIXME: join all the bg threads and stop them.
return 0;
}
}

148
ws/ws_cobra_to_statsd.cpp Normal file
View File

@ -0,0 +1,148 @@
/*
* ws_cobra_to_statsd.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
*/
#include <iostream>
#include <sstream>
#include <chrono>
#include <thread>
#include <atomic>
#include <vector>
#include "IXCobraConnection.h"
#include <statsd_client.h>
namespace ix
{
// fields are command line argument that can be specified multiple times
std::vector<std::string> parseFields(const std::string& fields)
{
std::vector<std::string> tokens;
// Split by \n
std::string token;
std::stringstream tokenStream(fields);
while (std::getline(tokenStream, token))
{
tokens.push_back(token);
}
return tokens;
}
//
// Extract an attribute from a Json Value.
// extractAttr("foo.bar", {"foo": {"bar": "baz"}}) => baz
//
std::string extractAttr(const std::string& attr,
const Json::Value& jsonValue)
{
// Split by .
std::string token;
std::stringstream tokenStream(attr);
Json::Value val(jsonValue);
int i = 0;
while (std::getline(tokenStream, token, '.'))
{
val = val[token];
}
return val.asString();
}
int ws_cobra_to_statsd_main(const std::string& appkey,
const std::string& endpoint,
const std::string& rolename,
const std::string& rolesecret,
const std::string& channel,
const std::string& host,
int port,
const std::string& prefix,
const std::string& fields,
bool verbose)
{
ix::CobraConnection conn;
conn.configure(appkey, endpoint,
rolename, rolesecret,
ix::WebSocketPerMessageDeflateOptions(true));
conn.connect();
auto tokens = parseFields(fields);
// statsd client
// test with netcat as a server: `nc -ul 8125`
bool statsdBatch = true;
statsd::StatsdClient statsdClient(host, port, prefix, statsdBatch);
Json::FastWriter jsonWriter;
uint64_t msgCount = 0;
conn.setEventCallback(
[&conn, &channel, &jsonWriter, &statsdClient, verbose, &tokens, &prefix, &msgCount]
(ix::CobraConnectionEventType eventType,
const std::string& errMsg,
const ix::WebSocketHttpHeaders& headers,
const std::string& subscriptionId)
{
if (eventType == ix::CobraConnection_EventType_Open)
{
std::cout << "Subscriber: connected" << std::endl;
}
if (eventType == ix::CobraConnection_EventType_Closed)
{
std::cout << "Subscriber: closed" << std::endl;
}
else if (eventType == ix::CobraConnection_EventType_Authenticated)
{
std::cout << "Subscriber authenticated" << std::endl;
conn.subscribe(channel,
[&jsonWriter, &statsdClient, &channel,
verbose, &tokens, &prefix, &msgCount]
(const Json::Value& msg)
{
if (verbose)
{
std::cout << jsonWriter.write(msg) << std::endl;
}
std::string id;
for (auto&& attr : tokens)
{
id += ".";
id += extractAttr(attr, msg);
}
std::cout << msgCount++ << " " << prefix << id << std::endl;
statsdClient.count(id, 1);
});
}
else if (eventType == ix::CobraConnection_EventType_Subscribed)
{
std::cout << "Subscriber: subscribed to channel " << subscriptionId << std::endl;
}
else if (eventType == ix::CobraConnection_EventType_UnSubscribed)
{
std::cout << "Subscriber: unsubscribed from channel " << subscriptionId << std::endl;
}
else if (eventType == ix::CobraConnection_EventType_Error)
{
std::cout << "Subscriber: error" << errMsg << std::endl;
}
}
);
while (true)
{
std::chrono::duration<double, std::milli> duration(1000);
std::this_thread::sleep_for(duration);
}
return 0;
}
}

View File

@ -17,10 +17,11 @@ namespace ix
ix::WebSocketServer server(port, hostname);
server.setOnConnectionCallback(
[](std::shared_ptr<ix::WebSocket> webSocket)
[](std::shared_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState)
{
webSocket->setOnMessageCallback(
[webSocket](ix::WebSocketMessageType messageType,
[webSocket, connectionState](ix::WebSocketMessageType messageType,
const std::string& str,
size_t wireSize,
const ix::WebSocketErrorInfo& error,
@ -30,6 +31,7 @@ namespace ix
if (messageType == ix::WebSocket_MessageType_Open)
{
std::cerr << "New connection" << std::endl;
std::cerr << "id: " << connectionState->getId() << std::endl;
std::cerr << "Uri: " << openInfo.uri << std::endl;
std::cerr << "Headers:" << std::endl;
for (auto it : openInfo.headers)

View File

@ -231,6 +231,7 @@ namespace ix
}
else if (messageType == ix::WebSocket_MessageType_Error)
{
ss << "ws_receive ";
ss << "Connection error: " << error.reason << std::endl;
ss << "#retries: " << error.retries << std::endl;
ss << "Wait time(ms): " << error.wait_time << std::endl;

52
ws/ws_redis_publish.cpp Normal file
View File

@ -0,0 +1,52 @@
/*
* ws_redis_publish.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
*/
#include <iostream>
#include <sstream>
#include "IXRedisClient.h"
namespace ix
{
int ws_redis_publish_main(const std::string& hostname,
int port,
const std::string& password,
const std::string& channel,
const std::string& message,
int count)
{
RedisClient redisClient;
if (!redisClient.connect(hostname, port))
{
std::cerr << "Cannot connect to redis host" << std::endl;
return 1;
}
if (!password.empty())
{
std::string authResponse;
if (!redisClient.auth(password, authResponse))
{
std::stringstream ss;
std::cerr << "Cannot authenticated to redis" << std::endl;
return 1;
}
std::cout << "Auth response: " << authResponse << ":" << port << std::endl;
}
for (int i = 0; i < count; i++)
{
//std::cerr << "Publishing message " << message
// << " to " << channel << "..." << std::endl;
if (!redisClient.publish(channel, message))
{
std::cerr << "Error publishing to channel " << channel << std::endl;
return 1;
}
}
return 0;
}
}

86
ws/ws_redis_subscribe.cpp Normal file
View File

@ -0,0 +1,86 @@
/*
* ws_redis_subscribe.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
*/
#include <iostream>
#include <sstream>
#include <chrono>
#include <thread>
#include <atomic>
#include "IXRedisClient.h"
namespace ix
{
int ws_redis_subscribe_main(const std::string& hostname,
int port,
const std::string& password,
const std::string& channel,
bool verbose)
{
RedisClient redisClient;
if (!redisClient.connect(hostname, port))
{
std::cerr << "Cannot connect to redis host" << std::endl;
return 1;
}
if (!password.empty())
{
std::string authResponse;
if (!redisClient.auth(password, authResponse))
{
std::stringstream ss;
std::cerr << "Cannot authenticated to redis" << std::endl;
return 1;
}
std::cout << "Auth response: " << authResponse << ":" << port << std::endl;
}
std::atomic<int> msgPerSeconds(0);
std::atomic<int> msgCount(0);
auto callback = [&msgPerSeconds, &msgCount, verbose]
(const std::string& message)
{
if (verbose)
{
std::cout << "received: " << message << std::endl;
}
msgPerSeconds++;
msgCount++;
};
auto responseCallback = [](const std::string& redisResponse)
{
std::cout << "Redis subscribe response: " << redisResponse << std::endl;
};
auto timer = [&msgPerSeconds, &msgCount]
{
while (true)
{
std::cout << "#messages " << msgCount << " "
<< "msg/s " << msgPerSeconds
<< std::endl;
msgPerSeconds = 0;
auto duration = std::chrono::seconds(1);
std::this_thread::sleep_for(duration);
}
};
std::thread t(timer);
std::cerr << "Subscribing to " << channel << "..." << std::endl;
if (!redisClient.subscribe(channel, responseCallback, callback))
{
std::cerr << "Error subscribing to channel " << channel << std::endl;
return 1;
}
return 0;
}
}

View File

@ -162,6 +162,7 @@ namespace ix
}
else if (messageType == ix::WebSocket_MessageType_Error)
{
ss << "ws_send ";
ss << "Connection error: " << error.reason << std::endl;
ss << "#retries: " << error.retries << std::endl;
ss << "Wait time(ms): " << error.wait_time << std::endl;

View File

@ -17,10 +17,11 @@ namespace ix
ix::WebSocketServer server(port, hostname);
server.setOnConnectionCallback(
[&server](std::shared_ptr<ix::WebSocket> webSocket)
[&server](std::shared_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState)
{
webSocket->setOnMessageCallback(
[webSocket, &server](ix::WebSocketMessageType messageType,
[webSocket, connectionState, &server](ix::WebSocketMessageType messageType,
const std::string& str,
size_t wireSize,
const ix::WebSocketErrorInfo& error,
@ -30,6 +31,7 @@ namespace ix
if (messageType == ix::WebSocket_MessageType_Open)
{
std::cerr << "New connection" << std::endl;
std::cerr << "id: " << connectionState->getId() << std::endl;
std::cerr << "Uri: " << openInfo.uri << std::endl;
std::cerr << "Headers:" << std::endl;
for (auto it : openInfo.headers)