Compare commits
181 Commits
v9.6.1
...
feature/li
Author | SHA1 | Date | |
---|---|---|---|
da497f126a | |||
bdadfcba19 | |||
fc9a014195 | |||
563ff09667 | |||
0c877f768a | |||
61dbcc2b84 | |||
e61680ff0f | |||
6f188a5131 | |||
6077f86af8 | |||
93167e3917 | |||
2526a94454 | |||
97cc543e53 | |||
62d220f49a | |||
49995e32f0 | |||
d525c28907 | |||
39c84c7d51 | |||
128bc0afa9 | |||
b04e5c5529 | |||
1e8c421d66 | |||
72d6651ded | |||
a4e5d1b47a | |||
9f51a54a83 | |||
b74f7319c6 | |||
0ad66a27f2 | |||
a40003e85a | |||
5534a7fdf9 | |||
efb245278d | |||
5896d3740f | |||
73b9c0b89b | |||
629c155044 | |||
08640d877f | |||
ed5c63144e | |||
ee69aed2b0 | |||
fcb92f862d | |||
e8e98e667d | |||
e1502017ce | |||
72472f2899 | |||
42f71364ca | |||
3dabd3a556 | |||
0498e2fa98 | |||
2aaf59651e | |||
cd4e51eacf | |||
785842de03 | |||
261095fa12 | |||
ed2ed0f7ae | |||
7ad5ead0f6 | |||
a8284e64e3 | |||
5423a31d5a | |||
53575f8d90 | |||
d3bcbdac26 | |||
8c5b28adce | |||
dcbafae35a | |||
eb197edcec | |||
b8265bf7f2 | |||
e7c4f0b171 | |||
12f36b61ff | |||
b15c4189f5 | |||
74d3278258 | |||
831152b906 | |||
7c81a98632 | |||
6e47c62c06 | |||
bcae7f326d | |||
d719c41e31 | |||
6f0307fb35 | |||
2e3d625c1e | |||
029289413c | |||
4d51098c86 | |||
c2b05af022 | |||
e85f975ab0 | |||
dc77d62a5d | |||
4f41f209a2 | |||
5940e53d77 | |||
22dffd5b7e | |||
af2f31045d | |||
5daa59f9f3 | |||
2ea9d06a93 | |||
847fc142d1 | |||
0388459bd0 | |||
9a47ec1217 | |||
45a40c8640 | |||
e34f1c30d6 | |||
c14a4c0e3e | |||
b146e93a3a | |||
9957ec9724 | |||
78a42f61bd | |||
e78019dad6 | |||
0f026c5da2 | |||
c26a2d5d39 | |||
2798886c0b | |||
ffde283a4b | |||
f7031d0d3e | |||
595e6c57df | |||
87709c201e | |||
e70d83ace1 | |||
ca829a3a98 | |||
26a1e63626 | |||
c98959b895 | |||
baf18648e9 | |||
b21306376b | |||
fbd17685a1 | |||
3a673575dd | |||
d5e51840ab | |||
543c2086b2 | |||
95eab59c08 | |||
e9e768a288 | |||
e2180a1f31 | |||
7c1b57c8cd | |||
89e7a35a81 | |||
de6acfe54e | |||
789e620451 | |||
4789e190a0 | |||
d6366587a0 | |||
dddf00e3b1 | |||
cc47fb1c83 | |||
8e8cea1bcd | |||
68c97da518 | |||
f8b8799799 | |||
615f1778c3 | |||
c45b197c85 | |||
78713895dd | |||
aae2402ed2 | |||
b62de6e516 | |||
6e747849d7 | |||
a3a73ce1ac | |||
10c014bf98 | |||
9bb3643fc7 | |||
56db55caca | |||
565a08b229 | |||
bf0f11fd65 | |||
558daf8911 | |||
7ba7ff4b2a | |||
c9854be1c4 | |||
2fbb1a846f | |||
aa142df486 | |||
5e200a440f | |||
6ed8723d7d | |||
ac9710d5d6 | |||
35d76c20dc | |||
ca7344d9dc | |||
7603d1a71b | |||
d0cd4aed5a | |||
c5aadffa08 | |||
ecfca1f905 | |||
e49bf24d2d | |||
2a1cd6bb3e | |||
766e33774c | |||
9b90b1d302 | |||
ee8a3a52ec | |||
531bd624b5 | |||
abd6581242 | |||
7095367b93 | |||
3bb359a774 | |||
1c6ff733f9 | |||
2ecf5d8a5a | |||
0f88969b77 | |||
c317100b47 | |||
b029f176b6 | |||
bcfcfb628e | |||
268f528423 | |||
31be2e2527 | |||
502f021a0e | |||
b0b451d2c7 | |||
4872b59fac | |||
bb1be240ec | |||
b008c97c83 | |||
9886a30490 | |||
4ed5206d79 | |||
33916869f1 | |||
9ddf707804 | |||
3a020a66b7 | |||
bd39e69185 | |||
9d4ca3f34e | |||
de6f3ded09 | |||
e0aace33ea | |||
16eb269e1e | |||
2319dec278 | |||
f1be48aff1 | |||
93fd44813a | |||
54d4d81bf4 | |||
ea207d8199 | |||
e8287e91e4 |
86
.github/workflows/ccpp.yml
vendored
86
.github/workflows/ccpp.yml
vendored
@ -1,86 +0,0 @@
|
||||
name: unittest
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: make test_make
|
||||
run: make test_make
|
||||
|
||||
mac_tsan_sectransport:
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: make test_tsan
|
||||
run: make test_tsan
|
||||
|
||||
mac_tsan_openssl:
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: install openssl
|
||||
run: brew install openssl@1.1
|
||||
- name: make test
|
||||
run: make test_tsan_openssl
|
||||
|
||||
mac_tsan_mbedtls:
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: install mbedtls
|
||||
run: brew install mbedtls
|
||||
- name: make test
|
||||
run: make test_tsan_mbedtls
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: seanmiddleditch/gha-setup-vsdevenv@master
|
||||
- run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -DCMAKE_CXX_COMPILER=cl.exe -DUSE_WS=1 -DUSE_TEST=1 ..
|
||||
- run: cmake --build build
|
||||
|
||||
# Running the unittest does not work, the binary cannot be found
|
||||
#- run: ../build/test/ixwebsocket_unittest.exe
|
||||
# working-directory: test
|
||||
|
||||
uwp:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: seanmiddleditch/gha-setup-vsdevenv@master
|
||||
- run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION="10.0" -DCMAKE_CXX_COMPILER=cl.exe -DUSE_TEST=1 ..
|
||||
- run: cmake --build build
|
||||
|
||||
#
|
||||
# Windows with OpenSSL is working but disabled as it takes 13 minutes (10 for openssl) to build with vcpkg
|
||||
#
|
||||
# windows_openssl:
|
||||
# runs-on: windows-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v1
|
||||
# - uses: seanmiddleditch/gha-setup-vsdevenv@master
|
||||
# - run: |
|
||||
# vcpkg install zlib:x64-windows
|
||||
# vcpkg install openssl:x64-windows
|
||||
# - run: |
|
||||
# mkdir build
|
||||
# cd build
|
||||
# cmake -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_CXX_COMPILER=cl.exe -DUSE_OPEN_SSL=1 -DUSE_TLS=1 -DUSE_WS=1 -DUSE_TEST=1 ..
|
||||
# - run: cmake --build build
|
||||
#
|
||||
# # Running the unittest does not work, the binary cannot be found
|
||||
# #- run: ../build/test/ixwebsocket_unittest.exe
|
||||
# # working-directory: test
|
||||
|
66
.github/workflows/docker.yml
vendored
Normal file
66
.github/workflows/docker.yml
vendored
Normal 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
|
13
.github/workflows/unittest_linux.yml
vendored
Normal file
13
.github/workflows/unittest_linux.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: linux
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: make test_make
|
||||
run: make test_make
|
15
.github/workflows/unittest_mac_tsan_mbedtls.yml
vendored
Normal file
15
.github/workflows/unittest_mac_tsan_mbedtls.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
name: mac_tsan_mbedtls
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
mac_tsan_mbedtls:
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: install mbedtls
|
||||
run: brew install mbedtls
|
||||
- name: make test
|
||||
run: make test_tsan_mbedtls
|
15
.github/workflows/unittest_mac_tsan_openssl.yml
vendored
Normal file
15
.github/workflows/unittest_mac_tsan_openssl.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
name: mac_tsan_openssl
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
mac_tsan_openssl:
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: install openssl
|
||||
run: brew install openssl@1.1
|
||||
- name: make test
|
||||
run: make test_tsan_openssl
|
13
.github/workflows/unittest_mac_tsan_sectransport.yml
vendored
Normal file
13
.github/workflows/unittest_mac_tsan_sectransport.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: mac_tsan_sectransport
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
mac_tsan_sectransport:
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: make test_tsan
|
||||
run: make test_tsan
|
38
.github/workflows/unittest_uwp.yml
vendored
Normal file
38
.github/workflows/unittest_uwp.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
name: uwp
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
uwp:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: seanmiddleditch/gha-setup-vsdevenv@master
|
||||
- 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 -DUSE_ZLIB=0 ..
|
||||
- run: cmake --build build
|
||||
|
||||
#
|
||||
# Windows with OpenSSL is working but disabled as it takes 13 minutes (10 for openssl) to build with vcpkg
|
||||
#
|
||||
# windows_openssl:
|
||||
# runs-on: windows-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v1
|
||||
# - uses: seanmiddleditch/gha-setup-vsdevenv@master
|
||||
# - run: |
|
||||
# vcpkg install zlib:x64-windows
|
||||
# vcpkg install openssl:x64-windows
|
||||
# - run: |
|
||||
# mkdir build
|
||||
# cd build
|
||||
# cmake -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_CXX_COMPILER=cl.exe -DUSE_OPEN_SSL=1 -DUSE_TLS=1 -DUSE_WS=1 -DUSE_TEST=1 ..
|
||||
# - run: cmake --build build
|
||||
#
|
||||
# # Running the unittest does not work, the binary cannot be found
|
||||
# #- run: ../build/test/ixwebsocket_unittest.exe
|
||||
# # working-directory: test
|
20
.github/workflows/unittest_windows.yml
vendored
Normal file
20
.github/workflows/unittest_windows.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
name: windows
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: seanmiddleditch/gha-setup-vsdevenv@master
|
||||
- run: |
|
||||
mkdir build
|
||||
cd build
|
||||
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
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,3 +5,4 @@ ixsnake/ixsnake/.certs/
|
||||
site/
|
||||
ws/.certs/
|
||||
ws/.srl
|
||||
ixhttpd
|
||||
|
19
CMake/FindDeflate.cmake
Normal file
19
CMake/FindDeflate.cmake
Normal 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})
|
@ -7,7 +7,7 @@ find_library(MBEDCRYPTO_LIBRARY mbedcrypto)
|
||||
set(MBEDTLS_LIBRARIES "${MBEDTLS_LIBRARY}" "${MBEDX509_LIBRARY}" "${MBEDCRYPTO_LIBRARY}")
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(MBEDTLS DEFAULT_MSG
|
||||
find_package_handle_standard_args(MbedTLS DEFAULT_MSG
|
||||
MBEDTLS_INCLUDE_DIRS MBEDTLS_LIBRARY MBEDX509_LIBRARY MBEDCRYPTO_LIBRARY)
|
||||
|
||||
mark_as_advanced(MBEDTLS_INCLUDE_DIRS MBEDTLS_LIBRARY MBEDX509_LIBRARY MBEDCRYPTO_LIBRARY)
|
||||
|
110
CMakeLists.txt
110
CMakeLists.txt
@ -3,7 +3,7 @@
|
||||
# Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||
#
|
||||
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
cmake_minimum_required(VERSION 3.4.1...3.17.2)
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMake;${CMAKE_MODULE_PATH}")
|
||||
|
||||
project(ixwebsocket C CXX)
|
||||
@ -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)
|
||||
@ -123,8 +115,8 @@ if (USE_TLS)
|
||||
if (NOT USE_MBED_TLS AND NOT USE_OPEN_SSL) # unless we want something else
|
||||
set(USE_SECURE_TRANSPORT ON)
|
||||
endif()
|
||||
# default to mbedtls on uwp (universal windows platform) if nothing is configured
|
||||
elseif (${CMAKE_SYSTEM_NAME} MATCHES "WindowsStore")
|
||||
# default to mbedtls on windows if nothing is configured
|
||||
elseif (WIN32)
|
||||
if (NOT USE_OPEN_SSL) # unless we want something else
|
||||
set(USE_MBED_TLS ON)
|
||||
endif()
|
||||
@ -153,8 +145,6 @@ add_library( ixwebsocket STATIC
|
||||
${IXWEBSOCKET_HEADERS}
|
||||
)
|
||||
|
||||
add_library ( ixwebsocket::ixwebsocket ALIAS ixwebsocket )
|
||||
|
||||
if (USE_TLS)
|
||||
target_compile_definitions(ixwebsocket PUBLIC IXWEBSOCKET_USE_TLS)
|
||||
if (USE_MBED_TLS)
|
||||
@ -182,50 +172,56 @@ if (USE_TLS)
|
||||
# set(CMAKE_INCLUDE_PATH ${CMAKE_INCLUDE_PATH} /opt/local/include/openssl-1.0)
|
||||
endif()
|
||||
|
||||
# This OPENSSL_FOUND check is to help find a cmake manually configured OpenSSL
|
||||
if (NOT OPENSSL_FOUND)
|
||||
include(FindOpenSSL)
|
||||
endif()
|
||||
# Use OPENSSL_ROOT_DIR CMake variable if you need to use your own openssl
|
||||
find_package(OpenSSL REQUIRED)
|
||||
message(STATUS "OpenSSL: " ${OPENSSL_VERSION})
|
||||
|
||||
target_link_libraries(ixwebsocket PUBLIC OpenSSL::SSL OpenSSL::Crypto)
|
||||
add_definitions(${OPENSSL_DEFINITIONS})
|
||||
target_include_directories(ixwebsocket PUBLIC ${OPENSSL_INCLUDE_DIR})
|
||||
target_link_libraries(ixwebsocket ${OPENSSL_LIBRARIES})
|
||||
elseif (USE_MBED_TLS)
|
||||
message(STATUS "TLS configured to use mbedtls")
|
||||
|
||||
find_package(MbedTLS REQUIRED)
|
||||
target_include_directories(ixwebsocket PUBLIC ${MBEDTLS_INCLUDE_DIRS})
|
||||
target_link_libraries(ixwebsocket PUBLIC ${MBEDTLS_LIBRARIES})
|
||||
target_link_libraries(ixwebsocket ${MBEDTLS_LIBRARIES})
|
||||
elseif (USE_SECURE_TRANSPORT)
|
||||
message(STATUS "TLS configured to use secure transport")
|
||||
target_link_libraries(ixwebsocket PUBLIC "-framework foundation" "-framework security")
|
||||
target_link_libraries(ixwebsocket "-framework foundation" "-framework security")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# This ZLIB_FOUND check is to help find a cmake manually configured zlib
|
||||
if (NOT ZLIB_FOUND)
|
||||
find_package(ZLIB)
|
||||
endif()
|
||||
if (ZLIB_FOUND)
|
||||
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 PUBLIC ${ZLIB_LIBRARIES})
|
||||
else()
|
||||
include_directories(third_party/zlib ${CMAKE_CURRENT_BINARY_DIR}/third_party/zlib)
|
||||
add_subdirectory(third_party/zlib EXCLUDE_FROM_ALL)
|
||||
target_link_libraries(ixwebsocket PRIVATE $<LINK_ONLY:zlibstatic>)
|
||||
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 PUBLIC wsock32 ws2_32 shlwapi)
|
||||
target_link_libraries(ixwebsocket wsock32 ws2_32 shlwapi)
|
||||
add_definitions(-D_CRT_SECURE_NO_WARNINGS)
|
||||
|
||||
if (USE_TLS)
|
||||
target_link_libraries(ixwebsocket PUBLIC Crypt32)
|
||||
target_link_libraries(ixwebsocket Crypt32)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (UNIX)
|
||||
find_package(Threads)
|
||||
target_link_libraries(ixwebsocket PUBLIC ${CMAKE_THREAD_LIBS_INIT})
|
||||
target_link_libraries(ixwebsocket ${CMAKE_THREAD_LIBS_INIT})
|
||||
endif()
|
||||
|
||||
|
||||
@ -238,27 +234,43 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
|
||||
target_compile_options(ixwebsocket PRIVATE /MP)
|
||||
endif()
|
||||
|
||||
target_include_directories(ixwebsocket PUBLIC $<BUILD_INTERFACE:${IXWEBSOCKET_INCLUDE_DIRS}> $<INSTALL_INTERFACE:include/ixwebsocket>)
|
||||
target_include_directories(ixwebsocket PUBLIC
|
||||
$<BUILD_INTERFACE:${IXWEBSOCKET_INCLUDE_DIRS}/>
|
||||
$<INSTALL_INTERFACE:include/ixwebsocket>
|
||||
)
|
||||
|
||||
set_target_properties(ixwebsocket PROPERTIES PUBLIC_HEADER "${IXWEBSOCKET_HEADERS}")
|
||||
|
||||
install(TARGETS ixwebsocket EXPORT ixwebsocket
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_PREFIX}/lib
|
||||
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_PREFIX}/include/ixwebsocket/
|
||||
install(TARGETS ixwebsocket
|
||||
EXPORT ixwebsocket
|
||||
ARCHIVE DESTINATION lib
|
||||
PUBLIC_HEADER DESTINATION include/ixwebsocket/
|
||||
)
|
||||
|
||||
install(EXPORT ixwebsocket NAMESPACE ixwebsocket:: DESTINATION lib/cmake/ixwebsocket)
|
||||
export(EXPORT ixwebsocket NAMESPACE ixwebsocket:: FILE ixwebsocketConfig.cmake)
|
||||
install(EXPORT ixwebsocket
|
||||
FILE ixwebsocket-config.cmake
|
||||
NAMESPACE ixwebsocket::
|
||||
DESTINATION lib/cmake/ixwebsocket)
|
||||
|
||||
if (USE_WS OR USE_TEST)
|
||||
add_subdirectory(ixcore)
|
||||
add_subdirectory(ixcrypto)
|
||||
add_subdirectory(ixcobra)
|
||||
add_subdirectory(ixredis)
|
||||
add_subdirectory(ixsnake)
|
||||
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_Declare(jsoncpp
|
||||
GIT_REPOSITORY "https://github.com/open-source-parsers/jsoncpp"
|
||||
GIT_TAG "1.9.4")
|
||||
|
||||
FetchContent_MakeAvailable(spdlog jsoncpp)
|
||||
|
||||
if (USE_WS)
|
||||
add_subdirectory(ws)
|
||||
|
101
README.md
101
README.md
@ -1,37 +1,72 @@
|
||||
## Hello world
|
||||
|
||||

|
||||
|
||||
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
|
||||
// Required on Windows
|
||||
ix::initNetSystem();
|
||||
/*
|
||||
* main.cpp
|
||||
* Author: Benjamin Sergeant
|
||||
* Copyright (c) 2020 Machine Zone, Inc. All rights reserved.
|
||||
*
|
||||
* Super simple standalone example. See ws folder, unittest and doc/usage.md for more.
|
||||
*
|
||||
* On macOS
|
||||
* $ mkdir -p build ; cd build ; cmake -DUSE_TLS=1 .. ; make -j ; make install
|
||||
* $ clang++ --std=c++14 --stdlib=libc++ main.cpp -lixwebsocket -lz -framework Security -framework Foundation
|
||||
* $ ./a.out
|
||||
*/
|
||||
|
||||
// Our websocket object
|
||||
ix::WebSocket webSocket;
|
||||
#include <ixwebsocket/IXNetSystem.h>
|
||||
#include <ixwebsocket/IXWebSocket.h>
|
||||
#include <iostream>
|
||||
|
||||
std::string url("ws://localhost:8080/");
|
||||
webSocket.setUrl(url);
|
||||
int main()
|
||||
{
|
||||
// Required on Windows
|
||||
ix::initNetSystem();
|
||||
|
||||
// Setup a callback to be fired (in a background thread, watch out for race conditions !)
|
||||
// when a message or an event (open, close, error) is received
|
||||
webSocket.setOnMessageCallback([](const ix::WebSocketMessagePtr& msg)
|
||||
{
|
||||
if (msg->type == ix::WebSocketMessageType::Message)
|
||||
// Our websocket object
|
||||
ix::WebSocket webSocket;
|
||||
|
||||
std::string url("wss://echo.websocket.org");
|
||||
webSocket.setUrl(url);
|
||||
|
||||
std::cout << "Connecting to " << url << "..." << std::endl;
|
||||
|
||||
// Setup a callback to be fired (in a background thread, watch out for race conditions !)
|
||||
// when a message or an event (open, close, error) is received
|
||||
webSocket.setOnMessageCallback([](const ix::WebSocketMessagePtr& msg)
|
||||
{
|
||||
std::cout << msg->str << std::endl;
|
||||
if (msg->type == ix::WebSocketMessageType::Message)
|
||||
{
|
||||
std::cout << "received message: " << msg->str << std::endl;
|
||||
}
|
||||
else if (msg->type == ix::WebSocketMessageType::Open)
|
||||
{
|
||||
std::cout << "Connection established" << std::endl;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Now that our callback is setup, we can start our background thread and receive messages
|
||||
webSocket.start();
|
||||
|
||||
// Send a message to the server (default to TEXT mode)
|
||||
webSocket.send("hello world");
|
||||
|
||||
while (true)
|
||||
{
|
||||
std::string text;
|
||||
std::cout << "> " << std::flush;
|
||||
std::getline(std::cin, text);
|
||||
|
||||
webSocket.send(text);
|
||||
}
|
||||
);
|
||||
|
||||
// Now that our callback is setup, we can start our background thread and receive messages
|
||||
webSocket.start();
|
||||
|
||||
// Send a message to the server (default to TEXT mode)
|
||||
webSocket.send("hello world");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Interested? Go read the [docs](https://machinezone.github.io/IXWebSocket/)! If things don't work as expected, please create an issue on GitHub, or even better a pull request if you know how to fix your problem.
|
||||
@ -45,7 +80,27 @@ IXWebSocket client code is autobahn compliant beginning with the 6.0.0 version.
|
||||
If your company or project is using this library, feel free to open an issue or PR to amend this list.
|
||||
|
||||
- [Machine Zone](https://www.mz.com)
|
||||
- [dis-light](https://gitlab.com/HCInk/dis-light), a discord library with a node frontend.
|
||||
- [libDiscordBot](https://github.com/tostc/libDiscordBot/tree/master), a work in progress discord library
|
||||
- [Tokio](https://gitlab.com/HCInk/tokio), a discord library focused on audio playback with node bindings.
|
||||
- [libDiscordBot](https://github.com/tostc/libDiscordBot/tree/master), an easy to use Discord-bot framework.
|
||||
- [gwebsocket](https://github.com/norrbotten/gwebsocket), a websocket (lua) module for Garry's Mod
|
||||
- [DisCPP](https://github.com/DisCPP/DisCPP), a simple but feature rich Discord API wrapper
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
| OS | TLS | Sanitizer | Status |
|
||||
|-------------------|-------------------|-------------------|-------------------|
|
||||
| Linux | OpenSSL | None | [![Build2][1]][7] |
|
||||
| macOS | Secure Transport | Thread Sanitizer | [![Build2][2]][7] |
|
||||
| macOS | OpenSSL | Thread Sanitizer | [![Build2][3]][7] |
|
||||
| macOS | MbedTLS | Thread Sanitizer | [![Build2][4]][7] |
|
||||
| Windows | Disabled | None | [![Build2][5]][7] |
|
||||
| UWP | Disabled | None | [![Build2][6]][7] |
|
||||
|
||||
[1]: https://github.com/machinezone/IXWebSocket/workflows/linux/badge.svg
|
||||
[2]: https://github.com/machinezone/IXWebSocket/workflows/mac_tsan_sectransport/badge.svg
|
||||
[3]: https://github.com/machinezone/IXWebSocket/workflows/mac_tsan_openssl/badge.svg
|
||||
[4]: https://github.com/machinezone/IXWebSocket/workflows/mac_tsan_mbedtls/badge.svg
|
||||
[5]: https://github.com/machinezone/IXWebSocket/workflows/windows/badge.svg
|
||||
[6]: https://github.com/machinezone/IXWebSocket/workflows/uwp/badge.svg
|
||||
[7]: https://github.com/machinezone/IXWebSocket
|
||||
|
||||
|
@ -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
|
||||
|
@ -1,8 +1,8 @@
|
||||
FROM alpine:3.11 as build
|
||||
FROM alpine:3.12 as build
|
||||
|
||||
RUN apk add --no-cache \
|
||||
gcc g++ musl-dev linux-headers \
|
||||
cmake mbedtls-dev make zlib-dev ninja
|
||||
cmake mbedtls-dev make zlib-dev python3-dev ninja git
|
||||
|
||||
RUN addgroup -S app && \
|
||||
adduser -S -G app app && \
|
||||
@ -18,9 +18,9 @@ USER app
|
||||
RUN make ws_mbedtls_install && \
|
||||
sh tools/trim_repo_for_docker.sh
|
||||
|
||||
FROM alpine:3.11 as runtime
|
||||
FROM alpine:3.12 as runtime
|
||||
|
||||
RUN apk add --no-cache libstdc++ mbedtls ca-certificates && \
|
||||
RUN apk add --no-cache libstdc++ mbedtls ca-certificates python3 strace && \
|
||||
addgroup -S app && \
|
||||
adduser -S -G app app
|
||||
|
||||
|
23
docker/Dockerfile.ubuntu_groovy
Normal file
23
docker/Dockerfile.ubuntu_groovy
Normal 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"]
|
@ -1,6 +1,303 @@
|
||||
# Changelog
|
||||
|
||||
All changes to this project will be documented in this file.
|
||||
|
||||
## [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
|
||||
|
||||
## [9.8.4] - 2020-06-26
|
||||
|
||||
(cobra bots) remove bots which is not required now that we can use Python extensions
|
||||
|
||||
## [9.8.3] - 2020-06-25
|
||||
|
||||
(cmake) new python code is optional and enabled at cmake time with -DUSE_PYTHON=1
|
||||
|
||||
## [9.8.2] - 2020-06-24
|
||||
|
||||
(cobra bots) new cobra metrics bot to send data to statsd using Python for processing the message
|
||||
|
||||
## [9.8.1] - 2020-06-19
|
||||
|
||||
(cobra metrics to statsd bot) fps slow frame info : do not include os name
|
||||
|
||||
## [9.8.0] - 2020-06-19
|
||||
|
||||
(cobra metrics to statsd bot) send info about memory warnings
|
||||
|
||||
## [9.7.9] - 2020-06-18
|
||||
|
||||
(http client) fix deadlock when following redirects
|
||||
|
||||
## [9.7.8] - 2020-06-18
|
||||
|
||||
(cobra metrics to statsd bot) send info about net requests
|
||||
|
||||
## [9.7.7] - 2020-06-17
|
||||
|
||||
(cobra client and bots) add batch_size subscription option for retrieving multiple messages at once
|
||||
|
||||
## [9.7.6] - 2020-06-15
|
||||
|
||||
(websocket) WebSocketServer is not a final class, so that users can extend it (fix #215)
|
||||
|
||||
## [9.7.5] - 2020-06-15
|
||||
|
||||
(cobra bots) minor aesthetic change, in how we display http headers with a : then space as key value separator instead of :: with no space
|
||||
|
||||
## [9.7.4] - 2020-06-11
|
||||
|
||||
(cobra metrics to statsd bot) change from a statsd type of gauge to a timing one
|
||||
|
||||
## [9.7.3] - 2020-06-11
|
||||
|
||||
(redis cobra bots) capture most used devices in a zset
|
||||
|
||||
## [9.7.2] - 2020-06-11
|
||||
|
||||
(ws) add bare bone redis-cli like sub-command, with command line editing powered by libnoise
|
||||
|
||||
## [9.7.1] - 2020-06-11
|
||||
|
||||
(redis cobra bots) ws cobra metrics to redis / hostname invalid parsing
|
||||
|
||||
## [9.7.0] - 2020-06-11
|
||||
|
||||
(redis cobra bots) xadd with maxlen + fix bug in xadd client implementation and ws cobra metrics to redis command argument parsing
|
||||
|
||||
## [9.6.9] - 2020-06-10
|
||||
|
||||
(redis cobra bots) update the cobra to redis bot to use the bot framework, and change it to report fps metrics into redis streams.
|
||||
|
||||
## [9.6.6] - 2020-06-04
|
||||
|
||||
(statsd cobra bots) statsd improvement: prefix does not need a dot as a suffix, message size can be larger than 256 bytes, error handling was invalid, use core logger for logging instead of std::cerr
|
||||
|
||||
## [9.6.5] - 2020-05-29
|
||||
|
||||
(http server) support gzip compression
|
||||
|
||||
## [9.6.4] - 2020-05-20
|
||||
|
||||
(compiler fix) support clang 5 and earlier (contributed by @LunarWatcher)
|
||||
|
||||
## [9.6.3] - 2020-05-18
|
||||
|
||||
(cmake) revert CMake changes to fix #203 and be able to use an external OpenSSL
|
||||
|
||||
## [9.6.2] - 2020-05-17
|
||||
|
||||
(cmake) make install cmake files optional to not conflict with vcpkg
|
||||
|
||||
## [9.6.1] - 2020-05-17
|
||||
|
||||
(windows + tls) mbedtls is the default windows tls backend + add ability to load system certificates with mbdetls on windows
|
||||
|
||||
## [9.6.0] - 2020-05-12
|
||||
|
||||
(ixbots) add options to limit how many messages per minute should be processed
|
||||
|
@ -17,13 +17,15 @@ 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
|
||||
* `-DUSE_WS=1` will build the ws interactive command line tool
|
||||
* `-DUSE_TEST=1` will build the unittest
|
||||
* `-DUSE_PYTHON=1` will use Python3 for cobra bots, require Python3 to be installed.
|
||||
|
||||
If you are on Windows, look at the [appveyor](https://github.com/machinezone/IXWebSocket/blob/master/appveyor.yml) file (not maintained much though) or rather the [github actions](https://github.com/machinezone/IXWebSocket/blob/master/.github/workflows/ccpp.yml#L40) which have instructions for building dependencies.
|
||||
If you are on Windows, look at the [appveyor](https://github.com/machinezone/IXWebSocket/blob/master/appveyor.yml) file (not maintained much though) or rather the [github actions](https://github.com/machinezone/IXWebSocket/blob/master/.github/workflows/unittest_windows.yml) which have instructions for building dependencies.
|
||||
|
||||
It is also possible to externally include the project, so that everything is fetched over the wire when you build like so:
|
||||
|
||||
@ -42,6 +44,19 @@ It is possible to get IXWebSocket through Microsoft [vcpkg](https://github.com/m
|
||||
```
|
||||
vcpkg install ixwebsocket
|
||||
```
|
||||
To use the installed package within a cmake project, use the following:
|
||||
```cmake
|
||||
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "") # this is super important in order for cmake to include the vcpkg search/lib paths!
|
||||
|
||||
# find library and its headers
|
||||
find_path(IXWEBSOCKET_INCLUDE_DIR ixwebsocket/IXWebSocket.h)
|
||||
find_library(IXWEBSOCKET_LIBRARY ixwebsocket)
|
||||
# include headers
|
||||
include_directories(${IXWEBSOCKET_INCLUDE_DIR})
|
||||
# ...
|
||||
target_link_libraries(${PROJECT_NAME} ... ${IXWEBSOCKET_LIBRARY}) # Cmake will automatically fail the generation if the lib was not found, i.e is set to NOTFOUNS
|
||||
|
||||
```
|
||||
|
||||
### Conan
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||

|
||||
|
||||
## Introduction
|
||||
|
||||
[*WebSocket*](https://en.wikipedia.org/wiki/WebSocket) is a computer communications protocol, providing full-duplex and bi-directionnal communication channels over a single TCP connection. *IXWebSocket* is a C++ library for client and server Websocket communication, and for client and server HTTP communication. *TLS* aka *SSL* is supported. The code is derived from [easywsclient](https://github.com/dhbaird/easywsclient) and from the [Satori C SDK](https://github.com/satori-com/satori-rtm-sdk-c). It has been tested on the following platforms.
|
||||
|
37
docs/performance.md
Normal file
37
docs/performance.md
Normal 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
|
||||
```
|
168
docs/usage.md
168
docs/usage.md
@ -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
|
||||
@ -369,7 +469,7 @@ out = httpClient.post(url, std::string("foo=bar"), args);
|
||||
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
|
||||
@ -392,6 +492,8 @@ bool ok = httpClient.performRequest(args, [](const HttpResponsePtr& response)
|
||||
// ok will be false if your httpClient is not async
|
||||
```
|
||||
|
||||
See this [issue](https://github.com/machinezone/IXWebSocket/issues/209) for links about uploading files with HTTP multipart.
|
||||
|
||||
## HTTP server API
|
||||
|
||||
```cpp
|
||||
@ -415,11 +517,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;
|
||||
|
||||
|
18
docs/ws.md
18
docs/ws.md
@ -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
|
||||
|
||||
```
|
||||
|
46
httpd.cpp
Normal file
46
httpd.cpp
Normal file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* httpd.cpp
|
||||
* Author: Benjamin Sergeant
|
||||
* Copyright (c) 2020 Machine Zone, Inc. All rights reserved.
|
||||
*
|
||||
* Buid with make httpd
|
||||
*/
|
||||
|
||||
#include <ixwebsocket/IXHttpServer.h>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc != 3)
|
||||
{
|
||||
std::cerr << "Usage: " << argv[0]
|
||||
<< " <port> <host>" << std::endl;
|
||||
std::cerr << " " << argv[0] << " 9090 127.0.0.1" << std::endl;
|
||||
std::cerr << " " << argv[0] << " 9090 0.0.0.0" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int port;
|
||||
std::stringstream ss;
|
||||
ss << argv[1];
|
||||
ss >> port;
|
||||
std::string hostname(argv[2]);
|
||||
|
||||
std::cout << "Listening on " << hostname
|
||||
<< ":" << port << std::endl;
|
||||
|
||||
ix::HttpServer server(port, hostname);
|
||||
|
||||
auto res = server.listen();
|
||||
if (!res.first)
|
||||
{
|
||||
std::cout << res.second << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
server.start();
|
||||
server.wait();
|
||||
|
||||
return 0;
|
||||
}
|
@ -5,18 +5,24 @@
|
||||
|
||||
set (IXBOTS_SOURCES
|
||||
ixbots/IXCobraBot.cpp
|
||||
ixbots/IXCobraToCobraBot.cpp
|
||||
ixbots/IXCobraToSentryBot.cpp
|
||||
ixbots/IXCobraToStatsdBot.cpp
|
||||
ixbots/IXCobraToStdoutBot.cpp
|
||||
ixbots/IXCobraMetricsToRedisBot.cpp
|
||||
ixbots/IXCobraToPythonBot.cpp
|
||||
ixbots/IXStatsdClient.cpp
|
||||
)
|
||||
|
||||
set (IXBOTS_HEADERS
|
||||
ixbots/IXCobraBot.h
|
||||
ixbots/IXCobraBotConfig.h
|
||||
ixbots/IXCobraToCobraBot.h
|
||||
ixbots/IXCobraToSentryBot.h
|
||||
ixbots/IXCobraToStatsdBot.h
|
||||
ixbots/IXCobraToStdoutBot.h
|
||||
ixbots/IXCobraMetricsToRedisBot.h
|
||||
ixbots/IXCobraToPythonBot.h
|
||||
ixbots/IXStatsdClient.h
|
||||
)
|
||||
|
||||
@ -25,9 +31,9 @@ add_library(ixbots STATIC
|
||||
${IXBOTS_HEADERS}
|
||||
)
|
||||
|
||||
find_package(JsonCpp)
|
||||
if (NOT JSONCPP_FOUND)
|
||||
set(JSONCPP_INCLUDE_DIRS ../third_party/jsoncpp)
|
||||
if (USE_PYTHON)
|
||||
target_compile_definitions(ixbots PUBLIC IXBOTS_USE_PYTHON)
|
||||
find_package(Python COMPONENTS Development)
|
||||
endif()
|
||||
|
||||
set(IXBOTS_INCLUDE_DIRS
|
||||
@ -36,8 +42,12 @@ set(IXBOTS_INCLUDE_DIRS
|
||||
../ixcore
|
||||
../ixwebsocket
|
||||
../ixcobra
|
||||
../ixsentry
|
||||
${JSONCPP_INCLUDE_DIRS}
|
||||
${SPDLOG_INCLUDE_DIRS})
|
||||
../ixredis
|
||||
../ixsentry)
|
||||
|
||||
if (USE_PYTHON)
|
||||
set(IXBOTS_INCLUDE_DIRS ${IXBOTS_INCLUDE_DIRS} ${Python_INCLUDE_DIRS})
|
||||
endif()
|
||||
|
||||
target_include_directories( ixbots PUBLIC ${IXBOTS_INCLUDE_DIRS} )
|
||||
target_link_libraries( ixbots jsoncpp_static )
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <ixcobra/IXCobraConnection.h>
|
||||
#include <ixcore/utils/IXCoreLogger.h>
|
||||
#include <ixwebsocket/IXSetThreadName.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@ -28,6 +29,9 @@ namespace ix
|
||||
auto runtime = botConfig.runtime;
|
||||
auto maxEventsPerMinute = botConfig.maxEventsPerMinute;
|
||||
auto limitReceivedEvents = botConfig.limitReceivedEvents;
|
||||
auto batchSize = botConfig.batchSize;
|
||||
|
||||
config.headers["X-Cobra-Channel"] = channel;
|
||||
|
||||
ix::CobraConnection conn;
|
||||
conn.configure(config);
|
||||
@ -43,6 +47,7 @@ namespace ix
|
||||
std::atomic<bool> stop(false);
|
||||
std::atomic<bool> throttled(false);
|
||||
std::atomic<bool> fatalCobraError(false);
|
||||
std::atomic<bool> stalledConnection(false);
|
||||
int minuteCounter = 0;
|
||||
|
||||
auto timer = [&sentCount,
|
||||
@ -53,7 +58,9 @@ namespace ix
|
||||
&receivedCountPerSecs,
|
||||
&receivedCountPerMinutes,
|
||||
&minuteCounter,
|
||||
&conn,
|
||||
&stop] {
|
||||
setThreadName("Bot progress");
|
||||
while (!stop)
|
||||
{
|
||||
//
|
||||
@ -70,7 +77,11 @@ namespace ix
|
||||
<< sentCountPerSecs
|
||||
<< " "
|
||||
<< sentCountTotal;
|
||||
CoreLogger::info(ss.str());
|
||||
|
||||
if (conn.isAuthenticated())
|
||||
{
|
||||
CoreLogger::info(ss.str());
|
||||
}
|
||||
|
||||
receivedCountPerSecs = receivedCount - receivedCountTotal;
|
||||
sentCountPerSecs = sentCount - sentCountTotal;
|
||||
@ -93,7 +104,14 @@ namespace ix
|
||||
|
||||
std::thread t1(timer);
|
||||
|
||||
auto heartbeat = [&sentCount, &receivedCount, &stop, &enableHeartbeat, &heartBeatTimeout, &fatalCobraError] {
|
||||
auto heartbeat = [&sentCount,
|
||||
&receivedCount,
|
||||
&stop,
|
||||
&enableHeartbeat,
|
||||
&heartBeatTimeout,
|
||||
&stalledConnection]
|
||||
{
|
||||
setThreadName("Bot heartbeat");
|
||||
std::string state("na");
|
||||
|
||||
if (!enableHeartbeat) return;
|
||||
@ -108,9 +126,12 @@ namespace ix
|
||||
|
||||
if (currentState == state)
|
||||
{
|
||||
CoreLogger::error("no messages received or sent for 1 minute, exiting");
|
||||
fatalCobraError = true;
|
||||
break;
|
||||
ss.str("");
|
||||
ss << "no messages received or sent for "
|
||||
<< heartBeatTimeout << " seconds, reconnecting";
|
||||
|
||||
CoreLogger::warn(ss.str());
|
||||
stalledConnection = true;
|
||||
}
|
||||
state = currentState;
|
||||
|
||||
@ -135,6 +156,7 @@ namespace ix
|
||||
&receivedCountPerMinutes,
|
||||
maxEventsPerMinute,
|
||||
limitReceivedEvents,
|
||||
batchSize,
|
||||
&fatalCobraError,
|
||||
&sentCount](const CobraEventPtr& event) {
|
||||
if (event->type == ix::CobraEventType::Open)
|
||||
@ -143,12 +165,16 @@ namespace ix
|
||||
|
||||
for (auto&& it : event->headers)
|
||||
{
|
||||
CoreLogger::info(it.first + "::" + it.second);
|
||||
CoreLogger::info(it.first + ": " + it.second);
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
@ -156,7 +182,7 @@ namespace ix
|
||||
CoreLogger::info("Subscribing to " + channel);
|
||||
CoreLogger::info("Subscribing at position " + subscriptionPosition);
|
||||
CoreLogger::info("Subscribing with filter " + filter);
|
||||
conn.subscribe(channel, filter, subscriptionPosition,
|
||||
conn.subscribe(channel, filter, subscriptionPosition, batchSize,
|
||||
[&sentCount, &receivedCountPerMinutes,
|
||||
maxEventsPerMinute, limitReceivedEvents,
|
||||
&throttled, &receivedCount,
|
||||
@ -231,6 +257,13 @@ namespace ix
|
||||
std::this_thread::sleep_for(duration);
|
||||
|
||||
if (fatalCobraError) break;
|
||||
|
||||
if (stalledConnection)
|
||||
{
|
||||
conn.disconnect();
|
||||
conn.connect();
|
||||
stalledConnection = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Run for a duration, used by unittesting now
|
||||
@ -242,6 +275,13 @@ namespace ix
|
||||
std::this_thread::sleep_for(duration);
|
||||
|
||||
if (fatalCobraError) break;
|
||||
|
||||
if (stalledConnection)
|
||||
{
|
||||
conn.disconnect();
|
||||
conn.connect();
|
||||
stalledConnection = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -265,4 +305,22 @@ namespace ix
|
||||
{
|
||||
_onBotMessageCallback = callback;
|
||||
}
|
||||
|
||||
std::string CobraBot::getDeviceIdentifier(const Json::Value& msg)
|
||||
{
|
||||
std::string deviceId("na");
|
||||
|
||||
auto osName = msg["device"]["os_name"];
|
||||
if (osName == "Android")
|
||||
{
|
||||
deviceId = msg["device"]["model"].asString();
|
||||
}
|
||||
else if (osName == "iOS")
|
||||
{
|
||||
deviceId = msg["device"]["hardware_model"].asString();
|
||||
}
|
||||
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
} // namespace ix
|
||||
|
@ -28,6 +28,8 @@ namespace ix
|
||||
int64_t run(const CobraBotConfig& botConfig);
|
||||
void setOnBotMessageCallback(const OnBotMessageCallback& callback);
|
||||
|
||||
std::string getDeviceIdentifier(const Json::Value& msg);
|
||||
|
||||
private:
|
||||
OnBotMessageCallback _onBotMessageCallback;
|
||||
};
|
||||
|
@ -27,5 +27,6 @@ namespace ix
|
||||
int runtime = -1;
|
||||
int maxEventsPerMinute = std::numeric_limits<int>::max();
|
||||
bool limitReceivedEvents = false;
|
||||
int batchSize = 1;
|
||||
};
|
||||
} // namespace ix
|
||||
|
149
ixbots/ixbots/IXCobraMetricsToRedisBot.cpp
Normal file
149
ixbots/ixbots/IXCobraMetricsToRedisBot.cpp
Normal file
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* IXCobraMetricsToRedisBot.cpp
|
||||
* Author: Benjamin Sergeant
|
||||
* Copyright (c) 2020 Machine Zone, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
#include "IXCobraMetricsToRedisBot.h"
|
||||
|
||||
#include "IXCobraBot.h"
|
||||
#include "IXStatsdClient.h"
|
||||
#include <chrono>
|
||||
#include <ixcobra/IXCobraConnection.h>
|
||||
#include <ixcore/utils/IXCoreLogger.h>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <cctype>
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string removeSpaces(const std::string& str)
|
||||
{
|
||||
std::string out(str);
|
||||
out.erase(
|
||||
std::remove_if(out.begin(), out.end(), [](unsigned char x) { return std::isspace(x); }),
|
||||
out.end());
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
namespace ix
|
||||
{
|
||||
bool processPerfMetricsEventSlowFrames(const Json::Value& msg,
|
||||
RedisClient& redisClient,
|
||||
const std::string& deviceId)
|
||||
{
|
||||
auto frameRateHistogramCounts = msg["data"]["FrameRateHistogramCounts"];
|
||||
|
||||
int slowFrames = 0;
|
||||
slowFrames += frameRateHistogramCounts[4].asInt();
|
||||
slowFrames += frameRateHistogramCounts[5].asInt();
|
||||
slowFrames += frameRateHistogramCounts[6].asInt();
|
||||
slowFrames += frameRateHistogramCounts[7].asInt();
|
||||
|
||||
//
|
||||
// XADD without a device id
|
||||
//
|
||||
std::stringstream ss;
|
||||
ss << msg["id"].asString() << "_slow_frames" << "."
|
||||
<< msg["device"]["game"].asString() << "."
|
||||
<< msg["device"]["os_name"].asString() << "."
|
||||
<< removeSpaces(msg["data"]["Tag"].asString());
|
||||
|
||||
int maxLen;
|
||||
maxLen = 100000;
|
||||
std::string id = ss.str();
|
||||
std::string errMsg;
|
||||
if (redisClient.xadd(id, std::to_string(slowFrames), maxLen, errMsg).empty())
|
||||
{
|
||||
CoreLogger::info(std::string("redis XADD error: ") + errMsg);
|
||||
}
|
||||
|
||||
//
|
||||
// XADD with a device id
|
||||
//
|
||||
ss.str(""); // reset the stringstream
|
||||
ss << msg["id"].asString() << "_slow_frames_by_device" << "."
|
||||
<< deviceId << "."
|
||||
<< msg["device"]["game"].asString() << "."
|
||||
<< msg["device"]["os_name"].asString() << "."
|
||||
<< removeSpaces(msg["data"]["Tag"].asString());
|
||||
|
||||
id = ss.str();
|
||||
maxLen = 1000;
|
||||
if (redisClient.xadd(id, std::to_string(slowFrames), maxLen, errMsg).empty())
|
||||
{
|
||||
CoreLogger::info(std::string("redis XADD error: ") + errMsg);
|
||||
}
|
||||
|
||||
//
|
||||
// Add device to the device zset, and increment the score
|
||||
// so that we know which devices are used more than others
|
||||
// ZINCRBY myzset 1 one
|
||||
//
|
||||
ss.str(""); // reset the stringstream
|
||||
ss << msg["id"].asString() << "_slow_frames_devices" << "."
|
||||
<< msg["device"]["game"].asString();
|
||||
|
||||
id = ss.str();
|
||||
std::vector<std::string> args = {
|
||||
"ZINCRBY", id, "1", deviceId
|
||||
};
|
||||
auto response = redisClient.send(args, errMsg);
|
||||
if (response.first == RespType::Error)
|
||||
{
|
||||
CoreLogger::info(std::string("redis ZINCRBY error: ") + errMsg);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int64_t cobra_metrics_to_redis_bot(const ix::CobraBotConfig& config,
|
||||
RedisClient& redisClient,
|
||||
bool verbose)
|
||||
{
|
||||
CobraBot bot;
|
||||
|
||||
bot.setOnBotMessageCallback(
|
||||
[&redisClient, &verbose, &bot]
|
||||
(const Json::Value& msg,
|
||||
const std::string& /*position*/,
|
||||
std::atomic<bool>& /*throttled*/,
|
||||
std::atomic<bool>& /*fatalCobraError*/,
|
||||
std::atomic<uint64_t>& sentCount) -> void {
|
||||
if (msg["device"].isNull())
|
||||
{
|
||||
CoreLogger::info("no device entry, skipping event");
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg["id"].isNull())
|
||||
{
|
||||
CoreLogger::info("no id entry, skipping event");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Display full message with
|
||||
if (verbose)
|
||||
{
|
||||
CoreLogger::info(msg.toStyledString());
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
if (msg["id"].asString() == "engine_performance_metrics_id")
|
||||
{
|
||||
auto deviceId = bot.getDeviceIdentifier(msg);
|
||||
success = processPerfMetricsEventSlowFrames(msg, redisClient, deviceId);
|
||||
}
|
||||
|
||||
if (success) sentCount++;
|
||||
});
|
||||
|
||||
return bot.run(config);
|
||||
}
|
||||
} // namespace ix
|
20
ixbots/ixbots/IXCobraMetricsToRedisBot.h
Normal file
20
ixbots/ixbots/IXCobraMetricsToRedisBot.h
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* IXCobraMetricsToRedisBot.h
|
||||
* Author: Benjamin Sergeant
|
||||
* Copyright (c) 2020 Machine Zone, Inc. All rights reserved.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <ixredis/IXRedisClient.h>
|
||||
#include "IXCobraBotConfig.h"
|
||||
#include <stddef.h>
|
||||
#include <string>
|
||||
|
||||
namespace ix
|
||||
{
|
||||
int64_t cobra_metrics_to_redis_bot(const ix::CobraBotConfig& config,
|
||||
RedisClient& redisClient,
|
||||
bool verbose);
|
||||
} // namespace ix
|
||||
|
45
ixbots/ixbots/IXCobraToCobraBot.cpp
Normal file
45
ixbots/ixbots/IXCobraToCobraBot.cpp
Normal 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
|
20
ixbots/ixbots/IXCobraToCobraBot.h
Normal file
20
ixbots/ixbots/IXCobraToCobraBot.h
Normal 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
|
329
ixbots/ixbots/IXCobraToPythonBot.cpp
Normal file
329
ixbots/ixbots/IXCobraToPythonBot.cpp
Normal file
@ -0,0 +1,329 @@
|
||||
/*
|
||||
* IXCobraToPythonBot.cpp
|
||||
* Author: Benjamin Sergeant
|
||||
* Copyright (c) 2020 Machine Zone, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
#include "IXCobraToPythonBot.h"
|
||||
|
||||
#include "IXCobraBot.h"
|
||||
#include "IXStatsdClient.h"
|
||||
#include <chrono>
|
||||
#include <ixcobra/IXCobraConnection.h>
|
||||
#include <ixcore/utils/IXCoreLogger.h>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <cctype>
|
||||
|
||||
//
|
||||
// I cannot get Windows to easily build on CI (github action) so support
|
||||
// is disabled for now. It should be a simple fix
|
||||
// (linking error about missing debug build)
|
||||
//
|
||||
|
||||
#ifdef IXBOTS_USE_PYTHON
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
#endif
|
||||
|
||||
#ifdef IXBOTS_USE_PYTHON
|
||||
namespace
|
||||
{
|
||||
//
|
||||
// This function is unused at this point. It produce a correct output,
|
||||
// but triggers memory leaks when called repeateadly, as I cannot figure out how to
|
||||
// make the reference counting Python functions to work properly (Py_DECREF and friends)
|
||||
//
|
||||
PyObject* jsonToPythonObject(const Json::Value& val)
|
||||
{
|
||||
switch(val.type())
|
||||
{
|
||||
case Json::nullValue:
|
||||
{
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
case Json::intValue:
|
||||
{
|
||||
return PyLong_FromLong(val.asInt64());
|
||||
}
|
||||
|
||||
case Json::uintValue:
|
||||
{
|
||||
return PyLong_FromLong(val.asUInt64());
|
||||
}
|
||||
|
||||
case Json::realValue:
|
||||
{
|
||||
return PyFloat_FromDouble(val.asDouble());
|
||||
}
|
||||
|
||||
case Json::stringValue:
|
||||
{
|
||||
return PyUnicode_FromString(val.asCString());
|
||||
}
|
||||
|
||||
case Json::booleanValue:
|
||||
{
|
||||
return val.asBool() ? Py_True : Py_False;
|
||||
}
|
||||
|
||||
case Json::arrayValue:
|
||||
{
|
||||
PyObject* list = PyList_New(val.size());
|
||||
Py_ssize_t i = 0;
|
||||
for (auto&& it = val.begin(); it != val.end(); ++it)
|
||||
{
|
||||
PyList_SetItem(list, i++, jsonToPythonObject(*it));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
case Json::objectValue:
|
||||
{
|
||||
PyObject* dict = PyDict_New();
|
||||
for (auto&& it = val.begin(); it != val.end(); ++it)
|
||||
{
|
||||
PyObject* key = jsonToPythonObject(it.key());
|
||||
PyObject* value = jsonToPythonObject(*it);
|
||||
|
||||
PyDict_SetItem(dict, key, value);
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace ix
|
||||
{
|
||||
int64_t cobra_to_python_bot(const ix::CobraBotConfig& config,
|
||||
StatsdClient& statsdClient,
|
||||
const std::string& moduleName)
|
||||
{
|
||||
#ifndef IXBOTS_USE_PYTHON
|
||||
CoreLogger::error("Command is disabled. "
|
||||
"Needs to be configured with USE_PYTHON=1");
|
||||
return -1;
|
||||
#else
|
||||
CobraBot bot;
|
||||
Py_InitializeEx(0); // 0 arg so that we do not install signal handlers
|
||||
// which prevent us from using Ctrl-C
|
||||
|
||||
PyObject* pyModuleName = PyUnicode_DecodeFSDefault(moduleName.c_str());
|
||||
|
||||
if (pyModuleName == nullptr)
|
||||
{
|
||||
CoreLogger::error("Python error: Cannot decode file system path");
|
||||
PyErr_Print();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Import module
|
||||
PyObject* pyModule = PyImport_Import(pyModuleName);
|
||||
Py_DECREF(pyModuleName);
|
||||
if (pyModule == nullptr)
|
||||
{
|
||||
CoreLogger::error("Python error: Cannot import module.");
|
||||
CoreLogger::error("Module name cannot countain dash characters.");
|
||||
CoreLogger::error("Is PYTHONPATH set correctly ?");
|
||||
PyErr_Print();
|
||||
return false;
|
||||
}
|
||||
|
||||
// module main funtion name is named 'run'
|
||||
const std::string entryPoint("run");
|
||||
PyObject* pyFunc = PyObject_GetAttrString(pyModule, entryPoint.c_str());
|
||||
|
||||
if (!pyFunc)
|
||||
{
|
||||
CoreLogger::error("run symbol is missing from module.");
|
||||
PyErr_Print();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PyCallable_Check(pyFunc))
|
||||
{
|
||||
CoreLogger::error("run symbol is not a function.");
|
||||
PyErr_Print();
|
||||
return false;
|
||||
}
|
||||
|
||||
bot.setOnBotMessageCallback(
|
||||
[&statsdClient, pyFunc]
|
||||
(const Json::Value& msg,
|
||||
const std::string& /*position*/,
|
||||
std::atomic<bool>& /*throttled*/,
|
||||
std::atomic<bool>& fatalCobraError,
|
||||
std::atomic<uint64_t>& sentCount) -> void {
|
||||
//
|
||||
// Invoke python script here. First build function parameters, a tuple
|
||||
//
|
||||
const int kVersion = 1; // We can bump this and let the interface evolve
|
||||
|
||||
PyObject *pyArgs = PyTuple_New(2);
|
||||
PyTuple_SetItem(pyArgs, 0, PyLong_FromLong(kVersion)); // First argument
|
||||
|
||||
//
|
||||
// It would be better to create a Python object (a dictionary)
|
||||
// from the json msg, but it is simpler to serialize it to a string
|
||||
// and decode it on the Python side of the fence
|
||||
//
|
||||
PyObject* pySerializedJson = PyUnicode_FromString(msg.toStyledString().c_str());
|
||||
PyTuple_SetItem(pyArgs, 1, pySerializedJson); // Second argument
|
||||
|
||||
// Invoke the python routine
|
||||
PyObject* pyList = PyObject_CallObject(pyFunc, pyArgs);
|
||||
|
||||
// Error calling the function
|
||||
if (pyList == nullptr)
|
||||
{
|
||||
fatalCobraError = true;
|
||||
CoreLogger::error("run() function call failed. Input msg: ");
|
||||
auto serializedMsg = msg.toStyledString();
|
||||
CoreLogger::error(serializedMsg);
|
||||
PyErr_Print();
|
||||
CoreLogger::error("================");
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalid return type
|
||||
if (!PyList_Check(pyList))
|
||||
{
|
||||
fatalCobraError = true;
|
||||
CoreLogger::error("run() return type should be a list");
|
||||
return;
|
||||
}
|
||||
|
||||
// The result is a list of dict containing sufficient info
|
||||
// to send messages to statsd
|
||||
auto listSize = PyList_Size(pyList);
|
||||
|
||||
for (Py_ssize_t i = 0 ; i < listSize; ++i)
|
||||
{
|
||||
PyObject* dict = PyList_GetItem(pyList, i);
|
||||
|
||||
// Make sure this is a dict
|
||||
if (!PyDict_Check(dict))
|
||||
{
|
||||
fatalCobraError = true;
|
||||
CoreLogger::error("list element is not a dict");
|
||||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
// Retrieve object kind
|
||||
//
|
||||
PyObject* pyKind = PyDict_GetItemString(dict, "kind");
|
||||
if (!PyUnicode_Check(pyKind))
|
||||
{
|
||||
fatalCobraError = true;
|
||||
CoreLogger::error("kind entry is not a string");
|
||||
continue;
|
||||
}
|
||||
std::string kind(PyUnicode_AsUTF8(pyKind));
|
||||
|
||||
bool counter = false;
|
||||
bool gauge = false;
|
||||
bool timing = false;
|
||||
|
||||
if (kind == "counter")
|
||||
{
|
||||
counter = true;
|
||||
}
|
||||
else if (kind == "gauge")
|
||||
{
|
||||
gauge = true;
|
||||
}
|
||||
else if (kind == "timing")
|
||||
{
|
||||
timing = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
fatalCobraError = true;
|
||||
CoreLogger::error(std::string("invalid kind entry: ") + kind +
|
||||
". Supported ones are counter, gauge, timing");
|
||||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
// Retrieve object key
|
||||
//
|
||||
PyObject* pyKey = PyDict_GetItemString(dict, "key");
|
||||
if (!PyUnicode_Check(pyKey))
|
||||
{
|
||||
fatalCobraError = true;
|
||||
CoreLogger::error("key entry is not a string");
|
||||
continue;
|
||||
}
|
||||
std::string key(PyUnicode_AsUTF8(pyKey));
|
||||
|
||||
//
|
||||
// Retrieve object value and send data to statsd
|
||||
//
|
||||
PyObject* pyValue = PyDict_GetItemString(dict, "value");
|
||||
|
||||
// Send data to statsd
|
||||
if (PyFloat_Check(pyValue))
|
||||
{
|
||||
double value = PyFloat_AsDouble(pyValue);
|
||||
|
||||
if (counter)
|
||||
{
|
||||
statsdClient.count(key, value);
|
||||
}
|
||||
else if (gauge)
|
||||
{
|
||||
statsdClient.gauge(key, value);
|
||||
}
|
||||
else if (timing)
|
||||
{
|
||||
statsdClient.timing(key, value);
|
||||
}
|
||||
}
|
||||
else if (PyLong_Check(pyValue))
|
||||
{
|
||||
long value = PyLong_AsLong(pyValue);
|
||||
|
||||
if (counter)
|
||||
{
|
||||
statsdClient.count(key, value);
|
||||
}
|
||||
else if (gauge)
|
||||
{
|
||||
statsdClient.gauge(key, value);
|
||||
}
|
||||
else if (timing)
|
||||
{
|
||||
statsdClient.timing(key, value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fatalCobraError = true;
|
||||
CoreLogger::error("value entry is neither an int or a float");
|
||||
continue;
|
||||
}
|
||||
|
||||
sentCount++; // should we update this for each statsd object sent ?
|
||||
}
|
||||
|
||||
Py_DECREF(pyArgs);
|
||||
Py_DECREF(pyList);
|
||||
});
|
||||
|
||||
bool status = bot.run(config);
|
||||
|
||||
// Cleanup - we should do something similar in all exit case ...
|
||||
Py_DECREF(pyFunc);
|
||||
Py_DECREF(pyModule);
|
||||
Py_FinalizeEx();
|
||||
|
||||
return status;
|
||||
#endif
|
||||
}
|
||||
}
|
19
ixbots/ixbots/IXCobraToPythonBot.h
Normal file
19
ixbots/ixbots/IXCobraToPythonBot.h
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* IXCobraMetricsToStatsdBot.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_python_bot(const ix::CobraBotConfig& config,
|
||||
StatsdClient& statsdClient,
|
||||
const std::string& moduleName);
|
||||
} // namespace ix
|
@ -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)
|
||||
|
@ -70,11 +70,17 @@ namespace ix
|
||||
std::atomic<bool>& fatalCobraError,
|
||||
std::atomic<uint64_t>& sentCount) -> void {
|
||||
std::string id;
|
||||
size_t idx = 0;
|
||||
for (auto&& attr : tokens)
|
||||
{
|
||||
id += ".";
|
||||
auto val = extractAttr(attr, msg);
|
||||
id += val.asString();
|
||||
|
||||
// We add a dot separator unless we are processing the last token
|
||||
if (idx++ != tokens.size() - 1)
|
||||
{
|
||||
id += ".";
|
||||
}
|
||||
}
|
||||
|
||||
if (gauge.empty() && timer.empty())
|
||||
|
@ -39,21 +39,28 @@
|
||||
|
||||
#include "IXStatsdClient.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <ixwebsocket/IXNetSystem.h>
|
||||
#include <stdio.h>
|
||||
#include <ixwebsocket/IXSetThreadName.h>
|
||||
#include <ixcore/utils/IXCoreLogger.h>
|
||||
#include <sstream>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace ix
|
||||
{
|
||||
StatsdClient::StatsdClient(const std::string& host, int port, const std::string& prefix)
|
||||
StatsdClient::StatsdClient(const std::string& host,
|
||||
int port,
|
||||
const std::string& prefix,
|
||||
bool verbose)
|
||||
: _host(host)
|
||||
, _port(port)
|
||||
, _prefix(prefix)
|
||||
, _stop(false)
|
||||
, _verbose(verbose)
|
||||
{
|
||||
_thread = std::thread([this] {
|
||||
setThreadName("Statsd");
|
||||
|
||||
while (!_stop)
|
||||
{
|
||||
flushQueue();
|
||||
@ -115,11 +122,15 @@ namespace ix
|
||||
{
|
||||
cleanup(key);
|
||||
|
||||
char buf[256];
|
||||
snprintf(
|
||||
buf, sizeof(buf), "%s%s:%zd|%s\n", _prefix.c_str(), key.c_str(), value, type.c_str());
|
||||
std::stringstream ss;
|
||||
ss << _prefix << "." << key << ":" << value << "|" << type;
|
||||
|
||||
enqueue(buf);
|
||||
if (_verbose)
|
||||
{
|
||||
CoreLogger::info(ss.str());
|
||||
}
|
||||
|
||||
enqueue(ss.str() + "\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -137,10 +148,13 @@ namespace ix
|
||||
{
|
||||
auto message = _queue.front();
|
||||
auto ret = _socket.sendto(message);
|
||||
if (ret != 0)
|
||||
if (ret == -1)
|
||||
{
|
||||
std::cerr << "error: " << strerror(UdpSocket::getErrno()) << std::endl;
|
||||
CoreLogger::error(std::string("statsd error: ") + strerror(UdpSocket::getErrno()));
|
||||
}
|
||||
|
||||
// we always dequeue regardless of the ability to send the message
|
||||
// so that we keep our queue size under control
|
||||
_queue.pop_front();
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,8 @@ namespace ix
|
||||
public:
|
||||
StatsdClient(const std::string& host = "127.0.0.1",
|
||||
int port = 8125,
|
||||
const std::string& prefix = "");
|
||||
const std::string& prefix = "",
|
||||
bool verbose = false);
|
||||
~StatsdClient();
|
||||
|
||||
bool init(std::string& errMsg);
|
||||
@ -52,6 +53,7 @@ namespace ix
|
||||
std::mutex _mutex; // for the queue
|
||||
|
||||
std::deque<std::string> _queue;
|
||||
bool _verbose;
|
||||
};
|
||||
|
||||
} // end namespace ix
|
||||
|
@ -22,16 +22,11 @@ add_library(ixcobra STATIC
|
||||
${IXCOBRA_HEADERS}
|
||||
)
|
||||
|
||||
find_package(JsonCpp)
|
||||
if (NOT JSONCPP_FOUND)
|
||||
set(JSONCPP_INCLUDE_DIRS ../third_party/jsoncpp)
|
||||
endif()
|
||||
|
||||
set(IXCOBRA_INCLUDE_DIRS
|
||||
.
|
||||
..
|
||||
../ixcore
|
||||
../ixcrypto
|
||||
${JSONCPP_INCLUDE_DIRS})
|
||||
../ixcrypto)
|
||||
|
||||
target_include_directories( ixcobra PUBLIC ${IXCOBRA_INCLUDE_DIRS} )
|
||||
target_link_libraries( ixcobra jsoncpp_static )
|
||||
|
@ -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(),
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
@ -562,11 +584,13 @@ namespace ix
|
||||
void CobraConnection::subscribe(const std::string& channel,
|
||||
const std::string& filter,
|
||||
const std::string& position,
|
||||
int batchSize,
|
||||
SubscriptionCallback cb)
|
||||
{
|
||||
// Create and send a subscribe pdu
|
||||
Json::Value body;
|
||||
body["channel"] = channel;
|
||||
body["batch_size"] = batchSize;
|
||||
|
||||
if (!filter.empty())
|
||||
{
|
||||
@ -612,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
|
||||
//
|
||||
|
@ -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);
|
||||
|
||||
@ -88,6 +89,7 @@ namespace ix
|
||||
void subscribe(const std::string& channel,
|
||||
const std::string& filter = std::string(),
|
||||
const std::string& position = std::string(),
|
||||
int batchSize = 1,
|
||||
SubscriptionCallback cb = nullptr);
|
||||
|
||||
/// Unsubscribe from a channel
|
||||
@ -156,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
|
||||
///
|
||||
|
@ -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)
|
||||
{
|
||||
;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ namespace ix
|
||||
Pong = 7,
|
||||
HandshakeError = 8,
|
||||
AuthenticationError = 9,
|
||||
SubscriptionError = 10
|
||||
SubscriptionError = 10,
|
||||
Handshake = 11
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -37,9 +37,7 @@ if (USE_MBED_TLS)
|
||||
target_include_directories(ixcrypto PUBLIC ${MBEDTLS_INCLUDE_DIRS})
|
||||
target_link_libraries(ixcrypto ${MBEDTLS_LIBRARIES})
|
||||
target_compile_definitions(ixcrypto PUBLIC IXCRYPTO_USE_MBED_TLS)
|
||||
elseif (APPLE)
|
||||
elseif (WIN32)
|
||||
else()
|
||||
elseif (USE_OPEN_SSL)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
add_definitions(${OPENSSL_DEFINITIONS})
|
||||
message(STATUS "OpenSSL: " ${OPENSSL_VERSION})
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
27
ixredis/CMakeLists.txt
Normal file
27
ixredis/CMakeLists.txt
Normal file
@ -0,0 +1,27 @@
|
||||
#
|
||||
# Author: Benjamin Sergeant
|
||||
# Copyright (c) 2020 Machine Zone, Inc. All rights reserved.
|
||||
#
|
||||
|
||||
set (IXREDIS_SOURCES
|
||||
ixredis/IXRedisClient.cpp
|
||||
ixredis/IXRedisServer.cpp
|
||||
)
|
||||
|
||||
set (IXREDIS_HEADERS
|
||||
ixredis/IXRedisClient.h
|
||||
ixredis/IXRedisServer.h
|
||||
)
|
||||
|
||||
add_library(ixredis STATIC
|
||||
${IXREDIS_SOURCES}
|
||||
${IXREDIS_HEADERS}
|
||||
)
|
||||
|
||||
set(IXREDIS_INCLUDE_DIRS
|
||||
.
|
||||
..
|
||||
../ixcore
|
||||
../ixwebsocket)
|
||||
|
||||
target_include_directories( ixredis PUBLIC ${IXREDIS_INCLUDE_DIRS} )
|
@ -9,6 +9,7 @@
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <ixwebsocket/IXSocket.h>
|
||||
#include <ixwebsocket/IXSocketFactory.h>
|
||||
#include <ixwebsocket/IXSocketTLSOptions.h>
|
||||
#include <sstream>
|
||||
@ -250,12 +251,16 @@ namespace ix
|
||||
}
|
||||
|
||||
std::string RedisClient::prepareXaddCommand(const std::string& stream,
|
||||
const std::string& message)
|
||||
const std::string& message,
|
||||
int maxLen)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "*5\r\n";
|
||||
ss << "*8\r\n";
|
||||
ss << writeString("XADD");
|
||||
ss << writeString(stream);
|
||||
ss << writeString("MAXLEN");
|
||||
ss << writeString("~");
|
||||
ss << writeString(std::to_string(maxLen));
|
||||
ss << writeString("*");
|
||||
ss << writeString("field");
|
||||
ss << writeString(message);
|
||||
@ -265,6 +270,7 @@ namespace ix
|
||||
|
||||
std::string RedisClient::xadd(const std::string& stream,
|
||||
const std::string& message,
|
||||
int maxLen,
|
||||
std::string& errMsg)
|
||||
{
|
||||
errMsg.clear();
|
||||
@ -275,7 +281,7 @@ namespace ix
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::string command = prepareXaddCommand(stream, message);
|
||||
std::string command = prepareXaddCommand(stream, message, maxLen);
|
||||
|
||||
bool sent = _socket->writeBytes(command, nullptr);
|
||||
if (!sent)
|
||||
@ -348,4 +354,104 @@ namespace ix
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
std::pair<RespType, std::string> RedisClient::send(
|
||||
const std::vector<std::string>& args,
|
||||
std::string& errMsg)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "*";
|
||||
ss << std::to_string(args.size());
|
||||
ss << "\r\n";
|
||||
|
||||
for (auto&& arg : args)
|
||||
{
|
||||
ss << writeString(arg);
|
||||
}
|
||||
|
||||
bool sent = _socket->writeBytes(ss.str(), nullptr);
|
||||
if (!sent)
|
||||
{
|
||||
errMsg = "Cannot write bytes to socket";
|
||||
return std::make_pair(RespType::Error, "");
|
||||
}
|
||||
|
||||
return readResponse(errMsg);
|
||||
}
|
||||
|
||||
std::pair<RespType, std::string> RedisClient::readResponse(std::string& errMsg)
|
||||
{
|
||||
// Read result
|
||||
auto pollResult = _socket->isReadyToRead(-1);
|
||||
if (pollResult == PollResultType::Error)
|
||||
{
|
||||
errMsg = "Error while polling for result";
|
||||
return std::make_pair(RespType::Error, "");
|
||||
}
|
||||
|
||||
// First line is the string length
|
||||
auto lineResult = _socket->readLine(nullptr);
|
||||
auto lineValid = lineResult.first;
|
||||
auto line = lineResult.second;
|
||||
|
||||
if (!lineValid)
|
||||
{
|
||||
errMsg = "Error while polling for result";
|
||||
return std::make_pair(RespType::Error, "");
|
||||
}
|
||||
|
||||
std::string response;
|
||||
|
||||
if (line[0] == '+') // Simple string
|
||||
{
|
||||
std::stringstream ss;
|
||||
response = line.substr(1, line.size() - 3);
|
||||
return std::make_pair(RespType::String, response);
|
||||
}
|
||||
else if (line[0] == '-') // Errors
|
||||
{
|
||||
std::stringstream ss;
|
||||
response = line.substr(1, line.size() - 3);
|
||||
return std::make_pair(RespType::Error, response);
|
||||
}
|
||||
else if (line[0] == ':') // Integers
|
||||
{
|
||||
std::stringstream ss;
|
||||
response = line.substr(1, line.size() - 3);
|
||||
return std::make_pair(RespType::Integer, response);
|
||||
}
|
||||
else if (line[0] == '$') // Bulk strings
|
||||
{
|
||||
int stringSize;
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << line.substr(1, line.size() - 1);
|
||||
ss >> stringSize;
|
||||
}
|
||||
|
||||
// Read the result, which is the stream id computed by the redis server
|
||||
lineResult = _socket->readLine(nullptr);
|
||||
lineValid = lineResult.first;
|
||||
line = lineResult.second;
|
||||
|
||||
std::string str = line.substr(0, stringSize);
|
||||
return std::make_pair(RespType::String, str);
|
||||
}
|
||||
else
|
||||
{
|
||||
errMsg = "Unhandled response type";
|
||||
return std::make_pair(RespType::Unknown, std::string());
|
||||
}
|
||||
}
|
||||
|
||||
std::string RedisClient::getRespTypeDescription(RespType respType)
|
||||
{
|
||||
switch (respType)
|
||||
{
|
||||
case RespType::Integer: return "integer";
|
||||
case RespType::Error: return "error";
|
||||
case RespType::String: return "string";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
} // namespace ix
|
@ -8,12 +8,20 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <ixwebsocket/IXSocket.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <ixwebsocket/IXSocket.h>
|
||||
|
||||
namespace ix
|
||||
{
|
||||
enum class RespType : int
|
||||
{
|
||||
String = 0,
|
||||
Error = 1,
|
||||
Integer = 2,
|
||||
Unknown = 3
|
||||
};
|
||||
|
||||
class RedisClient
|
||||
{
|
||||
public:
|
||||
@ -40,13 +48,22 @@ namespace ix
|
||||
// XADD
|
||||
std::string xadd(const std::string& channel,
|
||||
const std::string& message,
|
||||
int maxLen,
|
||||
std::string& errMsg);
|
||||
|
||||
std::string prepareXaddCommand(const std::string& stream, const std::string& message);
|
||||
|
||||
std::string prepareXaddCommand(const std::string& stream,
|
||||
const std::string& message,
|
||||
int maxLen);
|
||||
std::string readXaddReply(std::string& errMsg);
|
||||
bool sendCommand(
|
||||
const std::string& commands, int commandsCount, std::string& errMsg);
|
||||
|
||||
bool sendCommand(const std::string& commands, int commandsCount, std::string& errMsg);
|
||||
// Arbitrary commands
|
||||
std::pair<RespType, std::string> send(
|
||||
const std::vector<std::string>& args,
|
||||
std::string& errMsg);
|
||||
std::pair<RespType, std::string> readResponse(std::string& errMsg);
|
||||
|
||||
std::string getRespTypeDescription(RespType respType);
|
||||
|
||||
void stop();
|
||||
|
@ -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)
|
||||
@ -112,7 +114,7 @@ namespace ix
|
||||
it.second.erase(socket.get());
|
||||
}
|
||||
|
||||
for (auto it : _subscribers)
|
||||
for (auto&& it : _subscribers)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Subscription id: " << it.first << " #subscribers: " << it.second.size();
|
@ -6,8 +6,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IXSocket.h"
|
||||
#include "IXSocketServer.h"
|
||||
#include <ixwebsocket/IXSocket.h>
|
||||
#include <ixwebsocket/IXSocketServer.h>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
@ -16,15 +16,10 @@ add_library(ixsentry STATIC
|
||||
${IXSENTRY_HEADERS}
|
||||
)
|
||||
|
||||
find_package(JsonCpp)
|
||||
if (NOT JSONCPP_FOUND)
|
||||
set(JSONCPP_INCLUDE_DIRS ../third_party/jsoncpp)
|
||||
endif()
|
||||
|
||||
set(IXSENTRY_INCLUDE_DIRS
|
||||
.
|
||||
..
|
||||
../ixcore
|
||||
${JSONCPP_INCLUDE_DIRS})
|
||||
../ixcore)
|
||||
|
||||
target_include_directories( ixsentry PUBLIC ${IXSENTRY_INCLUDE_DIRS} )
|
||||
target_link_libraries( ixsentry jsoncpp_static )
|
||||
|
@ -7,16 +7,14 @@ set (IXSNAKE_SOURCES
|
||||
ixsnake/IXSnakeServer.cpp
|
||||
ixsnake/IXSnakeProtocol.cpp
|
||||
ixsnake/IXAppConfig.cpp
|
||||
ixsnake/IXRedisClient.cpp
|
||||
ixsnake/IXRedisServer.cpp
|
||||
ixsnake/IXStreamSql.cpp
|
||||
)
|
||||
|
||||
set (IXSNAKE_HEADERS
|
||||
ixsnake/IXSnakeServer.h
|
||||
ixsnake/IXSnakeProtocol.h
|
||||
ixsnake/IXAppConfig.h
|
||||
ixsnake/IXRedisClient.h
|
||||
ixsnake/IXRedisServer.h
|
||||
ixsnake/IXStreamSql.h
|
||||
)
|
||||
|
||||
add_library(ixsnake STATIC
|
||||
@ -30,6 +28,7 @@ set(IXSNAKE_INCLUDE_DIRS
|
||||
../ixcore
|
||||
../ixcrypto
|
||||
../ixwebsocket
|
||||
../ixredis
|
||||
../third_party)
|
||||
|
||||
target_include_directories( ixsnake PUBLIC ${IXSNAKE_INCLUDE_DIRS} )
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -6,16 +6,22 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IXRedisClient.h"
|
||||
#include <future>
|
||||
#include <ixredis/IXRedisClient.h>
|
||||
#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;
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
63
ixsnake/ixsnake/IXStreamSql.cpp
Normal file
63
ixsnake/ixsnake/IXStreamSql.cpp
Normal 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
|
29
ixsnake/ixsnake/IXStreamSql.h
Normal file
29
ixsnake/ixsnake/IXStreamSql.h
Normal 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;
|
||||
};
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
170
ixwebsocket/IXGzipCodec.cpp
Normal file
170
ixwebsocket/IXGzipCodec.cpp
Normal file
@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
{
|
||||
Bench bench("creating 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);
|
||||
{
|
||||
Bench bench("alloc data");
|
||||
compressed_data = malloc(max_compressed_size);
|
||||
}
|
||||
|
||||
if (compressed_data == NULL)
|
||||
{
|
||||
return std::string();
|
||||
}
|
||||
|
||||
{
|
||||
Bench bench("compressing data");
|
||||
actual_compressed_size = libdeflate_gzip_compress(
|
||||
compressor,
|
||||
uncompressed_data,
|
||||
uncompressed_size,
|
||||
compressed_data,
|
||||
max_compressed_size);
|
||||
}
|
||||
|
||||
if (actual_compressed_size == 0)
|
||||
{
|
||||
free(compressed_data);
|
||||
return std::string();
|
||||
}
|
||||
|
||||
libdeflate_free_compressor(compressor);
|
||||
|
||||
std::string out;
|
||||
{
|
||||
Bench bench("append data");
|
||||
out.append(reinterpret_cast<char*>(compressed_data), actual_compressed_size);
|
||||
}
|
||||
|
||||
{
|
||||
Bench bench("free data");
|
||||
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
15
ixwebsocket/IXGzipCodec.h
Normal 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
|
@ -93,14 +93,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 +128,36 @@ 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;
|
||||
}
|
||||
|
||||
httpRequest = std::make_shared<HttpRequest>(uri, method, httpVersion, body, headers);
|
||||
return std::make_tuple(true, "", httpRequest);
|
||||
}
|
||||
|
||||
@ -151,7 +178,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 +190,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
|
||||
|
@ -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)
|
||||
@ -95,15 +95,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 +118,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);
|
||||
|
@ -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
|
||||
{
|
||||
@ -127,7 +127,7 @@ namespace ix
|
||||
{
|
||||
// We only have one socket connection, so we cannot
|
||||
// make multiple requests concurrently.
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||
|
||||
uint64_t uploadSize = 0;
|
||||
uint64_t downloadSize = 0;
|
||||
@ -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)
|
||||
@ -498,8 +500,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 +515,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,
|
||||
@ -672,51 +686,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)
|
||||
|
@ -90,8 +90,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,7 +101,9 @@ namespace ix
|
||||
std::thread _thread;
|
||||
|
||||
std::unique_ptr<Socket> _socket;
|
||||
std::mutex _mutex; // to protect accessing the _socket (only one socket per client)
|
||||
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;
|
||||
|
||||
|
@ -6,9 +6,11 @@
|
||||
|
||||
#include "IXHttpServer.h"
|
||||
|
||||
#include "IXGzipCodec.h"
|
||||
#include "IXNetSystem.h"
|
||||
#include "IXSocketConnect.h"
|
||||
#include "IXUserAgent.h"
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
@ -42,10 +44,17 @@ 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();
|
||||
}
|
||||
@ -74,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))
|
||||
@ -99,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 == "/")
|
||||
{
|
||||
@ -120,9 +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());
|
||||
|
||||
@ -148,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());
|
||||
|
||||
|
@ -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;
|
||||
|
||||
@ -42,6 +43,9 @@ namespace ix
|
||||
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;
|
||||
|
@ -8,6 +8,9 @@
|
||||
|
||||
namespace ix
|
||||
{
|
||||
const uint64_t SelectInterrupt::kSendRequest = 1;
|
||||
const uint64_t SelectInterrupt::kCloseRequest = 2;
|
||||
|
||||
SelectInterrupt::SelectInterrupt()
|
||||
{
|
||||
;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
81
ixwebsocket/IXSetThreadName.cpp
Normal file
81
ixwebsocket/IXSetThreadName.cpp
Normal 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
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -43,6 +43,55 @@ namespace ix
|
||||
mbedtls_pk_init(&_pkey);
|
||||
}
|
||||
|
||||
bool SocketMbedTLS::loadSystemCertificates(std::string& errorMsg)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
DWORD flags = CERT_STORE_READONLY_FLAG | CERT_STORE_OPEN_EXISTING_FLAG |
|
||||
CERT_SYSTEM_STORE_CURRENT_USER;
|
||||
HCERTSTORE systemStore = CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, flags, L"Root");
|
||||
|
||||
if (!systemStore)
|
||||
{
|
||||
errorMsg = "CertOpenStore failed with ";
|
||||
errorMsg += std::to_string(GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
PCCERT_CONTEXT certificateIterator = NULL;
|
||||
|
||||
int certificateCount = 0;
|
||||
while (certificateIterator = CertEnumCertificatesInStore(systemStore, certificateIterator))
|
||||
{
|
||||
if (certificateIterator->dwCertEncodingType & X509_ASN_ENCODING)
|
||||
{
|
||||
int ret = mbedtls_x509_crt_parse(&_cacert,
|
||||
certificateIterator->pbCertEncoded,
|
||||
certificateIterator->cbCertEncoded);
|
||||
if (ret == 0)
|
||||
{
|
||||
++certificateCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CertFreeCertificateContext(certificateIterator);
|
||||
CertCloseStore(systemStore, 0);
|
||||
|
||||
if (certificateCount == 0)
|
||||
{
|
||||
errorMsg = "No certificates found";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
#else
|
||||
// On macOS we can query the system cert location from the keychain
|
||||
// On Linux we could try to fetch some local files based on the distribution
|
||||
// On Android we could use JNI to get to the system certs
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SocketMbedTLS::init(const std::string& host, bool isClient, std::string& errMsg)
|
||||
{
|
||||
initMBedTLS();
|
||||
@ -96,13 +145,15 @@ namespace ix
|
||||
}
|
||||
else
|
||||
{
|
||||
mbedtls_ssl_conf_authmode(&_conf, MBEDTLS_SSL_VERIFY_REQUIRED);
|
||||
|
||||
// FIXME: should we call mbedtls_ssl_conf_verify ?
|
||||
mbedtls_ssl_conf_authmode(&_conf, MBEDTLS_SSL_VERIFY_REQUIRED);
|
||||
|
||||
if (_tlsOptions.isUsingSystemDefaults())
|
||||
{
|
||||
; // FIXME
|
||||
if (!loadSystemCertificates(errMsg))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -52,6 +52,7 @@ namespace ix
|
||||
|
||||
bool init(const std::string& host, bool isClient, std::string& errMsg);
|
||||
void initMBedTLS();
|
||||
bool loadSystemCertificates(std::string& errMsg);
|
||||
};
|
||||
|
||||
} // namespace ix
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -32,6 +32,7 @@
|
||||
#include "IXUrlParser.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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())
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
137
ixwebsocket/IXWebSocketProxyServer.cpp
Normal file
137
ixwebsocket/IXWebSocketProxyServer.cpp
Normal 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
|
24
ixwebsocket/IXWebSocketProxyServer.h
Normal file
24
ixwebsocket/IXWebSocketProxyServer.h
Normal 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
|
@ -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);
|
||||
|
@ -19,11 +19,14 @@
|
||||
|
||||
namespace ix
|
||||
{
|
||||
class WebSocketServer final : public SocketServer
|
||||
class WebSocketServer : public SocketServer
|
||||
{
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user