Compare commits

...

110 Commits

Author SHA1 Message Date
d0562664ad (http code) With zlib disabled, some code should not be reached 2020-10-19 13:37:42 -07:00
d9b4beff8b Fix an issue with disabling zlib and getting linker errors from the http client. (#247)
* (http client) #ifdefs so we dont try to compress http requests with zlib

* (http client) Remove some #ifdefs for including zlib and removing fields
2020-10-19 13:36:04 -07:00
b2f21840c6 (ws curl) Add support for --data-binary option, to set the request body. When present the request will be sent with the POST verb 2020-10-12 14:03:01 -07:00
67cb48537a (http client + server + ws) Add support for compressing http client requests with gzip. --compress_request argument is used in ws to enable this. The Content-Encoding is set to gzip, and decoded on the server side if present. 2020-10-09 17:51:56 -07:00
fa0408e70b (http client + server + ws) Add support for uploading files with ws -F foo=@filename, new -D http server option to debug incoming client requests, internal api changed for http POST, PUT and PATCH to supply an HttpFormDataParameters 2020-10-08 12:43:18 -07:00
032ed9af9c IXExponentialBackoff.cpp: fix typo in source code file name in the header block 2020-10-05 10:39:11 -07:00
dc84080401 Add support for gzip compression through libdeflate 2020-09-30 14:34:03 -07:00
82e759732b (cmake) Stop using FetchContent cmake module to retrieve jsoncpp third party dependency 2020-09-30 14:24:04 -07:00
61dbcc2b84 fix docker and linux build 2020-09-28 11:56:49 -07:00
e61680ff0f linux build fix about memset not being found 2020-09-28 11:01:59 -07:00
6f188a5131 (ws) add gzip and gunzip ws sub commands 2020-09-28 10:19:27 -07:00
6077f86af8 (cmake) use FetchContent cmake module to retrieve jsoncpp third party dependency 2020-09-26 14:11:40 -07:00
93167e3917 cmake / move FetchContent spdlog to a single place 2020-09-26 13:55:03 -07:00
2526a94454 (cmake) use FetchContent cmake module to retrieve spdlog third party dependency 2020-09-26 13:51:19 -07:00
97cc543e53 (cobra connection) retrieve cobra server connection id from the cobra handshake message and display it in ws clients, metrics publisher and bots 2020-09-22 09:30:19 -07:00
62d220f49a (cobra 2 cobra) specify as an HTTP header which channel we will republish to 2020-09-22 08:55:21 -07:00
49995e32f0 (cobra bots) change an error log to a warning log when reconnecting because no messages were received for a minute 2020-09-18 15:25:10 -07:00
d525c28907 (cobra connection and bots) set an HTTP header when connecting to help with debugging bots 2020-09-18 15:11:20 -07:00
39c84c7d51 Rename HttpResponse's payload to body (#245)
* rename payload to body

* Fixed ws cmd line tool to use the renamed body

Co-authored-by: Jay <jasoncarr@Jasons-MacBook-Pro.local>
2020-09-12 19:01:37 -07:00
128bc0afa9 (http server) read body request when the Content-Length is specified + set timeout to read the request to 30 seconds max by default, and make it configurable as a constructor parameter 2020-09-12 14:17:06 -07:00
b04e5c5529 http server: use socket->readBytes which reads in bulk instead of N calls to socket->readByte 2020-09-12 14:09:25 -07:00
1e8c421d66 formatting 2020-09-12 13:55:27 -07:00
72d6651ded Read body in parseRequest for HttpServer (#244)
Co-authored-by: Jay <jasoncarr@Jasons-MacBook-Pro.local>
2020-09-12 13:53:56 -07:00
a4e5d1b47a (ws) autoroute command exit on its own once all messages have been received 2020-09-09 18:01:38 -07:00
9f51a54a83 (docker) ws docker file installs strace 2020-09-04 13:47:12 -07:00
b74f7319c6 add a note to the readme about the fact that the MinGW compiler is not supported. close #242 2020-09-03 13:50:46 -07:00
0ad66a27f2 Fix ws/ws.cpp:2875:10: warning: unused variable noSend [-Wunused-variable] 2020-09-03 09:17:52 -07:00
a40003e85a (ws) echo_client command renamed to autoroute. Command exit once the server close the connection. push_server commands exit once N messages have been sent. 2020-09-03 09:13:23 -07:00
5534a7fdf9 add a github action to publish a docker container for ws 2020-09-02 11:52:59 -07:00
efb245278d unittest / switch from using the REQUIRE macro, which halts (and usually crash) the test to the CHECK macro in IXWebSocketChatTest.cpp 2020-08-31 13:56:45 -07:00
5896d3740f (ws + cobra bots) add a cobra_to_cobra ws subcommand to subscribe to a channel and republish received events to a different channel 2020-08-31 13:45:00 -07:00
73b9c0b89b (socket servers) merge the ConnectionInfo class with the ConnectionState one, which simplify all the server apis 2020-08-28 14:55:40 -07:00
629c155044 (ws) fix silly compile error (missing ix:: namespace) 2020-08-26 14:30:58 -07:00
08640d877f (ws) set the main thread name, to help with debugging in XCode, gdb, lldb etc... 2020-08-26 13:38:45 -07:00
ed5c63144e (ws) cobra to python bot / take a module python name as argument foo.bar.baz instead of a path foo/bar/baz.py 2020-08-19 10:00:00 -07:00
ee69aed2b0 (ws) on Linux with mbedtls, when the system ca certs are specified (the default) pick up sensible OS supplied paths (tested with CentOS and Alpine) 2020-08-19 09:31:57 -07:00
fcb92f862d (ws push_server) on the server side, stop sending and close the connection when the remote end has disconnected 2020-08-18 14:09:27 -07:00
e8e98e667d add ruby websocket bencharking code using
faye-websocket-ruby to receive messages as fast as possible
2020-08-18 13:45:53 -07:00
e1502017ce (ixwebsocket) replace std::unique_ptr<unsigned char[]> with std::array for some fixed arrays (which are in C++11) 2020-08-17 16:48:26 -07:00
72472f2899 IXWebSocketPerMessageDeflateCodec: use std::array instead of std::unique_ptr for a fixed size array 2020-08-17 16:36:24 -07:00
42f71364ca IXHttpClient.cpp: use std::array instead of std::unique_ptr for a fixed size array 2020-08-17 16:25:55 -07:00
3dabd3a556 (ws) merge all ws_*.cpp files into a single one to speedup compilation 2020-08-15 19:30:17 -07:00
0498e2fa98 IXBench.h is missing a pragma once 2020-08-15 18:58:46 -07:00
2aaf59651e (socket server) in the loop accepting connections, call select without a timeout on unix to avoid busy looping, and only wake up when a new connection happens 2020-08-15 18:32:59 -07:00
cd4e51eacf (socket server) instead of busy looping with a sleep, only wake up the GC thread when a new thread will have to be joined, (we know that thanks to the ConnectionState OnSetTerminated callback 2020-08-15 16:24:35 -07:00
785842de03 (socket server) add a callback to the ConnectionState to be invoked when the connection is terminated. This will be used by the SocketServer in the future to know on time that the associated connection thread can be terminated. 2020-08-15 16:03:40 -07:00
261095fa12 (socket server) do not create a select interrupt object everytime when polling for notifications while waiting for new connections, instead use a persistent one which is a member variable 2020-08-15 15:28:15 -07:00
ed2ed0f7ae (ixwebsocket client) handle HTTP redirects 2020-08-14 18:13:34 -07:00
7ad5ead0f6 document the --config_path option in usage.md 2020-08-14 15:15:27 -07:00
a8284e64e3 add a simple shell script to test websocket proxy 2020-08-14 15:09:34 -07:00
5423a31d5a (ws) have more subcommand handle --pidfile, to write pid to a file 2020-08-14 15:09:12 -07:00
53575f8d90 change makefile openssl target to use ninja and install ws 2020-08-14 15:08:37 -07:00
d3bcbdac26 (ws) upgrade to latest version of nlohmann json (3.9.1 from 3.2.0) 2020-08-13 22:10:38 -07:00
8c5b28adce (websocket proxy server) add ability to map different hosts to different websocket servers, using a json config file 2020-08-13 21:20:42 -07:00
dcbafae35a (ws) on macOS, with OpenSSL or MbedTLS, use /etc/ssl/cert.pem as the system certs 2020-08-12 18:55:13 -07:00
eb197edcec ws --version does not get printed with a log prefix 2020-08-12 18:44:47 -07:00
b8265bf7f2 (ws) -q option imply info log level, not warning log level 2020-08-11 15:44:06 -07:00
e7c4f0b171 add documentation for the websocket send callback and the send return type (fix #239) 2020-08-11 11:24:00 -07:00
12f36b61ff (websocket server) Handle programmer error when the server callback is not registered properly (fix #227) 2020-08-06 04:40:32 -07:00
b15c4189f5 add csharp/dotnet devnull client to measure througput with different runtimes 2020-08-05 13:59:26 -07:00
74d3278258 add python test file to benchmark how many messages can be received per second 2020-08-04 10:53:35 -07:00
831152b906 add a devnull like sample code using libwebsockets C library, to see how many messages per second a client library can receive (answer is about the same as IXWebSocket) 2020-08-02 19:26:19 -07:00
7c81a98632 Add a node.js benchmarking test program, to see how fast node can receive messages. 2020-08-02 14:21:11 -07:00
6e47c62c06 (ws) Add a new ws sub-command, push_server. This command runs a server which sends many messages in a loop to a websocket client. We can receive above 200,000 messages per second (cf #235). 2020-08-02 12:41:34 -07:00
bcae7f326d (ws) Add a new ws sub-command, echo_client. This command send a message to an echo server, and send back to a server whatever message it does receive. When connecting to a local ws echo_server, on my MacBook Pro 2015 I can send/receive around 30,000 messages per second. (cf #235) 2020-08-02 12:09:13 -07:00
d719c41e31 (ws) ws echo_server. Add a -q option to only enable warning and error log levels. This is useful for bench-marking so that we do not print a lot of things on the console. (cf #235) 2020-08-02 11:53:21 -07:00
6f0307fb35 (build) make using zlib optional, with the caveat that some http and websocket features are not available when zlib is absent 2020-07-31 22:54:57 -07:00
2e3d625c1e (websocket client) onProgressCallback not called for short messages on a websocket (fix #233) 2020-07-29 17:47:33 -07:00
029289413c ws test shell script / add option so tune how large sent file will be 2020-07-29 17:46:37 -07:00
4d51098c86 (websocket client) heartbeat is not sent at the requested frequency (fix #232) 2020-07-29 11:24:42 -07:00
c2b05af022 can compile on macOS against jsoncpp installed from homebrew 2020-07-28 22:00:29 -07:00
e85f975ab0 compiler warning fixes 2020-07-28 21:46:26 -07:00
dc77d62a5d (ixcobra) CobraConnection: unsubscribe from all subscriptions when disconnecting 2020-07-28 10:32:18 -07:00
4f41f209a2 (socket utility) move ix::getFreePort to ixwebsocket library 2020-07-27 18:17:13 -07:00
5940e53d77 enable cobra tests which were disabled 2020-07-27 17:39:53 -07:00
22dffd5b7e WebSocket::close is re-entrant 2020-07-27 17:38:33 -07:00
af2f31045d snake server / join subscription background thread in the ConnectionState destructor + attach cobra message subscription id to the connection state instead of having it be a local reference that gets unbound 2020-07-27 17:35:03 -07:00
5daa59f9f3 minor makefile tweaks 2020-07-27 17:19:05 -07:00
2ea9d06a93 fix typo in unittest string description: ununexpected -> unsubscribed 2020-07-27 17:16:53 -07:00
847fc142d1 (ixwebsocket server) change legacy api with 2 nested callbacks, so that the first api takes a weak_ptr<WebSocket> as its first argument 2020-07-25 11:42:07 -07:00
0388459bd0 (ixwebsocket) add WebSocketProxyServer, from ws. Still need to make the interface better. 2020-07-25 11:26:06 -07:00
9a47ec1217 (ixsnake) uses an std::thread to handle redis subscriptions (2 unittest still failing) 2020-07-24 18:12:07 -07:00
45a40c8640 new Dockerfile to run locally the test on an Ubuntu 20.04 system 2020-07-24 14:46:09 -07:00
e34f1c30d6 (ws) port broadcast_server sub-command to the new server API 2020-07-24 14:35:07 -07:00
c14a4c0e3e formatting 2020-07-24 13:04:14 -07:00
b146e93a3a (unittest) port most unittests to the new server API 2020-07-24 12:49:36 -07:00
9957ec9724 (ws) port ws snake to the new server API 2020-07-24 12:33:17 -07:00
78a42f61bd add tool to ease making commits 2020-07-24 11:53:09 -07:00
e78019dad6 (ws) port ws transfer to the new server API 2020-07-24 11:52:16 -07:00
0f026c5da2 (websocket client) reset WebSocketTransport onClose callback in the WebSocket destructor 2020-07-24 10:03:29 -07:00
c26a2d5d39 (websocket server) reset client websocket callback when the connection is closed 2020-07-24 09:41:02 -07:00
2798886c0b (websocket server) add a new simpler API to handle client connections / that API does not trigger a memory leak while the previous one did 2020-07-23 19:29:41 -07:00
ffde283a4b (build) merge platform specific files which were used to have different implementations for setting a thread name into a single file, to make it easier to include every source files and build the ixwebsocket library (fix #226) 2020-07-17 11:58:06 -07:00
f7031d0d3e set thread name in only one file 2020-07-17 11:43:50 -07:00
595e6c57df IXSelectInterruptPipe.h included in cmake on windows but compiled out 2020-07-17 11:33:02 -07:00
87709c201e (socket server) bump default max connection count from 32 to 128 2020-07-10 17:11:11 -07:00
e70d83ace1 (snake) implement super simple stream sql expression support in snake server 2020-07-10 16:10:59 -07:00
ca829a3a98 implement very very simple stream sql support 2020-07-10 16:07:51 -07:00
26a1e63626 snake: stream sql mock + add republished channel option 2020-07-10 15:06:55 -07:00
c98959b895 comment out unittest which cannot be activated yet 2020-07-09 10:34:52 -07:00
baf18648e9 Added test for websocket leak (#225)
* Added test for websocket leak

* Fixed test
2020-07-09 10:19:44 -07:00
b21306376b uwp build fix + more ivp6 support 2020-07-08 12:38:55 -07:00
fbd17685a1 (socket+websocket+http+redis+snake servers) expose the remote ip and remote port when a new connection is made (see #222) / only ipv4 is handled 2020-07-08 12:10:35 -07:00
3a673575dd clang format 2020-07-08 10:39:46 -07:00
d5e51840ab use const iterators 2020-07-08 10:34:14 -07:00
543c2086b2 more templates in WebSocketTransport 2020-07-07 21:26:42 -07:00
95eab59c08 WebSocketPerMessageDeflateCompressor can work with vector or std::string 2020-07-07 21:26:04 -07:00
e9e768a288 better unittest for IXWebSocketPerMessageDeflateCompressor 2020-07-07 21:15:34 -07:00
e2180a1f31 add unittest for IXWebSocketPerMessageDeflateCompressor 2020-07-07 20:56:38 -07:00
7c1b57c8cd (cmake) change the way zlib and openssl are searched 2020-07-07 10:58:20 -07:00
292 changed files with 18663 additions and 52542 deletions

66
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,66 @@
name: docker
# When its time to do a release do a build for amd64
# and push all of them to Docker Hub.
# Only trigger on semver shaped tags.
on:
push:
tags:
- "v*.*.*"
jobs:
login:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Prepare
id: prep
run: |
DOCKER_IMAGE=machinezone/ws
VERSION=edge
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
fi
if [ "${{ github.event_name }}" = "schedule" ]; then
VERSION=nightly
fi
TAGS="${DOCKER_IMAGE}:${VERSION}"
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
TAGS="$TAGS,${DOCKER_IMAGE}:latest"
fi
echo ::set-output name=tags::${TAGS}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to GitHub Package Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2-build-push
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
target: prod
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.prep.outputs.tags }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache

View File

@ -10,12 +10,10 @@ jobs:
steps:
- uses: actions/checkout@v1
- uses: seanmiddleditch/gha-setup-vsdevenv@master
- run: |
vcpkg install zlib:x64-uwp
- run: |
mkdir build
cd build
cmake -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION="10.0" -DCMAKE_CXX_COMPILER=cl.exe -DUSE_TEST=1 ..
cmake -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION="10.0" -DCMAKE_CXX_COMPILER=cl.exe -DUSE_TEST=1 -DUSE_ZLIB=0 ..
- run: cmake --build build
#

View File

@ -10,10 +10,11 @@ jobs:
steps:
- uses: actions/checkout@v1
- uses: seanmiddleditch/gha-setup-vsdevenv@master
- run: |
vcpkg install zlib:x64-windows
- run: |
mkdir build
cd build
cmake -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_CXX_COMPILER=cl.exe -DUSE_WS=1 -DUSE_TEST=1 ..
cmake -DCMAKE_CXX_COMPILER=cl.exe -DUSE_WS=1 -DUSE_TEST=1 -DUSE_ZLIB=0 ..
- run: cmake --build build
#- run: ../build/test/ixwebsocket_unittest.exe
# working-directory: test

19
CMake/FindDeflate.cmake Normal file
View File

@ -0,0 +1,19 @@
# Find package structure taken from libcurl
include(FindPackageHandleStandardArgs)
find_path(DEFLATE_INCLUDE_DIRS libdeflate.h)
find_library(DEFLATE_LIBRARY deflate)
find_package_handle_standard_args(DEFLATE
FOUND_VAR
DEFLATE_FOUND
REQUIRED_VARS
DEFLATE_LIBRARY
DEFLATE_INCLUDE_DIRS
FAIL_MESSAGE
"Could NOT find deflate"
)
set(DEFLATE_INCLUDE_DIRS ${DEFLATE_INCLUDE_DIRS})
set(DEFLATE_LIBRARIES ${DEFLATE_LIBRARY})

View File

@ -30,12 +30,16 @@ set( IXWEBSOCKET_SOURCES
ixwebsocket/IXConnectionState.cpp
ixwebsocket/IXDNSLookup.cpp
ixwebsocket/IXExponentialBackoff.cpp
ixwebsocket/IXGetFreePort.cpp
ixwebsocket/IXGzipCodec.cpp
ixwebsocket/IXHttp.cpp
ixwebsocket/IXHttpClient.cpp
ixwebsocket/IXHttpServer.cpp
ixwebsocket/IXNetSystem.cpp
ixwebsocket/IXSelectInterrupt.cpp
ixwebsocket/IXSelectInterruptFactory.cpp
ixwebsocket/IXSelectInterruptPipe.cpp
ixwebsocket/IXSetThreadName.cpp
ixwebsocket/IXSocket.cpp
ixwebsocket/IXSocketConnect.cpp
ixwebsocket/IXSocketFactory.cpp
@ -51,6 +55,7 @@ set( IXWEBSOCKET_SOURCES
ixwebsocket/IXWebSocketPerMessageDeflate.cpp
ixwebsocket/IXWebSocketPerMessageDeflateCodec.cpp
ixwebsocket/IXWebSocketPerMessageDeflateOptions.cpp
ixwebsocket/IXWebSocketProxyServer.cpp
ixwebsocket/IXWebSocketServer.cpp
ixwebsocket/IXWebSocketTransport.cpp
)
@ -61,6 +66,8 @@ set( IXWEBSOCKET_HEADERS
ixwebsocket/IXConnectionState.h
ixwebsocket/IXDNSLookup.h
ixwebsocket/IXExponentialBackoff.h
ixwebsocket/IXGetFreePort.h
ixwebsocket/IXGzipCodec.h
ixwebsocket/IXHttp.h
ixwebsocket/IXHttpClient.h
ixwebsocket/IXHttpServer.h
@ -68,6 +75,7 @@ set( IXWEBSOCKET_HEADERS
ixwebsocket/IXProgressCallback.h
ixwebsocket/IXSelectInterrupt.h
ixwebsocket/IXSelectInterruptFactory.h
ixwebsocket/IXSelectInterruptPipe.h
ixwebsocket/IXSetThreadName.h
ixwebsocket/IXSocket.h
ixwebsocket/IXSocketConnect.h
@ -92,29 +100,13 @@ set( IXWEBSOCKET_HEADERS
ixwebsocket/IXWebSocketPerMessageDeflate.h
ixwebsocket/IXWebSocketPerMessageDeflateCodec.h
ixwebsocket/IXWebSocketPerMessageDeflateOptions.h
ixwebsocket/IXWebSocketProxyServer.h
ixwebsocket/IXWebSocketSendInfo.h
ixwebsocket/IXWebSocketServer.h
ixwebsocket/IXWebSocketTransport.h
ixwebsocket/IXWebSocketVersion.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)
elseif (WIN32)
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/windows/IXSetThreadName_windows.cpp)
elseif (${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD")
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/freebsd/IXSetThreadName_freebsd.cpp)
else()
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/linux/IXSetThreadName_linux.cpp)
endif()
option(USE_TLS "Enable TLS support" FALSE)
if (USE_TLS)
@ -199,10 +191,24 @@ if (USE_TLS)
endif()
endif()
# Use ZLIB_ROOT CMake variable if you need to use your own zlib
find_package(ZLIB REQUIRED)
include_directories(${ZLIB_INCLUDE_DIRS})
target_link_libraries(ixwebsocket ${ZLIB_LIBRARIES})
option(USE_ZLIB "Enable zlib support" TRUE)
if (USE_ZLIB)
# Use ZLIB_ROOT CMake variable if you need to use your own zlib
find_package(ZLIB REQUIRED)
include_directories(${ZLIB_INCLUDE_DIRS})
target_link_libraries(ixwebsocket ${ZLIB_LIBRARIES})
target_compile_definitions(ixwebsocket PUBLIC IXWEBSOCKET_USE_ZLIB)
endif()
# brew install libdeflate
find_package(DEFLATE)
if (DEFLATE_FOUND)
include_directories(${DEFLATE_INCLUDE_DIRS})
target_link_libraries(ixwebsocket ${DEFLATE_LIBRARIES})
target_compile_definitions(ixwebsocket PUBLIC IXWEBSOCKET_USE_DEFLATE)
endif()
if (WIN32)
target_link_libraries(ixwebsocket wsock32 ws2_32 shlwapi)
@ -255,7 +261,12 @@ if (USE_WS OR USE_TEST)
add_subdirectory(ixsentry)
add_subdirectory(ixbots)
add_subdirectory(third_party/spdlog spdlog)
include(FetchContent)
FetchContent_Declare(spdlog
GIT_REPOSITORY "https://github.com/gabime/spdlog"
GIT_TAG "v1.8.0")
FetchContent_MakeAvailable(spdlog)
if (USE_WS)
add_subdirectory(ws)

View File

@ -2,7 +2,7 @@
IXWebSocket is a C++ library for WebSocket client and server development. It has minimal dependencies (no boost), is very simple to use and support everything you'll likely need for websocket dev (SSL, deflate compression, compiles on most platforms, etc...). HTTP client and server code is also available, but it hasn't received as much testing.
It is been used on big mobile video game titles sending and receiving tons of messages since 2017 (iOS and Android). It was tested on macOS, iOS, Linux, Android, Windows and FreeBSD. Two important design goals are simplicity and correctness.
It is been used on big mobile video game titles sending and receiving tons of messages since 2017 (iOS and Android). It was tested on macOS, iOS, Linux, Android, Windows and FreeBSD. Note that the MinGW compiler is not supported at this point. Two important design goals are simplicity and correctness.
```cpp
/*

View File

@ -1,67 +1,11 @@
version: "3"
version: "3.3"
services:
# snake:
# image: bsergean/ws:build
# entrypoint: ws snake --port 8767 --host 0.0.0.0 --redis_hosts redis1
# ports:
# - "8767:8767"
# networks:
# - ws-net
# depends_on:
# - redis1
push:
entrypoint: ws push_server --host 0.0.0.0
image: ${DOCKER_REPO}/ws:build
# proxy:
# image: bsergean/ws:build
# entrypoint: strace ws proxy_server --remote_host 'wss://cobra.addsrv.com' --host 0.0.0.0 --port 8765 -v
# ports:
# - "8765:8765"
# networks:
# - ws-net
#pyproxy:
# image: bsergean/ws_proxy:build
# entrypoint: /usr/bin/ws_proxy.py --remote_url 'wss://cobra.addsrv.com' --host 0.0.0.0 --port 8765
# ports:
# - "8765:8765"
# networks:
# - ws-net
# # ws:
# # security_opt:
# # - seccomp:unconfined
# # cap_add:
# # - SYS_PTRACE
# # stdin_open: true
# # tty: true
# # image: bsergean/ws:build
# # entrypoint: sh
# # networks:
# # - ws-net
# # depends_on:
# # - redis1
# #
# # redis1:
# # image: redis:alpine
# # networks:
# # - ws-net
# #
# # statsd:
# # image: jaconel/statsd
# # ports:
# # - "8125:8125"
# # environment:
# # - STATSD_DUMP_MSG=true
# # - GRAPHITE_HOST=127.0.0.1
# # networks:
# # - ws-net
compile:
image: alpine
entrypoint: sh
stdin_open: true
tty: true
volumes:
- /Users/bsergeant/src/foss:/home/bsergean/src/foss
networks:
ws-net:
autoroute:
entrypoint: ws autoroute ws://push:8008
image: ${DOCKER_REPO}/ws:build
depends_on:
- push

View File

@ -2,7 +2,7 @@ FROM alpine:3.12 as build
RUN apk add --no-cache \
gcc g++ musl-dev linux-headers \
cmake mbedtls-dev make zlib-dev python3-dev ninja
cmake mbedtls-dev make zlib-dev python3-dev ninja git
RUN addgroup -S app && \
adduser -S -G app app && \
@ -20,7 +20,7 @@ RUN make ws_mbedtls_install && \
FROM alpine:3.12 as runtime
RUN apk add --no-cache libstdc++ mbedtls ca-certificates python3 && \
RUN apk add --no-cache libstdc++ mbedtls ca-certificates python3 strace && \
addgroup -S app && \
adduser -S -G app app

View File

@ -0,0 +1,23 @@
# Build time
FROM ubuntu:groovy as build
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get -y install g++ libssl-dev libz-dev make python ninja-build
RUN apt-get -y install cmake
RUN apt-get -y install gdb
COPY . /opt
WORKDIR /opt
#
# To use the container interactively for debugging/building
# 1. Build with
# CMD ["ls"]
# 2. Run with
# docker run --entrypoint sh -it docker-game-eng-dev.addsrv.com/ws:9.10.6
#
RUN ["make", "test"]
# CMD ["ls"]

View File

@ -1,6 +1,235 @@
# Changelog
All changes to this project will be documented in this file.
## [10.5.3] - 2020-10-19
(http code) With zlib disabled, some code should not be reached
## [10.5.2] - 2020-10-12
(ws curl) Add support for --data-binary option, to set the request body. When present the request will be sent with the POST verb
## [10.5.1] - 2020-10-09
(http client + server + ws) Add support for compressing http client requests with gzip. --compress_request argument is used in ws to enable this. The Content-Encoding is set to gzip, and decoded on the server side if present.
## [10.5.0] - 2020-09-30
(http client + server + ws) Add support for uploading files with ws -F foo=@filename, new -D http server option to debug incoming client requests, internal api changed for http POST, PUT and PATCH to supply an HttpFormDataParameters
## [10.4.9] - 2020-09-30
(http server + utility code) Add support for doing gzip compression with libdeflate library, if available
## [10.4.8] - 2020-09-30
(cmake) Stop using FetchContent cmake module to retrieve jsoncpp third party dependency
## [10.4.7] - 2020-09-28
(ws) add gzip and gunzip ws sub commands
## [10.4.6] - 2020-09-26
(cmake) use FetchContent cmake module to retrieve jsoncpp third party dependency
## [10.4.5] - 2020-09-26
(cmake) use FetchContent cmake module to retrieve spdlog third party dependency
## [10.4.4] - 2020-09-22
(cobra connection) retrieve cobra server connection id from the cobra handshake message and display it in ws clients, metrics publisher and bots
## [10.4.3] - 2020-09-22
(cobra 2 cobra) specify as an HTTP header which channel we will republish to
## [10.4.2] - 2020-09-18
(cobra bots) change an error log to a warning log when reconnecting because no messages were received for a minute
## [10.4.1] - 2020-09-18
(cobra connection and bots) set an HTTP header when connecting to help with debugging bots
## [10.4.0] - 2020-09-12
(http server) read body request when the Content-Length is specified + set timeout to read the request to 30 seconds max by default, and make it configurable as a constructor parameter
## [10.3.5] - 2020-09-09
(ws) autoroute command exit on its own once all messages have been received
## [10.3.4] - 2020-09-04
(docker) ws docker file installs strace
## [10.3.3] - 2020-09-02
(ws) echo_client command renamed to autoroute. Command exit once the server close the connection. push_server commands exit once N messages have been sent.
## [10.3.2] - 2020-08-31
(ws + cobra bots) add a cobra_to_cobra ws subcommand to subscribe to a channel and republish received events to a different channel
## [10.3.1] - 2020-08-28
(socket servers) merge the ConnectionInfo class with the ConnectionState one, which simplify all the server apis
## [10.3.0] - 2020-08-26
(ws) set the main thread name, to help with debugging in XCode, gdb, lldb etc...
## [10.2.9] - 2020-08-19
(ws) cobra to python bot / take a module python name as argument foo.bar.baz instead of a path foo/bar/baz.py
## [10.2.8] - 2020-08-19
(ws) on Linux with mbedtls, when the system ca certs are specified (the default) pick up sensible OS supplied paths (tested with CentOS and Alpine)
## [10.2.7] - 2020-08-18
(ws push_server) on the server side, stop sending and close the connection when the remote end has disconnected
## [10.2.6] - 2020-08-17
(ixwebsocket) replace std::unique_ptr<unsigned char[]> with std::array for some fixed arrays (which are in C++11)
## [10.2.5] - 2020-08-15
(ws) merge all ws_*.cpp files into a single one to speedup compilation
## [10.2.4] - 2020-08-15
(socket server) in the loop accepting connections, call select without a timeout on unix to avoid busy looping, and only wake up when a new connection happens
## [10.2.3] - 2020-08-15
(socket server) instead of busy looping with a sleep, only wake up the GC thread when a new thread will have to be joined, (we know that thanks to the ConnectionState OnSetTerminated callback
## [10.2.2] - 2020-08-15
(socket server) add a callback to the ConnectionState to be invoked when the connection is terminated. This will be used by the SocketServer in the future to know on time that the associated connection thread can be terminated.
## [10.2.1] - 2020-08-15
(socket server) do not create a select interrupt object everytime when polling for notifications while waiting for new connections, instead use a persistent one which is a member variable
## [10.2.0] - 2020-08-14
(ixwebsocket client) handle HTTP redirects
## [10.2.0] - 2020-08-13
(ws) upgrade to latest version of nlohmann json (3.9.1 from 3.2.0)
## [10.1.9] - 2020-08-13
(websocket proxy server) add ability to map different hosts to different websocket servers, using a json config file
## [10.1.8] - 2020-08-12
(ws) on macOS, with OpenSSL or MbedTLS, use /etc/ssl/cert.pem as the system certs
## [10.1.7] - 2020-08-11
(ws) -q option imply info log level, not warning log level
## [10.1.6] - 2020-08-06
(websocket server) Handle programmer error when the server callback is not registered properly (fix #227)
## [10.1.5] - 2020-08-02
(ws) Add a new ws sub-command, push_server. This command runs a server which sends many messages in a loop to a websocket client. We can receive above 200,000 messages per second (cf #235).
## [10.1.4] - 2020-08-02
(ws) Add a new ws sub-command, echo_client. This command sends a message to an echo server, and send back to a server whatever message it does receive. When connecting to a local ws echo_server, on my MacBook Pro 2015 I can send/receive around 30,000 messages per second. (cf #235)
## [10.1.3] - 2020-08-02
(ws) ws echo_server. Add a -q option to only enable warning and error log levels. This is useful for bench-marking so that we do not print a lot of things on the console. (cf #235)
## [10.1.2] - 2020-07-31
(build) make using zlib optional, with the caveat that some http and websocket features are not available when zlib is absent
## [10.1.1] - 2020-07-29
(websocket client) onProgressCallback not called for short messages on a websocket (fix #233)
## [10.1.0] - 2020-07-29
(websocket client) heartbeat is not sent at the requested frequency (fix #232)
## [10.0.3] - 2020-07-28
compiler warning fixes
## [10.0.2] - 2020-07-28
(ixcobra) CobraConnection: unsubscribe from all subscriptions when disconnecting
## [10.0.1] - 2020-07-27
(socket utility) move ix::getFreePort to ixwebsocket library
## [10.0.0] - 2020-07-25
(ixwebsocket server) change legacy api with 2 nested callbacks, so that the first api takes a weak_ptr<WebSocket> as its first argument
## [9.10.7] - 2020-07-25
(ixwebsocket) add WebSocketProxyServer, from ws. Still need to make the interface better.
## [9.10.6] - 2020-07-24
(ws) port broadcast_server sub-command to the new server API
## [9.10.5] - 2020-07-24
(unittest) port most unittests to the new server API
## [9.10.3] - 2020-07-24
(ws) port ws transfer to the new server API
## [9.10.2] - 2020-07-24
(websocket client) reset WebSocketTransport onClose callback in the WebSocket destructor
## [9.10.1] - 2020-07-24
(websocket server) reset client websocket callback when the connection is closed
## [9.10.0] - 2020-07-23
(websocket server) add a new simpler API to handle client connections / that API does not trigger a memory leak while the previous one did
## [9.9.3] - 2020-07-17
(build) merge platform specific files which were used to have different implementations for setting a thread name into a single file, to make it easier to include every source files and build the ixwebsocket library (fix #226)
## [9.9.2] - 2020-07-10
(socket server) bump default max connection count from 32 to 128
## [9.9.1] - 2020-07-10
(snake) implement super simple stream sql expression support in snake server
## [9.9.0] - 2020-07-08
(socket+websocket+http+redis+snake servers) expose the remote ip and remote port when a new connection is made
## [9.8.6] - 2020-07-06
(cmake) change the way zlib and openssl are searched
## [9.8.5] - 2020-07-06
(cobra python bots) remove the test which stop the bot when events do not follow cobra metrics system schema with an id and a device entry

View File

@ -17,6 +17,7 @@ There is a unittest which can be executed by typing `make test`.
Options for building:
* `-DUSE_ZLIB=1` will enable zlib support, required for http client + server + websocket per message deflate extension
* `-DUSE_TLS=1` will enable TLS support
* `-DUSE_OPEN_SSL=1` will use [openssl](https://www.openssl.org/) for the TLS support (default on Linux and Windows)
* `-DUSE_MBED_TLS=1` will use [mbedlts](https://tls.mbed.org/) for the TLS support

37
docs/performance.md Normal file
View File

@ -0,0 +1,37 @@
## WebSocket Client performance
We will run a client and a server on the same machine, connecting to localhost. This bench is run on a MacBook Pro from 2015. We can receive over 200,000 (small) messages per second, another way to put it is that it takes 5 micro-second to receive and process one message. This is an indication about the minimal latency to receive messages.
### Receiving messages
By using the push_server ws sub-command, the server will send the same message in a loop to any connected client.
```
ws push_server -q --send_msg 'yo'
```
By using the echo_client ws sub-command, with the -m (mute or no_send), we will display statistics on how many messages we can receive per second.
```
$ ws echo_client -m ws://localhost:8008
[2020-08-02 12:31:17.284] [info] ws_echo_client: connected
[2020-08-02 12:31:17.284] [info] Uri: /
[2020-08-02 12:31:17.284] [info] Headers:
[2020-08-02 12:31:17.284] [info] Connection: Upgrade
[2020-08-02 12:31:17.284] [info] Sec-WebSocket-Accept: byy/pMK2d0PtRwExaaiOnXJTQHo=
[2020-08-02 12:31:17.284] [info] Server: ixwebsocket/10.1.4 macos ssl/SecureTransport zlib 1.2.11
[2020-08-02 12:31:17.284] [info] Upgrade: websocket
[2020-08-02 12:31:17.663] [info] messages received: 0 per second 2595307 total
[2020-08-02 12:31:18.668] [info] messages received: 79679 per second 2674986 total
[2020-08-02 12:31:19.668] [info] messages received: 207438 per second 2882424 total
[2020-08-02 12:31:20.673] [info] messages received: 209207 per second 3091631 total
[2020-08-02 12:31:21.676] [info] messages received: 216056 per second 3307687 total
[2020-08-02 12:31:22.680] [info] messages received: 214927 per second 3522614 total
[2020-08-02 12:31:23.684] [info] messages received: 216960 per second 3739574 total
[2020-08-02 12:31:24.688] [info] messages received: 215232 per second 3954806 total
[2020-08-02 12:31:25.691] [info] messages received: 212300 per second 4167106 total
[2020-08-02 12:31:26.694] [info] messages received: 212501 per second 4379607 total
[2020-08-02 12:31:27.699] [info] messages received: 212330 per second 4591937 total
[2020-08-02 12:31:28.702] [info] messages received: 216511 per second 4808448 total
```

View File

@ -67,9 +67,28 @@ webSocket.stop()
### Sending messages
`websocket.send("foo")` will send a message.
`WebSocketSendInfo result = websocket.send("foo")` will send a message.
If the connection was closed and sending failed, the return value will be set to false.
If the connection was closed, sending will fail, and the success field of the result object will be set to false. There could also be a compression error in which case the compressError field will be set to true. The payloadSize field and wireSize fields will tell you respectively how much bytes the message weight, and how many bytes were sent on the wire (potentially compressed + counting the message header (a few bytes).
There is an optional progress callback that can be passed in as the second argument. If a message is large it will be fragmented into chunks which will be sent independantly. Everytime the we can write a fragment into the OS network cache, the callback will be invoked. If a user wants to cancel a slow send, false should be returned from within the callback.
Here is an example code snippet copied from the ws send sub-command. Each fragment weights 32K, so the total integer is the wireSize divided by 32K. As an example if you are sending 32M of data, uncompressed, total will be 1000. current will be set to 0 for the first fragment, then 1, 2 etc...
```
auto result =
_webSocket.sendBinary(serializedMsg, [this, throttle](int current, int total) -> bool {
spdlog::info("ws_send: Step {} out of {}", current + 1, total);
if (throttle)
{
std::chrono::duration<double, std::milli> duration(10);
std::this_thread::sleep_for(duration);
}
return _connected;
});
```
### ReadyState
@ -246,6 +265,10 @@ uint32_t m = webSocket.getMaxWaitBetweenReconnectionRetries();
## WebSocket server API
### Legacy api
This api was actually changed to take a weak_ptr<WebSocket> as the first argument to setOnConnectionCallback ; previously it would take a shared_ptr<WebSocket> which was creating cycles and then memory leaks problems.
```cpp
#include <ixwebsocket/IXWebSocketServer.h>
@ -256,38 +279,48 @@ uint32_t m = webSocket.getMaxWaitBetweenReconnectionRetries();
ix::WebSocketServer server(port);
server.setOnConnectionCallback(
[&server](std::shared_ptr<WebSocket> webSocket,
[&server](std::weak_ptr<WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState)
{
webSocket->setOnMessageCallback(
[webSocket, connectionState, &server](const ix::WebSocketMessagePtr msg)
{
if (msg->type == ix::WebSocketMessageType::Open)
std::cout << "Remote ip: " << connectionState->remoteIp << std::endl;
auto ws = webSocket.lock();
if (ws)
{
ws->setOnMessageCallback(
[webSocket, connectionState, &server](const ix::WebSocketMessagePtr msg)
{
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: " << msg->openInfo.uri << std::endl;
std::cerr << "Headers:" << std::endl;
for (auto it : msg->openInfo.headers)
if (msg->type == ix::WebSocketMessageType::Open)
{
std::cerr << it.first << ": " << it.second << std::endl;
std::cout << "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::cout << "id: " << connectionState->getId() << std::endl;
// The uri the client did connect to.
std::cout << "Uri: " << msg->openInfo.uri << std::endl;
std::cout << "Headers:" << std::endl;
for (auto it : msg->openInfo.headers)
{
std::cout << it.first << ": " << it.second << std::endl;
}
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
// For an echo server, we just send back to the client whatever was received by the server
// All connected clients are available in an std::set. See the broadcast cpp example.
// Second parameter tells whether we are sending the message in binary or text mode.
// Here we send it in the same mode as it was received.
auto ws = webSocket.lock();
if (ws)
{
ws->send(msg->str, msg->binary);
}
}
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
// For an echo server, we just send back to the client whatever was received by the server
// All connected clients are available in an std::set. See the broadcast cpp example.
// Second parameter tells whether we are sending the message in binary or text mode.
// Here we send it in the same mode as it was received.
webSocket->send(msg->str, msg->binary);
}
}
);
@ -309,6 +342,73 @@ server.wait();
```
### New api
The new API does not require to use 2 nested callbacks, which is a bit annoying. The real fix is that there was a memory leak due to a shared_ptr cycle, due to passing down a shared_ptr<WebSocket> down to the callbacks.
The webSocket reference is guaranteed to be always valid ; by design the callback will never be invoked with a null webSocket object.
```cpp
#include <ixwebsocket/IXWebSocketServer.h>
...
// Run a server on localhost at a given port.
// Bound host name, max connections and listen backlog can also be passed in as parameters.
ix::WebSocketServer server(port);
server.setOnClientMessageCallback(std::shared_ptr<ConnectionState> connectionState,
WebSocket& webSocket,
const WebSocketMessagePtr& msg)
{
// The ConnectionState object contains information about the connection,
// at this point only the client ip address and the port.
std::cout << "Remote ip: " << connectionState->getRemoteIp();
if (msg->type == ix::WebSocketMessageType::Open)
{
std::cout << "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::cout << "id: " << connectionState->getId() << std::endl;
// The uri the client did connect to.
std::cout << "Uri: " << msg->openInfo.uri << std::endl;
std::cout << "Headers:" << std::endl;
for (auto it : msg->openInfo.headers)
{
std::cout << it.first << ": " << it.second << std::endl;
}
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
// For an echo server, we just send back to the client whatever was received by the server
// All connected clients are available in an std::set. See the broadcast cpp example.
// Second parameter tells whether we are sending the message in binary or text mode.
// Here we send it in the same mode as it was received.
webSocket.send(msg->str, msg->binary);
}
);
auto res = server.listen();
if (!res.first)
{
// Error handling
return 1;
}
// Run the server in the background. Server can be stoped by calling server.stop()
server.start();
// Block until server.stop() is called.
server.wait();
```
## HTTP client API
```cpp
@ -358,18 +458,25 @@ out = httpClient.get(url, args);
// POST request with parameters
HttpParameters httpParameters;
httpParameters["foo"] = "bar";
out = httpClient.post(url, httpParameters, args);
// HTTP form data can be passed in as well, for multi-part upload of files
HttpFormDataParameters httpFormDataParameters;
httpParameters["baz"] = "booz";
out = httpClient.post(url, httpParameters, httpFormDataParameters, args);
// POST request with a body
out = httpClient.post(url, std::string("foo=bar"), args);
// PUT and PATCH are available too.
//
// Result
//
auto statusCode = response->statusCode; // Can be HttpErrorCode::Ok, HttpErrorCode::UrlMalformed, etc...
auto errorCode = response->errorCode; // 200, 404, etc...
auto responseHeaders = response->headers; // All the headers in a special case-insensitive unordered_map of (string, string)
auto payload = response->payload; // All the bytes from the response as an std::string
auto body = response->body; // All the bytes from the response as an std::string
auto errorMsg = response->errorMsg; // Descriptive error message in case of failure
auto uploadSize = response->uploadSize; // Byte count of uploaded data
auto downloadSize = response->downloadSize; // Byte count of downloaded data
@ -417,11 +524,13 @@ If you want to handle how requests are processed, implement the setOnConnectionC
```cpp
setOnConnectionCallback(
[this](HttpRequestPtr request,
std::shared_ptr<ConnectionState> /*connectionState*/) -> HttpResponsePtr
std::shared_ptr<ConnectionState> connectionState) -> HttpResponsePtr
{
// Build a string for the response
std::stringstream ss;
ss << request->method
ss << connectionState->getRemoteIp();
<< " "
<< request->method
<< " "
<< request->uri;

View File

@ -204,6 +204,24 @@ Listening on 127.0.0.1:8008
If you connect to ws://127.0.0.1:8008, the proxy will connect to ws://127.0.0.1:9000 and pass all traffic to this server.
You can also use a more complex setup if you want to redirect to different websocket servers based on the hostname your client is trying to connect to. If you have multiple CNAME aliases that point to the same server.
A JSON config file is used to express that mapping ; here connecting to echo.jeanserge.com will proxy the client to ws://localhost:8008 on the local machine (which actually runs ws echo_server), while connecting to bavarde.jeanserge.com will proxy the client to ws://localhost:5678 where a cobra python server is running. As a side note you will need a wildcard SSL certificate if you want to have SSL enabled on that machine.
```json
{
"remote_urls": {
"echo.jeanserge.com": "ws://localhost:8008",
"bavarde.jeanserge.com": "ws://localhost:5678"
}
}
```
The --config_path option is required to instruct ws proxy_server to read that file.
```
ws proxy_server --config_path proxyConfig.json --port 8765
```
## File transfer
```

View File

@ -5,6 +5,7 @@
set (IXBOTS_SOURCES
ixbots/IXCobraBot.cpp
ixbots/IXCobraToCobraBot.cpp
ixbots/IXCobraToSentryBot.cpp
ixbots/IXCobraToStatsdBot.cpp
ixbots/IXCobraToStdoutBot.cpp
@ -16,6 +17,7 @@ set (IXBOTS_SOURCES
set (IXBOTS_HEADERS
ixbots/IXCobraBot.h
ixbots/IXCobraBotConfig.h
ixbots/IXCobraToCobraBot.h
ixbots/IXCobraToSentryBot.h
ixbots/IXCobraToStatsdBot.h
ixbots/IXCobraToStdoutBot.h

View File

@ -31,6 +31,8 @@ namespace ix
auto limitReceivedEvents = botConfig.limitReceivedEvents;
auto batchSize = botConfig.batchSize;
config.headers["X-Cobra-Channel"] = channel;
ix::CobraConnection conn;
conn.configure(config);
conn.connect();
@ -128,7 +130,7 @@ namespace ix
ss << "no messages received or sent for "
<< heartBeatTimeout << " seconds, reconnecting";
CoreLogger::error(ss.str());
CoreLogger::warn(ss.str());
stalledConnection = true;
}
state = currentState;
@ -168,7 +170,11 @@ namespace ix
}
else if (event->type == ix::CobraEventType::Closed)
{
CoreLogger::info("Subscriber closed: {}" + event->errMsg);
CoreLogger::info("Subscriber closed: " + event->errMsg);
}
else if (event->type == ix::CobraEventType::Handshake)
{
CoreLogger::info("Subscriber: Cobra handshake connection id: " + event->connectionId);
}
else if (event->type == ix::CobraEventType::Authenticated)
{

View File

@ -0,0 +1,45 @@
/*
* IXCobraToCobraBot.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2020 Machine Zone, Inc. All rights reserved.
*/
#include "IXCobraToCobraBot.h"
#include "IXCobraBot.h"
#include <ixcobra/IXCobraMetricsPublisher.h>
#include <sstream>
namespace ix
{
int64_t cobra_to_cobra_bot(const ix::CobraBotConfig& cobraBotConfig,
const std::string& republishChannel,
const std::string& publisherRolename,
const std::string& publisherRolesecret)
{
CobraBot bot;
CobraMetricsPublisher cobraMetricsPublisher;
CobraConfig cobraPublisherConfig = cobraBotConfig.cobraConfig;
cobraPublisherConfig.rolename = publisherRolename;
cobraPublisherConfig.rolesecret = publisherRolesecret;
cobraPublisherConfig.headers["X-Cobra-Republish-Channel"] = republishChannel;
cobraMetricsPublisher.configure(cobraPublisherConfig, republishChannel);
bot.setOnBotMessageCallback(
[&republishChannel, &cobraMetricsPublisher](const Json::Value& msg,
const std::string& /*position*/,
std::atomic<bool>& /*throttled*/,
std::atomic<bool>& /*fatalCobraError*/,
std::atomic<uint64_t>& sentCount) -> void {
Json::Value msgWithNoId(msg);
msgWithNoId.removeMember("id");
cobraMetricsPublisher.push(republishChannel, msg);
sentCount++;
});
return bot.run(cobraBotConfig);
}
} // namespace ix

View File

@ -0,0 +1,20 @@
/*
* IXCobraToCobraBot.h
* Author: Benjamin Sergeant
* Copyright (c) 2020 Machine Zone, Inc. All rights reserved.
*/
#pragma once
#include <cstdint>
#include <ixbots/IXStatsdClient.h>
#include "IXCobraBotConfig.h"
#include <stddef.h>
#include <string>
namespace ix
{
int64_t cobra_to_cobra_bot(const ix::CobraBotConfig& config,
const std::string& republishChannel,
const std::string& publisherRolename,
const std::string& publisherRolesecret);
} // namespace ix

View File

@ -102,7 +102,7 @@ namespace ix
{
int64_t cobra_to_python_bot(const ix::CobraBotConfig& config,
StatsdClient& statsdClient,
const std::string& scriptPath)
const std::string& moduleName)
{
#ifndef IXBOTS_USE_PYTHON
CoreLogger::error("Command is disabled. "
@ -113,10 +113,7 @@ namespace ix
Py_InitializeEx(0); // 0 arg so that we do not install signal handlers
// which prevent us from using Ctrl-C
size_t lastIndex = scriptPath.find_last_of(".");
std::string modulePath = scriptPath.substr(0, lastIndex);
PyObject* pyModuleName = PyUnicode_DecodeFSDefault(modulePath.c_str());
PyObject* pyModuleName = PyUnicode_DecodeFSDefault(moduleName.c_str());
if (pyModuleName == nullptr)
{

View File

@ -15,5 +15,5 @@ namespace ix
{
int64_t cobra_to_python_bot(const ix::CobraBotConfig& config,
StatsdClient& statsdClient,
const std::string& scriptPath);
const std::string& moduleName);
} // namespace ix

View File

@ -41,7 +41,7 @@ namespace ix
else
{
CoreLogger::error("Error sending data to sentry: " + std::to_string(response->statusCode));
CoreLogger::error("Response: " + response->payload);
CoreLogger::error("Response: " + response->body);
// Error 429 Too Many Requests
if (response->statusCode == 429)

View File

@ -8,6 +8,7 @@
#include <ixwebsocket/IXSocketTLSOptions.h>
#include <ixwebsocket/IXWebSocketPerMessageDeflateOptions.h>
#include <ixwebsocket/IXWebSocketHttpHeaders.h>
namespace ix
{
@ -19,6 +20,7 @@ namespace ix
std::string rolesecret;
WebSocketPerMessageDeflateOptions webSocketPerMessageDeflateOptions;
SocketTLSOptions socketTLSOptions;
WebSocketHttpHeaders headers;
CobraConfig(const std::string& a = std::string(),
const std::string& e = std::string(),

View File

@ -91,13 +91,14 @@ namespace ix
const std::string& errorMsg,
const WebSocketHttpHeaders& headers,
const std::string& subscriptionId,
CobraConnection::MsgId msgId)
CobraConnection::MsgId msgId,
const std::string& connectionId)
{
std::lock_guard<std::mutex> lock(_eventCallbackMutex);
if (_eventCallback)
{
_eventCallback(
std::make_unique<CobraEvent>(eventType, errorMsg, headers, subscriptionId, msgId));
std::make_unique<CobraEvent>(eventType, errorMsg, headers, subscriptionId, msgId, connectionId));
}
}
@ -111,6 +112,12 @@ namespace ix
void CobraConnection::disconnect()
{
auto subscriptionIds = getSubscriptionsIds();
for (auto&& subscriptionId : subscriptionIds)
{
unsubscribe(subscriptionId);
}
_authenticated = false;
_webSocket->stop();
}
@ -249,7 +256,8 @@ namespace ix
const std::string& rolename,
const std::string& rolesecret,
const WebSocketPerMessageDeflateOptions& webSocketPerMessageDeflateOptions,
const SocketTLSOptions& socketTLSOptions)
const SocketTLSOptions& socketTLSOptions,
const WebSocketHttpHeaders& headers)
{
_roleName = rolename;
_roleSecret = rolesecret;
@ -263,6 +271,7 @@ namespace ix
_webSocket->setUrl(url);
_webSocket->setPerMessageDeflateOptions(webSocketPerMessageDeflateOptions);
_webSocket->setTLSOptions(socketTLSOptions);
_webSocket->setExtraHeaders(headers);
// Send a websocket ping every N seconds (N = 30) now
// This should keep the connection open and prevent some load balancers such as
@ -277,7 +286,8 @@ namespace ix
config.rolename,
config.rolesecret,
config.webSocketPerMessageDeflateOptions,
config.socketTLSOptions);
config.socketTLSOptions,
config.headers);
}
//
@ -343,6 +353,18 @@ namespace ix
if (!nonce.isString()) return false;
if (!data.isMember("connection_id")) return false;
Json::Value connectionId = data["connection_id"];
if (!connectionId.isString()) return false;
invokeEventCallback(ix::CobraEventType::Handshake,
std::string(),
WebSocketHttpHeaders(),
std::string(),
0,
connectionId.asString());
return sendAuthMessage(nonce.asString());
}
@ -614,6 +636,18 @@ namespace ix
_webSocket->send(pdu.toStyledString());
}
std::vector<std::string> CobraConnection::getSubscriptionsIds()
{
std::vector<std::string> subscriptionIds;
std::lock_guard<std::mutex> lock(_cbsMutex);
for (auto&& it : _cbs)
{
subscriptionIds.push_back(it.first);
}
return subscriptionIds;
}
//
// Enqueue strategy drops old messages when we are at full capacity
//

View File

@ -56,7 +56,8 @@ namespace ix
const std::string& rolename,
const std::string& rolesecret,
const WebSocketPerMessageDeflateOptions& webSocketPerMessageDeflateOptions,
const SocketTLSOptions& socketTLSOptions);
const SocketTLSOptions& socketTLSOptions,
const WebSocketHttpHeaders& headers);
void configure(const ix::CobraConfig& config);
@ -157,12 +158,17 @@ namespace ix
const std::string& errorMsg = std::string(),
const WebSocketHttpHeaders& headers = WebSocketHttpHeaders(),
const std::string& subscriptionId = std::string(),
uint64_t msgId = std::numeric_limits<uint64_t>::max());
uint64_t msgId = std::numeric_limits<uint64_t>::max(),
const std::string& connectionId = std::string());
void invokeErrorCallback(const std::string& errorMsg, const std::string& serializedPdu);
/// Tells whether the internal queue is empty or not
bool isQueueEmpty();
/// Retrieve all subscriptions ids
std::vector<std::string> getSubscriptionsIds();
///
/// Member variables
///

View File

@ -21,17 +21,20 @@ namespace ix
const ix::WebSocketHttpHeaders& headers;
const std::string& subscriptionId;
uint64_t msgId; // CobraConnection::MsgId
const std::string& connectionId;
CobraEvent(ix::CobraEventType t,
const std::string& e,
const ix::WebSocketHttpHeaders& h,
const std::string& s,
uint64_t m)
uint64_t m,
const std::string& c)
: type(t)
, errMsg(e)
, headers(h)
, subscriptionId(s)
, msgId(m)
, connectionId(c)
{
;
}

View File

@ -20,6 +20,7 @@ namespace ix
Pong = 7,
HandshakeError = 8,
AuthenticationError = 9,
SubscriptionError = 10
SubscriptionError = 10,
Handshake = 11
};
}

View File

@ -24,6 +24,7 @@ namespace ix
{
_cobra_connection.setEventCallback([](const CobraEventPtr& event) {
std::stringstream ss;
ix::LogLevel logLevel = LogLevel::Info;
if (event->type == ix::CobraEventType::Open)
{
@ -34,6 +35,10 @@ namespace ix
ss << it.first << ": " << it.second << std::endl;
}
}
else if (event->type == ix::CobraEventType::Handshake)
{
ss << "Cobra handshake connection id: " << event->connectionId;
}
else if (event->type == ix::CobraEventType::Authenticated)
{
ss << "Authenticated";
@ -41,6 +46,7 @@ namespace ix
else if (event->type == ix::CobraEventType::Error)
{
ss << "Error: " << event->errMsg;
logLevel = ix::LogLevel::Error;
}
else if (event->type == ix::CobraEventType::Closed)
{
@ -57,6 +63,7 @@ namespace ix
else if (event->type == ix::CobraEventType::Published)
{
ss << "Published message " << event->msgId << " acked";
logLevel = ix::LogLevel::Debug;
}
else if (event->type == ix::CobraEventType::Pong)
{
@ -65,17 +72,20 @@ namespace ix
else if (event->type == ix::CobraEventType::HandshakeError)
{
ss << "Handshake error: " << event->errMsg;
logLevel = ix::LogLevel::Error;
}
else if (event->type == ix::CobraEventType::AuthenticationError)
{
ss << "Authentication error: " << event->errMsg;
logLevel = ix::LogLevel::Error;
}
else if (event->type == ix::CobraEventType::SubscriptionError)
{
ss << "Subscription error: " << event->errMsg;
logLevel = ix::LogLevel::Error;
}
CoreLogger::log(ss.str().c_str());
CoreLogger::log(ss.str().c_str(), logLevel);
});
}

View File

@ -19,4 +19,16 @@ namespace ix
return hashAddress;
}
uint64_t djb2HashStr(const std::string& data)
{
uint64_t hashAddress = 5381;
for (size_t i = 0; i < data.size(); ++i)
{
hashAddress = ((hashAddress << 5) + hashAddress) + data[i];
}
return hashAddress;
}
} // namespace ix

View File

@ -8,8 +8,10 @@
#include <cstdint>
#include <vector>
#include <string>
namespace ix
{
uint64_t djb2Hash(const std::vector<uint8_t>& data);
uint64_t djb2HashStr(const std::string& data);
}

View File

@ -47,6 +47,8 @@ namespace ix
void RedisServer::handleConnection(std::unique_ptr<Socket> socket,
std::shared_ptr<ConnectionState> connectionState)
{
logInfo("New connection from remote ip " + connectionState->getRemoteIp());
_connectedClientsCount++;
while (!_stopHandlingConnections)

View File

@ -7,12 +7,14 @@ set (IXSNAKE_SOURCES
ixsnake/IXSnakeServer.cpp
ixsnake/IXSnakeProtocol.cpp
ixsnake/IXAppConfig.cpp
ixsnake/IXStreamSql.cpp
)
set (IXSNAKE_HEADERS
ixsnake/IXSnakeServer.h
ixsnake/IXSnakeProtocol.h
ixsnake/IXAppConfig.h
ixsnake/IXStreamSql.h
)
add_library(ixsnake STATIC

View File

@ -26,6 +26,12 @@ namespace snake
}
auto roles = appConfig.apps[appkey]["roles"];
if (roles.count(role) == 0)
{
std::cerr << "Missing role " << role << std::endl;
return std::string();
}
auto channel = roles[role]["secret"];
return channel;
}

View File

@ -33,6 +33,9 @@ namespace snake
// Misc
bool verbose;
bool disablePong;
// If non empty, every published message gets republished to a given channel
std::string republishChannel;
};
bool isAppKeyValid(const AppConfig& appConfig, std::string appkey);

View File

@ -7,15 +7,21 @@
#pragma once
#include <ixredis/IXRedisClient.h>
#include <future>
#include <thread>
#include <ixwebsocket/IXConnectionState.h>
#include <string>
#include "IXStreamSql.h"
namespace snake
{
class SnakeConnectionState : public ix::ConnectionState
{
public:
virtual ~SnakeConnectionState()
{
stopSubScriptionThread();
}
std::string getNonce()
{
return _nonce;
@ -30,6 +36,7 @@ namespace snake
{
return _appkey;
}
void setAppkey(const std::string& appkey)
{
_appkey = appkey;
@ -39,6 +46,7 @@ namespace snake
{
return _role;
}
void setRole(const std::string& role)
{
_role = role;
@ -49,7 +57,24 @@ namespace snake
return _redisClient;
}
std::future<void> fut;
void stopSubScriptionThread()
{
if (subscriptionThread.joinable())
{
subscriptionRedisClient.stop();
subscriptionThread.join();
}
}
// We could make those accessible through methods
std::thread subscriptionThread;
std::string appChannel;
std::string subscriptionId;
uint64_t id;
std::unique_ptr<StreamSql> streamSql;
ix::RedisClient subscriptionRedisClient;
ix::RedisClient::OnRedisSubscribeResponseCallback onRedisSubscribeResponseCallback;
ix::RedisClient::OnRedisSubscribeCallback onRedisSubscribeCallback;
private:
std::string _nonce;

View File

@ -18,21 +18,22 @@
namespace snake
{
void handleError(const std::string& action,
std::shared_ptr<ix::WebSocket> ws,
nlohmann::json pdu,
ix::WebSocket& ws,
uint64_t pduId,
const std::string& errMsg)
{
std::string actionError(action);
actionError += "/error";
nlohmann::json response = {
{"action", actionError}, {"id", pdu.value("id", 1)}, {"body", {{"reason", errMsg}}}};
ws->sendText(response.dump());
{"action", actionError}, {"id", pduId}, {"body", {{"reason", errMsg}}}};
ws.sendText(response.dump());
}
void handleHandshake(std::shared_ptr<SnakeConnectionState> state,
std::shared_ptr<ix::WebSocket> ws,
const nlohmann::json& pdu)
ix::WebSocket& ws,
const nlohmann::json& pdu,
uint64_t pduId)
{
std::string role = pdu["body"]["data"]["role"];
@ -41,7 +42,7 @@ namespace snake
nlohmann::json response = {
{"action", "auth/handshake/ok"},
{"id", pdu.value("id", 1)},
{"id", pduId},
{"body",
{
{"data", {{"nonce", state->getNonce()}, {"connection_id", state->getId()}}},
@ -49,13 +50,14 @@ namespace snake
auto serializedResponse = response.dump();
ws->sendText(serializedResponse);
ws.sendText(serializedResponse);
}
void handleAuth(std::shared_ptr<SnakeConnectionState> state,
std::shared_ptr<ix::WebSocket> ws,
ix::WebSocket& ws,
const AppConfig& appConfig,
const nlohmann::json& pdu)
const nlohmann::json& pdu,
uint64_t pduId)
{
auto secret = getRoleSecret(appConfig, state->appkey(), state->role());
@ -63,9 +65,9 @@ namespace snake
{
nlohmann::json response = {
{"action", "auth/authenticate/error"},
{"id", pdu.value("id", 1)},
{"id", pduId},
{"body", {{"error", "authentication_failed"}, {"reason", "invalid secret"}}}};
ws->sendText(response.dump());
ws.sendText(response.dump());
return;
}
@ -79,19 +81,21 @@ namespace snake
{"action", "auth/authenticate/error"},
{"id", pdu.value("id", 1)},
{"body", {{"error", "authentication_failed"}, {"reason", "invalid hash"}}}};
ws->sendText(response.dump());
ws.sendText(response.dump());
return;
}
nlohmann::json response = {
{"action", "auth/authenticate/ok"}, {"id", pdu.value("id", 1)}, {"body", {}}};
ws->sendText(response.dump());
ws.sendText(response.dump());
}
void handlePublish(std::shared_ptr<SnakeConnectionState> state,
std::shared_ptr<ix::WebSocket> ws,
const nlohmann::json& pdu)
ix::WebSocket& ws,
const AppConfig& appConfig,
const nlohmann::json& pdu,
uint64_t pduId)
{
std::vector<std::string> channels;
@ -111,10 +115,16 @@ namespace snake
{
std::stringstream ss;
ss << "Missing channels or channel field in publish data";
handleError("rtm/publish", ws, pdu, ss.str());
handleError("rtm/publish", ws, pduId, ss.str());
return;
}
// add an extra channel if the config has one specified
if (!appConfig.republishChannel.empty())
{
channels.push_back(appConfig.republishChannel);
}
for (auto&& channel : channels)
{
std::stringstream ss;
@ -125,7 +135,7 @@ namespace snake
{
std::stringstream ss;
ss << "Cannot publish to redis host " << errMsg;
handleError("rtm/publish", ws, pdu, ss.str());
handleError("rtm/publish", ws, pduId, ss.str());
return;
}
}
@ -133,26 +143,27 @@ namespace snake
nlohmann::json response = {
{"action", "rtm/publish/ok"}, {"id", pdu.value("id", 1)}, {"body", {}}};
ws->sendText(response.dump());
ws.sendText(response.dump());
}
//
// FIXME: this is not cancellable. We should be able to cancel the redis subscription
//
void handleRedisSubscription(std::shared_ptr<SnakeConnectionState> state,
std::shared_ptr<ix::WebSocket> ws,
const AppConfig& appConfig,
const nlohmann::json& pdu)
void handleSubscribe(std::shared_ptr<SnakeConnectionState> state,
ix::WebSocket& ws,
const AppConfig& appConfig,
const nlohmann::json& pdu,
uint64_t pduId)
{
std::string channel = pdu["body"]["channel"];
std::string subscriptionId = channel;
state->subscriptionId = channel;
std::stringstream ss;
ss << state->appkey() << "::" << channel;
std::string appChannel(ss.str());
state->appChannel = ss.str();
ix::RedisClient redisClient;
ix::RedisClient& redisClient = state->subscriptionRedisClient;
int port = appConfig.redisPort;
auto urls = appConfig.redisHosts;
@ -163,7 +174,7 @@ namespace snake
{
std::stringstream ss;
ss << "Cannot connect to redis host " << hostname << ":" << port;
handleError("rtm/subscribe", ws, pdu, ss.str());
handleError("rtm/subscribe", ws, pduId, ss.str());
return;
}
@ -175,80 +186,90 @@ namespace snake
{
std::stringstream ss;
ss << "Cannot authenticated to redis";
handleError("rtm/subscribe", ws, pdu, ss.str());
handleError("rtm/subscribe", ws, pduId, ss.str());
return;
}
}
int id = 0;
auto callback = [ws, &id, &subscriptionId](const std::string& messageStr) {
std::string filterStr;
if (pdu["body"].find("filter") != pdu["body"].end())
{
std::string filterStr = pdu["body"]["filter"];
}
state->streamSql = std::make_unique<StreamSql>(filterStr);
state->id = 0;
state->onRedisSubscribeCallback = [&ws, state](const std::string& messageStr) {
auto msg = nlohmann::json::parse(messageStr);
msg = msg["body"]["message"];
if (state->streamSql->valid() && !state->streamSql->match(msg))
{
return;
}
nlohmann::json response = {
{"action", "rtm/subscription/data"},
{"id", id++},
{"id", state->id++},
{"body",
{{"subscription_id", subscriptionId}, {"position", "0-0"}, {"messages", {msg}}}}};
{{"subscription_id", state->subscriptionId}, {"position", "0-0"}, {"messages", {msg}}}}};
ws->sendText(response.dump());
ws.sendText(response.dump());
};
auto responseCallback = [ws, pdu, &subscriptionId](const std::string& redisResponse) {
state->onRedisSubscribeResponseCallback = [&ws, state, pduId](const std::string& redisResponse) {
std::stringstream ss;
ss << "Redis Response: " << redisResponse << "...";
ix::CoreLogger::log(ss.str().c_str());
// Success
nlohmann::json response = {{"action", "rtm/subscribe/ok"},
{"id", pdu.value("id", 1)},
{"body", {{"subscription_id", subscriptionId}}}};
ws->sendText(response.dump());
{"id", pduId},
{"body", {{"subscription_id", state->subscriptionId}}}};
ws.sendText(response.dump());
};
{
std::stringstream ss;
ss << "Subscribing to " << appChannel << "...";
ss << "Subscribing to " << state->appChannel << "...";
ix::CoreLogger::log(ss.str().c_str());
}
if (!redisClient.subscribe(appChannel, responseCallback, callback))
auto subscription = [&redisClient, state, &ws, pduId]
{
std::stringstream ss;
ss << "Error subscribing to channel " << appChannel;
handleError("rtm/subscribe", ws, pdu, ss.str());
return;
}
}
if (!redisClient.subscribe(state->appChannel,
state->onRedisSubscribeResponseCallback,
state->onRedisSubscribeCallback))
{
std::stringstream ss;
ss << "Error subscribing to channel " << state->appChannel;
handleError("rtm/subscribe", ws, pduId, ss.str());
return;
}
};
void handleSubscribe(std::shared_ptr<SnakeConnectionState> state,
std::shared_ptr<ix::WebSocket> ws,
const AppConfig& appConfig,
const nlohmann::json& pdu)
{
state->fut =
std::async(std::launch::async, handleRedisSubscription, state, ws, appConfig, pdu);
state->subscriptionThread = std::thread(subscription);
}
void handleUnSubscribe(std::shared_ptr<SnakeConnectionState> state,
std::shared_ptr<ix::WebSocket> ws,
const nlohmann::json& pdu)
ix::WebSocket& ws,
const nlohmann::json& pdu,
uint64_t pduId)
{
// extract subscription_id
auto body = pdu["body"];
auto subscriptionId = body["subscription_id"];
state->redisClient().stop();
state->stopSubScriptionThread();
nlohmann::json response = {{"action", "rtm/unsubscribe/ok"},
{"id", pdu.value("id", 1)},
{"id", pduId},
{"body", {{"subscription_id", subscriptionId}}}};
ws->sendText(response.dump());
ws.sendText(response.dump());
}
void processCobraMessage(std::shared_ptr<SnakeConnectionState> state,
std::shared_ptr<ix::WebSocket> ws,
ix::WebSocket& ws,
const AppConfig& appConfig,
const std::string& str)
{
@ -263,31 +284,32 @@ namespace snake
ss << "malformed json pdu: " << e.what() << " -> " << str << "";
nlohmann::json response = {{"body", {{"error", "invalid_json"}, {"reason", ss.str()}}}};
ws->sendText(response.dump());
ws.sendText(response.dump());
return;
}
auto action = pdu["action"];
uint64_t pduId = pdu.value("id", 1);
if (action == "auth/handshake")
{
handleHandshake(state, ws, pdu);
handleHandshake(state, ws, pdu, pduId);
}
else if (action == "auth/authenticate")
{
handleAuth(state, ws, appConfig, pdu);
handleAuth(state, ws, appConfig, pdu, pduId);
}
else if (action == "rtm/publish")
{
handlePublish(state, ws, pdu);
handlePublish(state, ws, appConfig, pdu, pduId);
}
else if (action == "rtm/subscribe")
{
handleSubscribe(state, ws, appConfig, pdu);
handleSubscribe(state, ws, appConfig, pdu, pduId);
}
else if (action == "rtm/unsubscribe")
{
handleUnSubscribe(state, ws, pdu);
handleUnSubscribe(state, ws, pdu, pduId);
}
else
{

View File

@ -20,7 +20,7 @@ namespace snake
struct AppConfig;
void processCobraMessage(std::shared_ptr<SnakeConnectionState> state,
std::shared_ptr<ix::WebSocket> ws,
ix::WebSocket& ws,
const AppConfig& appConfig,
const std::string& str);
} // namespace snake

View File

@ -59,65 +59,67 @@ namespace snake
};
_server.setConnectionStateFactory(factory);
_server.setOnConnectionCallback(
[this](std::shared_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ix::ConnectionState> connectionState) {
_server.setOnClientMessageCallback(
[this](std::shared_ptr<ix::ConnectionState> connectionState,
ix::WebSocket& webSocket,
const ix::WebSocketMessagePtr& msg) {
auto state = std::dynamic_pointer_cast<SnakeConnectionState>(connectionState);
auto remoteIp = connectionState->getRemoteIp();
std::stringstream ss;
ss << "[" << state->getId() << "] ";
webSocket->setOnMessageCallback(
[this, webSocket, state](const ix::WebSocketMessagePtr& msg) {
std::stringstream ss;
ix::LogLevel logLevel = ix::LogLevel::Debug;
if (msg->type == ix::WebSocketMessageType::Open)
{
ss << "New connection" << std::endl;
ss << "id: " << state->getId() << std::endl;
ss << "Uri: " << msg->openInfo.uri << std::endl;
ss << "Headers:" << std::endl;
for (auto it : msg->openInfo.headers)
{
ss << it.first << ": " << it.second << std::endl;
}
ix::LogLevel logLevel = ix::LogLevel::Debug;
if (msg->type == ix::WebSocketMessageType::Open)
{
ss << "New connection" << std::endl;
ss << "remote ip: " << remoteIp << std::endl;
ss << "id: " << state->getId() << std::endl;
ss << "Uri: " << msg->openInfo.uri << std::endl;
ss << "Headers:" << std::endl;
for (auto it : msg->openInfo.headers)
{
ss << it.first << ": " << it.second << std::endl;
}
std::string appkey = parseAppKey(msg->openInfo.uri);
state->setAppkey(appkey);
std::string appkey = parseAppKey(msg->openInfo.uri);
state->setAppkey(appkey);
// Connect to redis first
if (!state->redisClient().connect(_appConfig.redisHosts[0],
_appConfig.redisPort))
{
ss << "Cannot connect to redis host" << std::endl;
logLevel = ix::LogLevel::Error;
}
}
else if (msg->type == ix::WebSocketMessageType::Close)
{
ss << "Closed connection"
<< " code " << msg->closeInfo.code << " reason "
<< msg->closeInfo.reason << std::endl;
}
else if (msg->type == ix::WebSocketMessageType::Error)
{
std::stringstream ss;
ss << "Connection error: " << msg->errorInfo.reason << std::endl;
ss << "#retries: " << msg->errorInfo.retries << std::endl;
ss << "Wait time(ms): " << msg->errorInfo.wait_time << std::endl;
ss << "HTTP Status: " << msg->errorInfo.http_status << std::endl;
logLevel = ix::LogLevel::Error;
}
else if (msg->type == ix::WebSocketMessageType::Fragment)
{
ss << "Received message fragment" << std::endl;
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
ss << "Received " << msg->wireSize << " bytes" << std::endl;
processCobraMessage(state, webSocket, _appConfig, msg->str);
}
// Connect to redis first
if (!state->redisClient().connect(_appConfig.redisHosts[0],
_appConfig.redisPort))
{
ss << "Cannot connect to redis host" << std::endl;
logLevel = ix::LogLevel::Error;
}
}
else if (msg->type == ix::WebSocketMessageType::Close)
{
ss << "Closed connection"
<< " code " << msg->closeInfo.code << " reason "
<< msg->closeInfo.reason << std::endl;
}
else if (msg->type == ix::WebSocketMessageType::Error)
{
std::stringstream ss;
ss << "Connection error: " << msg->errorInfo.reason << std::endl;
ss << "#retries: " << msg->errorInfo.retries << std::endl;
ss << "Wait time(ms): " << msg->errorInfo.wait_time << std::endl;
ss << "HTTP Status: " << msg->errorInfo.http_status << std::endl;
logLevel = ix::LogLevel::Error;
}
else if (msg->type == ix::WebSocketMessageType::Fragment)
{
ss << "Received message fragment" << std::endl;
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
ss << "Received " << msg->wireSize << " bytes" << " " << msg->str << std::endl;
processCobraMessage(state, webSocket, _appConfig, msg->str);
}
ix::CoreLogger::log(ss.str().c_str(), logLevel);
});
});
ix::CoreLogger::log(ss.str().c_str(), logLevel);
});
auto res = _server.listen();
if (!res.first)

View File

@ -0,0 +1,63 @@
/*
* IXStreamSql.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2020 Machine Zone, Inc. All rights reserved.
*
* Super simple hacked up version of a stream sql expression,
* that only supports non nested field evaluation
*/
#include "IXStreamSql.h"
#include <sstream>
#include <iostream>
namespace snake
{
StreamSql::StreamSql(const std::string& sqlFilter)
: _valid(false)
{
std::string token;
std::stringstream tokenStream(sqlFilter);
std::vector<std::string> tokens;
// Split by ' '
while (std::getline(tokenStream, token, ' '))
{
tokens.push_back(token);
}
_valid = tokens.size() == 8;
if (!_valid) return;
_field = tokens[5];
_operator = tokens[6];
_value = tokens[7];
// remove single quotes
_value = _value.substr(1, _value.size() - 2);
if (_operator == "LIKE")
{
_value = _value.substr(1, _value.size() - 2);
}
}
bool StreamSql::valid() const
{
return _valid;
}
bool StreamSql::match(const nlohmann::json& msg)
{
if (!_valid) return false;
if (msg.find(_field) == msg.end())
{
return false;
}
std::string value = msg[_field];
return value == _value;
}
} // namespace snake

View File

@ -0,0 +1,29 @@
/*
* IXStreamSql.h
* Author: Benjamin Sergeant
* Copyright (c) 2020 Machine Zone, Inc. All rights reserved.
*/
#pragma once
#include <string>
#include "nlohmann/json.hpp"
namespace snake
{
class StreamSql
{
public:
StreamSql(const std::string& sqlFilter = std::string());
~StreamSql() = default;
bool match(const nlohmann::json& msg);
bool valid() const;
private:
std::string _field;
std::string _operator;
std::string _value;
bool _valid;
};
}

View File

@ -12,10 +12,8 @@ namespace ix
{
Bench::Bench(const std::string& description)
: _description(description)
, _start(std::chrono::high_resolution_clock::now())
, _reported(false)
{
;
reset();
}
Bench::~Bench()
@ -26,19 +24,38 @@ namespace ix
}
}
void Bench::reset()
{
_start = std::chrono::high_resolution_clock::now();
_reported = false;
}
void Bench::report()
{
auto now = std::chrono::high_resolution_clock::now();
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now - _start);
auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(now - _start);
_ms = milliseconds.count();
std::cerr << _description << " completed in " << _ms << "ms" << std::endl;
_duration = microseconds.count();
std::cerr << _description << " completed in " << _duration << " us" << std::endl;
setReported();
}
void Bench::record()
{
auto now = std::chrono::high_resolution_clock::now();
auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(now - _start);
_duration = microseconds.count();
}
void Bench::setReported()
{
_reported = true;
}
uint64_t Bench::getDuration() const
{
return _ms;
return _duration;
}
} // namespace ix

View File

@ -3,6 +3,7 @@
* Author: Benjamin Sergeant
* Copyright (c) 2017-2020 Machine Zone, Inc. All rights reserved.
*/
#pragma once
#include <chrono>
#include <stdint.h>
@ -16,13 +17,16 @@ namespace ix
Bench(const std::string& description);
~Bench();
void reset();
void record();
void report();
void setReported();
uint64_t getDuration() const;
private:
std::string _description;
std::chrono::time_point<std::chrono::high_resolution_clock> _start;
uint64_t _ms;
uint64_t _duration;
bool _reported;
};
} // namespace ix

View File

@ -31,6 +31,11 @@ namespace ix
return std::make_shared<ConnectionState>();
}
void ConnectionState::setOnSetTerminatedCallback(const OnSetTerminatedCallback& callback)
{
_onSetTerminatedCallback = callback;
}
bool ConnectionState::isTerminated() const
{
return _terminated;
@ -39,5 +44,30 @@ namespace ix
void ConnectionState::setTerminated()
{
_terminated = true;
if (_onSetTerminatedCallback)
{
_onSetTerminatedCallback();
}
}
const std::string& ConnectionState::getRemoteIp()
{
return _remoteIp;
}
int ConnectionState::getRemotePort()
{
return _remotePort;
}
void ConnectionState::setRemoteIp(const std::string& remoteIp)
{
_remoteIp = remoteIp;
}
void ConnectionState::setRemotePort(int remotePort)
{
_remotePort = remotePort;
}
} // namespace ix

View File

@ -7,12 +7,15 @@
#pragma once
#include <atomic>
#include <functional>
#include <memory>
#include <stdint.h>
#include <string>
namespace ix
{
using OnSetTerminatedCallback = std::function<void()>;
class ConnectionState
{
public:
@ -25,12 +28,27 @@ namespace ix
void setTerminated();
bool isTerminated() const;
const std::string& getRemoteIp();
int getRemotePort();
static std::shared_ptr<ConnectionState> createConnectionState();
private:
void setOnSetTerminatedCallback(const OnSetTerminatedCallback& callback);
void setRemoteIp(const std::string& remoteIp);
void setRemotePort(int remotePort);
protected:
std::atomic<bool> _terminated;
std::string _id;
OnSetTerminatedCallback _onSetTerminatedCallback;
static std::atomic<uint64_t> _globalId;
std::string _remoteIp;
int _remotePort;
friend class SocketServer;
};
} // namespace ix

View File

@ -1,5 +1,5 @@
/*
* IXExponentialBackoff.h
* IXExponentialBackoff.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved.
*/

148
ixwebsocket/IXGzipCodec.cpp Normal file
View File

@ -0,0 +1,148 @@
/*
* IXGzipCodec.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2020 Machine Zone, Inc. All rights reserved.
*/
#include "IXGzipCodec.h"
#include "IXBench.h"
#include <array>
#include <string.h>
#ifdef IXWEBSOCKET_USE_ZLIB
#include <zlib.h>
#endif
#ifdef IXWEBSOCKET_USE_DEFLATE
#include <libdeflate.h>
#endif
namespace ix
{
#ifdef IXWEBSOCKET_USE_ZLIB
std::string gzipCompress(const std::string& str)
{
#ifdef IXWEBSOCKET_USE_DEFLATE
int compressionLevel = 6;
struct libdeflate_compressor* compressor;
compressor = libdeflate_alloc_compressor(compressionLevel);
const void* uncompressed_data = str.data();
size_t uncompressed_size = str.size();
void* compressed_data;
size_t actual_compressed_size;
size_t max_compressed_size;
max_compressed_size = libdeflate_gzip_compress_bound(compressor, uncompressed_size);
compressed_data = malloc(max_compressed_size);
if (compressed_data == NULL)
{
return std::string();
}
actual_compressed_size = libdeflate_gzip_compress(
compressor, uncompressed_data, uncompressed_size, compressed_data, max_compressed_size);
libdeflate_free_compressor(compressor);
if (actual_compressed_size == 0)
{
free(compressed_data);
return std::string();
}
std::string out;
out.assign(reinterpret_cast<char*>(compressed_data), actual_compressed_size);
free(compressed_data);
return out;
#else
z_stream zs; // z_stream is zlib's control structure
memset(&zs, 0, sizeof(zs));
// deflateInit2 configure the file format: request gzip instead of deflate
const int windowBits = 15;
const int GZIP_ENCODING = 16;
deflateInit2(&zs,
Z_DEFAULT_COMPRESSION,
Z_DEFLATED,
windowBits | GZIP_ENCODING,
8,
Z_DEFAULT_STRATEGY);
zs.next_in = (Bytef*) str.data();
zs.avail_in = (uInt) str.size(); // set the z_stream's input
int ret;
char outbuffer[32768];
std::string outstring;
// retrieve the compressed bytes blockwise
do
{
zs.next_out = reinterpret_cast<Bytef*>(outbuffer);
zs.avail_out = sizeof(outbuffer);
ret = deflate(&zs, Z_FINISH);
if (outstring.size() < zs.total_out)
{
// append the block to the output string
outstring.append(outbuffer, zs.total_out - outstring.size());
}
} while (ret == Z_OK);
deflateEnd(&zs);
return outstring;
#endif
}
bool gzipDecompress(const std::string& in, std::string& out)
{
z_stream inflateState;
memset(&inflateState, 0, sizeof(inflateState));
inflateState.zalloc = Z_NULL;
inflateState.zfree = Z_NULL;
inflateState.opaque = Z_NULL;
inflateState.avail_in = 0;
inflateState.next_in = Z_NULL;
if (inflateInit2(&inflateState, 16 + MAX_WBITS) != Z_OK)
{
return false;
}
inflateState.avail_in = (uInt) in.size();
inflateState.next_in = (unsigned char*) (const_cast<char*>(in.data()));
const int kBufferSize = 1 << 14;
std::array<unsigned char, kBufferSize> compressBuffer;
do
{
inflateState.avail_out = (uInt) kBufferSize;
inflateState.next_out = &compressBuffer.front();
int ret = inflate(&inflateState, Z_SYNC_FLUSH);
if (ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR)
{
inflateEnd(&inflateState);
return false;
}
out.append(reinterpret_cast<char*>(&compressBuffer.front()),
kBufferSize - inflateState.avail_out);
} while (inflateState.avail_out == 0);
inflateEnd(&inflateState);
return true;
}
#endif
} // namespace ix

15
ixwebsocket/IXGzipCodec.h Normal file
View File

@ -0,0 +1,15 @@
/*
* IXGzipCodec.h
* Author: Benjamin Sergeant
* Copyright (c) 2020 Machine Zone, Inc. All rights reserved.
*/
#pragma once
#include <string>
namespace ix
{
std::string gzipCompress(const std::string& str);
bool gzipDecompress(const std::string& in, std::string& out);
} // namespace ix

View File

@ -7,6 +7,7 @@
#include "IXHttp.h"
#include "IXCancellationRequest.h"
#include "IXGzipCodec.h"
#include "IXSocket.h"
#include <sstream>
#include <vector>
@ -93,14 +94,12 @@ namespace ix
}
std::tuple<bool, std::string, HttpRequestPtr> Http::parseRequest(
std::unique_ptr<Socket>& socket)
std::unique_ptr<Socket>& socket, int timeoutSecs)
{
HttpRequestPtr httpRequest;
std::atomic<bool> requestInitCancellation(false);
int timeoutSecs = 5; // FIXME
auto isCancellationRequested =
makeCancellationRequestWithTimeout(timeoutSecs, requestInitCancellation);
@ -130,7 +129,53 @@ namespace ix
return std::make_tuple(false, "Error parsing HTTP headers", httpRequest);
}
httpRequest = std::make_shared<HttpRequest>(uri, method, httpVersion, headers);
std::string body;
if (headers.find("Content-Length") != headers.end())
{
int contentLength = 0;
try
{
contentLength = std::stoi(headers["Content-Length"]);
}
catch (std::exception)
{
return std::make_tuple(
false, "Error parsing HTTP Header 'Content-Length'", httpRequest);
}
if (contentLength < 0)
{
return std::make_tuple(
false, "Error: 'Content-Length' should be a positive integer", httpRequest);
}
auto res = socket->readBytes(contentLength, nullptr, isCancellationRequested);
if (!res.first)
{
return std::make_tuple(
false, std::string("Error reading request: ") + res.second, httpRequest);
}
body = res.second;
}
// If the content was compressed with gzip, decode it
if (headers["Content-Encoding"] == "gzip")
{
#ifdef IXWEBSOCKET_USE_ZLIB
std::string decompressedPayload;
if (!gzipDecompress(body, decompressedPayload))
{
return std::make_tuple(
false, std::string("Error during gzip decompression of the body"), httpRequest);
}
body = decompressedPayload;
#else
std::string errorMsg("ixwebsocket was not compiled with gzip support on");
return std::make_tuple(false, errorMsg, httpRequest);
#endif
}
httpRequest = std::make_shared<HttpRequest>(uri, method, httpVersion, body, headers);
return std::make_tuple(true, "", httpRequest);
}
@ -151,7 +196,7 @@ namespace ix
// Write headers
ss.str("");
ss << "Content-Length: " << response->payload.size() << "\r\n";
ss << "Content-Length: " << response->body.size() << "\r\n";
for (auto&& it : response->headers)
{
ss << it.first << ": " << it.second << "\r\n";
@ -163,6 +208,6 @@ namespace ix
return false;
}
return response->payload.empty() ? true : socket->writeBytes(response->payload, nullptr);
return response->body.empty() ? true : socket->writeBytes(response->body, nullptr);
}
} // namespace ix

View File

@ -39,7 +39,7 @@ namespace ix
std::string description;
HttpErrorCode errorCode;
WebSocketHttpHeaders headers;
std::string payload;
std::string body;
std::string errorMsg;
uint64_t uploadSize;
uint64_t downloadSize;
@ -48,7 +48,7 @@ namespace ix
const std::string& des = std::string(),
const HttpErrorCode& c = HttpErrorCode::Ok,
const WebSocketHttpHeaders& h = WebSocketHttpHeaders(),
const std::string& p = std::string(),
const std::string& b = std::string(),
const std::string& e = std::string(),
uint64_t u = 0,
uint64_t d = 0)
@ -56,7 +56,7 @@ namespace ix
, description(des)
, errorCode(c)
, headers(h)
, payload(p)
, body(b)
, errorMsg(e)
, uploadSize(u)
, downloadSize(d)
@ -84,6 +84,7 @@ namespace ix
int maxRedirects = 5;
bool verbose = false;
bool compress = true;
bool compressRequest = false;
Logger logger;
OnProgressCallback onProgressCallback;
};
@ -95,15 +96,18 @@ namespace ix
std::string uri;
std::string method;
std::string version;
std::string body;
WebSocketHttpHeaders headers;
HttpRequest(const std::string& u,
const std::string& m,
const std::string& v,
const std::string& b,
const WebSocketHttpHeaders& h = WebSocketHttpHeaders())
: uri(u)
, method(m)
, version(v)
, body(b)
, headers(h)
{
}
@ -115,7 +119,7 @@ namespace ix
{
public:
static std::tuple<bool, std::string, HttpRequestPtr> parseRequest(
std::unique_ptr<Socket>& socket);
std::unique_ptr<Socket>& socket, int timeoutSecs);
static bool sendResponse(HttpResponsePtr response, std::unique_ptr<Socket>& socket);
static std::pair<std::string, int> parseStatusLine(const std::string& line);

View File

@ -6,6 +6,7 @@
#include "IXHttpClient.h"
#include "IXGzipCodec.h"
#include "IXSocketFactory.h"
#include "IXUrlParser.h"
#include "IXUserAgent.h"
@ -16,7 +17,6 @@
#include <random>
#include <sstream>
#include <vector>
#include <zlib.h>
namespace ix
{
@ -174,11 +174,13 @@ namespace ix
ss << verb << " " << path << " HTTP/1.1\r\n";
ss << "Host: " << host << "\r\n";
#ifdef IXWEBSOCKET_USE_ZLIB
if (args->compress)
{
ss << "Accept-Encoding: gzip"
<< "\r\n";
}
#endif
// Append extra headers
for (auto&& it : args->extraHeaders)
@ -201,6 +203,15 @@ namespace ix
if (verb == kPost || verb == kPut || verb == kPatch || _forceBody)
{
// Set request compression header
#ifdef IXWEBSOCKET_USE_ZLIB
if (args->compressRequest)
{
ss << "Content-Encoding: gzip"
<< "\r\n";
}
#endif
ss << "Content-Length: " << body.size() << "\r\n";
// Set default Content-Type if unspecified
@ -498,8 +509,9 @@ namespace ix
// If the content was compressed with gzip, decode it
if (headers["Content-Encoding"] == "gzip")
{
#ifdef IXWEBSOCKET_USE_ZLIB
std::string decompressedPayload;
if (!gzipInflate(payload, decompressedPayload))
if (!gzipDecompress(payload, decompressedPayload))
{
std::string errorMsg("Error decompressing payload");
return std::make_shared<HttpResponse>(code,
@ -512,6 +524,17 @@ namespace ix
downloadSize);
}
payload = decompressedPayload;
#else
std::string errorMsg("ixwebsocket was not compiled with gzip support on");
return std::make_shared<HttpResponse>(code,
description,
HttpErrorCode::Gzip,
headers,
payload,
errorMsg,
uploadSize,
downloadSize);
#endif
}
return std::make_shared<HttpResponse>(code,
@ -539,11 +562,42 @@ namespace ix
return request(url, kDel, std::string(), args);
}
HttpResponsePtr HttpClient::request(const std::string& url,
const std::string& verb,
const HttpParameters& httpParameters,
const HttpFormDataParameters& httpFormDataParameters,
HttpRequestArgsPtr args)
{
std::string body;
if (httpFormDataParameters.empty())
{
body = serializeHttpParameters(httpParameters);
}
else
{
std::string multipartBoundary = generateMultipartBoundary();
args->multipartBoundary = multipartBoundary;
body = serializeHttpFormDataParameters(
multipartBoundary, httpFormDataParameters, httpParameters);
}
#ifdef IXWEBSOCKET_USE_ZLIB
if (args->compressRequest)
{
body = gzipCompress(body);
}
#endif
return request(url, verb, body, args);
}
HttpResponsePtr HttpClient::post(const std::string& url,
const HttpParameters& httpParameters,
const HttpFormDataParameters& httpFormDataParameters,
HttpRequestArgsPtr args)
{
return request(url, kPost, serializeHttpParameters(httpParameters), args);
return request(url, kPost, httpParameters, httpFormDataParameters, args);
}
HttpResponsePtr HttpClient::post(const std::string& url,
@ -555,9 +609,10 @@ namespace ix
HttpResponsePtr HttpClient::put(const std::string& url,
const HttpParameters& httpParameters,
const HttpFormDataParameters& httpFormDataParameters,
HttpRequestArgsPtr args)
{
return request(url, kPut, serializeHttpParameters(httpParameters), args);
return request(url, kPut, httpParameters, httpFormDataParameters, args);
}
HttpResponsePtr HttpClient::put(const std::string& url,
@ -569,9 +624,10 @@ namespace ix
HttpResponsePtr HttpClient::patch(const std::string& url,
const HttpParameters& httpParameters,
const HttpFormDataParameters& httpFormDataParameters,
HttpRequestArgsPtr args)
{
return request(url, kPatch, serializeHttpParameters(httpParameters), args);
return request(url, kPatch, httpParameters, httpFormDataParameters, args);
}
HttpResponsePtr HttpClient::patch(const std::string& url,
@ -672,51 +728,6 @@ namespace ix
return ss.str();
}
bool HttpClient::gzipInflate(const std::string& in, std::string& out)
{
z_stream inflateState;
std::memset(&inflateState, 0, sizeof(inflateState));
inflateState.zalloc = Z_NULL;
inflateState.zfree = Z_NULL;
inflateState.opaque = Z_NULL;
inflateState.avail_in = 0;
inflateState.next_in = Z_NULL;
if (inflateInit2(&inflateState, 16 + MAX_WBITS) != Z_OK)
{
return false;
}
inflateState.avail_in = (uInt) in.size();
inflateState.next_in = (unsigned char*) (const_cast<char*>(in.data()));
const int kBufferSize = 1 << 14;
std::unique_ptr<unsigned char[]> compressBuffer =
std::make_unique<unsigned char[]>(kBufferSize);
do
{
inflateState.avail_out = (uInt) kBufferSize;
inflateState.next_out = compressBuffer.get();
int ret = inflate(&inflateState, Z_SYNC_FLUSH);
if (ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR)
{
inflateEnd(&inflateState);
return false;
}
out.append(reinterpret_cast<char*>(compressBuffer.get()),
kBufferSize - inflateState.avail_out);
} while (inflateState.avail_out == 0);
inflateEnd(&inflateState);
return true;
}
void HttpClient::log(const std::string& msg, HttpRequestArgsPtr args)
{
if (args->logger)

View File

@ -34,6 +34,7 @@ namespace ix
HttpResponsePtr post(const std::string& url,
const HttpParameters& httpParameters,
const HttpFormDataParameters& httpFormDataParameters,
HttpRequestArgsPtr args);
HttpResponsePtr post(const std::string& url,
const std::string& body,
@ -41,6 +42,7 @@ namespace ix
HttpResponsePtr put(const std::string& url,
const HttpParameters& httpParameters,
const HttpFormDataParameters& httpFormDataParameters,
HttpRequestArgsPtr args);
HttpResponsePtr put(const std::string& url,
const std::string& body,
@ -48,6 +50,7 @@ namespace ix
HttpResponsePtr patch(const std::string& url,
const HttpParameters& httpParameters,
const HttpFormDataParameters& httpFormDataParameters,
HttpRequestArgsPtr args);
HttpResponsePtr patch(const std::string& url,
const std::string& body,
@ -58,7 +61,15 @@ namespace ix
const std::string& body,
HttpRequestArgsPtr args,
int redirects = 0);
HttpResponsePtr request(const std::string& url,
const std::string& verb,
const HttpParameters& httpParameters,
const HttpFormDataParameters& httpFormDataParameters,
HttpRequestArgsPtr args);
void setForceBody(bool value);
// Async API
HttpRequestArgsPtr createRequest(const std::string& url = std::string(),
const std::string& verb = HttpClient::kGet);
@ -90,8 +101,6 @@ namespace ix
private:
void log(const std::string& msg, HttpRequestArgsPtr args);
bool gzipInflate(const std::string& in, std::string& out);
// Async API background thread runner
void run();
// Async API
@ -103,9 +112,9 @@ namespace ix
std::thread _thread;
std::unique_ptr<Socket> _socket;
std::recursive_mutex _mutex; // to protect accessing the _socket (only one socket per client)
// the mutex needs to be recursive as this function might
// be called recursively to follow HTTP redirections
std::recursive_mutex _mutex; // to protect accessing the _socket (only one socket per
// client) the mutex needs to be recursive as this function
// might be called recursively to follow HTTP redirections
SocketTLSOptions _tlsOptions;

View File

@ -6,14 +6,14 @@
#include "IXHttpServer.h"
#include "IXGzipCodec.h"
#include "IXNetSystem.h"
#include "IXSocketConnect.h"
#include "IXUserAgent.h"
#include <cstring>
#include <fstream>
#include <sstream>
#include <vector>
#include <zlib.h>
#include <cstring>
namespace
{
@ -40,55 +40,21 @@ namespace
auto vec = res.second;
return std::make_pair(res.first, std::string(vec.begin(), vec.end()));
}
std::string gzipCompress(const std::string& str)
{
z_stream zs; // z_stream is zlib's control structure
memset(&zs, 0, sizeof(zs));
// deflateInit2 configure the file format: request gzip instead of deflate
const int windowBits = 15;
const int GZIP_ENCODING = 16;
deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
windowBits | GZIP_ENCODING, 8,
Z_DEFAULT_STRATEGY);
zs.next_in = (Bytef*) str.data();
zs.avail_in = (uInt) str.size(); // set the z_stream's input
int ret;
char outbuffer[32768];
std::string outstring;
// retrieve the compressed bytes blockwise
do
{
zs.next_out = reinterpret_cast<Bytef*>(outbuffer);
zs.avail_out = sizeof(outbuffer);
ret = deflate(&zs, Z_FINISH);
if(outstring.size() < zs.total_out)
{
// append the block to the output string
outstring.append(outbuffer,
zs.total_out - outstring.size());
}
} while(ret == Z_OK);
deflateEnd(&zs);
return outstring;
}
} // namespace
namespace ix
{
HttpServer::HttpServer(
int port, const std::string& host, int backlog, size_t maxConnections, int addressFamily)
const int HttpServer::kDefaultTimeoutSecs(30);
HttpServer::HttpServer(int port,
const std::string& host,
int backlog,
size_t maxConnections,
int addressFamily,
int timeoutSecs)
: SocketServer(port, host, backlog, maxConnections, addressFamily)
, _connectedClientsCount(0)
, _timeoutSecs(timeoutSecs)
{
setDefaultConnectionCallback();
}
@ -117,7 +83,7 @@ namespace ix
{
_connectedClientsCount++;
auto ret = Http::parseRequest(socket);
auto ret = Http::parseRequest(socket, _timeoutSecs);
// FIXME: handle errors in parseRequest
if (std::get<0>(ret))
@ -142,7 +108,7 @@ namespace ix
{
setOnConnectionCallback(
[this](HttpRequestPtr request,
std::shared_ptr<ConnectionState> /*connectionState*/) -> HttpResponsePtr {
std::shared_ptr<ConnectionState> connectionState) -> HttpResponsePtr {
std::string uri(request->uri);
if (uri.empty() || uri == "/")
{
@ -163,16 +129,19 @@ namespace ix
std::string content = res.second;
#ifdef IXWEBSOCKET_USE_ZLIB
std::string acceptEncoding = request->headers["Accept-encoding"];
if (acceptEncoding == "*" || acceptEncoding.find("gzip") != std::string::npos)
{
content = gzipCompress(content);
headers["Content-Encoding"] = "gzip";
}
#endif
// Log request
std::stringstream ss;
ss << request->method << " " << request->headers["User-Agent"] << " "
ss << connectionState->getRemoteIp() << ":" << connectionState->getRemotePort()
<< " " << request->method << " " << request->headers["User-Agent"] << " "
<< request->uri << " " << content.size();
logInfo(ss.str());
@ -198,13 +167,14 @@ namespace ix
setOnConnectionCallback(
[this,
redirectUrl](HttpRequestPtr request,
std::shared_ptr<ConnectionState> /*connectionState*/) -> HttpResponsePtr {
std::shared_ptr<ConnectionState> connectionState) -> HttpResponsePtr {
WebSocketHttpHeaders headers;
headers["Server"] = userAgent();
// Log request
std::stringstream ss;
ss << request->method << " " << request->headers["User-Agent"] << " "
ss << connectionState->getRemoteIp() << ":" << connectionState->getRemotePort()
<< " " << request->method << " " << request->headers["User-Agent"] << " "
<< request->uri;
logInfo(ss.str());
@ -220,4 +190,40 @@ namespace ix
301, "OK", HttpErrorCode::Ok, headers, std::string());
});
}
//
// Display the client parameter and body on the console
//
void HttpServer::makeDebugServer()
{
setOnConnectionCallback(
[this](HttpRequestPtr request,
std::shared_ptr<ConnectionState> connectionState) -> HttpResponsePtr {
WebSocketHttpHeaders headers;
headers["Server"] = userAgent();
// Log request
std::stringstream ss;
ss << connectionState->getRemoteIp() << ":" << connectionState->getRemotePort()
<< " " << request->method << " " << request->headers["User-Agent"] << " "
<< request->uri;
logInfo(ss.str());
logInfo("== Headers == ");
for (auto&& it : request->headers)
{
std::ostringstream oss;
oss << it.first << ": " << it.second;
logInfo(oss.str());
}
logInfo("");
logInfo("== Body == ");
logInfo(request->body);
logInfo("");
return std::make_shared<HttpResponse>(
200, "OK", HttpErrorCode::Ok, headers, std::string("OK"));
});
}
} // namespace ix

View File

@ -29,7 +29,8 @@ namespace ix
const std::string& host = SocketServer::kDefaultHost,
int backlog = SocketServer::kDefaultTcpBacklog,
size_t maxConnections = SocketServer::kDefaultMaxConnections,
int addressFamily = SocketServer::kDefaultAddressFamily);
int addressFamily = SocketServer::kDefaultAddressFamily,
int timeoutSecs = HttpServer::kDefaultTimeoutSecs);
virtual ~HttpServer();
virtual void stop() final;
@ -37,11 +38,16 @@ namespace ix
void makeRedirectServer(const std::string& redirectUrl);
void makeDebugServer();
private:
// Member variables
OnConnectionCallback _onConnectionCallback;
std::atomic<int> _connectedClientsCount;
const static int kDefaultTimeoutSecs;
int _timeoutSecs;
// Methods
virtual void handleConnection(std::unique_ptr<Socket>,
std::shared_ptr<ConnectionState> connectionState) final;

View File

@ -8,6 +8,9 @@
namespace ix
{
const uint64_t SelectInterrupt::kSendRequest = 1;
const uint64_t SelectInterrupt::kCloseRequest = 2;
SelectInterrupt::SelectInterrupt()
{
;

View File

@ -6,6 +6,7 @@
#pragma once
#include <memory>
#include <stdint.h>
#include <string>
@ -23,5 +24,11 @@ namespace ix
virtual bool clear();
virtual uint64_t read();
virtual int getFd() const;
// Used as special codes for pipe communication
static const uint64_t kSendRequest;
static const uint64_t kCloseRequest;
};
using SelectInterruptPtr = std::unique_ptr<SelectInterrupt>;
} // namespace ix

View File

@ -5,8 +5,10 @@
*/
//
// On macOS we use UNIX pipes to wake up select.
// On UNIX we use pipes to wake up select. There is no way to do that
// on Windows so this file is compiled out on Windows.
//
#ifndef _WIN32
#include "IXSelectInterruptPipe.h"
@ -144,3 +146,5 @@ namespace ix
return _fildes[kPipeReadIndex];
}
} // namespace ix
#endif // !_WIN32

View File

@ -0,0 +1,81 @@
/*
* IXSetThreadName.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2018 2020 Machine Zone, Inc. All rights reserved.
*/
#include "IXSetThreadName.h"
// unix systems
#if defined(__APPLE__) || defined(__linux__) || defined(BSD)
#include <pthread.h>
#endif
// freebsd needs this header as well
#if defined(BSD)
#include <pthread_np.h>
#endif
// Windows
#ifdef _WIN32
#include <Windows.h>
#endif
namespace ix
{
#ifdef _WIN32
const DWORD MS_VC_EXCEPTION = 0x406D1388;
#pragma pack(push, 8)
typedef struct tagTHREADNAME_INFO
{
DWORD dwType; // Must be 0x1000.
LPCSTR szName; // Pointer to name (in user addr space).
DWORD dwThreadID; // Thread ID (-1=caller thread).
DWORD dwFlags; // Reserved for future use, must be zero.
} THREADNAME_INFO;
#pragma pack(pop)
void SetThreadName(DWORD dwThreadID, const char* threadName)
{
THREADNAME_INFO info;
info.dwType = 0x1000;
info.szName = threadName;
info.dwThreadID = dwThreadID;
info.dwFlags = 0;
__try
{
RaiseException(
MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*) &info);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
}
}
#endif
void setThreadName(const std::string& name)
{
#if defined(__APPLE__)
//
// Apple reserves 16 bytes for its thread names
// Notice that the Apple version of pthread_setname_np
// does not take a pthread_t argument
//
pthread_setname_np(name.substr(0, 63).c_str());
#elif defined(__linux__)
//
// Linux only reserves 16 bytes for its thread names
// See prctl and PR_SET_NAME property in
// http://man7.org/linux/man-pages/man2/prctl.2.html
//
pthread_setname_np(pthread_self(), name.substr(0, 15).c_str());
#elif defined(_WIN32)
SetThreadName(-1, name.c_str());
#elif defined(BSD)
pthread_set_name_np(pthread_self(), name.substr(0, 15).c_str());
#else
// ... assert here ?
#endif
}
} // namespace ix

View File

@ -27,8 +27,6 @@ namespace ix
{
const int Socket::kDefaultPollNoTimeout = -1; // No poll timeout by default
const int Socket::kDefaultPollTimeout = kDefaultPollNoTimeout;
const uint64_t Socket::kSendRequest = 1;
const uint64_t Socket::kCloseRequest = 2;
constexpr size_t Socket::kChunkSize;
Socket::Socket(int fd)
@ -96,11 +94,11 @@ namespace ix
{
uint64_t value = selectInterrupt->read();
if (value == kSendRequest)
if (value == SelectInterrupt::kSendRequest)
{
pollResult = PollResultType::SendRequest;
}
else if (value == kCloseRequest)
else if (value == SelectInterrupt::kCloseRequest)
{
pollResult = PollResultType::CloseRequest;
}

View File

@ -34,12 +34,10 @@ typedef SSIZE_T ssize_t;
#include "IXCancellationRequest.h"
#include "IXProgressCallback.h"
#include "IXSelectInterrupt.h"
namespace ix
{
class SelectInterrupt;
using SelectInterruptPtr = std::unique_ptr<SelectInterrupt>;
enum class PollResultType
{
ReadyForRead = 0,
@ -96,11 +94,6 @@ namespace ix
int sockfd,
const SelectInterruptPtr& selectInterrupt);
// Used as special codes for pipe communication
static const uint64_t kSendRequest;
static const uint64_t kCloseRequest;
protected:
std::atomic<int> _sockfd;
std::mutex _socketMutex;

View File

@ -8,6 +8,7 @@
#include "IXNetSystem.h"
#include "IXSelectInterrupt.h"
#include "IXSelectInterruptFactory.h"
#include "IXSetThreadName.h"
#include "IXSocket.h"
#include "IXSocketConnect.h"
@ -22,7 +23,7 @@ namespace ix
const int SocketServer::kDefaultPort(8080);
const std::string SocketServer::kDefaultHost("127.0.0.1");
const int SocketServer::kDefaultTcpBacklog(5);
const size_t SocketServer::kDefaultMaxConnections(32);
const size_t SocketServer::kDefaultMaxConnections(128);
const int SocketServer::kDefaultAddressFamily(AF_INET);
SocketServer::SocketServer(
@ -36,6 +37,7 @@ namespace ix
, _stop(false)
, _stopGc(false)
, _connectionStateFactory(&ConnectionState::createConnectionState)
, _acceptSelectInterrupt(createSelectInterrupt())
{
}
@ -58,6 +60,16 @@ namespace ix
std::pair<bool, std::string> SocketServer::listen()
{
std::string acceptSelectInterruptInitErrorMsg;
if (!_acceptSelectInterrupt->init(acceptSelectInterruptInitErrorMsg))
{
std::stringstream ss;
ss << "SocketServer::listen() error in SelectInterrupt::init: "
<< acceptSelectInterruptInitErrorMsg;
return std::make_pair(false, ss.str());
}
if (_addressFamily != AF_INET && _addressFamily != AF_INET6)
{
std::string errMsg("SocketServer::listen() AF_INET and AF_INET6 are currently "
@ -193,6 +205,12 @@ namespace ix
if (_thread.joinable())
{
_stop = true;
// Wake up select
if (!_acceptSelectInterrupt->notify(SelectInterrupt::kCloseRequest))
{
logError("SocketServer::stop: Cannot wake up from select");
}
_thread.join();
_stop = false;
}
@ -201,6 +219,7 @@ namespace ix
if (_gcThread.joinable())
{
_stopGc = true;
_conditionVariableGC.notify_one();
_gcThread.join();
_stopGc = false;
}
@ -249,18 +268,22 @@ namespace ix
// Set the socket to non blocking mode, so that accept calls are not blocking
SocketConnect::configure(_serverFd);
setThreadName("SocketServer::listen");
setThreadName("SocketServer::accept");
for (;;)
{
if (_stop) return;
// Use poll to check whether a new connection is in progress
int timeoutMs = 10;
int timeoutMs = -1;
#ifdef _WIN32
// select cannot be interrupted on Windows so we need to pass a small timeout
timeoutMs = 10;
#endif
bool readyToRead = true;
auto selectInterrupt = std::make_unique<SelectInterrupt>();
PollResultType pollResult =
Socket::poll(readyToRead, timeoutMs, _serverFd, selectInterrupt);
Socket::poll(readyToRead, timeoutMs, _serverFd, _acceptSelectInterrupt);
if (pollResult == PollResultType::Error)
{
@ -276,6 +299,7 @@ namespace ix
}
// Accept a connection.
// FIXME: Is this working for ipv6 ?
struct sockaddr_in client; // client address information
int clientFd; // socket connected to client
socklen_t addressLen = sizeof(client);
@ -307,11 +331,57 @@ namespace ix
continue;
}
// Retrieve connection info, the ip address of the remote peer/client)
std::string remoteIp;
int remotePort;
if (_addressFamily == AF_INET)
{
char remoteIp4[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &client.sin_addr, remoteIp4, INET_ADDRSTRLEN) == nullptr)
{
int err = Socket::getErrno();
std::stringstream ss;
ss << "SocketServer::run() error calling inet_ntop (ipv4): " << err << ", "
<< strerror(err);
logError(ss.str());
Socket::closeSocket(clientFd);
continue;
}
remotePort = client.sin_port;
remoteIp = remoteIp4;
}
else // AF_INET6
{
char remoteIp6[INET6_ADDRSTRLEN];
if (inet_ntop(AF_INET6, &client.sin_addr, remoteIp6, INET6_ADDRSTRLEN) == nullptr)
{
int err = Socket::getErrno();
std::stringstream ss;
ss << "SocketServer::run() error calling inet_ntop (ipv6): " << err << ", "
<< strerror(err);
logError(ss.str());
Socket::closeSocket(clientFd);
continue;
}
remotePort = client.sin_port;
remoteIp = remoteIp6;
}
std::shared_ptr<ConnectionState> connectionState;
if (_connectionStateFactory)
{
connectionState = _connectionStateFactory();
}
connectionState->setOnSetTerminatedCallback([this] { onSetTerminatedCallback(); });
connectionState->setRemoteIp(remoteIp);
connectionState->setRemotePort(remotePort);
if (_stop) return;
@ -368,8 +438,14 @@ namespace ix
break;
}
// Sleep a little bit then keep cleaning up
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Unless we are stopping the server, wait for a connection
// to be terminated to run the threads GC, instead of busy waiting
// with a sleep
if (!_stopGc)
{
std::unique_lock<std::mutex> lock(_conditionVariableMutexGC);
_conditionVariableGC.wait(lock);
}
}
}
@ -377,4 +453,11 @@ namespace ix
{
_socketTLSOptions = socketTLSOptions;
}
void SocketServer::onSetTerminatedCallback()
{
// a connection got terminated, we can run the connection thread GC,
// so wake up the thread responsible for that
_conditionVariableGC.notify_one();
}
} // namespace ix

View File

@ -7,6 +7,7 @@
#pragma once
#include "IXConnectionState.h"
#include "IXSelectInterrupt.h"
#include "IXSocketTLSOptions.h"
#include <atomic>
#include <condition_variable>
@ -83,6 +84,7 @@ namespace ix
// background thread to wait for incoming connections
std::thread _thread;
void run();
void onSetTerminatedCallback();
// background thread to cleanup (join) terminated threads
std::atomic<bool> _stopGc;
@ -110,5 +112,13 @@ namespace ix
size_t getConnectionsThreadsCount();
SocketTLSOptions _socketTLSOptions;
// to wake up from select
SelectInterruptPtr _acceptSelectInterrupt;
// used by the gc thread, to know that a thread needs to be garbage collected
// as a connection
std::condition_variable _conditionVariableGC;
std::mutex _conditionVariableMutexGC;
};
} // namespace ix

View File

@ -32,8 +32,8 @@
#include "IXUrlParser.h"
#include <algorithm>
#include <cstring>
#include <cstdlib>
#include <cstring>
namespace
{

View File

@ -8,7 +8,9 @@
#include "IXWebSocketVersion.h"
#include <sstream>
#ifdef IXWEBSOCKET_USE_ZLIB
#include <zlib.h>
#endif
// Platform name
#if defined(_WIN32)
@ -77,8 +79,10 @@ namespace ix
ss << " nossl";
#endif
#ifdef IXWEBSOCKET_USE_ZLIB
// Zlib version
ss << " zlib " << ZLIB_VERSION;
#endif
return ss.str();
}

View File

@ -46,6 +46,7 @@ namespace ix
WebSocket::~WebSocket()
{
stop();
_ws.setOnCloseCallback(nullptr);
}
void WebSocket::setUrl(const std::string& url)
@ -53,6 +54,7 @@ namespace ix
std::lock_guard<std::mutex> lock(_configMutex);
_url = url;
}
void WebSocket::setExtraHeaders(const WebSocketHttpHeaders& headers)
{
std::lock_guard<std::mutex> lock(_configMutex);
@ -404,6 +406,11 @@ namespace ix
_onMessageCallback = callback;
}
bool WebSocket::isOnMessageCallbackRegistered() const
{
return _onMessageCallback != nullptr;
}
void WebSocket::setTrafficTrackerCallback(const OnTrafficTrackerCallback& callback)
{
_onTrafficTrackerCallback = callback;

View File

@ -84,6 +84,7 @@ namespace ix
const std::string& reason = WebSocketCloseConstants::kNormalClosureMessage);
void setOnMessageCallback(const OnMessageCallback& callback);
bool isOnMessageCallbackRegistered() const;
static void setTrafficTrackerCallback(const OnTrafficTrackerCallback& callback);
static void resetTrafficTrackerCallback();

View File

@ -170,20 +170,11 @@ namespace ix
{
std::stringstream ss;
ss << "Expecting HTTP/1.1, got " << httpVersion << ". "
<< "Rejecting connection to " << host << ":" << port << ", status: " << status
<< "Rejecting connection to " << url << ", status: " << status
<< ", HTTP Status line: " << line;
return WebSocketInitResult(false, status, ss.str());
}
// We want an 101 HTTP status
if (status != 101)
{
std::stringstream ss;
ss << "Expecting status 101 (Switching Protocol), got " << status
<< " status connecting to " << host << ":" << port << ", HTTP Status line: " << line;
return WebSocketInitResult(false, status, ss.str());
}
auto result = parseHttpHeaders(_socket, isCancellationRequested);
auto headersValid = result.first;
auto headers = result.second;
@ -193,6 +184,17 @@ namespace ix
return WebSocketInitResult(false, status, "Error parsing HTTP headers");
}
// We want an 101 HTTP status for websocket, otherwise it could be
// a redirection (like 301)
if (status != 101)
{
std::stringstream ss;
ss << "Expecting status 101 (Switching Protocol), got " << status
<< " status connecting to " << url << ", HTTP Status line: " << line;
return WebSocketInitResult(false, status, ss.str(), headers, path);
}
// Check the presence of the connection field
if (headers.find("connection") == headers.end())
{

View File

@ -16,8 +16,6 @@ namespace
// is treated as a char* and the null termination (\x00) makes it
// look like an empty string.
const std::string kEmptyUncompressedBlock = std::string("\x00\x00\xff\xff", 4);
const int kBufferSize = 1 << 14;
} // namespace
namespace ix
@ -26,23 +24,27 @@ namespace ix
// Compressor
//
WebSocketPerMessageDeflateCompressor::WebSocketPerMessageDeflateCompressor()
: _compressBufferSize(kBufferSize)
{
#ifdef IXWEBSOCKET_USE_ZLIB
memset(&_deflateState, 0, sizeof(_deflateState));
_deflateState.zalloc = Z_NULL;
_deflateState.zfree = Z_NULL;
_deflateState.opaque = Z_NULL;
#endif
}
WebSocketPerMessageDeflateCompressor::~WebSocketPerMessageDeflateCompressor()
{
#ifdef IXWEBSOCKET_USE_ZLIB
deflateEnd(&_deflateState);
#endif
}
bool WebSocketPerMessageDeflateCompressor::init(uint8_t deflateBits,
bool clientNoContextTakeOver)
{
#ifdef IXWEBSOCKET_USE_ZLIB
int ret = deflateInit2(&_deflateState,
Z_DEFAULT_COMPRESSION,
Z_DEFLATED,
@ -52,22 +54,52 @@ namespace ix
if (ret != Z_OK) return false;
_compressBuffer = std::make_unique<unsigned char[]>(_compressBufferSize);
_flush = (clientNoContextTakeOver) ? Z_FULL_FLUSH : Z_SYNC_FLUSH;
return true;
#else
return false;
#endif
}
bool WebSocketPerMessageDeflateCompressor::endsWith(const std::string& value,
const std::string& ending)
template<typename T>
bool WebSocketPerMessageDeflateCompressor::endsWithEmptyUnCompressedBlock(const T& value)
{
if (ending.size() > value.size()) return false;
return std::equal(ending.rbegin(), ending.rend(), value.rbegin());
if (kEmptyUncompressedBlock.size() > value.size()) return false;
auto N = value.size();
return value[N - 1] == kEmptyUncompressedBlock[3] &&
value[N - 2] == kEmptyUncompressedBlock[2] &&
value[N - 3] == kEmptyUncompressedBlock[1] &&
value[N - 4] == kEmptyUncompressedBlock[0];
}
bool WebSocketPerMessageDeflateCompressor::compress(const std::string& in, std::string& out)
{
return compressData(in, out);
}
bool WebSocketPerMessageDeflateCompressor::compress(const std::string& in,
std::vector<uint8_t>& out)
{
return compressData(in, out);
}
bool WebSocketPerMessageDeflateCompressor::compress(const std::vector<uint8_t>& in,
std::string& out)
{
return compressData(in, out);
}
bool WebSocketPerMessageDeflateCompressor::compress(const std::vector<uint8_t>& in,
std::vector<uint8_t>& out)
{
return compressData(in, out);
}
template<typename T, typename S>
bool WebSocketPerMessageDeflateCompressor::compressData(const T& in, S& out)
{
#ifdef IXWEBSOCKET_USE_ZLIB
//
// 7.2.1. Compression
//
@ -96,7 +128,8 @@ namespace ix
// The normal buffer size should be 6 but
// we remove the 4 octets from the tail (#4)
uint8_t buf[2] = {0x02, 0x00};
out.append((char*) (buf), 2);
out.push_back(buf[0]);
out.push_back(buf[1]);
return true;
}
@ -107,30 +140,33 @@ namespace ix
do
{
// Output to local buffer
_deflateState.avail_out = (uInt) _compressBufferSize;
_deflateState.next_out = _compressBuffer.get();
_deflateState.avail_out = (uInt) _compressBuffer.size();
_deflateState.next_out = &_compressBuffer.front();
deflate(&_deflateState, _flush);
output = _compressBufferSize - _deflateState.avail_out;
output = _compressBuffer.size() - _deflateState.avail_out;
out.append((char*) (_compressBuffer.get()), output);
out.insert(out.end(), _compressBuffer.begin(), _compressBuffer.begin() + output);
} while (_deflateState.avail_out == 0);
if (endsWith(out, kEmptyUncompressedBlock))
if (endsWithEmptyUnCompressedBlock(out))
{
out.resize(out.size() - 4);
}
return true;
#else
return false;
#endif
}
//
// Decompressor
//
WebSocketPerMessageDeflateDecompressor::WebSocketPerMessageDeflateDecompressor()
: _compressBufferSize(kBufferSize)
{
#ifdef IXWEBSOCKET_USE_ZLIB
memset(&_inflateState, 0, sizeof(_inflateState));
_inflateState.zalloc = Z_NULL;
@ -138,29 +174,35 @@ namespace ix
_inflateState.opaque = Z_NULL;
_inflateState.avail_in = 0;
_inflateState.next_in = Z_NULL;
#endif
}
WebSocketPerMessageDeflateDecompressor::~WebSocketPerMessageDeflateDecompressor()
{
#ifdef IXWEBSOCKET_USE_ZLIB
inflateEnd(&_inflateState);
#endif
}
bool WebSocketPerMessageDeflateDecompressor::init(uint8_t inflateBits,
bool clientNoContextTakeOver)
{
#ifdef IXWEBSOCKET_USE_ZLIB
int ret = inflateInit2(&_inflateState, -1 * inflateBits);
if (ret != Z_OK) return false;
_compressBuffer = std::make_unique<unsigned char[]>(_compressBufferSize);
_flush = (clientNoContextTakeOver) ? Z_FULL_FLUSH : Z_SYNC_FLUSH;
return true;
#else
return false;
#endif
}
bool WebSocketPerMessageDeflateDecompressor::decompress(const std::string& in, std::string& out)
{
#ifdef IXWEBSOCKET_USE_ZLIB
//
// 7.2.2. Decompression
//
@ -182,8 +224,8 @@ namespace ix
do
{
_inflateState.avail_out = (uInt) _compressBufferSize;
_inflateState.next_out = _compressBuffer.get();
_inflateState.avail_out = (uInt) _compressBuffer.size();
_inflateState.next_out = &_compressBuffer.front();
int ret = inflate(&_inflateState, Z_SYNC_FLUSH);
@ -192,10 +234,13 @@ namespace ix
return false; // zlib error
}
out.append(reinterpret_cast<char*>(_compressBuffer.get()),
_compressBufferSize - _inflateState.avail_out);
out.append(reinterpret_cast<char*>(&_compressBuffer.front()),
_compressBuffer.size() - _inflateState.avail_out);
} while (_inflateState.avail_out == 0);
return true;
#else
return false;
#endif
}
} // namespace ix

View File

@ -6,9 +6,12 @@
#pragma once
#ifdef IXWEBSOCKET_USE_ZLIB
#include "zlib.h"
#include <memory>
#endif
#include <array>
#include <string>
#include <vector>
namespace ix
{
@ -20,14 +23,22 @@ namespace ix
bool init(uint8_t deflateBits, bool clientNoContextTakeOver);
bool compress(const std::string& in, std::string& out);
bool compress(const std::string& in, std::vector<uint8_t>& out);
bool compress(const std::vector<uint8_t>& in, std::string& out);
bool compress(const std::vector<uint8_t>& in, std::vector<uint8_t>& out);
private:
static bool endsWith(const std::string& value, const std::string& ending);
template<typename T, typename S>
bool compressData(const T& in, S& out);
template<typename T>
bool endsWithEmptyUnCompressedBlock(const T& value);
int _flush;
size_t _compressBufferSize;
std::unique_ptr<unsigned char[]> _compressBuffer;
std::array<unsigned char, 1 << 14> _compressBuffer;
#ifdef IXWEBSOCKET_USE_ZLIB
z_stream _deflateState;
#endif
};
class WebSocketPerMessageDeflateDecompressor
@ -41,9 +52,11 @@ namespace ix
private:
int _flush;
size_t _compressBufferSize;
std::unique_ptr<unsigned char[]> _compressBuffer;
std::array<unsigned char, 1 << 14> _compressBuffer;
#ifdef IXWEBSOCKET_USE_ZLIB
z_stream _inflateState;
#endif
};
} // namespace ix

View File

@ -61,6 +61,7 @@ namespace ix
_clientMaxWindowBits = kDefaultClientMaxWindowBits;
_serverMaxWindowBits = kDefaultServerMaxWindowBits;
#ifdef IXWEBSOCKET_USE_ZLIB
// Split by ;
std::string token;
std::stringstream tokenStream(extension);
@ -112,6 +113,7 @@ namespace ix
sanitizeClientMaxWindowBits();
}
}
#endif
}
void WebSocketPerMessageDeflateOptions::sanitizeClientMaxWindowBits()
@ -126,6 +128,7 @@ namespace ix
std::string WebSocketPerMessageDeflateOptions::generateHeader()
{
#ifdef IXWEBSOCKET_USE_ZLIB
std::stringstream ss;
ss << "Sec-WebSocket-Extensions: permessage-deflate";
@ -138,11 +141,18 @@ namespace ix
ss << "\r\n";
return ss.str();
#else
return std::string();
#endif
}
bool WebSocketPerMessageDeflateOptions::enabled() const
{
#ifdef IXWEBSOCKET_USE_ZLIB
return _enabled;
#else
return false;
#endif
}
bool WebSocketPerMessageDeflateOptions::getClientNoContextTakeover() const

View File

@ -0,0 +1,137 @@
/*
* IXWebSocketProxyServer.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
*/
#include "IXWebSocketProxyServer.h"
#include "IXWebSocketServer.h"
#include <sstream>
namespace ix
{
class ProxyConnectionState : public ix::ConnectionState
{
public:
ProxyConnectionState()
: _connected(false)
{
}
ix::WebSocket& webSocket()
{
return _serverWebSocket;
}
bool isConnected()
{
return _connected;
}
void setConnected()
{
_connected = true;
}
private:
ix::WebSocket _serverWebSocket;
bool _connected;
};
int websocket_proxy_server_main(int port,
const std::string& hostname,
const ix::SocketTLSOptions& tlsOptions,
const std::string& remoteUrl,
const RemoteUrlsMapping& remoteUrlsMapping,
bool /*verbose*/)
{
ix::WebSocketServer server(port, hostname);
server.setTLSOptions(tlsOptions);
auto factory = []() -> std::shared_ptr<ix::ConnectionState> {
return std::make_shared<ProxyConnectionState>();
};
server.setConnectionStateFactory(factory);
server.setOnConnectionCallback(
[remoteUrl, remoteUrlsMapping](std::weak_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState) {
auto state = std::dynamic_pointer_cast<ProxyConnectionState>(connectionState);
auto remoteIp = connectionState->getRemoteIp();
// Server connection
state->webSocket().setOnMessageCallback(
[webSocket, state, remoteIp](const WebSocketMessagePtr& msg) {
if (msg->type == ix::WebSocketMessageType::Close)
{
state->setTerminated();
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
auto ws = webSocket.lock();
if (ws)
{
ws->send(msg->str, msg->binary);
}
}
});
// Client connection
auto ws = webSocket.lock();
if (ws)
{
ws->setOnMessageCallback([state, remoteUrl, remoteUrlsMapping](
const WebSocketMessagePtr& msg) {
if (msg->type == ix::WebSocketMessageType::Open)
{
// Connect to the 'real' server
std::string url(remoteUrl);
// maybe we want a different url based on the mapping
std::string host = msg->openInfo.headers["Host"];
auto it = remoteUrlsMapping.find(host);
if (it != remoteUrlsMapping.end())
{
url = it->second;
}
// append the uri to form the full url
// (say ws://localhost:1234/foo/?bar=baz)
url += msg->openInfo.uri;
state->webSocket().setUrl(url);
state->webSocket().disableAutomaticReconnection();
state->webSocket().start();
// we should sleep here for a bit until we've established the
// connection with the remote server
while (state->webSocket().getReadyState() != ReadyState::Open)
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
else if (msg->type == ix::WebSocketMessageType::Close)
{
state->webSocket().close(msg->closeInfo.code, msg->closeInfo.reason);
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
state->webSocket().send(msg->str, msg->binary);
}
});
}
});
auto res = server.listen();
if (!res.first)
{
return 1;
}
server.start();
server.wait();
return 0;
}
} // namespace ix

View File

@ -0,0 +1,24 @@
/*
* IXWebSocketProxyServer.h
* Author: Benjamin Sergeant
* Copyright (c) 2019-2020 Machine Zone, Inc. All rights reserved.
*/
#pragma once
#include "IXSocketTLSOptions.h"
#include <cstdint>
#include <map>
#include <stddef.h>
#include <string>
namespace ix
{
using RemoteUrlsMapping = std::map<std::string, std::string>;
int websocket_proxy_server_main(int port,
const std::string& hostname,
const ix::SocketTLSOptions& tlsOptions,
const std::string& remoteUrl,
const RemoteUrlsMapping& remoteUrlsMapping,
bool verbose);
} // namespace ix

View File

@ -71,13 +71,45 @@ namespace ix
_onConnectionCallback = callback;
}
void WebSocketServer::setOnClientMessageCallback(const OnClientMessageCallback& callback)
{
_onClientMessageCallback = callback;
}
void WebSocketServer::handleConnection(std::unique_ptr<Socket> socket,
std::shared_ptr<ConnectionState> connectionState)
{
setThreadName("WebSocketServer::" + connectionState->getId());
auto webSocket = std::make_shared<WebSocket>();
_onConnectionCallback(webSocket, connectionState);
if (_onConnectionCallback)
{
_onConnectionCallback(webSocket, connectionState);
if (!webSocket->isOnMessageCallbackRegistered())
{
logError("WebSocketServer Application developer error: Server callback improperly "
"registerered.");
logError("Missing call to setOnMessageCallback inside setOnConnectionCallback.");
connectionState->setTerminated();
return;
}
}
else if (_onClientMessageCallback)
{
webSocket->setOnMessageCallback(
[this, &ws = *webSocket.get(), connectionState](const WebSocketMessagePtr& msg) {
_onClientMessageCallback(connectionState, ws, msg);
});
}
else
{
logError(
"WebSocketServer Application developer error: No server callback is registerered.");
logError("Missing call to setOnConnectionCallback or setOnClientMessageCallback.");
connectionState->setTerminated();
return;
}
webSocket->disableAutomaticReconnection();
@ -111,6 +143,8 @@ namespace ix
logError(ss.str());
}
webSocket->setOnMessageCallback(nullptr);
// Remove this client from our client set
{
std::lock_guard<std::mutex> lock(_clientsMutex);

View File

@ -23,7 +23,10 @@ namespace ix
{
public:
using OnConnectionCallback =
std::function<void(std::shared_ptr<WebSocket>, std::shared_ptr<ConnectionState>)>;
std::function<void(std::weak_ptr<WebSocket>, std::shared_ptr<ConnectionState>)>;
using OnClientMessageCallback = std::function<void(
std::shared_ptr<ConnectionState>, WebSocket&, const WebSocketMessagePtr&)>;
WebSocketServer(int port = SocketServer::kDefaultPort,
const std::string& host = SocketServer::kDefaultHost,
@ -39,6 +42,7 @@ namespace ix
void disablePerMessageDeflate();
void setOnConnectionCallback(const OnConnectionCallback& callback);
void setOnClientMessageCallback(const OnClientMessageCallback& callback);
// Get all the connected clients
std::set<std::shared_ptr<WebSocket>> getClients();
@ -52,6 +56,7 @@ namespace ix
bool _enablePerMessageDeflate;
OnConnectionCallback _onConnectionCallback;
OnClientMessageCallback _onClientMessageCallback;
std::mutex _clientsMutex;
std::set<std::shared_ptr<WebSocket>> _clients;
@ -60,7 +65,7 @@ namespace ix
// Methods
virtual void handleConnection(std::unique_ptr<Socket> socket,
std::shared_ptr<ConnectionState> connectionState) final;
std::shared_ptr<ConnectionState> connectionState);
virtual size_t getConnectedClientsCount() final;
};
} // namespace ix

View File

@ -65,7 +65,6 @@ namespace ix
, _receivedMessageCompressed(false)
, _readyState(ReadyState::CLOSED)
, _closeCode(WebSocketCloseConstants::kInternalErrorCode)
, _closeReason(WebSocketCloseConstants::kInternalErrorMessage)
, _closeWireSize(0)
, _closeRemote(false)
, _enablePerMessageDeflate(false)
@ -77,6 +76,7 @@ namespace ix
, _pingCount(0)
, _lastSendPingTimePoint(std::chrono::steady_clock::now())
{
setCloseReason(WebSocketCloseConstants::kInternalErrorMessage);
_readbuf.resize(kChunkSize);
}
@ -107,36 +107,62 @@ namespace ix
std::string protocol, host, path, query;
int port;
std::string remoteUrl(url);
if (!UrlParser::parse(url, protocol, host, path, query, port))
WebSocketInitResult result;
const int maxRedirections = 10;
for (int i = 0; i < maxRedirections; ++i)
{
std::stringstream ss;
ss << "Could not parse url: '" << url << "'";
return WebSocketInitResult(false, 0, ss.str());
if (!UrlParser::parse(remoteUrl, protocol, host, path, query, port))
{
std::stringstream ss;
ss << "Could not parse url: '" << url << "'";
return WebSocketInitResult(false, 0, ss.str());
}
std::string errorMsg;
bool tls = protocol == "wss";
_socket = createSocket(tls, -1, errorMsg, _socketTLSOptions);
_perMessageDeflate = std::make_unique<WebSocketPerMessageDeflate>();
if (!_socket)
{
return WebSocketInitResult(false, 0, errorMsg);
}
WebSocketHandshake webSocketHandshake(_requestInitCancellation,
_socket,
_perMessageDeflate,
_perMessageDeflateOptions,
_enablePerMessageDeflate);
result = webSocketHandshake.clientHandshake(
remoteUrl, headers, host, path, port, timeoutSecs);
if (result.http_status >= 300 && result.http_status < 400)
{
auto it = result.headers.find("Location");
if (it == result.headers.end())
{
std::stringstream ss;
ss << "Missing Location Header for HTTP Redirect response. "
<< "Rejecting connection to " << url << ", status: " << result.http_status;
result.errorStr = ss.str();
break;
}
remoteUrl = it->second;
continue;
}
if (result.success)
{
setReadyState(ReadyState::OPEN);
}
return result;
}
std::string errorMsg;
bool tls = protocol == "wss";
_socket = createSocket(tls, -1, errorMsg, _socketTLSOptions);
_perMessageDeflate = std::make_unique<WebSocketPerMessageDeflate>();
if (!_socket)
{
return WebSocketInitResult(false, 0, errorMsg);
}
WebSocketHandshake webSocketHandshake(_requestInitCancellation,
_socket,
_perMessageDeflate,
_perMessageDeflateOptions,
_enablePerMessageDeflate);
auto result =
webSocketHandshake.clientHandshake(url, headers, host, path, port, timeoutSecs);
if (result.success)
{
setReadyState(ReadyState::OPEN);
}
return result;
}
@ -179,10 +205,12 @@ namespace ix
if (readyState == ReadyState::CLOSED)
{
std::lock_guard<std::mutex> lock(_closeDataMutex);
_onCloseCallback(_closeCode, _closeReason, _closeWireSize, _closeRemote);
if (_onCloseCallback)
{
_onCloseCallback(_closeCode, getCloseReason(), _closeWireSize, _closeRemote);
}
setCloseReason(WebSocketCloseConstants::kInternalErrorMessage);
_closeCode = WebSocketCloseConstants::kInternalErrorCode;
_closeReason = WebSocketCloseConstants::kInternalErrorMessage;
_closeWireSize = 0;
_closeRemote = false;
}
@ -261,9 +289,10 @@ namespace ix
{
// compute lasting delay to wait for next ping / timeout, if at least one set
auto now = std::chrono::steady_clock::now();
lastingTimeoutDelayInMs = (int) std::chrono::duration_cast<std::chrono::milliseconds>(
int timeSinceLastPingMs = (int) std::chrono::duration_cast<std::chrono::milliseconds>(
now - _lastSendPingTimePoint)
.count();
lastingTimeoutDelayInMs = (1000 * _pingIntervalSecs) - timeSinceLastPingMs;
}
#ifdef _WIN32
@ -326,9 +355,10 @@ namespace ix
return _txbuf.empty();
}
template<class Iterator>
void WebSocketTransport::appendToSendBuffer(const std::vector<uint8_t>& header,
std::string::const_iterator begin,
std::string::const_iterator end,
Iterator begin,
Iterator end,
uint64_t message_size,
uint8_t masking_key[4])
{
@ -629,7 +659,7 @@ namespace ix
// send back the CLOSE frame
sendCloseFrame(code, reason);
wakeUpFromPoll(Socket::kCloseRequest);
wakeUpFromPoll(SelectInterrupt::kCloseRequest);
bool remote = true;
closeSocketAndSwitchToClosedState(code, reason, _rxbuf.size(), remote);
@ -638,11 +668,7 @@ namespace ix
{
// we got the CLOSE frame answer from our close, so we can close the connection
// if the code/reason are the same
bool identicalReason;
{
std::lock_guard<std::mutex> lock(_closeDataMutex);
identicalReason = _closeCode == code && _closeReason == reason;
}
bool identicalReason = _closeCode == code && getCloseReason() == reason;
if (identicalReason)
{
@ -750,8 +776,9 @@ namespace ix
return static_cast<unsigned>(seconds);
}
template<class T>
WebSocketSendInfo WebSocketTransport::sendData(wsheader_type::opcode_type type,
const std::string& message,
const T& message,
bool compress,
const OnProgressCallback& onProgressCallback)
{
@ -764,8 +791,8 @@ namespace ix
size_t wireSize = message.size();
bool compressionError = false;
std::string::const_iterator message_begin = message.begin();
std::string::const_iterator message_end = message.end();
auto message_begin = message.cbegin();
auto message_end = message.cend();
if (compress)
{
@ -780,8 +807,8 @@ namespace ix
compressionError = false;
wireSize = _compressedMessage.size();
message_begin = _compressedMessage.begin();
message_end = _compressedMessage.end();
message_begin = _compressedMessage.cbegin();
message_end = _compressedMessage.cend();
}
{
@ -795,6 +822,11 @@ namespace ix
if (wireSize < kChunkSize)
{
success = sendFragment(type, true, message_begin, message_end, compress);
if (onProgressCallback)
{
onProgressCallback(0, 1);
}
}
else
{
@ -847,7 +879,7 @@ namespace ix
// Request to flush the send buffer on the background thread if it isn't empty
if (!isSendBufferEmpty())
{
wakeUpFromPoll(Socket::kSendRequest);
wakeUpFromPoll(SelectInterrupt::kSendRequest);
// FIXME: we should have a timeout when sending large messages: see #131
if (_blockingSend && !flushSendBuffer())
@ -859,10 +891,11 @@ namespace ix
return WebSocketSendInfo(success, compressionError, payloadSize, wireSize);
}
template<class Iterator>
bool WebSocketTransport::sendFragment(wsheader_type::opcode_type type,
bool fin,
std::string::const_iterator message_begin,
std::string::const_iterator message_end,
Iterator message_begin,
Iterator message_end,
bool compress)
{
uint64_t message_size = static_cast<uint64_t>(message_end - message_begin);
@ -1055,7 +1088,7 @@ namespace ix
else
{
// no close code/reason set
sendData(wsheader_type::CLOSE, "", compress);
sendData(wsheader_type::CLOSE, std::string(""), compress);
}
}
@ -1078,13 +1111,10 @@ namespace ix
{
closeSocket();
{
std::lock_guard<std::mutex> lock(_closeDataMutex);
_closeCode = code;
_closeReason = reason;
_closeWireSize = closeWireSize;
_closeRemote = remote;
}
setCloseReason(reason);
_closeCode = code;
_closeWireSize = closeWireSize;
_closeRemote = remote;
setReadyState(ReadyState::CLOSED);
_requestInitCancellation = false;
@ -1104,13 +1134,11 @@ namespace ix
closeWireSize = reason.size();
}
{
std::lock_guard<std::mutex> lock(_closeDataMutex);
_closeCode = code;
_closeReason = reason;
_closeWireSize = closeWireSize;
_closeRemote = remote;
}
setCloseReason(reason);
_closeCode = code;
_closeWireSize = closeWireSize;
_closeRemote = remote;
{
std::lock_guard<std::mutex> lock(_closingTimePointMutex);
_closingTimePoint = std::chrono::steady_clock::now();
@ -1120,7 +1148,7 @@ namespace ix
sendCloseFrame(code, reason);
// wake up the poll, but do not close yet
wakeUpFromPoll(Socket::kSendRequest);
wakeUpFromPoll(SelectInterrupt::kSendRequest);
}
size_t WebSocketTransport::bufferedAmount() const
@ -1155,4 +1183,15 @@ namespace ix
return true;
}
void WebSocketTransport::setCloseReason(const std::string& reason)
{
std::lock_guard<std::mutex> lock(_closeReasonMutex);
_closeReason = reason;
}
const std::string& WebSocketTransport::getCloseReason() const
{
std::lock_guard<std::mutex> lock(_closeReasonMutex);
return _closeReason;
}
} // namespace ix

View File

@ -178,11 +178,11 @@ namespace ix
std::atomic<ReadyState> _readyState;
OnCloseCallback _onCloseCallback;
uint16_t _closeCode;
std::string _closeReason;
size_t _closeWireSize;
bool _closeRemote;
mutable std::mutex _closeDataMutex;
mutable std::mutex _closeReasonMutex;
std::atomic<uint16_t> _closeCode;
std::atomic<size_t> _closeWireSize;
std::atomic<bool> _closeRemote;
// Data used for Per Message Deflate compression (with zlib)
WebSocketPerMessageDeflatePtr _perMessageDeflate;
@ -239,16 +239,15 @@ namespace ix
bool sendOnSocket();
bool receiveFromSocket();
template<class T>
WebSocketSendInfo sendData(wsheader_type::opcode_type type,
const std::string& message,
const T& message,
bool compress,
const OnProgressCallback& onProgressCallback = nullptr);
bool sendFragment(wsheader_type::opcode_type type,
bool fin,
std::string::const_iterator begin,
std::string::const_iterator end,
bool compress);
template<class Iterator>
bool sendFragment(
wsheader_type::opcode_type type, bool fin, Iterator begin, Iterator end, bool compress);
void emitMessage(MessageKind messageKind,
const std::string& message,
@ -256,9 +255,11 @@ namespace ix
const OnMessageCallback& onMessageCallback);
bool isSendBufferEmpty() const;
template<class Iterator>
void appendToSendBuffer(const std::vector<uint8_t>& header,
std::string::const_iterator begin,
std::string::const_iterator end,
Iterator begin,
Iterator end,
uint64_t message_size,
uint8_t masking_key[4]);
@ -266,5 +267,8 @@ namespace ix
void unmaskReceiveBuffer(const wsheader_type& ws);
std::string getMergedChunks() const;
void setCloseReason(const std::string& reason);
const std::string& getCloseReason() const;
};
} // namespace ix

View File

@ -6,4 +6,4 @@
#pragma once
#define IX_WEBSOCKET_VERSION "9.8.5"
#define IX_WEBSOCKET_VERSION "10.5.3"

View File

@ -1,20 +0,0 @@
/*
* IXSetThreadName_apple.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
*/
#include "../IXSetThreadName.h"
#include <pthread.h>
namespace ix
{
void setThreadName(const std::string& name)
{
//
// Apple reserves 16 bytes for its thread names
// Notice that the Apple version of pthread_setname_np
// does not take a pthread_t argument
//
pthread_setname_np(name.substr(0, 63).c_str());
}
} // namespace ix

View File

@ -1,16 +0,0 @@
/*
* IXSetThreadName_freebsd.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
*/
#include "../IXSetThreadName.h"
#include <pthread.h>
#include <pthread_np.h>
namespace ix
{
void setThreadName(const std::string& name)
{
pthread_set_name_np(pthread_self(), name.substr(0, 15).c_str());
}
} // namespace ix

View File

@ -1,20 +0,0 @@
/*
* IXSetThreadName_linux.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
*/
#include "../IXSetThreadName.h"
#include <pthread.h>
namespace ix
{
void setThreadName(const std::string& name)
{
//
// Linux only reserves 16 bytes for its thread names
// See prctl and PR_SET_NAME property in
// http://man7.org/linux/man-pages/man2/prctl.2.html
//
pthread_setname_np(pthread_self(), name.substr(0, 15).c_str());
}
} // namespace ix

View File

@ -1,46 +0,0 @@
/*
* IXSetThreadName_windows.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
*/
#include "../IXSetThreadName.h"
#include <Windows.h>
namespace ix
{
const DWORD MS_VC_EXCEPTION = 0x406D1388;
#pragma pack(push, 8)
typedef struct tagTHREADNAME_INFO
{
DWORD dwType; // Must be 0x1000.
LPCSTR szName; // Pointer to name (in user addr space).
DWORD dwThreadID; // Thread ID (-1=caller thread).
DWORD dwFlags; // Reserved for future use, must be zero.
} THREADNAME_INFO;
#pragma pack(pop)
void SetThreadName(DWORD dwThreadID, const char* threadName)
{
THREADNAME_INFO info;
info.dwType = 0x1000;
info.szName = threadName;
info.dwThreadID = dwThreadID;
info.dwFlags = 0;
__try
{
RaiseException(
MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*) &info);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
}
}
void setThreadName(const std::string& name)
{
SetThreadName(-1, name.c_str());
}
} // namespace ix

View File

@ -34,10 +34,13 @@ ws:
mkdir -p build && (cd build ; cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -DUSE_PYTHON=1 -DUSE_TLS=1 -DUSE_WS=1 .. && ninja install)
ws_install:
mkdir -p build && (cd build ; cmake -DCMAKE_BUILD_TYPE=MinSizeRel -DUSE_PYTHON=1 -DUSE_TLS=1 -DUSE_WS=1 .. && make -j 4 install)
mkdir -p build && (cd build ; cmake -GNinja -DCMAKE_BUILD_TYPE=MinSizeRel -DUSE_PYTHON=1 -DUSE_TLS=1 -DUSE_WS=1 .. -DUSE_TEST=0 && ninja install)
ws_openssl:
mkdir -p build && (cd build ; cmake -DCMAKE_BUILD_TYPE=Debug -DUSE_PYTHON=1 -DUSE_TLS=1 -DUSE_WS=1 -DUSE_OPEN_SSL=1 .. ; make -j 4)
ws_install_release:
mkdir -p build && (cd build ; cmake -GNinja -DCMAKE_BUILD_TYPE=MinSizeRel -DUSE_PYTHON=1 -DUSE_TLS=1 -DUSE_WS=1 .. -DUSE_TEST=0 && ninja install)
ws_openssl_install:
mkdir -p build && (cd build ; cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -DUSE_TLS=1 -DUSE_WS=1 -DUSE_OPEN_SSL=1 .. ; ninja install)
ws_mbedtls:
mkdir -p build && (cd build ; cmake -DCMAKE_BUILD_TYPE=Debug -DUSE_PYTHON=1 -DUSE_TLS=1 -DUSE_WS=1 -DUSE_MBED_TLS=1 .. ; make -j 4)
@ -45,6 +48,9 @@ ws_mbedtls:
ws_no_ssl:
mkdir -p build && (cd build ; cmake -DCMAKE_BUILD_TYPE=Debug -DUSE_WS=1 .. ; make -j 4)
ws_no_python:
mkdir -p build && (cd build ; cmake -GNinja -DCMAKE_BUILD_TYPE=MinSizeRel -DUSE_TLS=1 -DUSE_WS=1 .. ; ninja install)
uninstall:
xargs rm -fv < build/install_manifest.txt
@ -104,8 +110,10 @@ test_server:
# env TEST=Websocket_server make test
# env TEST=Websocket_chat make test
# env TEST=heartbeat make test
test:
mkdir -p build && (cd build ; cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -DUSE_TLS=1 -DUSE_WS=1 -DUSE_TEST=1 .. ; ninja install)
build_test:
mkdir -p build && (cd build ; cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -DUSE_TLS=1 -DUSE_TEST=1 .. ; ninja install)
test: build_test
(cd test ; python2.7 run.py -r)
test_make:
@ -172,7 +180,7 @@ autobahn_report:
cp -rvf ~/sandbox/reports/clients/* ../bsergean.github.io/IXWebSocket/autobahn/
httpd:
clang++ --std=c++14 --stdlib=libc++ httpd.cpp \
clang++ --std=c++14 --stdlib=libc++ -o ixhttpd httpd.cpp \
ixwebsocket/IXSelectInterruptFactory.cpp \
ixwebsocket/IXCancellationRequest.cpp \
ixwebsocket/IXSocketTLSOptions.cpp \
@ -191,11 +199,11 @@ httpd:
ixwebsocket/IXConnectionState.cpp \
ixwebsocket/IXUrlParser.cpp \
ixwebsocket/IXSelectInterrupt.cpp \
ixwebsocket/apple/IXSetThreadName_apple.cpp \
ixwebsocket/IXSetThreadName.cpp \
-lz
httpd_linux:
g++ --std=c++11 -o ixhttpd httpd.cpp -Iixwebsocket \
g++ --std=c++14 -o ixhttpd httpd.cpp -Iixwebsocket \
ixwebsocket/IXSelectInterruptFactory.cpp \
ixwebsocket/IXCancellationRequest.cpp \
ixwebsocket/IXSocketTLSOptions.cpp \
@ -214,7 +222,7 @@ httpd_linux:
ixwebsocket/IXConnectionState.cpp \
ixwebsocket/IXUrlParser.cpp \
ixwebsocket/IXSelectInterrupt.cpp \
ixwebsocket/linux/IXSetThreadName_linux.cpp \
ixwebsocket/IXSetThreadName.cpp \
-lz -lpthread
cp -f ixhttpd /usr/local/bin
@ -236,9 +244,12 @@ install_cmake_for_linux:
doc:
mkdocs gh-deploy
change:
change: format
vim ixwebsocket/IXWebSocketVersion.h docs/CHANGELOG.md
commit:
git commit -am "`sh tools/extract_latest_change.sh`"
.PHONY: test
.PHONY: build
.PHONY: ws

View File

@ -2,24 +2,17 @@
# Author: Benjamin Sergeant
# Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
#
cmake_minimum_required (VERSION 3.4.1)
cmake_minimum_required (VERSION 3.14)
project (ixwebsocket_unittest)
set (CMAKE_CXX_STANDARD 14)
if (MAC)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/../third_party/sanitizers-cmake/cmake" ${CMAKE_MODULE_PATH})
find_package(Sanitizers)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread")
set(CMAKE_LD_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread")
option(USE_TLS "Add TLS support" ON)
endif()
option(USE_TLS "Add TLS support" ON)
include_directories(
${PROJECT_SOURCE_DIR}/Catch2/single_include
../third_party
../third_party/msgpack11
../third_party/spdlog/include
../ws
)
@ -37,11 +30,11 @@ set (SOURCES
test_runner.cpp
IXTest.cpp
IXGetFreePort.cpp
../third_party/msgpack11/msgpack11.cpp
IXSocketTest.cpp
IXSocketConnectTest.cpp
# IXWebSocketLeakTest.cpp # commented until we have a fix for #224
IXWebSocketServerTest.cpp
IXWebSocketTestConnectionDisconnection.cpp
IXUrlParserTest.cpp
@ -55,6 +48,8 @@ set (SOURCES
IXSentryClientTest.cpp
IXWebSocketChatTest.cpp
IXWebSocketBroadcastTest.cpp
IXWebSocketPerMessageDeflateCompressorTest.cpp
IXStreamSqlTest.cpp
)
# Some unittest don't work on windows yet

View File

@ -108,7 +108,7 @@ namespace
}
else if (event->type == ix::CobraEventType::UnSubscribed)
{
TLogger() << "Subscriber: ununexpected from channel " << event->subscriptionId;
TLogger() << "Subscriber: unsubscribed from channel " << event->subscriptionId;
if (event->subscriptionId != channel)
{
TLogger() << "Subscriber: unexpected channel " << event->subscriptionId;

View File

@ -12,8 +12,8 @@
#include <ixcobra/IXCobraConnection.h>
#include <ixcobra/IXCobraMetricsPublisher.h>
#include <ixcrypto/IXUuid.h>
#include <ixsentry/IXSentryClient.h>
#include <ixredis/IXRedisServer.h>
#include <ixsentry/IXSentryClient.h>
#include <ixsnake/IXSnakeServer.h>
#include <ixwebsocket/IXHttpServer.h>
#include <ixwebsocket/IXUserAgent.h>
@ -95,13 +95,14 @@ TEST_CASE("Cobra_to_sentry_bot", "[cobra_bots]")
sentryServer.setOnConnectionCallback(
[](HttpRequestPtr request,
std::shared_ptr<ConnectionState> /*connectionState*/) -> HttpResponsePtr {
std::shared_ptr<ConnectionState> connectionState) -> HttpResponsePtr {
WebSocketHttpHeaders headers;
headers["Server"] = userAgent();
// Log request
std::stringstream ss;
ss << request->method << " " << request->headers["User-Agent"] << " "
ss << connectionState->getRemoteIp() << ":" << connectionState->getRemotePort()
<< " " << request->method << " " << request->headers["User-Agent"] << " "
<< request->uri;
if (request->method == "POST")

View File

@ -12,8 +12,8 @@
#include <ixcobra/IXCobraConnection.h>
#include <ixcobra/IXCobraMetricsPublisher.h>
#include <ixcrypto/IXUuid.h>
#include <ixsentry/IXSentryClient.h>
#include <ixredis/IXRedisServer.h>
#include <ixsentry/IXSentryClient.h>
#include <ixsnake/IXSnakeServer.h>
#include <ixwebsocket/IXHttpServer.h>
#include <ixwebsocket/IXUserAgent.h>

View File

@ -12,8 +12,8 @@
#include <ixcobra/IXCobraConnection.h>
#include <ixcobra/IXCobraMetricsPublisher.h>
#include <ixcrypto/IXUuid.h>
#include <ixsentry/IXSentryClient.h>
#include <ixredis/IXRedisServer.h>
#include <ixsentry/IXSentryClient.h>
#include <ixsnake/IXSnakeServer.h>
#include <ixwebsocket/IXHttpServer.h>
#include <ixwebsocket/IXUserAgent.h>
@ -92,6 +92,9 @@ TEST_CASE("Cobra_to_stdout_bot", "[cobra_bots]")
cobraBotConfig.enableHeartbeat = false;
bool quiet = false;
cobraBotConfig.filter =
std::string("select * from `") + channel + "` where id = 'sms_metric_A_id'";
// We could try to capture the output ... not sure how.
bool fluentd = true;

View File

@ -4,9 +4,9 @@
* Copyright (c) 2019 Machine Zone. All rights reserved.
*/
#include "IXGetFreePort.h"
#include "catch.hpp"
#include <iostream>
#include <ixwebsocket/IXGetFreePort.h>
#include <ixwebsocket/IXHttpClient.h>
#include <ixwebsocket/IXHttpServer.h>
@ -63,11 +63,60 @@ TEST_CASE("http server", "[httpd]")
server.stop();
}
SECTION("Posting plain text data to a local HTTP server")
{
int port = getFreePort();
ix::HttpServer server(port, "127.0.0.1");
server.setOnConnectionCallback(
[](HttpRequestPtr request, std::shared_ptr<ConnectionState>) -> HttpResponsePtr {
if (request->method == "POST")
{
return std::make_shared<HttpResponse>(
200, "OK", HttpErrorCode::Ok, WebSocketHttpHeaders(), request->body);
}
return std::make_shared<HttpResponse>(400, "BAD REQUEST");
});
auto res = server.listen();
REQUIRE(res.first);
server.start();
HttpClient httpClient;
WebSocketHttpHeaders headers;
headers["Content-Type"] = "text/plain";
std::string url("http://127.0.0.1:");
url += std::to_string(port);
auto args = httpClient.createRequest(url);
args->extraHeaders = headers;
args->connectTimeout = 60;
args->transferTimeout = 60;
args->verbose = true;
args->logger = [](const std::string& msg) { std::cout << msg; };
args->body = "Hello World!";
auto response = httpClient.post(url, args->body, args);
std::cerr << "Status: " << response->statusCode << std::endl;
std::cerr << "Error message: " << response->errorMsg << std::endl;
std::cerr << "Body: " << response->body << std::endl;
REQUIRE(response->errorCode == HttpErrorCode::Ok);
REQUIRE(response->statusCode == 200);
REQUIRE(response->body == args->body);
server.stop();
}
}
TEST_CASE("http server redirection", "[httpd_redirect]")
{
SECTION("Connect to a local HTTP server, with redirection enabled, but we do not follow redirects")
SECTION(
"Connect to a local HTTP server, with redirection enabled, but we do not follow redirects")
{
int port = getFreePort();
ix::HttpServer server(port, "127.0.0.1");

42
test/IXStreamSqlTest.cpp Normal file
View File

@ -0,0 +1,42 @@
/*
* IXStreamSqlTest.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2020 Machine Zone. All rights reserved.
*/
#include "IXTest.h"
#include "catch.hpp"
#include <iostream>
#include <ixsnake/IXStreamSql.h>
#include <string.h>
using namespace ix;
namespace ix
{
TEST_CASE("stream_sql", "[streamsql]")
{
SECTION("expression A")
{
snake::StreamSql streamSql(
"select * from subscriber_republished_v1_neo where session LIKE '%123456%'");
nlohmann::json msg = {{"session", "123456"}, {"id", "foo_id"}, {"timestamp", 12}};
CHECK(streamSql.match(msg));
}
SECTION("expression A")
{
snake::StreamSql streamSql("select * from `subscriber_republished_v1_neo` where "
"session = '30091320ed8d4e50b758f8409b83bed7'");
nlohmann::json msg = {{"session", "30091320ed8d4e50b758f8409b83bed7"},
{"id", "foo_id"},
{"timestamp", 12}};
CHECK(streamSql.match(msg));
}
}
} // namespace ix

View File

@ -84,36 +84,37 @@ namespace ix
bool startWebSocketEchoServer(ix::WebSocketServer& server)
{
server.setOnConnectionCallback([&server](std::shared_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState) {
webSocket->setOnMessageCallback(
[webSocket, connectionState, &server](const ix::WebSocketMessagePtr& msg) {
if (msg->type == ix::WebSocketMessageType::Open)
server.setOnClientMessageCallback(
[&server](std::shared_ptr<ConnectionState> connectionState,
WebSocket& webSocket,
const ix::WebSocketMessagePtr& msg) {
auto remoteIp = connectionState->getRemoteIp();
if (msg->type == ix::WebSocketMessageType::Open)
{
TLogger() << "New connection";
TLogger() << "Remote ip: " << remoteIp;
TLogger() << "Uri: " << msg->openInfo.uri;
TLogger() << "Headers:";
for (auto it : msg->openInfo.headers)
{
TLogger() << "New connection";
TLogger() << "Uri: " << msg->openInfo.uri;
TLogger() << "Headers:";
for (auto it : msg->openInfo.headers)
TLogger() << it.first << ": " << it.second;
}
}
else if (msg->type == ix::WebSocketMessageType::Close)
{
TLogger() << "Closed connection";
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
for (auto&& client : server.getClients())
{
if (client.get() != &webSocket)
{
TLogger() << it.first << ": " << it.second;
client->send(msg->str, msg->binary);
}
}
else if (msg->type == ix::WebSocketMessageType::Close)
{
TLogger() << "Closed connection";
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
for (auto&& client : server.getClients())
{
if (client != webSocket)
{
client->send(msg->str, msg->binary);
}
}
}
});
});
}
});
auto res = server.listen();
if (!res.first)

View File

@ -6,9 +6,9 @@
#pragma once
#include "IXGetFreePort.h"
#include <iostream>
#include <ixsnake/IXAppConfig.h>
#include <ixwebsocket/IXGetFreePort.h>
#include <ixwebsocket/IXSocketTLSOptions.h>
#include <ixwebsocket/IXWebSocketServer.h>
#include <mutex>

View File

@ -189,15 +189,17 @@ namespace
bool preferTLS = true;
server.setTLSOptions(makeServerTLSOptions(preferTLS));
server.setOnConnectionCallback([&server, &connectionId](
std::shared_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState) {
webSocket->setOnMessageCallback([webSocket, connectionState, &connectionId, &server](
const ix::WebSocketMessagePtr& msg) {
server.setOnClientMessageCallback(
[&server, &connectionId](std::shared_ptr<ConnectionState> connectionState,
WebSocket& webSocket,
const ix::WebSocketMessagePtr& msg) {
auto remoteIp = connectionState->getRemoteIp();
if (msg->type == ix::WebSocketMessageType::Open)
{
TLogger() << "New connection";
connectionState->computeId();
TLogger() << "remote ip: " << remoteIp;
TLogger() << "id: " << connectionState->getId();
TLogger() << "Uri: " << msg->openInfo.uri;
TLogger() << "Headers:";
@ -216,14 +218,13 @@ namespace
{
for (auto&& client : server.getClients())
{
if (client != webSocket)
if (client.get() != &webSocket)
{
client->send(msg->str, msg->binary);
}
}
}
});
});
auto res = server.listen();
if (!res.first)

View File

@ -193,37 +193,38 @@ namespace
bool startServer(ix::WebSocketServer& server)
{
server.setOnConnectionCallback([&server](std::shared_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState) {
webSocket->setOnMessageCallback(
[webSocket, connectionState, &server](const ix::WebSocketMessagePtr& msg) {
if (msg->type == ix::WebSocketMessageType::Open)
server.setOnClientMessageCallback(
[&server](std::shared_ptr<ConnectionState> connectionState,
WebSocket& webSocket,
const ix::WebSocketMessagePtr& msg) {
auto remoteIp = connectionState->getRemoteIp();
if (msg->type == ix::WebSocketMessageType::Open)
{
TLogger() << "New connection";
TLogger() << "remote ip: " << remoteIp;
TLogger() << "id: " << connectionState->getId();
TLogger() << "Uri: " << msg->openInfo.uri;
TLogger() << "Headers:";
for (auto it : msg->openInfo.headers)
{
TLogger() << "New connection";
TLogger() << "id: " << connectionState->getId();
TLogger() << "Uri: " << msg->openInfo.uri;
TLogger() << "Headers:";
for (auto it : msg->openInfo.headers)
TLogger() << it.first << ": " << it.second;
}
}
else if (msg->type == ix::WebSocketMessageType::Close)
{
log("Closed connection");
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
for (auto&& client : server.getClients())
{
if (client.get() != &webSocket)
{
TLogger() << it.first << ": " << it.second;
client->sendBinary(msg->str);
}
}
else if (msg->type == ix::WebSocketMessageType::Close)
{
log("Closed connection");
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
for (auto&& client : server.getClients())
{
if (client != webSocket)
{
client->sendBinary(msg->str);
}
}
}
});
});
}
});
auto res = server.listen();
if (!res.first)
@ -284,27 +285,27 @@ TEST_CASE("Websocket_chat", "[websocket_chat]")
int attempts = 0;
while (chatA.getReceivedMessagesCount() != 3 || chatB.getReceivedMessagesCount() != 3)
{
REQUIRE(attempts++ < 10);
CHECK(attempts++ < 10);
ix::msleep(1000);
}
chatA.stop();
chatB.stop();
REQUIRE(chatA.getReceivedMessagesCount() == 3);
REQUIRE(chatB.getReceivedMessagesCount() == 3);
CHECK(chatA.getReceivedMessagesCount() == 3);
CHECK(chatB.getReceivedMessagesCount() == 3);
REQUIRE(chatB.getReceivedMessages()[0] == "from A1");
REQUIRE(chatB.getReceivedMessages()[1] == "from A2");
REQUIRE(chatB.getReceivedMessages()[2] == "from A3");
CHECK(chatB.getReceivedMessages()[0] == "from A1");
CHECK(chatB.getReceivedMessages()[1] == "from A2");
CHECK(chatB.getReceivedMessages()[2] == "from A3");
REQUIRE(chatA.getReceivedMessages()[0] == "from B1");
REQUIRE(chatA.getReceivedMessages()[1] == "from B2");
REQUIRE(chatA.getReceivedMessages()[2].size() == bigMessage.size());
CHECK(chatA.getReceivedMessages()[0] == "from B1");
CHECK(chatA.getReceivedMessages()[1] == "from B2");
CHECK(chatA.getReceivedMessages()[2].size() == bigMessage.size());
// Give us 1000ms for the server to notice that clients went away
ix::msleep(1000);
REQUIRE(server.getClients().size() == 0);
CHECK(server.getClients().size() == 0);
ix::reportWebSocketTraffic();
}

View File

@ -168,41 +168,37 @@ namespace
std::mutex& mutexWrite)
{
// A dev/null server
server.setOnConnectionCallback(
server.setOnClientMessageCallback(
[&receivedCloseCode, &receivedCloseReason, &receivedCloseRemote, &mutexWrite](
std::shared_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState) {
webSocket->setOnMessageCallback([webSocket,
connectionState,
&receivedCloseCode,
&receivedCloseReason,
&receivedCloseRemote,
&mutexWrite](const ix::WebSocketMessagePtr& msg) {
if (msg->type == ix::WebSocketMessageType::Open)
std::shared_ptr<ConnectionState> connectionState,
WebSocket& /*webSocket*/,
const ix::WebSocketMessagePtr& msg) {
auto remoteIp = connectionState->getRemoteIp();
if (msg->type == ix::WebSocketMessageType::Open)
{
TLogger() << "New server connection";
TLogger() << "remote ip: " << remoteIp;
TLogger() << "id: " << connectionState->getId();
TLogger() << "Uri: " << msg->openInfo.uri;
TLogger() << "Headers:";
for (auto it : msg->openInfo.headers)
{
TLogger() << "New server connection";
TLogger() << "id: " << connectionState->getId();
TLogger() << "Uri: " << msg->openInfo.uri;
TLogger() << "Headers:";
for (auto it : msg->openInfo.headers)
{
TLogger() << it.first << ": " << it.second;
}
TLogger() << it.first << ": " << it.second;
}
else if (msg->type == ix::WebSocketMessageType::Close)
{
std::stringstream ss;
ss << "Server closed connection(" << msg->closeInfo.code << ","
<< msg->closeInfo.reason << ")";
log(ss.str());
}
else if (msg->type == ix::WebSocketMessageType::Close)
{
std::stringstream ss;
ss << "Server closed connection(" << msg->closeInfo.code << ","
<< msg->closeInfo.reason << ")";
log(ss.str());
std::lock_guard<std::mutex> lck(mutexWrite);
std::lock_guard<std::mutex> lck(mutexWrite);
receivedCloseCode = msg->closeInfo.code;
receivedCloseReason = std::string(msg->closeInfo.reason);
receivedCloseRemote = msg->closeInfo.remote;
}
});
receivedCloseCode = msg->closeInfo.code;
receivedCloseReason = std::string(msg->closeInfo.reason);
receivedCloseRemote = msg->closeInfo.remote;
}
});
auto res = server.listen();

View File

@ -0,0 +1,183 @@
/*
* IXWebSocketServer.cpp
* Author: Benjamin Sergeant, @marcelkauf
* Copyright (c) 2020 Machine Zone, Inc. All rights reserved.
*/
#include "IXTest.h"
#include "catch.hpp"
#include <ixwebsocket/IXWebSocket.h>
#include <ixwebsocket/IXWebSocketServer.h>
#include <memory>
#include <sstream>
using namespace ix;
namespace
{
class WebSocketClient
{
public:
WebSocketClient(int port);
void start();
void stop();
bool isReady() const;
bool hasConnectionError() const;
private:
ix::WebSocket _webSocket;
int _port;
std::atomic<bool> _connectionError;
};
WebSocketClient::WebSocketClient(int port)
: _port(port)
, _connectionError(false)
{
}
bool WebSocketClient::hasConnectionError() const
{
return _connectionError;
}
bool WebSocketClient::isReady() const
{
return _webSocket.getReadyState() == ix::ReadyState::Open;
}
void WebSocketClient::stop()
{
_webSocket.stop();
}
void WebSocketClient::start()
{
std::string url;
{
std::stringstream ss;
ss << "ws://localhost:" << _port << "/";
url = ss.str();
}
_webSocket.setUrl(url);
_webSocket.disableAutomaticReconnection();
std::stringstream ss;
log(std::string("Connecting to url: ") + url);
_webSocket.setOnMessageCallback([this](const ix::WebSocketMessagePtr& msg) {
std::stringstream ss;
if (msg->type == ix::WebSocketMessageType::Open)
{
log("client connected");
}
else if (msg->type == ix::WebSocketMessageType::Close)
{
log("client disconnected");
}
else if (msg->type == ix::WebSocketMessageType::Error)
{
_connectionError = true;
log("error");
}
else if (msg->type == ix::WebSocketMessageType::Pong)
{
log("pong");
}
else if (msg->type == ix::WebSocketMessageType::Ping)
{
log("ping");
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
log("message");
}
else
{
log("invalid type");
}
});
_webSocket.start();
}
} // namespace
TEST_CASE("Websocket leak test")
{
SECTION("Websocket destructor is called when closing the connection.")
{
// stores the server websocket in order to check the use_count
std::shared_ptr<WebSocket> webSocketPtr;
{
int port = getFreePort();
WebSocketServer server(port);
server.setOnConnectionCallback(
[&webSocketPtr](std::shared_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState,
std::unique_ptr<ConnectionInfo> connectionInfo) {
// original ptr in WebSocketServer::handleConnection and the callback argument
REQUIRE(webSocket.use_count() == 2);
webSocket->setOnMessageCallback([&webSocketPtr, webSocket, connectionState](
const ix::WebSocketMessagePtr& msg) {
if (msg->type == ix::WebSocketMessageType::Open)
{
log(std::string("New connection id: ") + connectionState->getId());
// original ptr in WebSocketServer::handleConnection, captured ptr of
// this callback, and ptr in WebSocketServer::_clients
REQUIRE(webSocket.use_count() == 3);
webSocketPtr = std::shared_ptr<WebSocket>(webSocket);
REQUIRE(webSocket.use_count() == 4);
}
else if (msg->type == ix::WebSocketMessageType::Close)
{
log(std::string("Client closed connection id: ") +
connectionState->getId());
}
else
{
log(std::string(msg->str));
}
});
// original ptr in WebSocketServer::handleConnection, argument of this callback,
// and captured ptr in websocket callback
REQUIRE(webSocket.use_count() == 3);
});
server.listen();
server.start();
WebSocketClient webSocketClient(port);
webSocketClient.start();
while (true)
{
REQUIRE(!webSocketClient.hasConnectionError());
if (webSocketClient.isReady()) break;
ix::msleep(10);
}
REQUIRE(server.getClients().size() == 1);
// same value as in Open-handler above
REQUIRE(webSocketPtr.use_count() == 4);
ix::msleep(500);
webSocketClient.stop();
ix::msleep(500);
REQUIRE(server.getClients().size() == 0);
// websocket should only be referenced by webSocketPtr but is still used by the
// websocket callback
REQUIRE(webSocketPtr.use_count() == 1);
webSocketPtr->setOnMessageCallback(nullptr);
// websocket should only be referenced by webSocketPtr
REQUIRE(webSocketPtr.use_count() == 1);
server.stop();
}
// websocket should only be referenced by webSocketPtr
REQUIRE(webSocketPtr.use_count() == 1);
}
}

View File

@ -0,0 +1,76 @@
/*
* IXWebSocketPerMessageDeflateCodecTest.cpp
* Author: Benjamin Sergeant
* Copyright (c) 2020 Machine Zone. All rights reserved.
*
* make build_test && build/test/ixwebsocket_unittest per-message-deflate-codec
*/
#include "IXTest.h"
#include "catch.hpp"
#include <iostream>
#include <ixwebsocket/IXWebSocketPerMessageDeflateCodec.h>
#include <string.h>
using namespace ix;
namespace ix
{
std::string compressAndDecompress(const std::string& a)
{
std::string b, c;
WebSocketPerMessageDeflateCompressor compressor;
compressor.init(11, true);
compressor.compress(a, b);
WebSocketPerMessageDeflateDecompressor decompressor;
decompressor.init(11, true);
decompressor.decompress(b, c);
return c;
}
std::string compressAndDecompressVector(const std::string& a)
{
std::string b, c;
std::vector<uint8_t> vec(a.begin(), a.end());
WebSocketPerMessageDeflateCompressor compressor;
compressor.init(11, true);
compressor.compress(vec, b);
WebSocketPerMessageDeflateDecompressor decompressor;
decompressor.init(11, true);
decompressor.decompress(b, c);
return c;
}
TEST_CASE("per-message-deflate-codec", "[zlib]")
{
SECTION("string api")
{
REQUIRE(compressAndDecompress("") == "");
REQUIRE(compressAndDecompress("foo") == "foo");
REQUIRE(compressAndDecompress("bar") == "bar");
REQUIRE(compressAndDecompress("asdcaseqw`21897dehqwed") == "asdcaseqw`21897dehqwed");
REQUIRE(compressAndDecompress("/usr/local/include/ixwebsocket/IXSocketAppleSSL.h") ==
"/usr/local/include/ixwebsocket/IXSocketAppleSSL.h");
}
SECTION("vector api")
{
REQUIRE(compressAndDecompressVector("") == "");
REQUIRE(compressAndDecompressVector("foo") == "foo");
REQUIRE(compressAndDecompressVector("bar") == "bar");
REQUIRE(compressAndDecompressVector("asdcaseqw`21897dehqwed") ==
"asdcaseqw`21897dehqwed");
REQUIRE(
compressAndDecompressVector("/usr/local/include/ixwebsocket/IXSocketAppleSSL.h") ==
"/usr/local/include/ixwebsocket/IXSocketAppleSSL.h");
}
}
} // namespace ix

View File

@ -33,15 +33,17 @@ namespace ix
};
server.setConnectionStateFactory(factory);
server.setOnConnectionCallback([&server, &connectionId](
std::shared_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState) {
webSocket->setOnMessageCallback([webSocket, connectionState, &connectionId, &server](
const ix::WebSocketMessagePtr& msg) {
server.setOnClientMessageCallback(
[&server, &connectionId](std::shared_ptr<ConnectionState> connectionState,
WebSocket& webSocket,
const ix::WebSocketMessagePtr& msg) {
auto remoteIp = connectionState->getRemoteIp();
if (msg->type == ix::WebSocketMessageType::Open)
{
TLogger() << "New connection";
connectionState->computeId();
TLogger() << "remote ip: " << remoteIp;
TLogger() << "id: " << connectionState->getId();
TLogger() << "Uri: " << msg->openInfo.uri;
TLogger() << "Headers:";
@ -60,14 +62,13 @@ namespace ix
{
for (auto&& client : server.getClients())
{
if (client != webSocket)
if (client.get() != &webSocket)
{
client->send(msg->str, msg->binary);
}
}
}
});
});
auto res = server.listen();
if (!res.first)

View File

@ -16,39 +16,39 @@ using namespace ix;
bool startServer(ix::WebSocketServer& server, std::string& subProtocols)
{
server.setOnConnectionCallback(
[&server, &subProtocols](std::shared_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState) {
webSocket->setOnMessageCallback([webSocket, connectionState, &server, &subProtocols](
const ix::WebSocketMessagePtr& msg) {
if (msg->type == ix::WebSocketMessageType::Open)
server.setOnClientMessageCallback(
[&server, &subProtocols](std::shared_ptr<ConnectionState> connectionState,
WebSocket& webSocket,
const ix::WebSocketMessagePtr& msg) {
auto remoteIp = connectionState->getRemoteIp();
if (msg->type == ix::WebSocketMessageType::Open)
{
TLogger() << "New connection";
TLogger() << "remote ip: " << remoteIp;
TLogger() << "id: " << connectionState->getId();
TLogger() << "Uri: " << msg->openInfo.uri;
TLogger() << "Headers:";
for (auto it : msg->openInfo.headers)
{
TLogger() << "New connection";
TLogger() << "id: " << connectionState->getId();
TLogger() << "Uri: " << msg->openInfo.uri;
TLogger() << "Headers:";
for (auto it : msg->openInfo.headers)
{
TLogger() << it.first << ": " << it.second;
}
TLogger() << it.first << ": " << it.second;
}
subProtocols = msg->openInfo.headers["Sec-WebSocket-Protocol"];
}
else if (msg->type == ix::WebSocketMessageType::Close)
subProtocols = msg->openInfo.headers["Sec-WebSocket-Protocol"];
}
else if (msg->type == ix::WebSocketMessageType::Close)
{
log("Closed connection");
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
for (auto&& client : server.getClients())
{
log("Closed connection");
}
else if (msg->type == ix::WebSocketMessageType::Message)
{
for (auto&& client : server.getClients())
if (client.get() != &webSocket)
{
if (client != webSocket)
{
client->sendBinary(msg->str);
}
client->sendBinary(msg->str);
}
}
});
}
});
auto res = server.listen();

View File

@ -0,0 +1,171 @@
/*
* lws-minimal-ws-client
*
* Written in 2010-2019 by Andy Green <andy@warmcat.com>
*
* This file is made available under the Creative Commons CC0 1.0
* Universal Public Domain Dedication.
*
* This demonstrates the a minimal ws client using lws.
*
* Original programs connects to https://libwebsockets.org/ and makes a
* wss connection to the dumb-increment protocol there. While
* connected, it prints the numbers it is being sent by
* dumb-increment protocol.
*
* This is modified to make a test client which counts how much messages
* per second can be received.
*
* libwebsockets$ make && ./a.out
* g++ --std=c++14 -I/usr/local/opt/openssl/include devnull_client.cpp -lwebsockets
* messages received: 0 per second 0 total
* [2020/08/02 19:22:21:4774] U: LWS minimal ws client rx [-d <logs>] [--h2]
* [2020/08/02 19:22:21:4814] U: callback_dumb_increment: established
* messages received: 0 per second 0 total
* messages received: 180015 per second 180015 total
* messages received: 172866 per second 352881 total
* messages received: 176177 per second 529058 total
* messages received: 174191 per second 703249 total
* messages received: 193397 per second 896646 total
* messages received: 196385 per second 1093031 total
* messages received: 194593 per second 1287624 total
* messages received: 189484 per second 1477108 total
* messages received: 200825 per second 1677933 total
* messages received: 183542 per second 1861475 total
* ^C[2020/08/02 19:22:33:4450] U: Completed OK
*
*/
#include <atomic>
#include <iostream>
#include <libwebsockets.h>
#include <signal.h>
#include <string.h>
#include <thread>
static int interrupted;
static struct lws* client_wsi;
std::atomic<uint64_t> receivedCount(0);
static int callback_dumb_increment(
struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len)
{
switch (reason)
{
/* because we are protocols[0] ... */
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
lwsl_err("CLIENT_CONNECTION_ERROR: %s\n", in ? (char*) in : "(null)");
client_wsi = NULL;
break;
case LWS_CALLBACK_CLIENT_ESTABLISHED: lwsl_user("%s: established\n", __func__); break;
case LWS_CALLBACK_CLIENT_RECEIVE: receivedCount++; break;
case LWS_CALLBACK_CLIENT_CLOSED: client_wsi = NULL; break;
default: break;
}
return lws_callback_http_dummy(wsi, reason, user, in, len);
}
static const struct lws_protocols protocols[] = {{
"dumb-increment-protocol",
callback_dumb_increment,
0,
0,
},
{NULL, NULL, 0, 0}};
static void sigint_handler(int sig)
{
interrupted = 1;
}
int main(int argc, const char** argv)
{
uint64_t receivedCountTotal(0);
uint64_t receivedCountPerSecs(0);
auto timer = [&receivedCountTotal, &receivedCountPerSecs] {
while (!interrupted)
{
std::cerr << "messages received: " << receivedCountPerSecs << " per second "
<< receivedCountTotal << " total" << std::endl;
receivedCountPerSecs = receivedCount - receivedCountTotal;
receivedCountTotal += receivedCountPerSecs;
auto duration = std::chrono::seconds(1);
std::this_thread::sleep_for(duration);
}
};
std::thread t1(timer);
struct lws_context_creation_info info;
struct lws_client_connect_info i;
struct lws_context* context;
const char* p;
int n = 0, logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE
/* for LLL_ verbosity above NOTICE to be built into lws, lws
* must have been configured with -DCMAKE_BUILD_TYPE=DEBUG
* instead of =RELEASE */
/* | LLL_INFO */ /* | LLL_PARSER */ /* | LLL_HEADER */
/* | LLL_EXT */ /* | LLL_CLIENT */ /* | LLL_LATENCY */
/* | LLL_DEBUG */;
signal(SIGINT, sigint_handler);
if ((p = lws_cmdline_option(argc, argv, "-d"))) logs = atoi(p);
lws_set_log_level(logs, NULL);
lwsl_user("LWS minimal ws client rx [-d <logs>] [--h2]\n");
memset(&info, 0, sizeof info); /* otherwise uninitialized garbage */
info.port = CONTEXT_PORT_NO_LISTEN; /* we do not run any server */
info.protocols = protocols;
info.timeout_secs = 10;
/*
* since we know this lws context is only ever going to be used with
* one client wsis / fds / sockets at a time, let lws know it doesn't
* have to use the default allocations for fd tables up to ulimit -n.
* It will just allocate for 1 internal and 1 (+ 1 http2 nwsi) that we
* will use.
*/
info.fd_limit_per_thread = 1 + 1 + 1;
context = lws_create_context(&info);
if (!context)
{
lwsl_err("lws init failed\n");
return 1;
}
memset(&i, 0, sizeof i); /* otherwise uninitialized garbage */
i.context = context;
i.port = 8008;
i.address = "127.0.0.1";
i.path = "/";
i.host = i.address;
i.origin = i.address;
i.protocol = protocols[0].name; /* "dumb-increment-protocol" */
i.pwsi = &client_wsi;
if (lws_cmdline_option(argc, argv, "--h2")) i.alpn = "h2";
lws_client_connect_via_info(&i);
while (n >= 0 && client_wsi && !interrupted)
n = lws_service(context, 0);
lws_context_destroy(context);
lwsl_user("Completed %s\n", receivedCount > 10 ? "OK" : "Failed");
t1.join();
return receivedCount > 10;
}

2
test/compatibility/csharp/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
bin
obj

Some files were not shown because too many files have changed in this diff Show More