├── .gitignore ├── .gitmodules ├── .travis.yml ├── AdminVirtualMachine.hpp ├── CMakeLists.txt ├── CapnpMessageFrameBuilder.hpp ├── CaptchaVerifier.hpp ├── CaseInsensitiveUtils.hpp ├── CollabVmChannel.hpp ├── CollabVmChatRoom.hpp ├── CollabVmGuacamoleClient.hpp ├── CollabVmServer.hpp ├── Database ├── Database.cpp └── Database.h ├── FileUploadReader.hpp ├── GuacamoleClient.hpp ├── GuacamoleScreenshot.hpp ├── IPData.hpp ├── LICENSE ├── Main.cpp ├── README.md ├── RecordingController.hpp ├── ReusableSocketMessage.hpp ├── SharedStrandGuard.hpp ├── SocketMessage.hpp ├── StrandGuard.hpp ├── Totp.hpp ├── TurnController.hpp ├── UserChannel.hpp ├── Utils.hpp ├── VoteController.hpp ├── WebSocketServer.hpp ├── appveyor.yml ├── capnp-list.hpp ├── cmake ├── FindFilesystem.cmake ├── FindOpenSSL.cmake ├── FindPNG.cmake └── MSVCStaticToolchain.cmake ├── file_body.hpp └── tests ├── CMakeLists.txt ├── Captcha.cpp ├── Guacamole.cpp ├── Totp.cpp └── TurnTest.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/argon2"] 2 | path = submodules/argon2 3 | url = https://github.com/P-H-C/phc-winner-argon2.git 4 | branch = master 5 | [submodule "submodules/collab-vm-common"] 6 | path = submodules/collab-vm-common 7 | url = https://github.com/Cosmic-Sans/collab-vm-common.git 8 | branch = master 9 | [submodule "submodules/FreeRDP"] 10 | path = submodules/FreeRDP 11 | url = https://github.com/FreeRDP/FreeRDP.git 12 | branch = master 13 | [submodule "submodules/GSL"] 14 | path = submodules/GSL 15 | url = https://github.com/Microsoft/GSL.git 16 | branch = master 17 | [submodule "submodules/guacamole-server"] 18 | path = submodules/guacamole-server 19 | url = https://github.com/Cosmic-Sans/guacamole-server.git 20 | branch = master 21 | [submodule "submodules/libvncserver"] 22 | path = submodules/libvncserver 23 | url = https://github.com/LibVNC/libvncserver.git 24 | branch = master 25 | [submodule "submodules/sqlite_modern_cpp"] 26 | path = submodules/sqlite_modern_cpp 27 | url = https://github.com/SqliteModernCpp/sqlite_modern_cpp.git 28 | [submodule "submodules/clipp"] 29 | path = submodules/clipp 30 | url = https://github.com/muellan/clipp.git 31 | branch = master 32 | [submodule "submodules/boost-algorithm"] 33 | path = submodules/boost-algorithm 34 | url = https://github.com/boostorg/algorithm.git 35 | branch = master 36 | [submodule "submodules/boost-align"] 37 | path = submodules/boost-align 38 | url = https://github.com/boostorg/align.git 39 | branch = master 40 | [submodule "submodules/boost-any"] 41 | path = submodules/boost-any 42 | url = https://github.com/boostorg/any.git 43 | branch = master 44 | [submodule "submodules/boost-array"] 45 | path = submodules/boost-array 46 | url = https://github.com/boostorg/array.git 47 | branch = master 48 | [submodule "submodules/boost-asio"] 49 | path = submodules/boost-asio 50 | url = https://github.com/boostorg/asio.git 51 | branch = master 52 | [submodule "submodules/boost-assert"] 53 | path = submodules/boost-assert 54 | url = https://github.com/boostorg/assert.git 55 | branch = master 56 | [submodule "submodules/boost-beast"] 57 | path = submodules/boost-beast 58 | url = https://github.com/boostorg/beast.git 59 | branch = master 60 | [submodule "submodules/boost-bind"] 61 | path = submodules/boost-bind 62 | url = https://github.com/boostorg/bind.git 63 | branch = master 64 | [submodule "submodules/boost-concept_check"] 65 | path = submodules/boost-concept_check 66 | url = https://github.com/boostorg/concept_check.git 67 | branch = master 68 | [submodule "submodules/boost-config"] 69 | path = submodules/boost-config 70 | url = https://github.com/boostorg/config.git 71 | branch = master 72 | [submodule "submodules/boost-container"] 73 | path = submodules/boost-container 74 | url = https://github.com/boostorg/container.git 75 | branch = master 76 | [submodule "submodules/boost-core"] 77 | path = submodules/boost-core 78 | url = https://github.com/boostorg/core.git 79 | branch = master 80 | [submodule "submodules/boost-detail"] 81 | path = submodules/boost-detail 82 | url = https://github.com/boostorg/detail.git 83 | branch = master 84 | [submodule "submodules/boost-endian"] 85 | path = submodules/boost-endian 86 | url = https://github.com/boostorg/endian.git 87 | branch = master 88 | [submodule "submodules/boost-foreach"] 89 | path = submodules/boost-foreach 90 | url = https://github.com/boostorg/foreach.git 91 | branch = master 92 | [submodule "submodules/boost-format"] 93 | path = submodules/boost-format 94 | url = https://github.com/boostorg/format.git 95 | branch = master 96 | [submodule "submodules/boost-function"] 97 | path = submodules/boost-function 98 | url = https://github.com/boostorg/function.git 99 | branch = master 100 | [submodule "submodules/boost-functional"] 101 | path = submodules/boost-functional 102 | url = https://github.com/boostorg/functional.git 103 | branch = master 104 | [submodule "submodules/boost-integer"] 105 | path = submodules/boost-integer 106 | url = https://github.com/boostorg/integer.git 107 | branch = master 108 | [submodule "submodules/boost-intrusive"] 109 | path = submodules/boost-intrusive 110 | url = https://github.com/boostorg/intrusive.git 111 | branch = master 112 | [submodule "submodules/boost-io"] 113 | path = submodules/boost-io 114 | url = https://github.com/boostorg/io.git 115 | branch = master 116 | [submodule "submodules/boost-iterator"] 117 | path = submodules/boost-iterator 118 | url = https://github.com/boostorg/iterator.git 119 | branch = master 120 | [submodule "submodules/boost-logic"] 121 | path = submodules/boost-logic 122 | url = https://github.com/boostorg/logic.git 123 | branch = master 124 | [submodule "submodules/boost-move"] 125 | path = submodules/boost-move 126 | url = https://github.com/boostorg/move.git 127 | branch = master 128 | [submodule "submodules/boost-mp11"] 129 | path = submodules/boost-mp11 130 | url = https://github.com/boostorg/mp11.git 131 | branch = master 132 | [submodule "submodules/boost-mpl"] 133 | path = submodules/boost-mpl 134 | url = https://github.com/boostorg/mpl.git 135 | branch = master 136 | [submodule "submodules/boost-system"] 137 | path = submodules/boost-system 138 | url = https://github.com/boostorg/system.git 139 | branch = master 140 | [submodule "submodules/boost-tuple"] 141 | path = submodules/boost-tuple 142 | url = https://github.com/boostorg/tuple.git 143 | branch = master 144 | [submodule "submodules/boost-utility"] 145 | path = submodules/boost-utility 146 | url = https://github.com/boostorg/utility.git 147 | branch = master 148 | [submodule "submodules/boost-winapi"] 149 | path = submodules/boost-winapi 150 | url = https://github.com/boostorg/winapi.git 151 | branch = master 152 | [submodule "submodules/boost-optional"] 153 | path = submodules/boost-optional 154 | url = https://github.com/boostorg/optional.git 155 | branch = master 156 | [submodule "submodules/boost-predef"] 157 | path = submodules/boost-predef 158 | url = https://github.com/boostorg/predef.git 159 | branch = master 160 | [submodule "submodules/boost-preprocessor"] 161 | path = submodules/boost-preprocessor 162 | url = https://github.com/boostorg/preprocessor.git 163 | branch = master 164 | [submodule "submodules/boost-range"] 165 | path = submodules/boost-range 166 | url = https://github.com/boostorg/range.git 167 | branch = master 168 | [submodule "submodules/boost-serialization"] 169 | path = submodules/boost-serialization 170 | url = https://github.com/boostorg/serialization.git 171 | branch = master 172 | [submodule "submodules/boost-test"] 173 | path = submodules/boost-test 174 | url = https://github.com/boostorg/test.git 175 | branch = master 176 | [submodule "submodules/boost-smart_ptr"] 177 | path = submodules/boost-smart_ptr 178 | url = https://github.com/boostorg/smart_ptr.git 179 | branch = master 180 | [submodule "submodules/boost-static_assert"] 181 | path = submodules/boost-static_assert 182 | url = https://github.com/boostorg/static_assert.git 183 | branch = master 184 | [submodule "submodules/boost-throw_exception"] 185 | path = submodules/boost-throw_exception 186 | url = https://github.com/boostorg/throw_exception.git 187 | branch = master 188 | [submodule "submodules/boost-numeric_conversion"] 189 | path = submodules/boost-numeric_conversion 190 | url = https://github.com/boostorg/numeric_conversion.git 191 | branch = master 192 | [submodule "submodules/boost-multi_index"] 193 | path = submodules/boost-multi_index 194 | url = https://github.com/boostorg/multi_index.git 195 | branch = master 196 | [submodule "submodules/boost-container_hash"] 197 | path = submodules/boost-container_hash 198 | url = https://github.com/boostorg/container_hash.git 199 | branch = master 200 | [submodule "submodules/boost-property_tree"] 201 | path = submodules/boost-property_tree 202 | url = https://github.com/boostorg/property_tree.git 203 | branch = master 204 | [submodule "submodules/boost-type_traits"] 205 | path = submodules/boost-type_traits 206 | url = https://github.com/boostorg/type_traits.git 207 | branch = master 208 | [submodule "submodules/boost-type_index"] 209 | path = submodules/boost-type_index 210 | url = https://github.com/boostorg/type_index.git 211 | branch = master 212 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | email: false 3 | branches: 4 | except: 5 | - prerelease 6 | language: minimal 7 | sudo: required 8 | services: 9 | - docker 10 | before_install: 11 | - | 12 | mkdir ../vcpkg/ 13 | pushd ../vcpkg/ 14 | git init 15 | git remote add origin https://github.com/Microsoft/vcpkg.git 16 | git fetch origin master 17 | git checkout -b master origin/master 18 | cp triplets/x64-linux.cmake triplets/x64-linux-musl.cmake 19 | echo 'set(CMAKE_SYSROOT /x86_64-linux-musl/)' >> scripts/toolchains/linux.cmake 20 | popd 21 | # Prevent FreeRDP from changing RPATH 22 | - sed -i -e '/RPATH/d' submodules/FreeRDP/CMakeLists.txt 23 | - docker run -dit --name musl -v $(pwd)/../vcpkg:/src/vcpkg/ -v $(pwd):/src/collab-vm-server/ muslcc/x86_64:x86_64-linux-musl sh 24 | cache: 25 | directories: 26 | - ../vcpkg/ 27 | script: 28 | # - Update Alpine Linux package repository to edge 29 | # - Install Alpine Linux packages 30 | # - Create libintl.h to workaround a bug with the gettext vcpkg package 31 | # - Install vcpkg packages 32 | # - Build collab-vm-server 33 | - docker exec -it musl sh -c "sed -i -e 's/v[[:digit:]]\..*\//edge\//g' /etc/apk/repositories && apk update && apk add --no-cache curl perl unzip tar make cmake ninja git && mkdir -p /usr/include/ && touch /usr/include/libintl.h && /src/vcpkg/bootstrap-vcpkg.sh -useSystemBinaries && /src/vcpkg/vcpkg install cairo libjpeg-turbo sqlite3 libpng openssl && mkdir /src/collab-vm-server/build/ && cd /src/collab-vm-server/build/ && cmake -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_FLAGS='-static -static-libgcc -static-libstdc++' -DCMAKE_CXX_FLAGS='-static -static-libgcc -static-libstdc++' -DCMAKE_TOOLCHAIN_FILE=/src/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-linux-musl .. && cmake --build . --target collab-vm-server && ls -l -a" 34 | before_deploy: 35 | - mkdir $OUTPUT 36 | # Download and extract web-app 37 | - wget https://github.com/Cosmic-Sans/collab-vm-web-app/releases/download/prerelease/web-app.tar.gz 38 | - tar xfz web-app.tar.gz --directory=$OUTPUT/ 39 | # Find libvncserver example and copy it into the output directory 40 | - cp $(find build/ -name example -type f -print -quit) $OUTPUT/vnc-demo 41 | - cp build/collab-vm-server $OUTPUT/ 42 | - tar -zcvf $OUTPUT.tar.gz $OUTPUT/ 43 | - git tag --force prerelease HEAD 44 | - export TRAVIS_TAG=prerelease 45 | # Move the tag to the most recent commit, but delete it first 46 | # so GitHub updates the timestamp of the release 47 | - git remote -vv 48 | - git branch -vv 49 | - git checkout "$TRAVIS_BRANCH" 50 | - git config credential.helper "store --file=.git/credentials" 51 | - echo "https://${GH_TOKEN}:@github.com" > .git/credentials 52 | - git push --delete origin prerelease || true 53 | - git tag --force prerelease HEAD 54 | - git push --force --tags origin prerelease 55 | deploy: 56 | provider: releases 57 | tag_name: prerelease 58 | file: $OUTPUT.tar.gz 59 | prerelease: true 60 | draft: false 61 | overwrite: true 62 | skip_cleanup: true 63 | on: 64 | branch: master 65 | tags: false 66 | api_key: 67 | secure: Oen8hhOac4Un0ntolrtZEk41AVFl46hrV4hFOSiRifrJfbCMIkJHWaSM4urZBC1+9+PTH0gwDMHNG0EMo/gkHo7S0ov/C0V/tBC0XgVFjWSVaFkfni4cbYrIYD78r6Ci9c5zg7vP2+6tl0JcVcmRqo6rSEKa7Qc3wO5pnBP14LTiAA/ATP8IYdPJIKfnG4xR44ZXFV0Aetu/c97elYfWy+nOUDN1V81Tf3zjyToj6wqNncq20EmH47+JkP9lxjBvnGlx+UvQqKitvCqnAFDHdGAO8lPEYAuYGUdV7FpX4g3D5bx9D1/vSokP7Phit6lkCmTU/y4sxvm8T1i8EjpyNeevILhuhJEMDFFnghCRzVwBdxt4OV/KuafYQhBrtcPNi3ew0Osq0XBcw00qLaftXbp2sBW44GCHXTxnGhodQsEsYiBEc16inOtY4gxAL1dqdBRccE3RdRI4X96yf+eGCaaEyRp7YUwZUI2XTJop58Dzb0SMvzqYF6foDTunmrhRsdDrFEtgDmqHGaVyjKDmCr5Y7S//zNRBP9Fm7m2/AHa8YJ38lqhXdEyNm73+qOBkcOH7kTttMumCJxGg6P2c8ykmwo0jKkkUjLfcYMz7KLBYg4EYtNy8S9l3P9LVjILvcS8oJol3WnPk0M80lJj+lhcNXiBlf0+ItDJCOS3fxmE= 68 | env: 69 | matrix: 70 | - OUTPUT=collab-vm-linux64 71 | global: 72 | secure: cThjFkP6UjMk6O2qHmox7UlqOkpCwU+nm9aSXdOsB8KjNX8y2e90DqzEqLcWutiLinX3MfTdUDyYkESqarkXSeTlCKudbkQ+dWkenG8r0mSdzz+WvY+zPq+uEX+BBcK9OPEWdZADQOoByOE6gV2NeZMw+HM3EEo2aTziAwAeV7xFuXToMWArZNdbU74l0drRabh7nSP+63WegtzS+dUOhLKyFa8fRvKvDD57Ylr3lX4FzHrEsVHcj4uAcB+xPyZ4ACxTVFdTiQwlQ2SLYdN8xrYkCpBH9BvYK4EwUchIKvS4LD6FdqcnRmO/yfGOxw4X+8vrCVQYaaungEkt1//5PAeSpTUhjW1lXOJ+3biNLRA52U5Q7gT9qTVA+Yka4d/aUzoMyhJWjGv3ZYS3WWGY/EcLFbBFtPPaNyHxLSp7D4gWB5xWmhIG4plmUxEgGifaeVN1GhCz9F9L2lmn7YCqW7LrGslsaz6TT0dcXyhl+H7tTvaXYiu/HuLTGmmK9m18dwnWVc1/KNs4mRSV9fxeufiT0YaxyQ85XdH1NWxTMW/CNeUz/1qEAj0k67LsT50o8s0Vz6JXfrD9McusBCmYtnAiu/Khz7eh6fnoXHz4AkYQWw1DiaoTk3Wvq99tTTz6IZpzAO9NVeoLPK6K6CqRRiPKKhFNEu9Bxo/STDvQrjs= 73 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.7) 2 | 3 | if(POLICY CMP0091) 4 | # Select MSVC runtime based on CMAKE_MSVC_RUNTIME_LIBRARY 5 | cmake_policy(SET CMP0091 NEW) 6 | endif() 7 | 8 | enable_language(C CXX) 9 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake/") 10 | 11 | if(NOT MSVC) 12 | find_package(Filesystem REQUIRED) 13 | set(FILESYSTEM_LIBRARY std::filesystem) 14 | else() 15 | # MSVC doesn't require linking to a filesystem library 16 | set(FILESYSTEM_LIBRARY "") 17 | endif() 18 | 19 | set(Boost_INCLUDE_DIRS "") 20 | foreach(boost_library IN ITEMS 21 | algorithm 22 | align 23 | any 24 | array 25 | asio 26 | assert 27 | beast 28 | bind 29 | concept_check 30 | config 31 | container 32 | container_hash 33 | core 34 | detail 35 | endian 36 | foreach 37 | format 38 | function 39 | functional 40 | integer 41 | intrusive 42 | io 43 | iterator 44 | logic 45 | move 46 | mp11 47 | mpl 48 | multi_index 49 | numeric_conversion 50 | optional 51 | predef 52 | preprocessor 53 | property_tree 54 | range 55 | serialization 56 | smart_ptr 57 | static_assert 58 | system 59 | test 60 | throw_exception 61 | tuple 62 | type_index 63 | type_traits 64 | utility 65 | winapi 66 | ) 67 | list(APPEND Boost_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/submodules/boost-${boost_library}/include") 68 | endforeach() 69 | 70 | find_path(sqlite-modern-cpp_INCLUDE_DIR 71 | sqlite_modern_cpp.h 72 | PATHS "${CMAKE_SOURCE_DIR}/submodules/sqlite_modern_cpp/hdr/") 73 | 74 | find_path(clipp_INCLUDE_DIR 75 | clipp.h 76 | PATHS "${CMAKE_SOURCE_DIR}/submodules/clipp/include/") 77 | 78 | if (BUILD_SHARED_LIBS) 79 | message(FATAL_ERROR "Building shared versions" 80 | " of FreeRDP, libvncserver, or Cap'N Proto is not supported") 81 | endif() 82 | # Create a list containing all variables passed as command line arguments 83 | # to this script so they can be forwarded to ExternalProject_Add() 84 | # This must be at the beginning of this script. 85 | # Source: https://cmake.org/pipermail/cmake/2018-January/067002.html 86 | get_cmake_property(CACHE_VARS CACHE_VARIABLES) 87 | foreach(CACHE_VAR ${CACHE_VARS}) 88 | if("${${CACHE_VAR}}" STREQUAL "" 89 | OR CACHE_VAR STREQUAL "CMAKE_INSTALL_PREFIX") 90 | continue() 91 | endif() 92 | get_property(CACHE_VAR_TYPE CACHE ${CACHE_VAR} PROPERTY TYPE) 93 | if(CACHE_VAR_TYPE STREQUAL "INTERNAL") 94 | continue() 95 | endif() 96 | if(CACHE_VAR_TYPE STREQUAL "UNINITIALIZED") 97 | set(CACHE_VAR_TYPE ":STRING") 98 | else() 99 | set(CACHE_VAR_TYPE ":${CACHE_VAR_TYPE}") 100 | endif() 101 | string(REPLACE ";" "\;" CL_ARG "${${CACHE_VAR}}") 102 | list(APPEND CL_ARGS "-D${CACHE_VAR}${CACHE_VAR_TYPE}=${CL_ARG}") 103 | endforeach() 104 | 105 | if (NOT DEFINED DEPENDENCIES_BINARY_DIR) 106 | set(DEPENDENCIES_BINARY_DIR ${CMAKE_BINARY_DIR}/dependencies) 107 | endif() 108 | 109 | set(FREERDP_BINARY_DIR ${DEPENDENCIES_BINARY_DIR}/FreeRDP) 110 | set(FREERDP_INSTALL_DIR ${DEPENDENCIES_BINARY_DIR}/FreeRDP-install) 111 | file(MAKE_DIRECTORY ${FREERDP_BINARY_DIR} ${FREERDP_INSTALL_DIR}) 112 | if (NOT WIN32) 113 | # Override FreeRDP's FindOpenSSL.cmake to use vcpkg's port 114 | execute_process( 115 | COMMAND ${CMAKE_COMMAND} -E copy 116 | ${CMAKE_SOURCE_DIR}/cmake/FindOpenSSL.cmake 117 | "${CMAKE_SOURCE_DIR}/submodules/FreeRDP/cmake/FindOpenSSL.cmake" 118 | WORKING_DIRECTORY ${FREERDP_BINARY_DIR}) 119 | endif() 120 | # Workaround FreeRDP overriding CMAKE_MSVC_RUNTIME_LIBRARY 121 | set(FREERDP_MSVC_RUNTIME "") 122 | if(CMAKE_MSVC_RUNTIME_LIBRARY STREQUAL "MultiThreaded" 123 | OR CMAKE_MSVC_RUNTIME_LIBRARY STREQUAL "MultiThreadedDebug") 124 | set(FREERDP_MSVC_RUNTIME "-DMSVC_RUNTIME=static") 125 | endif() 126 | # Configure FreeRDP 127 | execute_process( 128 | COMMAND ${CMAKE_COMMAND} 129 | -G ${CMAKE_GENERATOR} ${CL_ARGS} 130 | -DCMAKE_INSTALL_PREFIX:PATH=${FREERDP_INSTALL_DIR} 131 | -DCMAKE_INSTALL_LIBDIR:STRING=lib 132 | -DBUILD_SHARED_LIBS:BOOL=OFF 133 | # TODO: Don't hardcore this 134 | -DCMAKE_WINDOWS_VERSION=WIN7 135 | ${FREERDP_MSVC_RUNTIME} 136 | ${CMAKE_SOURCE_DIR}/submodules/FreeRDP 137 | WORKING_DIRECTORY ${FREERDP_BINARY_DIR}) 138 | # Build FreeRDP 139 | execute_process( 140 | COMMAND ${CMAKE_COMMAND} 141 | --build ${FREERDP_BINARY_DIR} 142 | --config ${CMAKE_BUILD_TYPE} 143 | --target install 144 | WORKING_DIRECTORY ${FREERDP_BINARY_DIR}) 145 | # Patch header for static linking 146 | file(READ "${FREERDP_INSTALL_DIR}/include/freerdp2/freerdp/api.h" FREERDP_API_HEADER) 147 | string(REPLACE 148 | "#define FREERDP_API __declspec(dllimport)" 149 | "#define FREERDP_API" 150 | FREERDP_API_HEADER "${FREERDP_API_HEADER}") 151 | file(WRITE "${FREERDP_INSTALL_DIR}/include/freerdp2/freerdp/api.h" "${FREERDP_API_HEADER}") 152 | 153 | set(WinPR_DIR "${FREERDP_INSTALL_DIR}/lib/cmake/WinPR2/") 154 | find_package(WinPR REQUIRED CONFIG) 155 | set(FreeRDP_DIR "${FREERDP_INSTALL_DIR}/lib/cmake/FreeRDP2/") 156 | find_package(FreeRDP REQUIRED CONFIG) 157 | set(FreeRDP-Client_DIR "${FREERDP_INSTALL_DIR}/lib/cmake/FreeRDP-Client2/") 158 | find_package(FreeRDP-Client REQUIRED CONFIG) 159 | 160 | set(COLLAB_VM_COMMON "submodules/collab-vm-common" CACHE PATH "Path to collab-vm-common directory") 161 | set(COLLAB_VM_COMMON_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/submodules/collab-vm-common" CACHE PATH "Path to output collab-vm-common binaries") 162 | 163 | project(collab-vm-server VERSION 2.0.0) 164 | 165 | set(ARGON2_ROOT "${CMAKE_SOURCE_DIR}/submodules/argon2") 166 | set(ARGON2_INCLUDE_DIR "${ARGON2_ROOT}/include") 167 | set(ARGON2_SOURCES src/argon2.c src/core.c src/blake2/blake2b.c src/thread.c src/encoding.c src/opt.c) 168 | string(REPLACE ";" ";${ARGON2_ROOT}/" ARGON2_SOURCES ";${ARGON2_SOURCES}") 169 | if(NOT EXISTS "${ARGON2_INCLUDE_DIR}/argon2.h") 170 | message(SEND_ERROR "Can't find argon2.h in ${ARGON2_INCLUDE_DIR}") 171 | endif() 172 | foreach(ARGON2_FILE ${ARGON2_SOURCES}) 173 | if(NOT EXISTS "${ARGON2_FILE}") 174 | message(SEND_ERROR "Can't find ${ARGON2_FILE}") 175 | endif() 176 | endforeach() 177 | 178 | add_library(argon2 ${ARGON2_SOURCES}) 179 | if(MSVC) 180 | # TODO: Find a better way to prevent the argon2 functions from being exported with MVSC 181 | #set_target_properties(argon2 PROPERTIES COMPILE_FLAGS -Ddllexport) 182 | # The MSVC equivalient of -isystem is /external:I 183 | set(CMAKE_INCLUDE_SYSTEM_FLAG_CXX "/external:I ") 184 | # Suppress all warnings for external headers 185 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /experimental:external /external:W0") 186 | endif() 187 | target_include_directories(argon2 PUBLIC ${ARGON2_INCLUDE_DIR}) 188 | 189 | find_package(OpenSSL REQUIRED) 190 | 191 | find_package(SQLite3 CONFIG REQUIRED) 192 | 193 | #set(BUILD_TESTING OFF) 194 | #set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MDd") 195 | #set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") 196 | #set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /MTd") 197 | #add_subdirectory(submodules/capnproto/c++) 198 | 199 | # NOTE: The Cap'n Proto compiler doesn't generate files to the correct directory 200 | #set(CAPNPC_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}) 201 | #capnp_generate_cpp(CAPNP_SRCS CAPNP_HDRS submodules/collab-vm-common/CollabVm.capnp) 202 | add_subdirectory(${COLLAB_VM_COMMON} ${COLLAB_VM_COMMON_BINARY_DIR}) 203 | add_definitions("-DCAPNP_VERSION_STR=${CapnProto_VERSION}") 204 | 205 | add_definitions("-DPROJECT_VERSION=${PROJECT_VERSION}") 206 | add_definitions( 207 | # Prevent Boost from trying to link 208 | -DBOOST_ALL_NO_LIB 209 | # Boost Asio options 210 | -DBOOST_ASIO_HAS_MOVE 211 | -DBOOST_ASIO_HAS_STD_ATOMIC 212 | -DBOOST_ASIO_HAS_VARIADIC_TEMPLATES 213 | -DBOOST_ASIO_HAS_STD_CHRONO 214 | -DBOOST_ASIO_DISABLE_BOOST_ARRAY 215 | -DBOOST_ASIO_DISABLE_BOOST_ASSERT 216 | -DBOOST_ASIO_DISABLE_BOOST_BIND 217 | -DBOOST_ASIO_DISABLE_BOOST_CHRONO 218 | -DBOOST_ASIO_DISABLE_BOOST_DATE_TIME 219 | -DBOOST_ASIO_DISABLE_BOOST_LIMITS 220 | -DBOOST_ASIO_DISABLE_BOOST_REGEX 221 | -DBOOST_ASIO_DISABLE_BOOST_STATIC_CONSTANT 222 | -DBOOST_ASIO_DISABLE_BOOST_THROW_EXCEPTION 223 | -DBOOST_ASIO_DISABLE_BOOST_WORKAROUND 224 | -DBOOST_ASIO_NO_DEPRECATED 225 | -DBOOST_BEAST_USE_STD_STRING_VIEW) 226 | 227 | # Why is this necessary? 228 | if(MSVC) 229 | add_definitions(-D_WIN32_WINNT=0x0501) 230 | endif() 231 | 232 | set(CMAKE_CXX_STANDARD 17) 233 | add_executable(${PROJECT_NAME} Main.cpp Database/Database.cpp ${CAPNP_SRCS} ${CAPNP_HDRS}) 234 | 235 | if(MSVC) 236 | target_compile_options(${PROJECT_NAME} PRIVATE /permissive- /bigobj) 237 | set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "/MANIFEST:NO") 238 | endif() 239 | 240 | find_package(PNG REQUIRED) 241 | 242 | find_package(unofficial-cairo CONFIG) 243 | set(Cairo_LIBRARY unofficial::cairo::cairo) 244 | 245 | include(ExternalProject) 246 | set(LIBVNCSERVER_CMAKE_CACHE_ARGS ${CL_ARGS}) 247 | list(FILTER LIBVNCSERVER_CMAKE_CACHE_ARGS EXCLUDE REGEX "CMAKE_INSTALL_PREFIX") 248 | set(LIBVNCSERVER_INSTALL_DIR ${CMAKE_BINARY_DIR}/libvncserver) 249 | set(LIBVNCSERVER_LIBRARIES ${LIBVNCSERVER_INSTALL_DIR}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}vncclient${CMAKE_STATIC_LIBRARY_SUFFIX}) 250 | set(LIBVNCSERVER_INCLUDE_DIRS ${LIBVNCSERVER_INSTALL_DIR}/include) 251 | if (WIN32) 252 | set(LIBVNCSERVER_CMAKE_EXE_LINKER_FLAGS -DCMAKE_EXE_LINKER_FLAGS:STRING=crypt32.lib) 253 | else() 254 | # Workaround for vcpkg's libpng port 255 | set(LIBVNCSERVER_CMAKE_MODULE_PATH 256 | "-DCMAKE_MODULE_PATH:PATH=${CMAKE_SOURCE_DIR}/cmake/") 257 | endif() 258 | # Replace backslashes with forward slashes to avoid errors 259 | # when using ExternalProject_Add(CMAKE_CACHE_ARGS ...) 260 | string(REPLACE "\\" "\\\\" LIBVNCSERVER_CMAKE_CACHE_ARGS "${LIBVNCSERVER_CMAKE_CACHE_ARGS}") 261 | ExternalProject_Add(libvncserver SOURCE_DIR "${CMAKE_SOURCE_DIR}/submodules/libvncserver" 262 | DEPENDS OpenSSL::SSL OpenSSL::Crypto PNG::PNG 263 | CMAKE_CACHE_ARGS ${LIBVNCSERVER_CMAKE_CACHE_ARGS} 264 | -DCMAKE_INSTALL_PREFIX:PATH=${LIBVNCSERVER_INSTALL_DIR} 265 | -DCMAKE_INSTALL_LIBDIR:STRING=lib 266 | "${LIBVNCSERVER_CMAKE_MODULE_PATH}" 267 | -DWITH_LZO:BOOL=OFF 268 | -DWITH_SASL:BOOL=OFF 269 | -DWITH_SDL:BOOL=OFF 270 | -DWITH_GNUTLS:BOOL=OFF 271 | -DWITH_GCRYPT:BOOL=OFF 272 | -DWITH_FFMPEG:BOOL=OFF 273 | -DWITH_WEBSOCKETS:BOOL=OFF 274 | # libnvcserver doesn't support shared libraries on Windows 275 | -DBUILD_SHARED_LIBS:BOOL=OFF 276 | ${LIBVNCSERVER_CMAKE_EXE_LINKER_FLAGS} 277 | BUILD_BYPRODUCTS ${LIBVNCSERVER_LIBRARIES} 278 | ) 279 | add_dependencies(${PROJECT_NAME} libvncserver) 280 | 281 | add_library(libvncclient STATIC IMPORTED) 282 | set_target_properties(libvncclient PROPERTIES 283 | IMPORTED_LOCATION "${LIBVNCSERVER_LIBRARIES}" 284 | INTERFACE_LINK_LIBRARIES "OpenSSL::SSL;OpenSSL::Crypto") 285 | 286 | find_package(JPEG REQUIRED) 287 | 288 | set(GUACAMOLE_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/submodules/guacamole-server) 289 | file(GLOB GUACAMOLE_SOURCES 290 | ${GUACAMOLE_ROOT_DIR}/src/common/*.c 291 | ${GUACAMOLE_ROOT_DIR}/src/guacenc/instruction*.cpp 292 | ${GUACAMOLE_ROOT_DIR}/src/guacenc/log.c 293 | ${GUACAMOLE_ROOT_DIR}/src/guacenc/image-stream.c 294 | ${GUACAMOLE_ROOT_DIR}/src/guacenc/jpeg.c 295 | ${GUACAMOLE_ROOT_DIR}/src/guacenc/png.c 296 | ${GUACAMOLE_ROOT_DIR}/src/guacenc/layer.c 297 | ${GUACAMOLE_ROOT_DIR}/src/guacenc/buffer.c 298 | ${GUACAMOLE_ROOT_DIR}/src/guacenc/cursor.c 299 | ${GUACAMOLE_ROOT_DIR}/src/guacenc/display*.c 300 | ${GUACAMOLE_ROOT_DIR}/src/libguac/protocol.cpp 301 | ${GUACAMOLE_ROOT_DIR}/src/libguac/timestamp.cpp 302 | ${GUACAMOLE_ROOT_DIR}/src/libguac/user-handlers.cpp 303 | ${GUACAMOLE_ROOT_DIR}/src/libguac/*.c 304 | ${GUACAMOLE_ROOT_DIR}/src/protocols/rdp/*.c 305 | ${GUACAMOLE_ROOT_DIR}/src/protocols/rdp/guac_rdpsnd/*.c 306 | ${GUACAMOLE_ROOT_DIR}/src/protocols/rdp/_generated_keymaps.c 307 | ${GUACAMOLE_ROOT_DIR}/src/protocols/vnc/*.c) 308 | 309 | list(REMOVE_ITEM GUACAMOLE_SOURCES 310 | ${GUACAMOLE_ROOT_DIR}/src/libguac/wait-fd.c 311 | ${GUACAMOLE_ROOT_DIR}/src/libguac/socket-broadcast.c 312 | ${GUACAMOLE_ROOT_DIR}/src/libguac/socket-fd.c 313 | ${GUACAMOLE_ROOT_DIR}/src/libguac/socket-nest.c 314 | ${GUACAMOLE_ROOT_DIR}/src/libguac/socket-ssl.c 315 | ${GUACAMOLE_ROOT_DIR}/src/libguac/socket-tee.c 316 | ${GUACAMOLE_ROOT_DIR}/src/libguac/socket-wsa.c 317 | ${GUACAMOLE_ROOT_DIR}/src/libguac/id.c 318 | ${GUACAMOLE_ROOT_DIR}/src/libguac/user-handshake.c 319 | ${GUACAMOLE_ROOT_DIR}/src/libguac/encode-webp.c 320 | ${GUACAMOLE_ROOT_DIR}/src/protocols/rdp/rdp_fs.c 321 | ${GUACAMOLE_ROOT_DIR}/src/protocols/rdp/audio_input.c 322 | ${GUACAMOLE_ROOT_DIR}/src/protocols/rdp/rdp_print_job.c 323 | ${GUACAMOLE_ROOT_DIR}/src/protocols/vnc/sftp.c) 324 | add_library(guacamole ${GUACAMOLE_SOURCES}) 325 | target_link_libraries(guacamole PRIVATE ${Cairo_LIBRARY} CapnProto::capnp collab-vm-common 326 | PNG::PNG ${JPEG_LIBRARIES} freerdp freerdp-client libvncclient OpenSSL::SSL OpenSSL::Crypto) 327 | add_dependencies(guacamole freerdp libvncserver) 328 | set(GUACAMOLE_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/submodules/guacamole-server/src/common" "${CMAKE_SOURCE_DIR}/submodules/guacamole-server/src/libguac" "${CMAKE_SOURCE_DIR}/submodules/guacamole-server/src/libguac/guacamole" "${CMAKE_SOURCE_DIR}/submodules/guacamole-server/src" ${GUACAMOLE_ROOT_DIR}/src/protocols/rdp ${COLLAB_VM_COMMON_BINARY_DIR}) 329 | target_compile_definitions(guacamole PRIVATE -DENABLE_WINPR) 330 | target_include_directories(guacamole PUBLIC ${GUACAMOLE_INCLUDE_DIRS} ${FreeRDP_INCLUDE_DIR} ${FreeRDP-Client_INCLUDE_DIR} ${WinPR_INCLUDE_DIR} ${LIBVNCSERVER_INCLUDE_DIRS} ${JPEG_INCLUDE_DIR} ${GUACAMOLE_ROOT_DIR}) 331 | if (MSVC) 332 | find_package(pthreads REQUIRED) 333 | if (TARGET PThreads4W::PThreads4W) 334 | target_link_libraries(guacamole PRIVATE PThreads4W::PThreads4W) 335 | else() 336 | target_link_libraries(guacamole PRIVATE ${PTHREADS_LIBRARIES}) 337 | endif() 338 | target_include_directories(guacamole PUBLIC ${GUACAMOLE_ROOT_DIR}/src/win-compat) 339 | endif() 340 | 341 | target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC 342 | ${FreeRDP_INCLUDE_DIR} 343 | ${LIBVNCSERVER_INCLUDE_DIRS} 344 | ${COLLAB_VM_COMMON} 345 | ${OPENSSL_INCLUDE_DIR} 346 | ${CMAKE_SOURCE_DIR}/submodules/GSL/include 347 | ${Boost_INCLUDE_DIRS} 348 | ${SQLITE3_INCLUDE_DIR} 349 | ${sqlite-modern-cpp_INCLUDE_DIR} 350 | ${clipp_INCLUDE_DIR} 351 | ${ARGON2_INCLUDE_DIR}) 352 | target_link_libraries(${PROJECT_NAME} PRIVATE 353 | argon2 CapnProto::capnp ${Cairo_LIBRARY} collab-vm-common 354 | guacamole OpenSSL::Crypto OpenSSL::SSL sqlite3 ${FILESYSTEM_LIBRARY}) 355 | 356 | install(TARGETS ${PROJECT_NAME} DESTINATION .) 357 | if(MSVC) 358 | install(FILES $ DESTINATION . OPTIONAL) 359 | endif() 360 | install(DIRECTORY $/ 361 | DESTINATION . FILES_MATCHING PATTERN *.dll) 362 | 363 | add_subdirectory(tests) 364 | -------------------------------------------------------------------------------- /CapnpMessageFrameBuilder.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | template > 5 | class CapnpMessageFrameBuilder final { 6 | public: 7 | CapnpMessageFrameBuilder(const Allocator& alloc = Allocator()) 8 | : frame_(alloc) {} 9 | 10 | void Init(size_t segmentCount) { 11 | frame_.resize((segmentCount + 2) & ~size_t(1)); 12 | frame_[0] = segmentCount - 1; 13 | segment_num_ = 1; 14 | } 15 | 16 | // TODO: Remove parameter? 17 | void Finalize(size_t segmentCount) { 18 | if (segmentCount % 2 == 0) { 19 | // Set padding byte 20 | frame_[segmentCount + 1] = 0; 21 | } 22 | } 23 | 24 | void AddSegment(size_t segmentSize) { frame_[segment_num_++] = segmentSize; } 25 | 26 | const std::vector& GetFrame() { return frame_; } 27 | 28 | private: 29 | std::vector frame_; 30 | size_t segment_num_; 31 | }; 32 | -------------------------------------------------------------------------------- /CaptchaVerifier.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "CollabVm.capnp.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | // TODO: Replace Boost Property Tree with Cap'N Proto's JSON parser 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace CollabVm::Server { 19 | class CaptchaVerifier { 20 | struct VerifyRequest { 21 | VerifyRequest(std::function&& callback, 22 | const std::string_view response_token, 23 | const std::string_view remote_ip) 24 | : callback_(std::move(callback)), 25 | response_token_(response_token), 26 | remote_ip_(remote_ip) {} 27 | std::function callback_; 28 | std::string response_token_; 29 | std::string remote_ip_; 30 | }; 31 | 32 | std::optional request_; 33 | std::queue queue_; 34 | boost::asio::io_context::strand strand_; 35 | boost::asio::ip::tcp::resolver resolver_; 36 | boost::asio::ssl::stream stream_; 37 | boost::beast::flat_buffer buffer_; 38 | boost::beast::http::request req_; 39 | boost::beast::http::response response_; 40 | std::unique_ptr settings_; 41 | std::unique_ptr pending_settings_; 42 | boost::asio::steady_timer retry_timer_; 43 | int failed_count_ = 0; 44 | constexpr static int max_fail_count_ = 3; 45 | 46 | public: 47 | explicit CaptchaVerifier(boost::asio::io_context& io_context, 48 | boost::asio::ssl::context& ssl_ctx) 49 | : strand_(io_context), 50 | resolver_(io_context), 51 | stream_(io_context, ssl_ctx), 52 | retry_timer_(io_context, std::chrono::seconds(10)) { 53 | req_.keep_alive(true); 54 | } 55 | 56 | void SetSettings(ServerSetting::Captcha::Reader settings) { 57 | auto message_builder = std::make_unique(); 58 | message_builder->setRoot(settings); 59 | SetSettings([message_builder = std::move(message_builder)]() mutable { 60 | return std::move(message_builder); 61 | }); 62 | } 63 | 64 | template 65 | void SetSettings(TGetSettings&& getSettings) { 66 | boost::asio::dispatch(strand_, [this, getSettings = std::forward(getSettings)]() mutable { 67 | if constexpr (std::is_same_v, std::unique_ptr>) { 68 | pending_settings_ = getSettings(); 69 | } else { 70 | pending_settings_ = std::make_unique(); 71 | pending_settings_->setRoot(ServerSetting::Captcha::Reader(getSettings())); 72 | } 73 | auto new_settings = pending_settings_->getRoot(); 74 | req_.set(boost::beast::http::field::host, 75 | std::string_view(new_settings.getUrlHost().cStr(), 76 | new_settings.getUrlHost().size())); 77 | if (new_settings.getPostParams().size()) { 78 | req_.method(boost::beast::http::verb::post); 79 | req_.set(boost::beast::http::field::content_type, 80 | "application/x-www-form-urlencoded"); 81 | } else { 82 | req_.method(boost::beast::http::verb::get); 83 | req_.erase(boost::beast::http::field::content_type); 84 | } 85 | 86 | // Close the socket in case the host changed 87 | boost::system::error_code ec; 88 | stream_.lowest_layer().close(ec); 89 | }); 90 | } 91 | 92 | void Verify(const std::string_view token, 93 | std::function callback, 94 | const std::string_view remote_ip = "") { 95 | boost::asio::dispatch(strand_, [&, callback = std::move(callback)]() mutable { 96 | auto settings = pending_settings_ ? pending_settings_.get() : settings_.get(); 97 | if (!settings || !settings->getRoot().getEnabled()) { 98 | callback(true); 99 | return; 100 | } 101 | if (token.empty()) { 102 | callback(false); 103 | return; 104 | } 105 | auto request = VerifyRequest(std::move(callback), token, remote_ip); 106 | if (request_.has_value()) { 107 | queue_.emplace(std::move(request)); 108 | return; 109 | } 110 | request_.emplace(std::move(request)); 111 | if (stream_.lowest_layer().is_open()) { 112 | SendRequest(); 113 | } else { 114 | Connect(); 115 | } 116 | }); 117 | } 118 | private: 119 | void Connect() { 120 | boost::asio::dispatch(strand_, [this]() { 121 | if (pending_settings_) { 122 | settings_.reset(pending_settings_.release()); 123 | failed_count_ = 0; 124 | } 125 | auto settings = settings_->getRoot(); 126 | const auto host = settings.getUrlHost(); 127 | resolver_.async_resolve( 128 | { host.cStr(), host.size() }, std::to_string(settings.getUrlPort()), 129 | [this, useSSL = settings.getHttps()](const boost::system::error_code& ec, 130 | boost::asio::ip::tcp::resolver::results_type results) { 131 | boost::asio::async_connect( 132 | stream_.lowest_layer(), results, 133 | [this, useSSL](const boost::system::error_code& ec, 134 | const boost::asio::ip::tcp::endpoint& endpoint) { 135 | if (ec) { 136 | std::cout << "Failed to connect to captcha service" << std::endl; 137 | Retry(); 138 | return; 139 | } 140 | if (!useSSL) { 141 | SendRequest(); 142 | return; 143 | } 144 | stream_.async_handshake( 145 | boost::asio::ssl::stream_base::client, 146 | [this](const boost::system::error_code& ec) { 147 | if (ec) { 148 | std::cout << "SSL handshake with captcha service failed" << std::endl; 149 | Retry(); 150 | return; 151 | } 152 | 153 | SendRequest(); 154 | }); 155 | }); 156 | }); 157 | }); 158 | } 159 | 160 | static void ReplaceVariables(std::string& body, const VerifyRequest& request) { 161 | boost::algorithm::replace_all(body, "$TOKEN", 162 | request.response_token_); 163 | boost::algorithm::replace_all(body, "$IP", 164 | request.remote_ip_); 165 | } 166 | 167 | void SendRequest() { 168 | boost::asio::dispatch(strand_, [this]() { 169 | failed_count_ = 0; 170 | auto settings = settings_->getRoot(); 171 | auto url_path = std::string(settings.getUrlPath().asString()); 172 | ReplaceVariables(url_path, request_.value()); 173 | req_.target(url_path); 174 | if (settings.getPostParams().size()) { 175 | req_.body() = settings.getPostParams().asString(); 176 | ReplaceVariables(req_.body(), request_.value()); 177 | req_.set(boost::beast::http::field::content_length, req_.body().length()); 178 | } 179 | auto send_request = [this](auto& stream) mutable { 180 | boost::beast::http::async_write( 181 | stream, req_, 182 | [this, &stream](const boost::system::error_code& ec, 183 | std::size_t bytes_transferred) mutable { 184 | if (ec) { 185 | std::cout << "Failed to send HTTP request for captcha verification" << std::endl; 186 | Retry(); 187 | return; 188 | } 189 | boost::beast::http::async_read( 190 | stream, buffer_, response_, 191 | boost::asio::bind_executor(strand_, 192 | [this](const boost::system::error_code& ec, 193 | std::size_t bytes_transferred) { 194 | if (ec) { 195 | std::cout << "Failed to read HTTP response for captcha verification" << std::endl; 196 | Retry(); 197 | return; 198 | } 199 | auto success = false; 200 | try { 201 | boost::property_tree::ptree pt; 202 | auto string_stream = std::istringstream(response_.body()); 203 | boost::property_tree::read_json(string_stream, pt); 204 | auto settings = settings_->getRoot(); 205 | success = pt.get(settings.getValidJSONVariableName().cStr()); 206 | } catch (boost::property_tree::ptree_error&) { 207 | std::cout << "Failed to parse JSON response for captcha" << std::endl; 208 | } 209 | request_->callback_(success); 210 | 211 | // Destruct and reconstruct the response in preparation 212 | // for another request 213 | ([](auto& response) { 214 | using T = std::remove_reference_t; 215 | response.~T(); 216 | new (&response) T; 217 | })(response_); 218 | 219 | if (queue_.empty()) { 220 | request_.reset(); 221 | return; 222 | } 223 | request_.emplace(std::move(queue_.front())); 224 | queue_.pop(); 225 | SendRequest(); 226 | })); 227 | }); 228 | }; 229 | if (settings.getHttps()) { 230 | send_request(stream_); 231 | } else { 232 | send_request(static_cast(stream_.lowest_layer())); 233 | } 234 | }); 235 | } 236 | 237 | void Retry() { 238 | boost::asio::dispatch(strand_, [this]() { 239 | if (++failed_count_ >= max_fail_count_) { 240 | // Reject all captcha verification requests because 241 | // the tokens may have timed out 242 | request_->callback_(false); 243 | request_.reset(); 244 | while (!queue_.empty()) { 245 | queue_.front().callback_(false); 246 | queue_.pop(); 247 | } 248 | return; 249 | } 250 | retry_timer_.async_wait([this](auto& ec) { 251 | Connect(); 252 | }); 253 | }); 254 | } 255 | }; 256 | } // namespace CollabVm::Server 257 | -------------------------------------------------------------------------------- /CaseInsensitiveUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | // Based on an example from Boost Functional 7 | struct CaseInsensitiveComparator 8 | { 9 | CaseInsensitiveComparator() {} 10 | explicit CaseInsensitiveComparator(std::locale const& l) : locale_(l) {} 11 | 12 | template 13 | bool operator()(String1 const& x1, String2 const& x2) const 14 | { 15 | return boost::algorithm::iequals(x1, x2, locale_); 16 | } 17 | private: 18 | std::locale locale_; 19 | }; 20 | 21 | struct CaseInsensitiveHasher 22 | { 23 | CaseInsensitiveHasher() {} 24 | explicit CaseInsensitiveHasher(std::locale const& l) : locale_(l) {} 25 | 26 | template 27 | std::size_t operator()(String const& x) const 28 | { 29 | std::size_t seed = 0; 30 | 31 | for (typename String::const_iterator it = x.begin(); 32 | it != x.end(); ++it) 33 | { 34 | boost::hash_combine(seed, std::toupper(*it, locale_)); 35 | } 36 | 37 | return seed; 38 | } 39 | private: 40 | std::locale locale_; 41 | }; 42 | -------------------------------------------------------------------------------- /CollabVmChannel.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace CollabVm::Server { 4 | template 5 | class CollabVmChannel{ 6 | public: 7 | // CollabVmChannel(const std::uint32_t id) : chat_room_(id) {} 8 | // void AddClient(const std::shared_ptr& client) { 9 | // chat_room_.AddClient(client); 10 | // } 11 | // TChatRoom& GetChatRoom() { 12 | // return chat_room_; 13 | // } 14 | // std::vector>& GetClients() { 15 | // return chat_room_.GetClients(); 16 | // } 17 | // private: 18 | // TChatRoom chat_room_; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /CollabVmChatRoom.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "CollabVm.capnp.h" 6 | 7 | namespace CollabVm::Server { 8 | template 9 | class CollabVmChatRoom { 10 | const std::uint32_t id_; 11 | std::uint8_t next_message_offset_; 12 | capnp::MallocMessageBuilder history_message_builder_; 13 | CollabVmServerMessage::ChannelChatMessages::Builder channel_messages_; 14 | constexpr static auto max_chat_message_history = 100; 15 | public: 16 | 17 | explicit CollabVmChatRoom(const std::uint32_t id) 18 | : id_(id), 19 | next_message_offset_(0), 20 | channel_messages_( 21 | history_message_builder_.initRoot() 22 | .initMessage() 23 | .initChatMessages()) { 24 | channel_messages_.setChannel(id); 25 | auto messages = channel_messages_.initMessages(max_chat_message_history); 26 | for (auto&& message : messages) { 27 | message.initSender(MaxUsernameLen); 28 | message.initMessage(MaxMessageLen); 29 | } 30 | } 31 | 32 | void AddUserMessage( 33 | CollabVmServerMessage::ChannelChatMessage::Builder channel_chat_message, 34 | const std::string& username, 35 | CollabVmServerMessage::UserType user_type, 36 | const std::string& message) { 37 | const auto timestamp = 38 | std::chrono::duration_cast( 39 | std::chrono::system_clock::now().time_since_epoch()) 40 | .count(); 41 | channel_chat_message.setChannel(id_); 42 | auto chat_message = channel_chat_message.initMessage(); 43 | chat_message.setMessage(message); 44 | chat_message.setUserType(user_type); 45 | chat_message.setSender(username); 46 | chat_message.setTimestamp(timestamp); 47 | 48 | channel_messages_.setCount( 49 | (std::min)(channel_messages_.getCount() + 1, max_chat_message_history)); 50 | chat_message = channel_messages_.getMessages()[next_message_offset_]; 51 | next_message_offset_ = (next_message_offset_ + 1) % max_chat_message_history; 52 | const auto message_body = chat_message.getMessage(); 53 | copyStringToTextBuilder(message, message_body); 54 | copyStringToTextBuilder(username, chat_message.getSender()); 55 | chat_message.setTimestamp(timestamp); 56 | } 57 | 58 | static void copyStringToTextBuilder(const std::string& string, 59 | capnp::Text::Builder text_builder) 60 | { 61 | const auto new_text_end = std::copy(string.begin(), 62 | string.end(), 63 | text_builder.begin()); 64 | std::fill(new_text_end, text_builder.end(), '\0'); 65 | } 66 | 67 | void GetChatHistory( 68 | CollabVmServerMessage::ChannelConnectResponse::ConnectInfo::Builder 69 | connect_success) { 70 | const auto messages_count = channel_messages_.getCount(); 71 | auto message_index = messages_count == max_chat_message_history ? next_message_offset_ : 0; 72 | auto messages = connect_success.initChatMessages(messages_count); 73 | for (auto i = 0; i < messages_count; i++) { 74 | messages.setWithCaveats(i, channel_messages_.getMessages()[message_index]); 75 | message_index = (message_index + 1) % channel_messages_.getMessages().size(); 76 | } 77 | } 78 | 79 | std::uint32_t GetId() const { 80 | return id_; 81 | } 82 | }; 83 | } // namespace CollabVm::Server 84 | -------------------------------------------------------------------------------- /CollabVmGuacamoleClient.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "SocketMessage.hpp" 8 | #include "GuacamoleClient.hpp" 9 | 10 | namespace CollabVm::Server { 11 | 12 | template 13 | struct CollabVmGuacamoleClient final 14 | : GuacamoleClient> 15 | { 16 | CollabVmGuacamoleClient( 17 | boost::asio::io_context::strand& execution_context, 18 | TAdminVirtualMachine& admin_vm) 19 | : GuacamoleClient(execution_context), 20 | admin_vm_(admin_vm) 21 | { 22 | } 23 | 24 | void OnStart() 25 | { 26 | admin_vm_.OnStart(); 27 | } 28 | 29 | void OnStop() 30 | { 31 | admin_vm_.OnStop(); 32 | } 33 | 34 | void OnLog(const std::string_view message) 35 | { 36 | std::cout << message << std::endl; 37 | } 38 | 39 | void OnInstruction(capnp::MallocMessageBuilder& message_builder) 40 | { 41 | // TODO: Avoid copying 42 | auto guac_instr = 43 | message_builder.getRoot(); 44 | auto socket_message = SocketMessage::CreateShared(); 45 | socket_message->GetMessageBuilder() 46 | .initRoot() 47 | .initMessage() 48 | .setGuacInstr(guac_instr); 49 | socket_message->CreateFrame(); 50 | 51 | const auto lock = std::lock_guard(instruction_queue_mutex_); 52 | instruction_queue_.emplace_back(std::move(socket_message)); 53 | } 54 | 55 | void OnFlush() 56 | { 57 | auto lock = std::unique_lock(instruction_queue_mutex_); 58 | if (instruction_queue_.empty()) { 59 | return; 60 | } 61 | auto instruction_queue = 62 | std::make_shared>>( 63 | std::move(instruction_queue_)); 64 | lock.unlock(); 65 | 66 | admin_vm_.OnGuacamoleInstructions(std::move(instruction_queue)); 67 | } 68 | 69 | TAdminVirtualMachine& admin_vm_; 70 | std::vector> instruction_queue_; 71 | std::mutex instruction_queue_mutex_; 72 | }; 73 | 74 | } 75 | -------------------------------------------------------------------------------- /Database/Database.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #ifdef WIN32 5 | #include 6 | #undef VOID 7 | #undef CONST 8 | #endif 9 | #include 10 | #include 11 | 12 | #include 13 | #include "Database.h" 14 | 15 | namespace CollabVm::Server { 16 | 17 | Database::Database() : db_("collab-vm.db") { 18 | db_ << "PRAGMA foreign_keys = ON"; 19 | auto created_new = false; 20 | db_ << "SELECT COUNT(*) = 0 FROM sqlite_master WHERE type = 'table' and name = 'VmConfig'" >> created_new; 21 | db_ << 22 | "CREATE TABLE IF NOT EXISTS User (" 23 | " Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," 24 | " Username TEXT NOT NULL," 25 | " PasswordHash BLOB(32) NOT NULL," 26 | " PasswordSalt BLOB(32) NOT NULL," 27 | " TotpKey BLOB(20) NULL," 28 | " SessionId BLOB(16) NULL UNIQUE," 29 | " RegistrationDate INTEGER NOT NULL," 30 | " RegistrationIpAddr BLOB(16) NOT NULL," 31 | " LastActiveIpAddr BLOB(16) NOT NULL," 32 | " LastLogin INTEGER NOT NULL," 33 | " LastFailedLogin INTEGER NULL," 34 | " LastOnline INTEGER NOT NULL," 35 | " FailedLogins INTEGER NOT NULL," 36 | " IsAdmin INTEGER NOT NULL," 37 | " IsDisabled INTEGER NOT NULL)"; 38 | db_ << 39 | "CREATE TABLE IF NOT EXISTS UserInvite (" 40 | " Id BLOB NOT NULL PRIMARY KEY," 41 | " Username TEXT NULL UNIQUE," 42 | " InviteName TEXT NOT NULL," 43 | " IsAdmin INTEGER NOT NULL," 44 | " Accepted INTEGER NOT NULL DEFAULT 0)"; 45 | db_ << 46 | "CREATE TABLE IF NOT EXISTS UnavailableUsername(" 47 | " Username TEXT NOT NULL PRIMARY KEY)"; 48 | db_ << 49 | "CREATE TABLE IF NOT EXISTS ServerConfig (" 50 | " ID INTEGER NOT NULL PRIMARY KEY," 51 | " Setting BLOB NOT NULL)"; 52 | db_ << 53 | "CREATE TABLE IF NOT EXISTS VmConfig (" 54 | " IDs_VmId INTEGER NOT NULL," 55 | " IDs_SettingID INTEGER NOT NULL," 56 | " Setting BLOB NOT NULL," 57 | " PRIMARY KEY (IDs_VmId," 58 | " IDs_SettingID))"; 59 | db_ << 60 | "CREATE TABLE IF NOT EXISTS Recordings (" 61 | " VmId INTEGER NOT NULL," 62 | " StartTime INTEGER," 63 | " StopTime INTEGER," 64 | " FilePath TEXT NOT NULL UNIQUE," 65 | " PRIMARY KEY (VmId, StartTime))"; 66 | 67 | if (created_new) { 68 | std::cout << "A new database has been created" << std::endl; 69 | CreateTestVm(); 70 | SetReCaptchaSettings(); 71 | } 72 | } 73 | 74 | void Database::SetReCaptchaSettings() { 75 | capnp::MallocMessageBuilder message_builder; 76 | // Create default settings 77 | const auto fields_count = 78 | capnp::Schema::from().getUnionFields().size(); 79 | LoadServerSettings( 80 | message_builder.initRoot>(fields_count)); 81 | 82 | auto settings = message_builder.initRoot>(1); 83 | auto captcha_settings = settings[0].initSetting().initCaptcha(); 84 | captcha_settings.setEnabled(false); 85 | captcha_settings.setHttps(true); 86 | captcha_settings.setUrlHost("google.com"); 87 | captcha_settings.setUrlPort(443); 88 | captcha_settings.setUrlPath("/recaptcha/api/siteverify"); 89 | captcha_settings.setPostParams("secret=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe&response=$TOKEN"); 90 | captcha_settings.setValidJSONVariableName("success"); 91 | SaveServerSettings(settings); 92 | } 93 | 94 | void Database::CreateTestVm() { 95 | auto message_builder = capnp::MallocMessageBuilder(); 96 | auto fields = capnp::Schema::from().getUnionFields(); 97 | auto vm_settings = message_builder.initRoot>(fields.size()); 98 | for (auto i = 0u; i < fields.size(); i++) { 99 | auto new_setting = capnp::DynamicStruct::Builder(vm_settings[i].getSetting()); 100 | new_setting.clear(fields[i]); 101 | } 102 | vm_settings[VmSetting::Setting::AUTO_START].getSetting().setAutoStart(true); 103 | vm_settings[VmSetting::Setting::NAME].getSetting().setName("Test VM"); 104 | vm_settings[VmSetting::Setting::DESCRIPTION].getSetting().setDescription("draw stuff"); 105 | vm_settings[VmSetting::Setting::START_COMMAND].getSetting().setStartCommand( 106 | #ifdef _WIN32 107 | "vnc-demo.exe" 108 | #else 109 | "./vnc-demo" 110 | #endif 111 | " -listen localhost -rfbport 59000" 112 | ); 113 | vm_settings[VmSetting::Setting::STOP_COMMAND].getSetting().setStopCommand( 114 | #ifdef _WIN32 115 | "taskkill /f /im vnc-demo.exe" 116 | #else 117 | "pkill vnc-demo" 118 | #endif 119 | ); 120 | vm_settings[VmSetting::Setting::TURNS_ENABLED].getSetting().setTurnsEnabled(true); 121 | vm_settings[VmSetting::Setting::TURN_TIME].getSetting().setTurnTime(30); 122 | vm_settings[VmSetting::Setting::PROTOCOL].getSetting().setProtocol(VmSetting::Protocol::VNC); 123 | auto guac_params = vm_settings[VmSetting::Setting::GUACAMOLE_PARAMETERS].getSetting().initGuacamoleParameters(2); 124 | guac_params[0].setName("hostname"); 125 | guac_params[0].setValue("localhost"); 126 | guac_params[1].setName("port"); 127 | guac_params[1].setValue("59000"); 128 | CreateVm(GetNewVmId(), vm_settings); 129 | } 130 | 131 | void Database::LoadServerSettings( 132 | capnp::List::Builder settings_list) { 133 | auto fields = capnp::Schema::from().getUnionFields(); 134 | const auto fields_count = fields.size(); 135 | 136 | auto setting_id = 0u; 137 | db_ << 138 | "SELECT ServerConfig.ID, ServerConfig.Setting" 139 | " FROM ServerConfig" 140 | " ORDER BY ServerConfig.ID" 141 | >> [&](const std::uint8_t id, const std::vector& setting) 142 | { 143 | auto server_setting = settings_list[id].initSetting(); 144 | capnp::DynamicStruct::Builder dynamic_server_setting = server_setting; 145 | const auto field = fields[id]; 146 | auto db_setting = capnp::readMessageUnchecked( 147 | reinterpret_cast(setting.data())); 148 | if (db_setting.which() != setting_id) { 149 | // The Setting union has the wrong field set 150 | std::cout << "Warning: the server setting '" 151 | << field.getProto().getName().cStr() << "' was invalid" 152 | << std::endl; 153 | dynamic_server_setting.clear(field); 154 | db_ << 155 | "INSERT INTO ServerConfig (ID, Setting) VALUES (?, ?)" 156 | << setting_id << CreateBlob(server_setting.asReader()); 157 | setting_id++; 158 | return; 159 | } 160 | const capnp::DynamicStruct::Reader dynamic_db_setting = db_setting; 161 | dynamic_server_setting.set(field, dynamic_db_setting.get(field)); 162 | setting_id++; 163 | }; 164 | // Set missing settings to their defaults 165 | for (; setting_id < fields_count; setting_id++) { 166 | auto server_setting = settings_list[setting_id].initSetting(); 167 | capnp::DynamicStruct::Builder dynamic_server_setting = server_setting; 168 | const auto field = fields[setting_id]; 169 | dynamic_server_setting.clear(field); 170 | db_ << 171 | "INSERT INTO ServerConfig (ID, Setting) VALUES (?, ?)" 172 | << setting_id << CreateBlob(server_setting.asReader()); 173 | } 174 | } 175 | 176 | void Database::SaveServerSettings( 177 | const capnp::List::Reader settings_list) { 178 | for (auto update : settings_list) { 179 | auto server_setting = update.getSetting(); 180 | db_ << 181 | "UPDATE ServerConfig SET Setting=? WHERE ID=?" 182 | << CreateBlob(server_setting) 183 | << static_cast(server_setting.which()); 184 | } 185 | } 186 | 187 | void Database::CreateVm(const std::uint32_t vm_id, 188 | const capnp::List::Reader settings_list) { 189 | for (auto update : settings_list) { 190 | auto server_setting = update.getSetting(); 191 | db_ << 192 | "INSERT INTO VmConfig (IDs_VmId, IDs_SettingID, Setting) VALUES (?, ?, ?)" 193 | << vm_id 194 | << static_cast(server_setting.which()) 195 | << CreateBlob(server_setting); 196 | } 197 | } 198 | 199 | void Database::UpdateVmSettings(const std::uint32_t vm_id, 200 | const capnp::List::Reader settings_list) { 201 | for (auto update : settings_list) { 202 | auto server_setting = update.getSetting(); 203 | db_ << 204 | "UPDATE VmConfig SET Setting=? WHERE IDs_VmId=? AND IDs_SettingID=?" 205 | << CreateBlob(server_setting) 206 | << vm_id 207 | << static_cast(server_setting.which()); 208 | } 209 | } 210 | 211 | Database::PasswordHash Database::HashPassword(const std::string& password, 212 | const PasswordSalt& salt) { 213 | const auto time_cost = 2; 214 | const auto mem_cost = 32 * 1024; 215 | const auto parallelism = 1; 216 | PasswordHash hash(User::password_hash_len); 217 | argon2i_hash_raw(time_cost, mem_cost, parallelism, password.c_str(), 218 | password.length(), salt.data(), User::password_salt_len, 219 | hash.data(), User::password_hash_len); 220 | return hash; 221 | } 222 | 223 | CollabVmServerMessage::RegisterAccountResponse::RegisterAccountError 224 | Database::CreateAccount( 225 | const std::string& username, 226 | const std::string& password, 227 | const std::optional> totp_key, 228 | const std::optional> invite_id, 229 | const IpAddress& ip_address) { 230 | // if (!std::regex_match(username, username_re_)) { 231 | // error = 232 | // CollabVmServerMessage::RegisterAccountResponse::RegisterAccountError::USERNAME_INVALID; 233 | // return; 234 | // } 235 | 236 | // First, check if the username is available so time isn't wasted hashing the 237 | // password 238 | /* 239 | { 240 | odb::transaction t(db_.begin()); 241 | if (db_.query_one(odb::query::Username == username)) { 242 | return CollabVmServerMessage::RegisterAccountResponse:: 243 | RegisterAccountError::USERNAME_TAKEN; 244 | } 245 | } 246 | */ 247 | 248 | if (invite_id.has_value()) { 249 | db_ << 250 | "UPDATE UserInvite" 251 | " SET Accepted = 1" 252 | " WHERE Id = ? AND Accepted = 0" 253 | << std::vector(invite_id.value().cbegin(), invite_id.value().cend()); 254 | if (!db_.rows_modified()) { 255 | return CollabVmServerMessage::RegisterAccountResponse:: 256 | RegisterAccountError::INVITE_INVALID; 257 | } 258 | } else { 259 | auto username_taken = false; 260 | db_ << "select count(*) from (" 261 | "select 1 from user" 262 | " where username = ?" 263 | " union" 264 | " select 1 from unavailableusername" 265 | " where username = ?" 266 | " union" 267 | " select 1 from userinvite" 268 | " where username = ?)" 269 | << username 270 | >> username_taken; 271 | if (username_taken) { 272 | return CollabVmServerMessage::RegisterAccountResponse:: 273 | RegisterAccountError::USERNAME_TAKEN; 274 | } 275 | } 276 | 277 | const auto salt = GeneratePasswordSalt(); 278 | const auto hash = HashPassword(password, salt); 279 | User user; 280 | user.username = username; 281 | user.password_hash = hash; 282 | user.password_salt = salt; 283 | if (totp_key) 284 | { 285 | for (auto byte : totp_key.value()) 286 | { 287 | user.totp_key.push_back(std::byte(byte)); 288 | } 289 | } 290 | user.last_online = user.last_login = user.registration_date = GetCurrentTimestamp(); 291 | user.last_active_ip_address = 292 | user.registration_ip_address = ip_address; 293 | try { 294 | if (invite_id.has_value()) { 295 | db_ << 296 | "SELECT IsAdmin FROM UserInvite" 297 | " WHERE Id = ?" 298 | << std::vector(invite_id.value().cbegin(), invite_id.value().cend()) 299 | >> user.is_admin; 300 | } else { 301 | db_ << 302 | "SELECT COUNT(*) = 0 FROM User" 303 | >> user.is_admin; 304 | } 305 | CreateUser(user); 306 | } catch (const sqlite::errors::constraint&) { 307 | // The username was registered after the first check 308 | return CollabVmServerMessage::RegisterAccountResponse:: 309 | RegisterAccountError::USERNAME_TAKEN; 310 | } 311 | return CollabVmServerMessage::RegisterAccountResponse::RegisterAccountError:: 312 | SUCCESS; 313 | } 314 | 315 | using User2=Database::User; 316 | using UserInvite2=Database::UserInvite; 317 | using SessionId2=Database::SessionId; 318 | 319 | std::optional Database::GetUser(const std::string& username) 320 | { 321 | std::optional optional_user; 322 | db_ << "SELECT * FROM User WHERE Username = ?" << username 323 | >> [&optional_user] ( 324 | std::uint32_t id, 325 | const std::string& username, 326 | const std::vector& password_hash, 327 | const std::vector& password_salt, 328 | const std::vector& totp_key, 329 | const std::vector& session_id, 330 | Timestamp registration_date, 331 | const IpAddress& registration_ip_address, 332 | const IpAddress& last_active_ip_address, 333 | Timestamp last_login, 334 | Timestamp last_failed_login, 335 | Timestamp last_online, 336 | std::uint32_t failed_logins, 337 | bool is_admin, 338 | bool is_disabled) 339 | { 340 | auto& user = optional_user.emplace(); 341 | user.id = id; 342 | user.username = username; 343 | user.password_hash = password_hash; 344 | user.password_salt = password_salt; 345 | user.totp_key = totp_key; 346 | user.session_id = session_id; 347 | user.registration_date = registration_date; 348 | user.registration_ip_address = registration_ip_address; 349 | user.last_active_ip_address = last_active_ip_address; 350 | user.last_login = last_login; 351 | user.last_failed_login = last_failed_login; 352 | user.last_online = last_online; 353 | user.failed_logins = failed_logins; 354 | user.is_admin = is_admin; 355 | user.is_disabled = is_disabled; 356 | }; 357 | return optional_user; 358 | } 359 | 360 | void Database::UpdateUser(const User& user) 361 | { 362 | db_ << "UPDATE User SET " 363 | " Id = ?," 364 | " Username = ?," 365 | " PasswordHash = ?," 366 | " PasswordSalt = ?," 367 | " TotpKey = ?," 368 | " SessionId = ?," 369 | " RegistrationDate = ?," 370 | " RegistrationIpAddr = ?," 371 | " LastActiveIpAddr = ?," 372 | " LastLogin = ?," 373 | " LastFailedLogin = ?," 374 | " LastOnline = ?," 375 | " FailedLogins = ?," 376 | " IsAdmin = ?," 377 | " IsDisabled = ?" 378 | " WHERE Id = ?" 379 | << user.id 380 | << user.username 381 | << user.password_hash 382 | << user.password_salt 383 | << user.totp_key 384 | << user.session_id 385 | << user.registration_date 386 | << user.registration_ip_address 387 | << user.last_active_ip_address 388 | << user.last_login 389 | << user.last_failed_login 390 | << user.last_online 391 | << user.failed_logins 392 | << user.is_admin 393 | << user.is_disabled 394 | << user.id; 395 | } 396 | 397 | void Database::CreateUser(User& user) 398 | { 399 | db_ << "INSERT INTO User (" 400 | " Username," 401 | " PasswordHash," 402 | " PasswordSalt," 403 | " TotpKey," 404 | " SessionId," 405 | " RegistrationDate," 406 | " RegistrationIpAddr," 407 | " LastActiveIpAddr," 408 | " LastLogin," 409 | " LastFailedLogin," 410 | " LastOnline," 411 | " FailedLogins," 412 | " IsAdmin," 413 | " IsDisabled)" 414 | " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" 415 | << user.username 416 | << user.password_hash 417 | << user.password_salt 418 | << user.totp_key 419 | << user.session_id 420 | << user.registration_date 421 | << user.registration_ip_address 422 | << user.last_active_ip_address 423 | << user.last_login 424 | << user.last_failed_login 425 | << user.last_online 426 | << user.failed_logins 427 | << user.is_admin 428 | << user.is_disabled; 429 | user.id = db_.last_insert_rowid(); 430 | } 431 | 432 | Database::Timestamp Database::GetCurrentTimestamp() 433 | { 434 | return std::chrono::duration_cast( 435 | std::chrono::system_clock::now().time_since_epoch()) 436 | .count(); 437 | } 438 | 439 | std::tuple Database::CreateSession( 440 | const std::string& username, 441 | const IpAddress& ip_address) { 442 | const auto last_login = GetCurrentTimestamp(); 443 | auto user = std::optional(); 444 | auto success = false; 445 | SessionId old_session_id; 446 | SessionId new_session_id; 447 | do { 448 | new_session_id = GenerateSessionId(); 449 | try { 450 | user = GetUser(username); 451 | if (!user || user->is_disabled) { 452 | return {}; 453 | } 454 | user->last_login = last_login; 455 | user->last_active_ip_address = ip_address; 456 | old_session_id = new_session_id; 457 | user->session_id = new_session_id; 458 | UpdateUser(user.value()); 459 | success = true; 460 | } catch (const sqlite::errors::constraint&) { 461 | // Session ID already exists so generate another one and try again 462 | success = false; 463 | } 464 | } while (!success); 465 | return {std::move(user->username), user->is_admin, 466 | std::move(old_session_id), std::move(new_session_id)}; 467 | } 468 | 469 | std::pair< 470 | CollabVmServerMessage::LoginResponse::LoginResult, 471 | std::vector> 472 | Database::Login(const std::string& username, const std::string& password) { 473 | auto user = GetUser(username); 474 | if (!user) { 475 | return { 476 | CollabVmServerMessage::LoginResponse::LoginResult::INVALID_USERNAME, 477 | {}}; 478 | } 479 | if (user->is_disabled) { 480 | return {CollabVmServerMessage::LoginResponse::LoginResult::ACCOUNT_DISABLED, 481 | {}}; 482 | } 483 | const auto hash = HashPassword(password, user->password_salt); 484 | if (!std::equal(user->password_hash.cbegin(), user->password_hash.cend(), 485 | hash.cbegin())) { 486 | return {CollabVmServerMessage::LoginResponse::LoginResult::INVALID_PASSWORD, 487 | {}}; 488 | } 489 | if (!user->totp_key.empty()) { 490 | return { 491 | CollabVmServerMessage::LoginResponse::LoginResult::TWO_FACTOR_REQUIRED, 492 | std::move(user->totp_key)}; 493 | } 494 | return {CollabVmServerMessage::LoginResponse::LoginResult::SUCCESS, {}}; 495 | } 496 | 497 | bool Database::ChangePassword(const std::string& username, 498 | const std::string& old_password, 499 | const std::string& new_password) 500 | { 501 | auto user = GetUser(username); 502 | if (!user) { 503 | return false; 504 | } 505 | const auto old_hash = HashPassword(old_password, user->password_salt); 506 | if (!std::equal(user->password_hash.cbegin(), user->password_hash.cend(), 507 | old_hash.cbegin())) { 508 | return false; 509 | } 510 | // TODO: Require TOTP for changing the password 511 | /* 512 | if (!user.TotpKey.empty()) { 513 | return false; 514 | } 515 | */ 516 | user->password_hash = HashPassword(new_password, user->password_salt); 517 | UpdateUser(user.value()); 518 | return true; 519 | } 520 | 521 | bool Database::CreateReservedUsername(const std::string& username) { 522 | try { 523 | db_ << 524 | "INSERT INTO UnavailableUsername (Username)" 525 | " VALUES (?)" 526 | << username; 527 | } catch (const sqlite::sqlite_exception&) { 528 | return false; 529 | } 530 | return true; 531 | } 532 | 533 | std::optional Database::CreateInvite( 534 | const std::string_view invite_name, 535 | const std::string_view username, 536 | const bool is_admin) { 537 | if (!username.empty()) 538 | { 539 | bool username_taken; 540 | db_ << "select count(*) from user" 541 | " where username = ?" 542 | << std::string(username) 543 | >> username_taken; 544 | if (username_taken) { 545 | return {}; 546 | } 547 | } 548 | 549 | UserInvite invite; 550 | invite.name = invite_name; 551 | invite.username = username; 552 | invite.is_admin = is_admin; 553 | auto success = false; 554 | do { 555 | invite.id = GenerateInviteId(); 556 | try { 557 | db_ << 558 | "INSERT INTO UserInvite (" 559 | " Id," 560 | " Username," 561 | " InviteName," 562 | " IsAdmin)" 563 | " VALUES (?, ?, ?, ?)" 564 | << invite.id 565 | << invite.username 566 | << invite.name 567 | << invite.is_admin; 568 | success = true; 569 | } catch (const sqlite::errors::constraint_primarykey&) { 570 | // Session ID already exists so generate another one and try again 571 | success = false; 572 | } catch (const sqlite::errors::constraint_unique&) { 573 | // Username already exists 574 | break; 575 | } 576 | } while (!success); 577 | 578 | if (success) { 579 | return invite.id; 580 | } 581 | return {}; 582 | } 583 | 584 | bool Database::DeleteReservedUsername(const std::string_view username) { 585 | try { 586 | db_ << "DELETE FROM UnavailableUsername WHERE Username = ?" 587 | << std::string(username); 588 | } catch (const sqlite::sqlite_exception&) { 589 | return false; 590 | } 591 | return true; 592 | } 593 | 594 | bool Database::UpdateInvite(const gsl::span id, 595 | const std::string_view username, 596 | const bool is_admin) { 597 | db_ << 598 | "UPDATE UserInvite" 599 | " SET Username = ?, IsAdmin = ?" 600 | " WHERE Id = ?" 601 | << std::string(username) 602 | << is_admin 603 | << std::vector(id.cbegin(), id.cend()); 604 | return db_.rows_modified(); 605 | } 606 | 607 | bool Database::DeleteInvite(const gsl::span id) { 608 | db_ << 609 | "DELETE FROM UserInvite WHERE Id = ?" 610 | << std::vector(id.cbegin(), id.cend()); 611 | return db_.rows_modified(); 612 | } 613 | 614 | std::pair Database::ValidateInvite(const gsl::span id) { 615 | std::string username; 616 | try { 617 | db_ << 618 | "select username FROM UserInvite WHERE Id = ? and Accepted = 0" 619 | << std::vector(id.cbegin(), id.cend()) 620 | >> username; 621 | } catch (const sqlite::sqlite_exception&) { 622 | return {false, {}}; 623 | } 624 | return {true, username}; 625 | } 626 | 627 | void Database::SetRecordingStartStopTime( 628 | const std::uint32_t vm_id, 629 | const std::string_view file_path, 630 | const std::chrono::time_point time, 631 | bool start_time) { 632 | const auto column_name = start_time ? "StartTime" : "StopTime"; 633 | db_ << std::string( 634 | "INSERT INTO Recordings (VmId, FilePath, ") + column_name + ") VALUES (?1, ?2, ?3)" 635 | " ON CONFLICT(FilePath) DO UPDATE SET " + column_name + " = ?3" 636 | << vm_id 637 | << file_path.data() 638 | << std::chrono::duration_cast( 639 | time.time_since_epoch()).count(); 640 | } 641 | 642 | std::tuple, 644 | std::chrono::time_point> 645 | Database::GetRecordingFilePath( 646 | const std::uint32_t vm_id, 647 | const std::chrono::time_point start_time, 648 | const std::chrono::time_point stop_time) 649 | { 650 | std::string file_path; 651 | std::uint64_t file_start_time = 0; 652 | std::uint64_t file_stop_time = 0; 653 | try { 654 | db_ << 655 | "SELECT FilePath, StartTime, StopTime FROM Recordings" 656 | " WHERE VmId = ? AND StartTime >= ? AND StartTime < ?" 657 | " ORDER BY StartTime ASC" 658 | " LIMIT 1" 659 | << vm_id 660 | << std::chrono::duration_cast( 661 | start_time.time_since_epoch()).count() 662 | << std::chrono::duration_cast( 663 | stop_time.time_since_epoch()).count() 664 | >> std::tie(file_path, file_start_time, file_stop_time); 665 | } catch (const sqlite::sqlite_exception&) { 666 | } 667 | return { file_path, 668 | file_start_time 669 | ? std::chrono::time_point( 670 | std::chrono::milliseconds(file_start_time)) 671 | : std::chrono::time_point(), 672 | file_stop_time 673 | ? std::chrono::time_point( 674 | std::chrono::milliseconds(file_stop_time)) 675 | : std::chrono::time_point() 676 | }; 677 | } 678 | 679 | } // namespace CollabVm::Server 680 | -------------------------------------------------------------------------------- /Database/Database.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #define MODERN_SQLITE_STD_OPTIONAL_SUPPORT 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #undef CONST 13 | #include 14 | #include 15 | #include 16 | #include "CollabVm.capnp.h" 17 | 18 | namespace CollabVm::Server { 19 | 20 | class Database final { 21 | public: 22 | using Timestamp = std::uint64_t; 23 | using IpAddress = std::vector; 24 | using SessionId = std::vector; 25 | using PasswordSalt = std::vector; 26 | using PasswordHash = std::vector; 27 | 28 | struct User { 29 | constexpr static std::size_t password_hash_len = 32; 30 | constexpr static std::size_t password_salt_len = 32; 31 | constexpr static std::size_t totp_key_len = 20; 32 | constexpr static std::size_t session_id_len = 16; 33 | std::uint32_t id = 0; 34 | std::string username; 35 | std::vector password_hash; 36 | std::vector password_salt; 37 | std::vector totp_key; 38 | std::vector session_id; 39 | Timestamp registration_date = 0; 40 | IpAddress registration_ip_address; 41 | IpAddress last_active_ip_address; 42 | Timestamp last_login = 0; 43 | std::optional last_failed_login; 44 | Timestamp last_online = 0; 45 | std::uint32_t failed_logins = 0; 46 | bool is_admin = false; 47 | bool is_disabled = false; 48 | }; 49 | 50 | struct UserInvite { 51 | constexpr static std::size_t id_length = 32; 52 | std::vector id; 53 | std::string username; 54 | std::string name; 55 | bool is_admin; 56 | // bool IsHost; 57 | }; 58 | 59 | constexpr static int max_password_len = 160; 60 | constexpr static int invite_id_len = UserInvite::id_length; 61 | using InviteId = std::vector; 62 | using SessionIdHasher = boost::hash; 63 | 64 | Database(); 65 | 66 | void SetReCaptchaSettings(); 67 | 68 | static Database::PasswordHash HashPassword(const std::string& password, 69 | const PasswordSalt& salt); 70 | 71 | CollabVmServerMessage::RegisterAccountResponse::RegisterAccountError 72 | CreateAccount( 73 | const std::string& username, 74 | const std::string& password, 75 | const std::optional> totp_key, 76 | const std::optional> invite_id, 77 | const IpAddress& ip_address); 78 | std::optional GetUser(const std::string& username); 79 | void UpdateUser(const User& user); 80 | void CreateUser(User& user); 81 | static Timestamp GetCurrentTimestamp(); 82 | 83 | /** 84 | * @returns The username with correct case and a pair of session IDs, old and 85 | * new. 86 | */ 87 | std::tuple CreateSession( 88 | const std::string& username, 89 | const IpAddress& ip_address); 90 | 91 | /** 92 | * Attempts to log in a user. 93 | * @returns A pair containing the login result and a vector 94 | * that will contain the TOTP key if two-factor authentication is required. 95 | */ 96 | std::pair> 98 | Login(const std::string& username, const std::string& password); 99 | 100 | bool ChangePassword(const std::string& username, 101 | const std::string& old_password, 102 | const std::string& new_password); 103 | 104 | bool CreateReservedUsername(const std::string& username); 105 | 106 | std::uint32_t GetReservedUsernamesCount() 107 | { 108 | auto count = 0u; 109 | db_ << 110 | "SELECT COUNT(*)" 111 | " FROM UnavailableUsername" 112 | >> count; 113 | return count; 114 | } 115 | 116 | template 117 | void ReadReservedUsernames(TCallback callback) { 118 | db_ << 119 | "SELECT Username" 120 | " FROM UnavailableUsername" 121 | >> [callback = std::forward(callback)] 122 | (const std::string& username) 123 | { 124 | callback(username); 125 | }; 126 | } 127 | 128 | bool DeleteReservedUsername(const std::string_view username); 129 | 130 | std::uint32_t GetInvitesCount() 131 | { 132 | std::uint32_t invites_count; 133 | db_ << 134 | "SELECT COUNT(DISTINCT VmConfig.IDs_VmId) FROM VmConfig" 135 | >> invites_count; 136 | return invites_count; 137 | } 138 | 139 | std::optional CreateInvite(const std::string_view invite_name, 140 | const std::string_view username, 141 | const bool is_admin); 142 | 143 | template 144 | void ReadInvites(TCallback callback) { 145 | db_ << 146 | "SELECT" 147 | " Id," 148 | " Username," 149 | " InviteName," 150 | " IsAdmin" 151 | " FROM UserInvite" 152 | >> [callback = std::forward(callback)] 153 | (const std::vector id, 154 | const std::string& username, 155 | const std::string& name, 156 | bool is_admin) 157 | { 158 | UserInvite invite; 159 | invite.id = id; 160 | invite.username = username; 161 | invite.name = name; 162 | invite.is_admin = is_admin; 163 | callback(std::move(invite)); 164 | }; 165 | } 166 | 167 | 168 | bool UpdateInvite(const gsl::span id, 169 | const std::string_view username, 170 | const bool is_admin); 171 | 172 | bool DeleteInvite(const gsl::span id); 173 | 174 | std::pair ValidateInvite(const gsl::span id); 175 | 176 | std::uint32_t GetNewVmId() { 177 | std::uint32_t new_vm_id; 178 | db_ << "SELECT MAX(VmConfig.IDs_VmId) + 1 FROM VmConfig" 179 | >> new_vm_id; 180 | return (std::max)(new_vm_id, 1u); 181 | } 182 | 183 | std::uint32_t GetVmCount() 184 | { 185 | std::uint32_t vm_count; 186 | db_ << 187 | "SELECT COUNT(DISTINCT VmConfig.IDs_VmId) FROM VmConfig" 188 | >> vm_count; 189 | return vm_count; 190 | } 191 | 192 | void CreateVm(const std::uint32_t vm_id, 193 | const capnp::List::Reader settings_list); 194 | 195 | template 196 | void ReadVmSettings(TCallback&& callback) { 197 | auto fields = capnp::Schema::from().getUnionFields(); 198 | const auto fields_count = fields.size(); 199 | auto expected_setting_id = 0u; 200 | auto message_builder = capnp::MallocMessageBuilder(); 201 | auto previous_vm_id = std::optional(); 202 | const auto generateNewSettings = [&](std::uint32_t vm_id) { 203 | // Set missing settings to their defaults 204 | for (; expected_setting_id < fields_count; expected_setting_id++) { 205 | auto vm_setting = message_builder.initRoot().initSetting(); 206 | capnp::DynamicStruct::Builder dynamic_vm_setting = vm_setting; 207 | const auto field = fields[expected_setting_id]; 208 | dynamic_vm_setting.clear(field); 209 | db_ << 210 | "INSERT INTO VmConfig (IDs_VmId, IDs_SettingID, Setting) VALUES (?, ?, ?)" 211 | << vm_id << expected_setting_id << CreateBlob(vm_setting.asReader()); 212 | callback(vm_id, expected_setting_id, message_builder.getRoot()); 213 | } 214 | }; 215 | db_ << 216 | "SELECT VmConfig.IDs_VmId, VmConfig.IDs_SettingID, VmConfig.Setting" 217 | " FROM VmConfig" 218 | " ORDER BY VmConfig.IDs_VmId, VmConfig.IDs_SettingID" 219 | >> [&, callback = std::forward(callback)] 220 | (std::uint32_t vm_id, 221 | std::uint32_t setting_id, 222 | const std::vector& setting) mutable 223 | { 224 | if (previous_vm_id.has_value() && previous_vm_id != vm_id) { 225 | generateNewSettings(*previous_vm_id); 226 | expected_setting_id = 0; 227 | } 228 | previous_vm_id = vm_id; 229 | const auto field = fields[setting_id]; 230 | auto db_setting = capnp::readMessageUnchecked( 231 | reinterpret_cast(setting.data())); 232 | if (db_setting.getSetting().which() != setting_id) { 233 | // The Setting union has the wrong field set 234 | std::cout << "Warning: the VM setting '" 235 | << field.getProto().getName().cStr() << "' was invalid" 236 | << std::endl; 237 | auto vm_setting = message_builder.initRoot().initSetting(); 238 | capnp::DynamicStruct::Builder dynamic_vm_setting = vm_setting; 239 | dynamic_vm_setting.clear(field); 240 | db_ << 241 | "INSERT INTO VmConfig (IDs_VmId, IDs_SettingID, Setting) VALUES (?, ?, ?)" 242 | << vm_id << setting_id << CreateBlob(vm_setting.asReader()); 243 | expected_setting_id++; 244 | return; 245 | } 246 | callback(vm_id, setting_id, db_setting); 247 | expected_setting_id++; 248 | }; 249 | if (previous_vm_id.has_value()) { 250 | generateNewSettings(*previous_vm_id); 251 | } 252 | } 253 | 254 | void UpdateVmSettings(const std::uint32_t vm_id, 255 | const capnp::List::Reader settings_list); 256 | 257 | void DeleteVm(std::uint32_t id) 258 | { 259 | db_ << "DELETE FROM VmConfig WHERE IDs_VmId = ?" 260 | << id; 261 | } 262 | 263 | template 264 | static void UpdateList(const typename capnp::List::Reader old_list, 265 | typename capnp::List::Builder new_list, 266 | const typename capnp::List::Reader list_updates) { 267 | assert(old_list.size() == new_list.size()); 268 | for (auto old_setting : old_list) { 269 | const auto setting_type = old_setting.getSetting().which(); 270 | // TODO: Make this more generic so it doesn't depend on getSetting() 271 | capnp::DynamicStruct::Builder current_setting = new_list[setting_type].getSetting(); 272 | const auto updated_setting = std::find_if(list_updates.begin(), 273 | list_updates.end(), 274 | [setting_type](const auto updated_setting) 275 | { 276 | return updated_setting.getSetting().which() == setting_type; 277 | }); 278 | if (updated_setting != list_updates.end()) 279 | { 280 | const capnp::DynamicStruct::Reader reader = updated_setting->getSetting(); 281 | KJ_IF_MAYBE(field, reader.which()) { 282 | current_setting.set(*field, reader.get(*field)); 283 | continue; 284 | } 285 | } 286 | const capnp::DynamicStruct::Reader reader = old_setting.getSetting(); 287 | KJ_IF_MAYBE(field, reader.which()) { 288 | current_setting.set(*field, reader.get(*field)); 289 | } 290 | } 291 | } 292 | 293 | void LoadServerSettings(capnp::List::Builder settings_list); 294 | 295 | void SaveServerSettings( 296 | const capnp::List::Reader settings_list); 297 | 298 | void SetRecordingStartTime( 299 | const std::uint32_t vm_id, 300 | const std::string_view file_path, 301 | const std::chrono::time_point time) 302 | { 303 | SetRecordingStartStopTime(vm_id, file_path, time, true); 304 | } 305 | 306 | void SetRecordingStopTime( 307 | const std::uint32_t vm_id, 308 | const std::string_view file_path, 309 | const std::chrono::time_point time) 310 | { 311 | SetRecordingStartStopTime(vm_id, file_path, time, false); 312 | } 313 | 314 | std::tuple, 316 | std::chrono::time_point> 317 | GetRecordingFilePath( 318 | const std::uint32_t vm_id, 319 | const std::chrono::time_point start_time, 320 | const std::chrono::time_point stop_time); 321 | 322 | private: 323 | void CreateTestVm(); 324 | 325 | void SetRecordingStartStopTime( 326 | const std::uint32_t vm_id, 327 | const std::string_view file_path, 328 | const std::chrono::time_point time, 329 | bool start_time); 330 | 331 | template 332 | static std::vector CreateBlob(const TReader server_setting) 333 | { 334 | // capnp::copyToUnchecked requires the target buffer to have an extra word 335 | const auto blob_word_size = server_setting.totalSize().wordCount + 1; 336 | auto blob = std::vector(blob_word_size * sizeof(capnp::word)); 337 | capnp::copyToUnchecked( 338 | server_setting, 339 | kj::arrayPtr(reinterpret_cast(&*blob.begin()), 340 | blob_word_size)); 341 | return blob; 342 | } 343 | 344 | template 345 | static std::vector GetRandomBytes() { 346 | auto bytes = std::vector(N); 347 | RAND_bytes(reinterpret_cast(bytes.data()), N); 348 | return bytes; 349 | } 350 | constexpr static auto GenerateSessionId = GetRandomBytes; 351 | constexpr static auto GenerateInviteId = GetRandomBytes; 352 | constexpr static auto GeneratePasswordSalt = GetRandomBytes; 353 | 354 | sqlite::database db_; 355 | }; 356 | } // namespace CollabVm::Server 357 | 358 | namespace std { 359 | template <> 360 | struct hash 361 | { 362 | std::size_t operator()(const CollabVm::Server::Database::SessionId& session_id) const noexcept 363 | { 364 | std::size_t seed = 0; 365 | for (auto byte : session_id) 366 | { 367 | boost::hash_combine(seed, static_cast(byte)); 368 | } 369 | return seed; 370 | } 371 | }; 372 | } 373 | -------------------------------------------------------------------------------- /FileUploadReader.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace WebSocketServer 8 | { 9 | enum class UploadErrorCode 10 | { 11 | kNoError, 12 | kInvalidPath, 13 | kInvalidContentType, 14 | kNoContentLen 15 | 16 | }; 17 | 18 | class UploadErrorCategory : public boost::system::error_category 19 | { 20 | public: 21 | const char* name() const noexcept override 22 | { 23 | return "Upload error"; 24 | } 25 | 26 | std::string message(int ev) const override 27 | { 28 | switch (static_cast(ev)) 29 | { 30 | default: 31 | case UploadErrorCode::kNoError: 32 | return "No error"; 33 | case UploadErrorCode::kInvalidPath: 34 | return "Invalid path"; 35 | case UploadErrorCode::kInvalidContentType: 36 | return "Invalid content type"; 37 | case UploadErrorCode::kNoContentLen: 38 | return "No content length"; 39 | } 40 | } 41 | }; 42 | 43 | inline const UploadErrorCategory& GetUploadErrorCategory() 44 | { 45 | static const UploadErrorCategory category; 46 | return category; 47 | } 48 | 49 | struct FileUploadReader 50 | { 51 | using value_type = std::string; 52 | 53 | class reader 54 | { 55 | std::string& body_; 56 | UploadErrorCode error_code_; 57 | public: 58 | template 59 | reader(beast::http::message& msg) noexcept : 60 | body_(msg.body) 61 | { 62 | if (msg.url != "/upload") 63 | { 64 | error_code_ = UploadErrorCode::kInvalidPath; 65 | return; 66 | } 67 | 68 | const auto content_type = msg.fields.find("Content-Type"); 69 | if (content_type == msg.fields.end() || 70 | !beast::detail::ci_equal(content_type->second, "application/octet-stream")) 71 | { 72 | error_code_ = UploadErrorCode::kInvalidContentType; 73 | return; 74 | } 75 | 76 | // NOTE: The content length should have already been parsed by the parser 77 | // but it can't be accessed so it needs to be done again 78 | const auto content_length = msg.fields.find("Content-Length"); 79 | if (content_length != msg.fields.end()) 80 | { 81 | unsigned long len = std::strtoul(content_length->second.c_str(), nullptr, 10); 82 | 83 | } 84 | 85 | // TODO: Respond to Expect headers 86 | //const auto expect_header = m.fields.find("expect"); 87 | //if (expect_header != m.fields.cend()) 88 | //{ 89 | // if (beast::detail::ci_equal(expect_header->second, "100-continue")) 90 | // { 91 | // // Respond with 100 (Continue) to accept upload 92 | // } 93 | // else 94 | // { 95 | // // Respond to unrecognized expectation with 417 (Expectation Failed) 96 | // } 97 | //} 98 | 99 | 100 | } 101 | 102 | void init(boost::system::error_code& ec) noexcept 103 | { 104 | if (error_code_ != UploadErrorCode::kNoError) 105 | ec = boost::system::error_code(static_cast(error_code_), GetUploadErrorCategory()); 106 | } 107 | 108 | void write(const void* data, std::size_t size, boost::system::error_code&) noexcept 109 | { 110 | 111 | } 112 | }; 113 | }; 114 | } 115 | -------------------------------------------------------------------------------- /GuacamoleClient.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern "C" { 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | } 14 | #ifdef WIN32 15 | # undef CONST 16 | # undef ERROR 17 | # undef min 18 | # undef max 19 | #endif 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | #include "Guacamole.capnp.h" 35 | #include "GuacamoleScreenshot.hpp" 36 | 37 | namespace CollabVm::Server 38 | { 39 | template 40 | class GuacamoleClient 41 | { 42 | public: 43 | GuacamoleClient(boost::asio::io_context::strand& execution_context) 44 | : execution_context_(execution_context), 45 | user_(nullptr, &guac_user_free), 46 | client_(nullptr, &guac_client_free) 47 | { 48 | } 49 | 50 | void StartRDP() 51 | { 52 | Start(guac_rdp_client_init); 53 | } 54 | 55 | void StartVNC() 56 | { 57 | Start(guac_vnc_client_init); 58 | } 59 | 60 | void SetArguments( 61 | const std::unordered_map& args) 62 | { 63 | args_map_ = args; 64 | if (client_) 65 | { 66 | args_ = CreateArgsArray(args_map_, client_->args); 67 | } 68 | } 69 | 70 | void Stop() 71 | { 72 | if (state_.exchange(State::kStopping) == State::kRunning) 73 | { 74 | // This function doesn't appear to be thread-safe because it 75 | // manipulates the state of the guac_client without any 76 | // synchronization, but Guacamole also uses it this way 77 | guac_client_stop(client_.get()); 78 | } 79 | } 80 | 81 | template 82 | void AddUser(TJoinInstructionsCallback&& callback) 83 | { 84 | if (state_ != State::kRunning) 85 | { 86 | return; 87 | } 88 | const auto user = guac_user_alloc(); 89 | user->client = client_.get(); 90 | user->socket = guac_socket_alloc(); 91 | struct SocketData 92 | { 93 | SocketData(GuacamoleClient& guacamole_client, TJoinInstructionsCallback&& callback) 94 | : guacamole_client(guacamole_client), 95 | callback(callback) 96 | {} 97 | GuacamoleClient& guacamole_client; 98 | TJoinInstructionsCallback callback; 99 | } socket_data(*this, std::move(callback)); 100 | user->socket->data = &socket_data; 101 | user->socket->write_handler = [](auto* socket, auto* data) 102 | { 103 | auto& socket_data = *static_cast(socket->data); 104 | auto& guacamole_client = socket_data.guacamole_client; 105 | auto& message_builder = *static_cast(data); 106 | socket_data.callback(std::move(message_builder)); 107 | return ssize_t(0); 108 | }; 109 | user->info.optimal_resolution = 96; 110 | user->info.optimal_width = 800; 111 | user->info.optimal_height = 600; 112 | const char* audio_mimetypes[] = { 113 | static_cast("audio/L8"), 114 | static_cast("audio/L16"), 115 | NULL 116 | }; 117 | user->info.audio_mimetypes = audio_mimetypes; 118 | client_->join_handler(user, args_.size(), const_cast(args_.data())); 119 | client_->leave_handler(user); 120 | guac_socket_free(user->socket); 121 | guac_user_free(user); 122 | } 123 | 124 | void ReadInstruction(Guacamole::GuacClientInstruction::Reader instr) 125 | { 126 | if (user_) 127 | { 128 | guac_call_instruction_handler(user_.get(), instr); 129 | } 130 | } 131 | 132 | /* 133 | * Creates a scaled screenshot and uses the callback to return the PNG bytes. 134 | * @returns true if successful 135 | */ 136 | template 137 | bool CreateScreenshot(TWriteCallback&& callback) 138 | { 139 | if (state_ != State::kRunning) 140 | { 141 | return false; 142 | } 143 | 144 | auto screenshot = GuacamoleScreenshot(); 145 | AddUser([&screenshot](auto&& message_builder) 146 | { 147 | screenshot.WriteInstruction( 148 | message_builder.template getRoot()); 149 | }); 150 | 151 | return screenshot.CreateScreenshot(400, 400, std::forward(callback)); 152 | } 153 | 154 | private: 155 | static ssize_t SocketWriteHandler(guac_socket* socket, void* data) 156 | { 157 | auto& guacamole_client = 158 | *static_cast(socket->data); 159 | auto& message_builder = *static_cast(data); 160 | auto instr = message_builder.getRoot(); 161 | if ( instr.which() == Guacamole::GuacServerInstruction::Which::DISCONNECT 162 | || instr.which() == Guacamole::GuacServerInstruction::Which::ERROR) 163 | { 164 | guacamole_client.state_ = State::kStopping; 165 | } 166 | static_cast(guacamole_client).OnInstruction( 167 | message_builder); 168 | return ssize_t(0); 169 | } 170 | 171 | void Start(guac_client_init_handler& init_handler) 172 | { 173 | CreateClient(); 174 | if (init_handler(client_.get())) 175 | { 176 | throw std::exception(); 177 | } 178 | CreateUser(); 179 | args_ = CreateArgsArray(args_map_, client_->args); 180 | guac_client_add_user(client_.get(), user_.get(), args_.size(), 181 | const_cast(args_.data())); 182 | } 183 | 184 | void CreateClient() 185 | { 186 | state_ = State::kStarting; 187 | client_.reset(guac_client_alloc()); 188 | const auto broadcast_socket = guac_socket_alloc(); 189 | broadcast_socket->data = this; 190 | broadcast_socket->flush_handler = [](auto* socket) 191 | { 192 | auto& guacamole_client = 193 | *static_cast(socket->data); 194 | static_cast(guacamole_client).OnFlush(); 195 | return ssize_t(0); 196 | }; 197 | broadcast_socket->write_handler = [](auto* socket, auto* data) 198 | { 199 | auto& message_builder = *static_cast(data); 200 | auto instr = message_builder.getRoot(); 201 | if (instr.which() == Guacamole::GuacServerInstruction::Which::NOP) 202 | { 203 | // Ignore NOPs because they may be sent from a keep-alive thread 204 | return SocketWriteHandler(socket, data); 205 | } 206 | // This callback is invoked from a new thread and 207 | // when it exits it is safe to deallocate the guac_client 208 | auto& guacamole_client = 209 | *static_cast(socket->data); 210 | auto* destructor_key = &guacamole_client.guacamole_thread_destructor_key; 211 | pthread_key_create(destructor_key, &GuacamoleThreadDestructor); 212 | pthread_setspecific(*destructor_key, &guacamole_client); 213 | 214 | socket->write_handler = [](auto* socket, auto* data) 215 | { 216 | auto& message_builder = *static_cast(data); 217 | auto instr = message_builder.getRoot(); 218 | if (instr.which() == Guacamole::GuacServerInstruction::Which::IMG 219 | && instr.getImg().getLayer() == 0) 220 | { 221 | auto& guacamole_client = 222 | *static_cast(socket->data); 223 | auto state = State::kStarting; 224 | const auto was_starting = 225 | guacamole_client.state_.compare_exchange_strong(state, State::kRunning); 226 | if (was_starting) 227 | { 228 | boost::asio::post(guacamole_client.execution_context_, 229 | [&guacamole_client] 230 | { 231 | static_cast(guacamole_client).OnStart(); 232 | }); 233 | } 234 | else if (state == State::kStopping) 235 | { 236 | // Now that the client thread is known, the client can be stopped 237 | guac_client_stop(guacamole_client.client_.get()); 238 | } 239 | socket->write_handler = SocketWriteHandler; 240 | } 241 | return SocketWriteHandler(socket, data); 242 | }; 243 | return SocketWriteHandler(socket, data); 244 | }; 245 | client_->socket = broadcast_socket; 246 | client_->data = this; 247 | client_->log_handler = 248 | [](auto* client, 249 | guac_client_log_level level, 250 | const char* format, 251 | va_list args) 252 | { 253 | auto& guacamole_client = 254 | *static_cast(client->data); 255 | auto message = std::array(); 256 | if (const auto message_len = ::vsnprintf( 257 | message.data(), sizeof(message), format, args); 258 | message_len > 0) 259 | { 260 | static_cast(guacamole_client).OnLog( 261 | std::string_view(message.data(), message_len)); 262 | } 263 | }; 264 | } 265 | 266 | void CreateUser() 267 | { 268 | user_.reset(guac_user_alloc()); 269 | user_->client = client_.get(); 270 | user_->owner = true; 271 | user_->info.optimal_resolution = 96; 272 | user_->info.optimal_width = 800; 273 | user_->info.optimal_height = 600; 274 | static const char* audio_mimetypes[1] = { 275 | static_cast("audio/L16") 276 | }; 277 | user_->info.audio_mimetypes = audio_mimetypes; 278 | user_->socket = guac_socket_alloc(); 279 | } 280 | 281 | static std::vector CreateArgsArray( 282 | const std::unordered_map& args_map, 283 | const char** arg_names) 284 | { 285 | auto args_count = 0u; 286 | while (arg_names[args_count]) 287 | { 288 | args_count++; 289 | } 290 | if (args_map.empty()) 291 | { 292 | return std::vector(args_count, ""); 293 | } 294 | auto args = std::vector(); 295 | args.reserve(args_count); 296 | for (const auto arg_name : gsl::span(arg_names, args_count)) 297 | { 298 | auto it = args_map.find(arg_name); 299 | args.push_back(it == args_map.end() ? "" : it->second.data()); 300 | } 301 | return args; 302 | } 303 | 304 | static void GuacamoleThreadDestructor(void* data) 305 | { 306 | auto& guacamole_client = 307 | *static_cast(data); 308 | 309 | pthread_key_delete(guacamole_client.guacamole_thread_destructor_key); 310 | guacamole_client.state_ = State::kStopped; 311 | boost::asio::post(guacamole_client.execution_context_, 312 | [&guacamole_client] 313 | { 314 | guacamole_client.client_.reset(); 315 | guacamole_client.user_.reset(); 316 | static_cast(guacamole_client).OnStop(); 317 | }); 318 | } 319 | 320 | boost::asio::io_context::strand& execution_context_; 321 | std::unique_ptr user_; 322 | std::unique_ptr client_; 323 | std::vector args_; 324 | std::unordered_map args_map_; 325 | ::pthread_key_t guacamole_thread_destructor_key; 326 | 327 | enum class State : std::uint8_t 328 | { 329 | kStopped, 330 | kStarting, 331 | kRunning, 332 | kStopping 333 | }; 334 | std::atomic state_; 335 | }; 336 | } 337 | -------------------------------------------------------------------------------- /GuacamoleScreenshot.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace CollabVm::Server 6 | { 7 | struct GuacamoleScreenshot 8 | { 9 | std::unique_ptr 11 | display_ = {guacenc_display_alloc(nullptr, nullptr, 0, 0, 0), &guacenc_display_free}; 12 | 13 | void WriteInstruction(Guacamole::GuacServerInstruction::Reader instruction) 14 | { 15 | guacenc_handle_instruction(display_.get(), instruction); 16 | } 17 | 18 | template 19 | bool CreateScreenshot(std::uint32_t max_width, std::uint32_t max_height, 20 | TWriteCallback&& callback) 21 | { 22 | // The default layer should now contain the flattened image 23 | const auto default_layer = guacenc_display_get_layer(display_.get(), 0); 24 | if (!default_layer->buffer) 25 | { 26 | return false; 27 | } 28 | const auto& surface = *default_layer->buffer; 29 | if (!surface.cairo || !surface.surface) 30 | { 31 | return false; 32 | } 33 | 34 | int width; 35 | int height; 36 | float scale_xy; 37 | if (max_width == 0 || max_height == 0) 38 | { 39 | width = surface.width; 40 | height = surface.height; 41 | scale_xy = 1; 42 | } 43 | else if (surface.width > surface.height) 44 | { 45 | width = max_width; 46 | scale_xy = float(width) / surface.width; 47 | height = scale_xy * surface.height; 48 | } 49 | else 50 | { 51 | height = max_height; 52 | scale_xy = float(height) / surface.height; 53 | width = scale_xy * surface.width; 54 | } 55 | 56 | const auto target = 57 | cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height); 58 | const auto cairo_context = cairo_create(target); 59 | 60 | if (scale_xy != 1) 61 | { 62 | cairo_scale(cairo_context, scale_xy, scale_xy); 63 | } 64 | 65 | cairo_set_source_surface(cairo_context, surface.surface, 0, 0); 66 | cairo_paint(cairo_context); 67 | 68 | const auto result = 69 | cairo_surface_write_to_png_stream(target, 70 | [](void* closure, 71 | const unsigned char* data, 72 | unsigned int length) 73 | { 74 | const auto& callback = *reinterpret_cast(closure); 75 | callback(gsl::span(reinterpret_cast(data), length)); 76 | return CAIRO_STATUS_SUCCESS; 77 | }, &callback); 78 | 79 | cairo_destroy(cairo_context); 80 | cairo_surface_destroy(target); 81 | 82 | return result == CAIRO_STATUS_SUCCESS; 83 | } 84 | }; 85 | } // namespace CollabVm::Server 86 | -------------------------------------------------------------------------------- /IPData.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace CollabVm::Server 7 | { 8 | /** 9 | * Data associated with a user's IP address to be used for spam prevention. 10 | */ 11 | struct IPData 12 | { 13 | /** 14 | * The number of active connections from the IP. 15 | */ 16 | std::uint8_t connections = 0; 17 | 18 | /** 19 | * IP data associated with a VM. 20 | */ 21 | struct ChannelData 22 | { 23 | bool voted = false; 24 | }; 25 | }; 26 | } // namespace CollabVm::Server 27 | -------------------------------------------------------------------------------- /Main.cpp: -------------------------------------------------------------------------------- 1 | #include "CollabVm.capnp.h" 2 | #include "Guacamole.capnp.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "GuacamoleClient.hpp" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "CollabVmServer.hpp" 25 | #include "WebSocketServer.hpp" 26 | 27 | int main(int argc, char* argv[]) { 28 | using namespace clipp; 29 | using namespace std::string_literals; 30 | 31 | auto host = "localhost"s; 32 | // Use half the cores so the remaining can be used by 33 | // the hypervisor and Guacamole client threads 34 | const auto cores = std::thread::hardware_concurrency(); 35 | auto threads = std::max(cores / 2, 1u); 36 | auto port = 0u; 37 | auto root = "./web-app/"s; 38 | auto auto_start_vms = true; 39 | auto invalid_arguments = std::vector(); 40 | enum { 41 | start, 42 | help, 43 | version 44 | } mode = start; 45 | const auto cli_arguments = ( 46 | (option("--host", "-l") & value("address", host)) 47 | .doc("ip or host to listen on (default: localhost)"), 48 | (option("--threads", "-t") & integer("number", threads)) 49 | .doc("the number of threads the server will use (default: " 50 | + std::to_string(threads) + " - half the number of cores)"), 51 | (option("--port", "-p") & integer("number", port)) 52 | .doc("the port to listen on (default: random)"), 53 | (option("--root", "-r") & value("path", root)) 54 | .doc("the root directory to serve files from (default: '" + root + "')"), 55 | option("--cert", "-c") // TODO: use this argument 56 | .doc("path to PEM certificate to use for SSL/TLS"), 57 | option("--no-autostart", "-n").set(auto_start_vms, false) 58 | .doc("don't automatically start any VMs"), 59 | option("--version", "-v").set(mode, version) 60 | .doc("show version and dependencies"), 61 | option("--help", "-h").set(mode, help) 62 | .doc("show this help message"), 63 | any_other(invalid_arguments) 64 | ); 65 | 66 | if (!parse(argc, argv, cli_arguments) 67 | || !invalid_arguments.empty() 68 | || mode == help) { 69 | std::for_each( 70 | invalid_arguments.begin(), 71 | invalid_arguments.end(), 72 | [](const auto& arg) 73 | { 74 | std::cout << "invalid argument '" << arg << "'\n"; 75 | }); 76 | std::cout << usage_lines(cli_arguments, "collab-vm-server") << '\n' 77 | << documentation(cli_arguments) << std::endl; 78 | return 0; 79 | } 80 | if (mode == version) { 81 | std::cout << "collab-vm-server " BOOST_STRINGIZE(PROJECT_VERSION) "\n\n" 82 | "Third-Party Libraries:\n" 83 | "Argon2 " << ARGON2_VERSION_NUMBER << "\n" 84 | "Boost " << BOOST_VERSION / 100000 << '.' 85 | << BOOST_VERSION / 100 % 1000 << '.' 86 | << BOOST_VERSION % 100 << "\n" 87 | "cairo " << cairo_version_string() << "\n" 88 | "Cap'n Proto " BOOST_STRINGIZE(CAPNP_VERSION_STR) "\n" 89 | "FreeRDP " << freerdp_get_version_string() << "\n" 90 | "Guacamole (patched)" "\n" 91 | LIBVNCSERVER_PACKAGE_STRING "\n" 92 | "sqlite modern cpp " << MODERN_SQLITE_VERSION / 1000000 << '.' 93 | << MODERN_SQLITE_VERSION / 1000 % 1000 << '.' 94 | << MODERN_SQLITE_VERSION / 1000 % 1000 << "\n" 95 | "OpenSSL " OPENSSL_VERSION_TEXT "\n" 96 | "SQLite3 " SQLITE_VERSION "\n" << std::endl; 97 | return 0; 98 | } 99 | 100 | using Server = CollabVm::Server::CollabVmServer; 101 | Server(root).Start(threads, host, port, auto_start_vms); 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # collab-vm-server 2 | [![AppVeyor build status](https://ci.appveyor.com/api/projects/status/8h1gjvnf22h4hmbq/branch/master?svg=true)](https://ci.appveyor.com/project/Cosmic-Sans/collab-vm-server/branch/master) 3 | [![Travis build status](https://travis-ci.org/Cosmic-Sans/collab-vm-server.svg?branch=master)](https://travis-ci.org/Cosmic-Sans/collab-vm-server) 4 | 5 | This repository contains the necessary files to compile the collab-vm-server. collab-vm-server powers CollabVM and it is what you will use to host it. Compilation instructions are below. 6 | 7 | Please note that this is currently an incomplete project. This may not build properly, and it does not have full functionality yet. Please use [this](https://github.com/computernewb/collab-vm-server) repository to build/use the current stable version of collab-vm-server. 8 | 9 | ## Building on Windows 10 | 11 | ### Visual Studio 12 | Requirements: 13 | * Visual Studio 2019 (any edition) 14 | * Make sure to install the "Desktop development with C++" workload 15 | * [vcpkg](https://github.com/Microsoft/vcpkg) 16 | 17 | 1. This repository relies on submodules. To clone both the repo and all of its submodules do: 18 | ```git clone --recursive https://github.com/Cosmic-Sans/collab-vm-server.git``` 19 | Or if you've already cloned it, you can download only the submodules by doing: 20 | ```git submodule update --init --recursive``` 21 | 1. After downloading vcpkg and running bootstrap-vcpkg.bat, use the following command to install all the required dependencies: 22 | ``` 23 | ./vcpkg.exe install --triplet x86-windows cairo libjpeg-turbo sqlite3 libpng openssl pthreads 24 | ``` 25 | 1. Open the collab-vm-server folder in Visual Studio 2019, right-click on the CMakeLists.txt file in the Solution Explorer and click "Change CMake Settings" to create a CMakeSettings.json file. Then add a variables property to the configuration so it looks similar to the following: 26 | ``` 27 | ... 28 | { 29 | "name": "x86-Debug", 30 | "generator": "Ninja", 31 | "configurationType": "Debug", 32 | "inheritEnvironments": [ "msvc_x86" ], 33 | "buildRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\build\\${name}", 34 | "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", 35 | "cmakeCommandArgs": "", 36 | "buildCommandArgs": "-v", 37 | "ctestCommandArgs": "", 38 | "variables": [ 39 | { 40 | "name": "CMAKE_TOOLCHAIN_FILE", 41 | // Fix this path 42 | "value": "C:\\vcpkg\\scripts\\buildsystems\\vcpkg.cmake" 43 | }, 44 | { 45 | "name": "VCPKG_TARGET_TRIPLET", 46 | "value": "x86-windows" 47 | }, 48 | { 49 | "name": "OPENSSL_ROOT_DIR", 50 | // Fix this path 51 | "value": "C:\\vcpkg\\installed\\x86-windows" 52 | } 53 | ] 54 | }, 55 | ... 56 | ``` 57 | 1. Verify the correct configuration is selected in the dropdown (e.g. x86-Debug) and build the solution. 58 | 59 | ## Building on Linux and macOS 60 | GCC (minimum version: 8) or Clang (minimum version: 8) are required. Clang must be used on macOS. 61 | 62 | Build vcpkg and required packages: 63 | ``` 64 | git clone https://github.com/Microsoft/vcpkg.git 65 | cd vcpkg 66 | ./bootstrap-vcpkg.sh 67 | ./vcpkg install cairo libjpeg-turbo sqlite3 libpng openssl 68 | ``` 69 | 70 | Build collab-vm-server: 71 | ``` 72 | mkdir build 73 | cd build 74 | cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_CXX_COMPILER=/path/to/bin/clang++ -DCMAKE_C_COMPILER=/path/to/bin/clang .. 75 | cmake --build . 76 | ``` 77 | 78 | ## Building on anything else 79 | It is currently unknown if this project compiles on any other operating systems. The main focus is Windows and Linux. However, if you can successfully get the collab-vm-server to build on another OS (e.g. MacOS, FreeBSD) then please make a pull request with instructions. 80 | -------------------------------------------------------------------------------- /RecordingController.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include "CollabVm.capnp.h" 16 | #include "SocketMessage.hpp" 17 | 18 | namespace CollabVm::Server { 19 | template 20 | struct RecordingController { 21 | template 22 | RecordingController(TExecutionContext& context, std::uint32_t vm_id) 23 | : vm_id_(vm_id), 24 | stop_timer_(context), 25 | keyframe_timer_(context) { 26 | } 27 | 28 | void SetRecordingSettings(ServerSetting::Recordings::Reader settings) { 29 | const auto keyframe_interval = std::chrono::seconds(settings.getKeyframeInterval()); 30 | const auto create_keyframe = 31 | keyframe_interval_ != keyframe_interval 32 | || settings_.getCaptureDisplay() != settings.getCaptureDisplay() 33 | || settings_.getCaptureAudio() != settings.getCaptureAudio(); 34 | settings_message_builder_.setRoot(settings); 35 | settings_ = settings_message_builder_.getRoot(); 36 | file_duration_ = std::chrono::minutes(settings.getFileDuration()); 37 | keyframe_interval_ = keyframe_interval; 38 | 39 | if (!IsRecording()) { 40 | return; 41 | } 42 | if (const auto expiration = 43 | stop_timer_.expiry() - std::chrono::steady_clock::now(); 44 | expiration < file_duration_) { 45 | Start(); 46 | return; 47 | } 48 | if (create_keyframe) { 49 | StartKeyframeTimer(); 50 | } 51 | } 52 | 53 | void Start() { 54 | if (!file_duration_.count()) { 55 | return; 56 | } 57 | auto start_time = Stop(); 58 | if (start_time == std::chrono::time_point::min()) { 59 | start_time = std::chrono::system_clock::now(); 60 | } 61 | auto error_code = std::error_code(); 62 | std::filesystem::create_directories(recordings_directory, error_code); 63 | if (error_code) { 64 | return; 65 | } 66 | const auto date_time = GetCurrentDateTime(); 67 | if (date_time.empty()) { 68 | return; 69 | } 70 | filename_ = std::string(recordings_directory) + "vm" 71 | + std::to_string(vm_id_) + '_' + date_time + ".bin"; 72 | file_stream_.open(filename_, std::fstream::binary | std::fstream::out); 73 | if (!IsRecording()) { 74 | std::cout << "Failed to create recording file \"" << filename_ << '"' << std::endl; 75 | return; 76 | } 77 | auto file_header = file_header_.initRoot(); 78 | file_header.setVmId(vm_id_); 79 | file_header.setStartTime( 80 | std::chrono::duration_cast( 81 | start_time.time_since_epoch()).count()); 82 | auto keyframe_offsets = file_header.initKeyframes( 83 | keyframe_interval_.count() ? file_duration_ / keyframe_interval_ : 0); 84 | next_keyframe_offset_ = keyframe_offsets.begin(); 85 | stop_timer_.expires_after(file_duration_); 86 | stop_timer_.async_wait([this](const auto error_code) { 87 | if (error_code) { 88 | Stop(); 89 | } else { 90 | Start(); 91 | } 92 | }); 93 | WriteFileHeader(); 94 | static_cast(*this).OnRecordingStarted(start_time); 95 | StartKeyframeTimer(); 96 | } 97 | 98 | std::chrono::time_point Stop() { 99 | if (!IsRecording()) { 100 | return std::chrono::time_point::min(); 101 | } 102 | keyframe_timer_.cancel(); 103 | stop_timer_.cancel(); 104 | const auto now = std::chrono::system_clock::now(); 105 | auto file_header = file_header_.getRoot(); 106 | file_header.setStopTime( 107 | std::chrono::duration_cast( 108 | now.time_since_epoch()).count()); 109 | WriteFileHeader(); 110 | file_stream_.close(); 111 | static_cast(*this).OnRecordingStopped(now); 112 | filename_ = ""; 113 | return now; 114 | } 115 | 116 | [[nodiscard]] 117 | bool IsRecording() const { 118 | return file_stream_.is_open() && file_stream_.good(); 119 | } 120 | 121 | [[nodiscard]] 122 | std::string_view GetFilename() const { 123 | return filename_; 124 | } 125 | 126 | void WriteMessage(SocketMessage& message) { 127 | auto collab_vm_message = message.GetRoot(); 128 | if (!IsRecording() 129 | || !ShouldRecordMessage(collab_vm_message)) { 130 | return; 131 | } 132 | IncludeTimestamp(collab_vm_message); 133 | message.CreateFrame(); 134 | for (auto&& buffer : message.GetBuffers()) { 135 | file_stream_.write( 136 | static_cast(buffer.data()), buffer.size()); 137 | } 138 | } 139 | 140 | void WriteMessage(capnp::MessageBuilder& message_builder) { 141 | auto message = message_builder.getRoot(); 142 | if (!IsRecording() 143 | || !ShouldRecordMessage(message)) { 144 | return; 145 | } 146 | auto output_stream = kj::std::StdOutputStream(file_stream_); 147 | IncludeTimestamp(message); 148 | capnp::writeMessage(output_stream, message_builder); 149 | } 150 | 151 | 152 | void WriteMessage( 153 | Guacamole::GuacClientInstruction::Reader guacamole_instruction) { 154 | if (!IsRecording() || !settings_.getCaptureInput()) { 155 | return; 156 | } 157 | if (guacamole_instruction.which() == Guacamole::GuacClientInstruction::MOUSE) { 158 | auto message_builder = capnp::MallocMessageBuilder(); 159 | auto server_mouse = message_builder 160 | .initRoot() 161 | .initMessage() 162 | .initGuacInstr() 163 | .initMouse(); 164 | auto client_mouse = guacamole_instruction.getMouse(); 165 | server_mouse.setX(client_mouse.getX()); 166 | server_mouse.setY(client_mouse.getY()); 167 | server_mouse.setButtonMask(client_mouse.getButtonMask()); 168 | server_mouse.setTimestamp( 169 | std::chrono::duration_cast( 170 | std::chrono::system_clock::now().time_since_epoch()).count()); 171 | WriteMessage(message_builder); 172 | } else if (guacamole_instruction.which() == Guacamole::GuacClientInstruction::KEY) { 173 | auto message_builder = capnp::MallocMessageBuilder(); 174 | auto client_key = guacamole_instruction.getKey(); 175 | auto server_key = message_builder 176 | .initRoot() 177 | .initMessage() 178 | .initGuacInstr() 179 | .initKey(); 180 | server_key.setKeysym(client_key.getKeysym()); 181 | server_key.setPressed(client_key.getPressed()); 182 | server_key.setTimestamp( 183 | std::chrono::duration_cast( 184 | std::chrono::system_clock::now().time_since_epoch()).count()); 185 | WriteMessage(message_builder); 186 | } 187 | } 188 | 189 | private: 190 | [[nodiscard]] 191 | bool ShouldRecordMessage(CollabVmServerMessage::Message::Reader message) { 192 | if (message.which() != CollabVmServerMessage::Message::GUAC_INSTR) { 193 | return true; 194 | } 195 | auto guacamole_instruction = message.getGuacInstr(); 196 | switch (guacamole_instruction.which()) { 197 | case Guacamole::GuacServerInstruction::SYNC: 198 | return settings_.getCaptureDisplay() 199 | || settings_.getCaptureInput() 200 | || settings_.getCaptureAudio(); 201 | case Guacamole::GuacServerInstruction::BLOB: 202 | return !ignored_streams_.count(guacamole_instruction.getBlob().getStream()); 203 | case Guacamole::GuacServerInstruction::END: 204 | return !ignored_streams_.erase(guacamole_instruction.getEnd()); 205 | case Guacamole::GuacServerInstruction::AUDIO: 206 | if (settings_.getCaptureAudio()) { 207 | return true; 208 | } 209 | ignored_streams_.insert(guacamole_instruction.getAudio().getStream()); 210 | return false; 211 | case Guacamole::GuacServerInstruction::MOUSE: 212 | case Guacamole::GuacServerInstruction::KEY: 213 | return settings_.getCaptureInput(); 214 | default: 215 | if (!settings_.getCaptureDisplay()) { 216 | // Assume all other instructions are display-related 217 | return false; 218 | } 219 | break; 220 | } 221 | return true; 222 | } 223 | 224 | void IncludeTimestamp(CollabVmServerMessage::Message::Reader message) { 225 | switch (message.which()) { 226 | case CollabVmServerMessage::Message::ADMIN_USER_LIST_ADD: 227 | case CollabVmServerMessage::Message::USER_LIST_REMOVE: 228 | case CollabVmServerMessage::Message::CHANGE_USERNAME: 229 | case CollabVmServerMessage::Message::VM_TURN_INFO: 230 | case CollabVmServerMessage::Message::VOTE_RESULT: 231 | { 232 | timestamp_message_builder 233 | .initRoot() 234 | .setRecordingTimestamp( 235 | std::chrono::duration_cast( 236 | std::chrono::system_clock::now().time_since_epoch()).count()); 237 | auto output_stream = kj::std::StdOutputStream(file_stream_); 238 | capnp::writeMessage(output_stream, timestamp_message_builder); 239 | } 240 | default: 241 | break; 242 | } 243 | } 244 | 245 | void StartKeyframeTimer() { 246 | ignored_streams_.clear(); 247 | static_cast(*this).OnKeyframeInRecording(); 248 | 249 | if (!keyframe_interval_.count()) { 250 | keyframe_timer_.cancel(); 251 | return; 252 | } 253 | keyframe_timer_.expires_after(keyframe_interval_); 254 | keyframe_timer_.async_wait([this](const auto error_code) { 255 | if (error_code) { 256 | return; 257 | } 258 | auto file_header = file_header_.getRoot(); 259 | auto keyframe_offsets = file_header.getKeyframes(); 260 | if (next_keyframe_offset_ == keyframe_offsets.end()) { 261 | Start(); 262 | return; 263 | } 264 | 265 | auto keyframe = 266 | keyframe_offsets[next_keyframe_offset_ - keyframe_offsets.begin()]; 267 | keyframe.setFileOffset(file_stream_.tellp()); 268 | keyframe.setTimestamp( 269 | std::chrono::duration_cast( 270 | std::chrono::system_clock::now().time_since_epoch()).count()); 271 | ++next_keyframe_offset_; 272 | file_header.setKeyframesCount(file_header.getKeyframesCount() + 1); 273 | WriteFileHeader(); 274 | StartKeyframeTimer(); 275 | }); 276 | }; 277 | 278 | void WriteFileHeader() { 279 | const auto original_position = file_stream_.tellp(); 280 | file_stream_.seekp(0); 281 | auto output_stream = kj::std::StdOutputStream(file_stream_); 282 | capnp::writeMessage(output_stream, file_header_); 283 | if (original_position) { 284 | file_stream_.seekp(original_position); 285 | } 286 | } 287 | 288 | static std::string GetCurrentDateTime() { 289 | const auto now = std::time(nullptr); 290 | char buffer[64]; 291 | std::strftime(buffer, sizeof(buffer), "%Y-%m-%d_%I-%M-%S_%p", std::localtime(&now)); 292 | return buffer; 293 | } 294 | 295 | const std::uint32_t vm_id_; 296 | std::ofstream file_stream_; 297 | capnp::MallocMessageBuilder file_header_; 298 | capnp::List::Builder::Iterator next_keyframe_offset_; 299 | boost::asio::steady_timer stop_timer_; 300 | boost::asio::steady_timer keyframe_timer_; 301 | std::chrono::minutes file_duration_ = std::chrono::minutes::zero(); 302 | std::chrono::seconds keyframe_interval_ = std::chrono::seconds::zero(); 303 | std::string filename_; 304 | capnp::MallocMessageBuilder settings_message_builder_; 305 | ServerSetting::Recordings::Reader settings_ = settings_message_builder_.initRoot(); 306 | capnp::MallocMessageBuilder timestamp_message_builder; 307 | std::unordered_set ignored_streams_; 308 | static constexpr std::string_view recordings_directory = "./recordings/"; 309 | }; 310 | } 311 | -------------------------------------------------------------------------------- /ReusableSocketMessage.hpp: -------------------------------------------------------------------------------- 1 | struct ReusableSocketMessage : SocketMessage { 2 | struct CountedMessage { 3 | capnp::MallocMessageBuilder message_builder; 4 | std::atomic read_count; 5 | }; 6 | struct message_ptr { 7 | CountedMessage& message_; 8 | message_ptr(CountedMessage& message) : message_(message) { 9 | message_.read_count++; 10 | } 11 | ~message_ptr() { 12 | if (!--message_.read_count) { 13 | delete &message_; 14 | } 15 | } 16 | }; 17 | 18 | CountedMessage* message_builder; 19 | std::queue> write_queue_; 20 | std::queue> read_queue_; 21 | bool writing; 22 | 23 | template 24 | void ReadMessage(TCallback&& callback) const { 25 | message_ptr message(*this); 26 | if (writing) { 27 | read_queue_.push(callback); 28 | } else { 29 | callback(message); 30 | } 31 | } 32 | 33 | void WriteMessage(TCallback&& callback) { 34 | if (writing) { 35 | write_queue_.push(callback); 36 | } else { 37 | if (message_builder->read_count) { 38 | message_builder = new CountedMessage(); 39 | } 40 | writing = true; 41 | callback(*message_builder); 42 | while (!write_queue_.empty()) { 43 | write_queue_.pop()(*message_builder); 44 | } 45 | writing = false; 46 | while (!read_queue_.empty()) { 47 | read_queue_.pop()(*message_builder); 48 | } 49 | } 50 | } 51 | 52 | capnp::MallocMessageBuilder& GetMessageBuilder() { 53 | if (message_builder.use_count() != 1) { 54 | message_builder = std::make_shared(*message_builder); 55 | } 56 | return *message_builder; 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /SharedStrandGuard.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | template 5 | struct StrandGuardBase : protected TBase 6 | { 7 | using TBase::TBase; 8 | 9 | template 10 | void dispatch(TCompletionHandler&& handler) { 11 | TBase::strand_.dispatch( 12 | [ this, handler = std::move(handler) ]() mutable { handler(TBase::obj_); }); 13 | } 14 | 15 | template 16 | void post(TCompletionHandler&& handler) { 17 | TBase::strand_.post( 18 | [ this, handler = std::move(handler) ]() mutable { handler(TBase::obj_); }); 19 | } 20 | 21 | template 22 | auto wrap(THandler&& handler) { 23 | return [ this, handler = std::move(handler) ](auto... args) mutable { 24 | dispatch([ handler = std::move(handler), args... ](auto&& obj) mutable { 25 | handler(obj, std::move(args)...); 26 | }); 27 | }; 28 | } 29 | 30 | bool running_in_this_thread() const { 31 | return TBase::strand_.running_in_this_thread(); 32 | } 33 | }; 34 | 35 | template 36 | struct StrandGuard2 { 37 | template 38 | explicit StrandGuard2(boost::asio::io_context& io_context, TArgs&&... args) 39 | : strand_(io_context), obj_(std::forward(args)...) {} 40 | 41 | struct SharedStrandGuard2 { 42 | template 43 | explicit SharedStrandGuard2(StrandGuard2& strand, TArgs&&... args) 44 | : strand_(strand.strand_), obj_(std::forward(args)...) {} 45 | 46 | TStrand& strand_; 47 | T obj_; 48 | }; 49 | using SharedStrandGuard = StrandGuardBase; 50 | 51 | TStrand strand_; 52 | T obj_; 53 | }; 54 | 55 | template 56 | using StrandGuard = StrandGuardBase>; 57 | 58 | /* 59 | template 60 | struct SharedStrandGuard 61 | { 62 | template 63 | explicit StrandGuard(StrandGuard<& shared_guard, TArgs&&... args) 64 | : strand_(io_context), obj_(std::forward(args)...) {} 65 | 66 | }; 67 | */ 68 | -------------------------------------------------------------------------------- /SocketMessage.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace CollabVm::Server { 7 | 8 | struct CopiedSocketMessage; 9 | struct SharedSocketMessage; 10 | 11 | struct SocketMessage : std::enable_shared_from_this 12 | { 13 | virtual ~SocketMessage() noexcept = default; 14 | 15 | virtual const std::vector& GetBuffers() const = 0; 16 | virtual void CreateFrame() = 0; 17 | virtual capnp::AnyPointer::Reader GetRoot() const = 0; 18 | template 19 | typename T::Reader GetRoot() const { 20 | return GetRoot().getAs(); 21 | } 22 | 23 | static std::shared_ptr CreateShared() { 24 | return std::make_shared(); 25 | } 26 | 27 | static std::shared_ptr CopyFromMessageBuilder( 28 | capnp::MessageBuilder& message_builder) { 29 | return std::make_shared(message_builder); 30 | } 31 | }; 32 | 33 | struct SharedSocketMessage final : SocketMessage 34 | { 35 | const std::vector& GetBuffers() const override { 36 | assert(!framed_buffers_.empty()); 37 | return framed_buffers_; 38 | } 39 | // An alternate implementation of capnp::messageToFlatArray() 40 | // that doesn't copy segment data 41 | void CreateFrame() override { 42 | if (!framed_buffers_.empty()) { 43 | return; 44 | } 45 | auto segments = shared_message_builder.getSegmentsForOutput(); 46 | const auto segment_count = segments.size(); 47 | const auto frame_size = (segment_count + 2) & ~size_t(1); 48 | frame_.reserve(frame_size); 49 | frame_.push_back(segment_count - 1); 50 | framed_buffers_.reserve(segment_count + 1); 51 | framed_buffers_.push_back({ 52 | frame_.data(), frame_size * sizeof(decltype(frame_)::value_type) 53 | }); 54 | for (auto segment : segments) { 55 | frame_.push_back(segment.size()); 56 | const auto segment_bytes = segment.asBytes(); 57 | framed_buffers_.push_back( 58 | { segment_bytes.begin(), segment_bytes.size() }); 59 | } 60 | if (segment_count % 2 == 0) { 61 | // Set padding byte 62 | frame_.push_back(0); 63 | } 64 | } 65 | 66 | capnp::AnyPointer::Reader GetRoot() const override { 67 | return const_cast( 68 | shared_message_builder).getRoot(); 69 | } 70 | 71 | ~SharedSocketMessage() noexcept override { } 72 | 73 | capnp::MessageBuilder& GetMessageBuilder() { 74 | assert(frame_.empty() && framed_buffers_.empty()); 75 | return shared_message_builder; 76 | } 77 | 78 | private: 79 | std::vector frame_; 80 | capnp::MallocMessageBuilder shared_message_builder; 81 | std::vector framed_buffers_; 82 | }; 83 | 84 | struct CopiedSocketMessage final : SocketMessage { 85 | CopiedSocketMessage(capnp::MessageBuilder& message_builder) 86 | : buffer_(capnp::messageToFlatArray(message_builder)), 87 | framed_buffers_( 88 | { boost::asio::const_buffer(buffer_.asBytes().begin(), 89 | buffer_.asBytes().size()) }), 90 | reader_(buffer_) { 91 | } 92 | 93 | ~CopiedSocketMessage() noexcept override { } 94 | 95 | const std::vector& GetBuffers() const override { 96 | return framed_buffers_; 97 | } 98 | void CreateFrame() override { 99 | } 100 | capnp::AnyPointer::Reader GetRoot() const override { 101 | return const_cast( 102 | reader_).getRoot(); 103 | } 104 | private: 105 | const kj::Array buffer_; 106 | const std::vector framed_buffers_; 107 | capnp::FlatArrayMessageReader reader_; 108 | }; 109 | 110 | } 111 | -------------------------------------------------------------------------------- /StrandGuard.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | template 5 | struct StrandGuard { 6 | template 7 | explicit StrandGuard(boost::asio::io_context& io_context, TArgs&&... args) 8 | : strand_(io_context), obj_(std::forward(args)...) {} 9 | 10 | static struct {} ConstructWithStrand; 11 | template 12 | explicit StrandGuard(boost::asio::io_context& io_context, decltype(ConstructWithStrand), TArgs&&... args) 13 | : strand_(io_context), obj_(strand_, std::forward(args)...) {} 14 | 15 | template 16 | void dispatch(TCompletionHandler&& handler) { 17 | boost::asio::dispatch(strand_, 18 | [ this, handler = std::forward(handler) ]() mutable { handler(obj_); }); 19 | } 20 | 21 | template 22 | void post(TCompletionHandler&& handler) { 23 | boost::asio::post(strand_, 24 | [ this, handler = std::forward(handler) ]() mutable { handler(obj_); }); 25 | } 26 | 27 | template 28 | auto wrap(THandler&& handler) { 29 | return boost::asio::bind_executor(strand_, 30 | [this, handler = std::forward(handler)](auto&&... args) mutable { 31 | handler(obj_, std::forward(args)...); 32 | }); 33 | } 34 | 35 | bool running_in_this_thread() const { 36 | return strand_.running_in_this_thread(); 37 | } 38 | private: 39 | TStrand strand_; 40 | T obj_; 41 | }; 42 | -------------------------------------------------------------------------------- /Totp.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // Implements RFC 6238 (TOTP) and RFC 4226 (HOTP) for SHA1 13 | namespace CollabVm::Server::Totp { 14 | inline int GenerateTotp( 15 | const gsl::span key, 16 | const int digits = 6, 17 | const std::chrono::seconds time_step = std::chrono::seconds(30), 18 | const int time_window = 0, 19 | const std::chrono::seconds timestamp = 20 | std::chrono::duration_cast( 21 | std::chrono::system_clock::now().time_since_epoch())) { 22 | const auto timer = 23 | boost::endian::native_to_big(timestamp / time_step - time_window); 24 | const auto digest = 25 | gsl::make_span(HMAC(EVP_sha1(), key.data(), key.size(), 26 | reinterpret_cast(&timer), 27 | sizeof(timer), nullptr, nullptr), 28 | SHA_DIGEST_LENGTH); 29 | constexpr unsigned long lowest_4_bits = (1u << 4) - 1; 30 | const int offset = digest[SHA_DIGEST_LENGTH - 1] & lowest_4_bits; 31 | constexpr unsigned long lowest_31_bits = (1u << 31) - 1; 32 | const int binary = boost::endian::endian_reverse( 33 | *reinterpret_cast(&digest[offset])) & 34 | lowest_31_bits; 35 | return binary % static_cast(std::pow(10, digits)); 36 | } 37 | 38 | inline bool ValidateTotp( 39 | const int input, 40 | const gsl::span key, 41 | const int digits = 6, 42 | const std::chrono::seconds time_step = std::chrono::seconds(30), 43 | const int time_window = 1) { 44 | const auto now = std::chrono::duration_cast( 45 | std::chrono::system_clock::now().time_since_epoch()); 46 | const auto counter = boost::counting_range(0, time_window + 1); 47 | return std::any_of( 48 | counter.begin(), counter.end(), [&](int current_time_window) { 49 | return GenerateTotp(key, digits, time_step, current_time_window, now) == 50 | input; 51 | }); 52 | } 53 | } // namespace CollabVm::Server::Totp 54 | -------------------------------------------------------------------------------- /TurnController.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | namespace CollabVm::Server { 9 | template 10 | class TurnController { 11 | boost::asio::steady_timer turn_timer_; 12 | typename decltype(turn_timer_)::duration turn_time_; 13 | std::deque turn_queue_; 14 | std::optional paused_time_; 15 | 16 | void UpdateCurrentTurn(typename decltype(turn_timer_)::duration time_remaining) 17 | { 18 | if (turn_queue_.empty()) 19 | { 20 | OnCurrentUserChanged(turn_queue_, std::chrono::milliseconds(0)); 21 | return; 22 | } 23 | 24 | if (!IsPaused()) 25 | { 26 | turn_timer_.expires_after(time_remaining); 27 | turn_timer_.async_wait( 28 | [this, current_user = *turn_queue_.begin()](auto ec) 29 | { 30 | if (!ec) 31 | { 32 | RemoveUser(current_user); 33 | } 34 | }); 35 | } 36 | OnCurrentUserChanged( 37 | turn_queue_, 38 | std::chrono::duration_cast(time_remaining)); 39 | } 40 | public: 41 | template 42 | explicit TurnController(TExecutionContext& context) : 43 | turn_timer_(context), 44 | turn_time_(0) 45 | { 46 | } 47 | 48 | [[nodiscard]] 49 | std::chrono::milliseconds GetTimeRemaining() const 50 | { 51 | return turn_queue_.empty() 52 | ? std::chrono::milliseconds(0) 53 | : std::chrono::duration_cast( 54 | turn_timer_.expiry() - std::chrono::steady_clock::now()); 55 | } 56 | 57 | [[nodiscard]] 58 | const std::deque& GetTurnQueue() const 59 | { 60 | return turn_queue_; 61 | } 62 | 63 | class UserTurnData 64 | { 65 | using TurnQueuePositionType = typename decltype(turn_queue_)::size_type; 66 | std::optional turn_queue_position_; 67 | friend class TurnController; 68 | }; 69 | 70 | auto GetCurrentUser() const 71 | { 72 | return turn_queue_.empty() 73 | ? std::optional() 74 | : turn_queue_.front(); 75 | } 76 | 77 | bool RequestTurn(TUserPtr user) 78 | { 79 | if (user->turn_queue_position_.has_value()) 80 | { 81 | return false; 82 | } 83 | turn_queue_.emplace_back(user); 84 | user->turn_queue_position_ = turn_queue_.size() - 1; 85 | 86 | if (user->turn_queue_position_ == 0) 87 | { 88 | UpdateCurrentTurn(turn_time_); 89 | } 90 | else 91 | { 92 | OnUserAdded(turn_queue_, GetTimeRemaining()); 93 | } 94 | return true; 95 | } 96 | 97 | bool RemoveUser(const TUserPtr& user) 98 | { 99 | if (!user->turn_queue_position_.has_value()) 100 | { 101 | return false; 102 | } 103 | const auto user_position = 104 | turn_queue_.begin() + user->turn_queue_position_.value(); 105 | // Remove the user and update the queue position for all users behind them 106 | for (auto after_removed = turn_queue_.erase(user_position); 107 | after_removed != turn_queue_.end(); after_removed++) 108 | { 109 | after_removed->get()->turn_queue_position_.value()--; 110 | } 111 | const auto old_position = user->turn_queue_position_.value(); 112 | user->turn_queue_position_.reset(); 113 | if (old_position == 0) 114 | { 115 | UpdateCurrentTurn(turn_time_); 116 | } 117 | else 118 | { 119 | OnUserRemoved(turn_queue_, GetTimeRemaining()); 120 | } 121 | return true; 122 | } 123 | 124 | template 125 | void SetTurnTime(TDuration time) 126 | { 127 | turn_time_ = time; 128 | } 129 | 130 | void PauseTurnTimer() 131 | { 132 | const auto time_remaining = GetTimeRemaining(); 133 | const auto was_running = turn_timer_.cancel(); 134 | paused_time_ = was_running && time_remaining.count() > 0 135 | ? time_remaining 136 | : std::chrono::duration_cast(turn_time_); 137 | UpdateCurrentTurn(paused_time_.value()); 138 | } 139 | 140 | bool IsPaused() const 141 | { 142 | return paused_time_.has_value(); 143 | } 144 | 145 | void ResumeTurnTimer() 146 | { 147 | if (IsPaused()) 148 | { 149 | const auto time_remaining = paused_time_.value(); 150 | paused_time_.reset(); 151 | UpdateCurrentTurn(time_remaining); 152 | } 153 | } 154 | 155 | void EndCurrentTurn() 156 | { 157 | if (turn_queue_.empty()) 158 | { 159 | return; 160 | } 161 | RemoveUser(turn_queue_.front()); 162 | } 163 | 164 | void Clear() 165 | { 166 | turn_timer_.cancel(); 167 | 168 | if (!turn_queue_.empty()) 169 | { 170 | for (auto& user : turn_queue_) 171 | { 172 | user->turn_queue_position_.reset(); 173 | } 174 | turn_queue_.clear(); 175 | OnCurrentUserChanged(turn_queue_, std::chrono::milliseconds(0)); 176 | } 177 | } 178 | 179 | protected: 180 | virtual void OnCurrentUserChanged( 181 | const std::deque& users, std::chrono::milliseconds time_remaining) = 0; 182 | virtual void OnUserAdded( 183 | const std::deque& users, std::chrono::milliseconds time_remaining) = 0; 184 | virtual void OnUserRemoved( 185 | const std::deque& users, std::chrono::milliseconds time_remaining) = 0; 186 | }; 187 | 188 | } 189 | -------------------------------------------------------------------------------- /UserChannel.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "IPData.hpp" 7 | 8 | namespace CollabVm::Server 9 | { 10 | template 13 | struct UserChannel 14 | { 15 | explicit UserChannel(const std::uint32_t id) : 16 | chat_room_(id) 17 | { 18 | } 19 | 20 | void Clear() 21 | { 22 | users_.clear(); 23 | } 24 | 25 | const auto& GetChatRoom() const 26 | { 27 | return chat_room_; 28 | } 29 | 30 | auto& GetChatRoom() 31 | { 32 | return chat_room_; 33 | } 34 | 35 | const auto& GetUsers() const 36 | { 37 | return users_; 38 | } 39 | 40 | template 41 | void ForEachUser(TCallback&& callback) { 42 | OnForEachUsers(callback); 43 | std::for_each( 44 | users_.begin(), 45 | users_.end(), 46 | [callback=std::forward(callback)] 47 | (auto& user) mutable { 48 | callback(user.second, *user.first); 49 | }); 50 | } 51 | 52 | template 53 | void OnForEachUsers(TCallback& callback) { 54 | if constexpr (!std::is_null_pointer_v) { 55 | if constexpr (!std::is_same_v< 56 | decltype(&UserChannel::OnForEachUsers), 57 | decltype(&TBase::template OnForEachUsers)>) { 58 | static_cast(*this).OnForEachUsers(callback); 59 | } 60 | } 61 | } 62 | 63 | void AddUser(TUserData&& initial_user_data, std::shared_ptr user) 64 | { 65 | OnAddUser(user); 66 | auto& ip_data = ip_data_[initial_user_data.ip_address]; 67 | ip_data.reference_count_++; 68 | auto& user_data = users_.emplace(user, UserData(std::move(initial_user_data), ip_data)).first->second; 69 | admins_count_ += !!(user_data.user_type == CollabVmServerMessage::UserType::ADMIN); 70 | user->QueueMessage( 71 | user_data.IsAdmin() 72 | ? CreateAdminUserListMessage() 73 | : CreateUserListMessage()); 74 | 75 | if (users_.size() <= 1) { 76 | return; 77 | } 78 | 79 | auto user_message = SocketMessage::CreateShared(); 80 | auto add_user = user_message->GetMessageBuilder() 81 | .initRoot() 82 | .initMessage() 83 | .initUserListAdd(); 84 | add_user.setChannel(GetId()); 85 | AddUserToList(user_data, add_user); 86 | 87 | auto admin_user_message = SocketMessage::CreateShared(); 88 | auto add_admin_user = admin_user_message->GetMessageBuilder() 89 | .initRoot() 90 | .initMessage() 91 | .initAdminUserListAdd(); 92 | add_admin_user.setChannel(GetId()); 93 | AddUserToList(user_data, add_admin_user.initUser()); 94 | 95 | ForEachUser([excluded_user = user.get(), user_message=std::move(user_message), 96 | admin_user_message=std::move(admin_user_message)] 97 | (const auto& user_data, TClient& user) 98 | { 99 | if (&user == excluded_user) { 100 | return; 101 | } 102 | user.QueueMessage( 103 | user_data.IsAdmin() ? admin_user_message : user_message); 104 | }); 105 | } 106 | 107 | void OnAddUser(std::shared_ptr user) { 108 | if constexpr (!std::is_same_v) { 109 | if constexpr (!std::is_same_v< 110 | decltype(&UserChannel::OnAddUser), 111 | decltype(&TBase::OnAddUser)>) { 112 | static_cast(*this).OnAddUser(user); 113 | } 114 | } 115 | } 116 | 117 | auto GetUserData(std::shared_ptr user_ptr) 118 | { 119 | return GetUserData(*this, user_ptr); 120 | } 121 | 122 | auto GetUserData(std::shared_ptr user_ptr) const 123 | { 124 | return GetUserData(*this, user_ptr); 125 | } 126 | 127 | void BroadcastMessage(std::shared_ptr&& message) { 128 | ForEachUser( 129 | [message = 130 | std::forward>(message)] 131 | (const auto&, auto& user) 132 | { 133 | user.QueueMessage(message); 134 | }); 135 | } 136 | 137 | auto CreateUserListMessage() { 138 | return CreateUserListMessages( 139 | &CollabVmServerMessage::Message::Builder::initUserList); 140 | } 141 | 142 | auto CreateAdminUserListMessage() { 143 | return CreateUserListMessages( 144 | &CollabVmServerMessage::Message::Builder::initAdminUserList); 145 | } 146 | 147 | void RemoveUser(std::shared_ptr user) 148 | { 149 | auto user_it = users_.find(user); 150 | if (user_it == users_.end()) { 151 | return; 152 | } 153 | auto& user_data = user_it->second; 154 | if (!--user_data.ip_data.reference_count_) { 155 | ip_data_.erase(user_data.ip_address); 156 | } 157 | admins_count_ -= !!(user_data.user_type == CollabVmServerMessage::UserType::ADMIN); 158 | 159 | auto message = SocketMessage::CreateShared(); 160 | auto user_list_remove = message->GetMessageBuilder() 161 | .initRoot() 162 | .initMessage() 163 | .initUserListRemove(); 164 | user_list_remove.setChannel(GetId()); 165 | user_list_remove.setUsername(user_data.username); 166 | 167 | OnRemoveUser(user); 168 | users_.erase(user_it); 169 | 170 | BroadcastMessage(std::move(message)); 171 | } 172 | 173 | void OnRemoveUser(std::shared_ptr user) { 174 | if constexpr (!std::is_same_v) { 175 | static_cast(*this).OnRemoveUser(user); 176 | } 177 | } 178 | 179 | std::uint32_t GetId() const 180 | { 181 | return chat_room_.GetId(); 182 | } 183 | 184 | private: 185 | template 186 | static auto GetUserData(TUserChannel& user_channel, std::shared_ptr user_ptr) 187 | { 188 | static_assert(std::is_same_v< 189 | std::remove_const_t, UserChannel>); 190 | using UserData = std::conditional_t< 191 | std::is_const_v, const UserData, UserData>; 192 | auto& users_ = user_channel.users_; 193 | auto user = users_.find(user_ptr); 194 | return user == users_.end() 195 | ? std::optional>() 196 | : std::optional>(user->second); 197 | } 198 | 199 | template 200 | auto CreateUserListMessages(TInitFunction init) 201 | { 202 | auto message = SocketMessage::CreateShared(); 203 | auto user_list = (message->GetMessageBuilder() 204 | .initRoot() 205 | .initMessage() 206 | .*init)(); 207 | user_list.setChannel(GetId()); 208 | auto users = user_list.initUsers(users_.size()); 209 | ForEachUser( 210 | [this, users_it = users.begin()](auto& user_data, TClient&) mutable 211 | { 212 | AddUserToList(user_data, *users_it++); 213 | }); 214 | return message; 215 | } 216 | 217 | template 218 | void AddUserToList(const TUserData& user, TListElement list_info) 219 | { 220 | auto& username = user.username; 221 | list_info.setUsername( 222 | kj::StringPtr(username.data(), username.length())); 223 | list_info.setUserType(user.user_type); 224 | 225 | if constexpr ( 226 | std::is_same_v) 227 | { 228 | auto ip_address = list_info.initIpAddress(); 229 | const auto& ip_address_bytes = user.ip_address; 230 | ip_address.setFirst( 231 | boost::endian::native_to_big( 232 | *reinterpret_cast(&ip_address_bytes[0]))); 233 | ip_address.setSecond( 234 | boost::endian::native_to_big( 235 | *reinterpret_cast(&ip_address_bytes[8]))); 236 | } 237 | } 238 | 239 | struct ChannelIPData : IPData::ChannelData 240 | { 241 | std::size_t reference_count_ = 0; 242 | }; 243 | 244 | struct UserData : TUserData 245 | { 246 | UserData(TUserData&& user_data, ChannelIPData& ip_data) 247 | : TUserData(std::move(user_data)), 248 | ip_data(ip_data) 249 | { 250 | } 251 | ChannelIPData& ip_data; 252 | }; 253 | 254 | std::unordered_map, UserData> users_; 255 | std::unordered_map< 256 | typename TClient::IpAddress::IpBytes, 257 | ChannelIPData, 258 | boost::hash 259 | > ip_data_; 260 | std::uint32_t admins_count_ = 0; 261 | CollabVmChatRoom chat_room_; 264 | capnp::MallocMessageBuilder message_builder_; 265 | }; 266 | } 267 | -------------------------------------------------------------------------------- /Utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | // Taken from GNU Cgicc 7 | /* 8 | From the HTML standard: 9 | 10 | application/x-www-form-urlencoded 11 | This is the default content type. Forms submitted with this content 12 | type must be encoded as follows: 13 | 1. Control names and values are escaped. Space characters are 14 | replaced by `+', and then reserved characters are escaped as 15 | described in [RFC1738], section 2.2: Non-alphanumeric characters 16 | are replaced by `%HH', a percent sign and two hexadecimal digits 17 | representing the ASCII code of the character. Line breaks are 18 | represented as "CR LF" pairs (i.e., `%0D%0A'). 19 | 2. The control names/values are listed in the order they appear in 20 | the document. The name is separated from the value by `=' and 21 | name/value pairs are separated from each other by `&'. 22 | Note RFC 1738 is obsoleted by RFC 2396. Basically it says to 23 | escape out the reserved characters in the standard %xx format. It 24 | also says this about the query string: 25 | 26 | query = *uric 27 | uric = reserved | unreserved | escaped 28 | reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | 29 | "$" | "," 30 | unreserved = alphanum | mark 31 | mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | 32 | "(" | ")" 33 | escaped = "%" hex hex */ 34 | 35 | std::string form_urlencode(const std::string& src) { 36 | std::ostringstream result; 37 | result.fill('0'); 38 | result << std::hex; 39 | 40 | for (const auto& c : src) { 41 | switch (c) { 42 | case ' ': 43 | result << '+'; 44 | break; 45 | // alnum 46 | case 'A': 47 | case 'B': 48 | case 'C': 49 | case 'D': 50 | case 'E': 51 | case 'F': 52 | case 'G': 53 | case 'H': 54 | case 'I': 55 | case 'J': 56 | case 'K': 57 | case 'L': 58 | case 'M': 59 | case 'N': 60 | case 'O': 61 | case 'P': 62 | case 'Q': 63 | case 'R': 64 | case 'S': 65 | case 'T': 66 | case 'U': 67 | case 'V': 68 | case 'W': 69 | case 'X': 70 | case 'Y': 71 | case 'Z': 72 | case 'a': 73 | case 'b': 74 | case 'c': 75 | case 'd': 76 | case 'e': 77 | case 'f': 78 | case 'g': 79 | case 'h': 80 | case 'i': 81 | case 'j': 82 | case 'k': 83 | case 'l': 84 | case 'm': 85 | case 'n': 86 | case 'o': 87 | case 'p': 88 | case 'q': 89 | case 'r': 90 | case 's': 91 | case 't': 92 | case 'u': 93 | case 'v': 94 | case 'w': 95 | case 'x': 96 | case 'y': 97 | case 'z': 98 | case '0': 99 | case '1': 100 | case '2': 101 | case '3': 102 | case '4': 103 | case '5': 104 | case '6': 105 | case '7': 106 | case '8': 107 | case '9': 108 | // mark 109 | case '-': 110 | case '_': 111 | case '.': 112 | case '!': 113 | case '~': 114 | case '*': 115 | case '\'': 116 | case '(': 117 | case ')': 118 | result << c; 119 | break; 120 | // escape 121 | default: 122 | result << std::uppercase; 123 | result << '%' << std::setw(2) << int((unsigned char)c); 124 | result << std::nouppercase; 125 | break; 126 | } 127 | } 128 | 129 | return result.str(); 130 | } -------------------------------------------------------------------------------- /VoteController.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace CollabVm::Server { 6 | struct UserVoteData 7 | { 8 | enum class VoteDecision 9 | { 10 | kUndecided, 11 | kYes, 12 | kNo 13 | } last_vote = VoteDecision::kUndecided; 14 | int voted_amount = 0; 15 | bool voted_limit = false; 16 | }; 17 | 18 | template 19 | class VoteController { 20 | boost::asio::steady_timer vote_timer_; 21 | 22 | enum class VoteState 23 | { 24 | kIdle, 25 | kVoting, 26 | kCoolingdown 27 | } vote_state_ = VoteState::kIdle; 28 | std::uint32_t yes_vote_count_ = 0; 29 | std::uint32_t no_vote_count_ = 0; 30 | 31 | public: 32 | template 33 | explicit VoteController(TExecutionContext& context) : 34 | vote_timer_(context) 35 | { 36 | } 37 | 38 | [[nodiscard]] 39 | bool IsCoolingDown() const 40 | { 41 | return vote_state_ == VoteState::kCoolingdown; 42 | } 43 | 44 | [[nodiscard]] 45 | std::chrono::milliseconds GetTimeRemaining() const 46 | { 47 | const auto expiry = vote_timer_.expiry(); 48 | const auto now = std::chrono::steady_clock::now(); 49 | return vote_state_ == VoteState::kVoting && expiry > now 50 | ? std::chrono::duration_cast< 51 | std::chrono::milliseconds>(expiry - now) 52 | : std::chrono::milliseconds::zero(); 53 | } 54 | 55 | [[nodiscard]] 56 | std::uint32_t GetYesVoteCount() const 57 | { 58 | return yes_vote_count_; 59 | } 60 | 61 | [[nodiscard]] 62 | std::uint32_t GetNoVoteCount() const 63 | { 64 | return no_vote_count_; 65 | } 66 | 67 | // Returns true when the vote was counted 68 | bool AddVote(UserVoteData& data, bool voted_yes) { 69 | const auto votes_enabled = static_cast(*this).GetVotesEnabled(); 70 | if (!votes_enabled) { 71 | return false; 72 | } 73 | switch (vote_state_) 74 | { 75 | case VoteState::kIdle: 76 | { 77 | if (!voted_yes) 78 | { 79 | // First vote must be a yes 80 | return false; 81 | } 82 | // Start a new vote 83 | vote_state_ = VoteState::kVoting; 84 | data.last_vote = voted_yes ? UserVoteData::VoteDecision::kYes : UserVoteData::VoteDecision::kNo; 85 | yes_vote_count_ = 1; 86 | no_vote_count_ = 0; 87 | const auto vote_time = static_cast(*this).GetVoteTime(); 88 | vote_timer_.expires_after(vote_time); 89 | vote_timer_.async_wait( 90 | [this](const auto ec) 91 | { 92 | if (ec || !static_cast(*this).GetVotesEnabled()) { 93 | vote_state_ = VoteState::kIdle; 94 | static_cast(*this).OnVoteIdle(); 95 | static_cast(*this).OnVoteEnd(false); 96 | return; 97 | } 98 | 99 | const auto cooldown_time = static_cast(*this).GetVoteCooldownTime(); 100 | if (cooldown_time.count()) 101 | { 102 | vote_state_ = VoteState::kCoolingdown; 103 | vote_timer_.expires_after(cooldown_time); 104 | vote_timer_.async_wait([this](const auto ec) 105 | { 106 | vote_state_ = VoteState::kIdle; 107 | static_cast(*this).OnVoteIdle(); 108 | }); 109 | } 110 | else 111 | { 112 | vote_state_ = VoteState::kIdle; 113 | static_cast(*this).OnVoteIdle(); 114 | } 115 | 116 | const auto vote_passed = yes_vote_count_ >= no_vote_count_; 117 | static_cast(*this).OnVoteEnd(vote_passed); 118 | }); 119 | 120 | static_cast(*this).OnVoteStart(); 121 | return true; 122 | } 123 | case VoteState::kVoting: 124 | { 125 | if (data.voted_limit || ++data.voted_amount >= Common::vote_limit) { 126 | data.voted_limit = true; 127 | return false; 128 | } 129 | 130 | const auto prev_vote = data.last_vote; 131 | const auto vote_decision = voted_yes ? UserVoteData::VoteDecision::kYes : UserVoteData::VoteDecision::kNo; 132 | if (prev_vote == vote_decision) { 133 | // The user's vote hasn't changed 134 | return false; 135 | } 136 | (voted_yes ? yes_vote_count_ : no_vote_count_)++; 137 | if (prev_vote != UserVoteData::VoteDecision::kUndecided) { 138 | (voted_yes ? no_vote_count_ : yes_vote_count_)--; 139 | } 140 | data.last_vote = vote_decision; 141 | return true; 142 | } 143 | case VoteState::kCoolingdown: 144 | default: 145 | break; 146 | } 147 | return false; 148 | } 149 | 150 | bool RemoveVote(UserVoteData& data) { 151 | if (data.last_vote == UserVoteData::VoteDecision::kUndecided) { 152 | return false; 153 | } 154 | --(data.last_vote == UserVoteData::VoteDecision::kYes 155 | ? yes_vote_count_ 156 | : no_vote_count_); 157 | data.last_vote = UserVoteData::VoteDecision::kUndecided; 158 | return true; 159 | } 160 | 161 | void StopVote() { 162 | vote_timer_.cancel(); 163 | } 164 | }; 165 | 166 | } 167 | -------------------------------------------------------------------------------- /WebSocketServer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include "StrandGuard.hpp" 22 | // #include "file_body.hpp" 23 | 24 | namespace CollabVm::Server { 25 | namespace asio = boost::asio; 26 | namespace beast = boost::beast; 27 | 28 | template 29 | class WebServerSocket : public std::enable_shared_from_this< 30 | WebServerSocket> { 31 | public: 32 | WebServerSocket(asio::io_context& io_context, 33 | const std::filesystem::path& doc_root) 34 | : socket_(io_context, io_context), 35 | request_deadline_(io_context, 36 | std::chrono::steady_clock::time_point::max()), 37 | doc_root_(doc_root) {} 38 | 39 | virtual ~WebServerSocket() noexcept = default; 40 | 41 | void Start() { 42 | socket_.dispatch([ this, 43 | self = this->shared_from_this() ](auto& socket) mutable { 44 | boost::system::error_code ec; 45 | ip_address_ = socket.socket.lowest_layer().remote_endpoint(ec).address(); 46 | if (ec) { 47 | Close(); 48 | return; 49 | } 50 | socket.socket.set_option(asio::ip::tcp::no_delay(true), ec); 51 | ReadHttpRequest(std::move(self)); 52 | }); 53 | } 54 | 55 | class MessageBuffer : public std::enable_shared_from_this 56 | { 57 | public: 58 | MessageBuffer() = default; 59 | MessageBuffer(const MessageBuffer&) = delete; 60 | virtual ~MessageBuffer() noexcept = default; 61 | 62 | private: 63 | friend class WebServerSocket; 64 | virtual void StartRead(std::shared_ptr&& socket) = 0; 65 | }; 66 | 67 | virtual std::shared_ptr CreateMessageBuffer() = 0; 68 | 69 | template 70 | void ReadWebSocketMessage(std::shared_ptr&& self, 71 | std::shared_ptr&& buffer_ptr) { 72 | socket_.dispatch([this, self = std::move(self), 73 | buffer_ptr = std::move(buffer_ptr)](auto& socket) mutable { 74 | auto& buffer = buffer_ptr->GetBuffer(); 75 | socket.websocket.async_read( 76 | buffer, socket_.wrap([ 77 | this, self = std::move(self), buffer_ptr = std::move(buffer_ptr) 78 | ](auto& sockets, const boost::system::error_code& ec, 79 | std::size_t bytes_transferred) mutable { 80 | if (ec) { 81 | Close(); 82 | return; 83 | } 84 | OnMessage(std::move(buffer_ptr)); 85 | CreateMessageBuffer()->StartRead(std::move(self)); 86 | })); 87 | }); 88 | } 89 | 90 | template 91 | bool SendFileResponse(std::shared_ptr& self, TSockets& sockets, const TRequest& request, std::filesystem::path path) { 92 | // Verify that the path exists within the doc root and is not a directory 93 | auto err = std::error_code(); 94 | path = std::filesystem::canonical(doc_root_ / path, err); 95 | if (err || path.compare(doc_root_) < 0 || 96 | !std::equal(doc_root_.begin(), doc_root_.end(), path.begin())) { 97 | return false; 98 | } 99 | auto status = std::filesystem::status(path, err); 100 | if (err || status.type() == std::filesystem::file_type::directory) { 101 | return false; 102 | } 103 | 104 | auto file_open_error = boost::system::error_code(); 105 | auto file = beast::http::file_body::value_type(); 106 | auto path_string = path.string(); 107 | file.open(path_string.c_str(), beast::file_mode::read, file_open_error); 108 | if (file_open_error) { 109 | return false; 110 | } 111 | auto resp = beast::http::response(); 112 | resp.result(beast::http::status::ok); 113 | resp.version(request.version()); 114 | resp.set(beast::http::field::server, "collab-vm-server"); 115 | resp.set(beast::http::field::content_type, mime_type(path_string)); 116 | resp.body() = std::move(file); 117 | try { 118 | // prepare calls FileBody::write::init() which could fail 119 | // and cause an exception to be thrown 120 | resp.prepare_payload(); 121 | response_ = std::move(resp); 122 | 123 | serializer_.emplace>( 124 | std::get>(response_)); 126 | beast::http::async_write( 127 | sockets.socket, 128 | std::get>(serializer_), 129 | socket_.wrap([ this, self = std::move(self) ]( 130 | auto& sockets, 131 | const boost::system::error_code ec, 132 | std::size_t bytes_transferred) mutable { 133 | std::get< 134 | beast::http::response>( 135 | response_) 136 | .body() 137 | .close(); 138 | if (!ec) { 139 | ReadHttpRequest(std::move(self)); 140 | } 141 | })); 142 | } catch (const boost::system::system_error&) { 143 | return false; 144 | } 145 | return true; 146 | } 147 | 148 | void ReadHttpRequest(std::shared_ptr&& self) { 149 | // Request must be fully processed within 60 seconds. 150 | request_deadline_.expires_after(std::chrono::seconds(60)); 151 | 152 | socket_.dispatch([ this, self = std::move(self) ](auto& socket) { 153 | buffer_.clear(); 154 | // Destruct and reconstruct the parser 155 | ([](auto& response) { 156 | using T = std::remove_reference_t; 157 | response.~T(); 158 | new (&response) T; 159 | })(parser_); 160 | 161 | beast::http::async_read_header( 162 | socket.socket, buffer_, parser_, 163 | socket_.wrap([ this, self = std::move(self) ]( 164 | auto& sockets, boost::system::error_code ec, 165 | std::size_t bytes_transferred) mutable { 166 | if (ec) { 167 | return; 168 | } 169 | auto& request = parser_.get(); 170 | // Try to get the client's actual IP from the headers 171 | if (sockets.socket.lowest_layer().remote_endpoint(ec).address().is_loopback()) { 172 | const auto ip_address_str = GetIpAddressFromHeader(request); 173 | if (!ip_address_str.empty()) { 174 | auto error_code = boost::system::error_code(); 175 | const auto ip_address = boost::asio::ip::make_address(ip_address_str, error_code); 176 | if (!error_code) { 177 | ip_address_ = ip_address; 178 | } 179 | } 180 | } 181 | 182 | if (request.method() == beast::http::verb::get) { 183 | // Accept WebSocket connections 184 | if (request.target() == "/") { 185 | const auto connection_header = 186 | request.find(beast::http::field::connection); 187 | if (connection_header != request.end() && 188 | beast::http::token_list(connection_header->value()) 189 | .exists("upgrade")) { 190 | const auto upgrade_header = 191 | request.find(beast::http::field::upgrade); 192 | if (upgrade_header != request.end() && 193 | beast::http::token_list(upgrade_header->value()) 194 | .exists("websocket")) { 195 | buffer_.consume(buffer_.size()); 196 | OnPreConnect(); 197 | return; 198 | } 199 | } 200 | } 201 | 202 | // Serve static content from doc root 203 | std::filesystem::path path(request.target().substr(1)); 204 | // Disallow relative paths 205 | if (std::none_of(path.begin(), path.end(), [](const auto& e) { 206 | return e == ".." || e == "."; 207 | })) { 208 | // First try the path without modifying it 209 | if ((!path.empty() && (SendFileResponse(self, sockets, request, path) 210 | // Then try appending .html to the first part 211 | || SendFileResponse(self, sockets, request, (path = *path.begin(), path += ".html")))) 212 | // Default to index.html if the previous attempts failed 213 | || SendFileResponse(self, sockets, request, "index.html")) { 214 | return; 215 | } 216 | } 217 | 218 | // Send 404 response 219 | auto resp = beast::http::response(); 220 | resp.result(beast::http::status::not_found); 221 | resp.version(request.version()); 222 | resp.set(beast::http::field::server, "collab-vm-server"); 223 | resp.set(beast::http::field::content_type, "text/html"); 224 | resp.body() = "The file '" + std::string(request.target()) + "' was not found"; 225 | resp.prepare_payload(); 226 | response_ = std::move(resp); 227 | 228 | serializer_.emplace>( 229 | std::get>( 230 | response_)); 231 | beast::http::async_write( 232 | sockets.socket, 233 | std::get>(serializer_), 234 | socket_.wrap([ this, self = std::move(self) ]( 235 | auto& sockets, const boost::system::error_code ec, 236 | std::size_t bytes_transferred) mutable { 237 | if (!ec) { 238 | ReadHttpRequest(std::move(self)); 239 | } 240 | })); 241 | 242 | return; 243 | } else if (request.method() == beast::http::verb::post) { 244 | // File uploads 245 | // RFC 2616 § 8.2.2 requires clients to stop sending a message 246 | // body when an error response is received, but most browsers 247 | // don't comply with it 248 | if (request.target() == "/upload") { 249 | if (boost::iequals(request[beast::http::field::content_type], 250 | "application/octet-stream")) { 251 | const auto content_length = 252 | request.find(beast::http::field::content_length); 253 | if (content_length != request.end()) { 254 | unsigned long len = std::strtoul( 255 | content_length->value().data(), nullptr, 10); 256 | } 257 | } 258 | } 259 | 260 | // Disconnect socket to prevent data from being received 261 | auto err = boost::system::error_code(); 262 | sockets.socket.close(err); 263 | return; 264 | } 265 | 266 | // Send 405 (Method Not Allowed) 267 | auto resp = beast::http::response(); 268 | resp.result(beast::http::status::method_not_allowed); 269 | resp.version(request.version()); 270 | resp.set(beast::http::field::server, "collab-vm-server"); 271 | resp.set(beast::http::field::content_type, "text/html"); 272 | resp.body() = "The method '" + std::string(request.method_string()) + "' is not allowed"; 273 | resp.prepare_payload(); 274 | response_ = std::move(resp); 275 | 276 | serializer_.emplace>( 277 | std::get>( 278 | response_)); 279 | beast::http::async_write( 280 | sockets.socket, 281 | std::get>(serializer_), 282 | socket_.wrap([ this, self = std::move(self) ]( 283 | auto& sockets, const boost::system::error_code ec, 284 | std::size_t bytes_transferred) mutable { 285 | if (!ec) { 286 | ReadHttpRequest(std::move(self)); 287 | } 288 | })); 289 | })); 290 | }); 291 | } 292 | 293 | /*template 294 | beast::async_return_type 295 | async_write(WriteHandler&& handler) 296 | { 297 | auto self = shared_from_this(); 298 | return beast::http::async_write(socket_, serializer_, 299 | [this, self, handler](const boost::system::error_code& ec) 300 | { 301 | handler(ec); 302 | if (!ec) 303 | { 304 | //typedef 305 | beast::http::response_serializer serializer; 306 | //(&serializer_)->~serializer(); 307 | //new(&serializer_) 308 | beast::http::response_serializer(response_); 309 | DoRead(); 310 | } 311 | }); 312 | }*/ 313 | 314 | void read_body() { 315 | buffer_.consume(buffer_.size()); 316 | beast::http::async_read( 317 | socket_, buffer_, parser_, 318 | [ this, self = this->shared_from_this() ]( 319 | const boost::system::error_code ec, std::size_t bytes_transferred){ 320 | // if (ec) 321 | // accept(); 322 | // else 323 | // process_request(parser_.get()); 324 | }); 325 | } 326 | 327 | template 328 | void WriteMessage(ConstBufferSequence&& buffers, 329 | WriteHandler&& handler) { 330 | socket_.dispatch([ 331 | self = this->shared_from_this(), 332 | buffers = std::forward(buffers), 333 | handler = std::forward(handler) 334 | ](auto& sockets) mutable { 335 | sockets.websocket.async_write( 336 | std::forward(buffers), 337 | std::forward(handler)); 338 | }); 339 | } 340 | 341 | void Close() { 342 | socket_.post([ this, self = this->shared_from_this() ](auto& sockets) { 343 | auto ec = boost::system::error_code(); 344 | sockets.socket.shutdown( 345 | asio::ip::tcp::socket::shutdown_type::shutdown_both, ec); 346 | sockets.socket.close(ec); 347 | if (close_callback_) { 348 | close_callback_(); 349 | close_callback_ = nullptr; 350 | OnDisconnect(); 351 | } 352 | }); 353 | } 354 | 355 | struct IpAddress { 356 | using IpBytes = std::array; 357 | 358 | IpAddress() = default; 359 | IpAddress(const boost::asio::ip::address& ip_address) 360 | : str_(ip_address.to_string()) { 361 | if (ip_address.is_v4()) { 362 | bytes_ = GetIpv4MappedBytes(ip_address.to_v4()); 363 | } else { 364 | CopyIpAddressBytes(ip_address.to_v6(), bytes_.begin()); 365 | } 366 | } 367 | 368 | const std::string& AsString() const { return str_; } 369 | const IpBytes& AsBytes() const { return bytes_; } 370 | auto AsVector() const 371 | { 372 | return std::vector(bytes_.begin(), bytes_.end()); 373 | } 374 | 375 | private: 376 | // Creates an IPv4-mapped IPv6 address as described in section 2.5.5.2 of 377 | // RFC4291 378 | static IpBytes GetIpv4MappedBytes( 379 | const boost::asio::ip::address_v4& ip_address) { 380 | auto bytes = IpBytes(); 381 | bytes[10] = std::byte(0xFF); 382 | bytes[11] = std::byte(0xFF); 383 | CopyIpAddressBytes(ip_address, bytes.begin() + 12); 384 | return bytes; 385 | } 386 | 387 | template 388 | static void CopyIpAddressBytes(const TIpAddress& ip_address, 389 | TDestination dest) { 390 | const auto bytes = ip_address.to_bytes(); 391 | std::transform(bytes.begin(), bytes.end(), dest, 392 | [](auto byte) { 393 | return typename std::iterator_traits::value_type(byte); 394 | }); 395 | } 396 | 397 | IpBytes bytes_; 398 | std::string str_; 399 | }; 400 | 401 | const IpAddress& GetIpAddress() { return ip_address_; } 402 | 403 | // Return a reasonable mime type based on the extension of a file. 404 | std::string_view mime_type(const std::string_view path) { 405 | using boost::beast::iequals; 406 | const auto ext = [&path] { 407 | const auto pos = path.rfind("."); 408 | if (pos == std::string_view::npos) 409 | return std::string_view{}; 410 | return path.substr(pos); 411 | }(); 412 | if (iequals(ext, ".htm")) 413 | return "text/html"; 414 | if (iequals(ext, ".html")) 415 | return "text/html"; 416 | if (iequals(ext, ".php")) 417 | return "text/html"; 418 | if (iequals(ext, ".css")) 419 | return "text/css"; 420 | if (iequals(ext, ".txt")) 421 | return "text/plain"; 422 | if (iequals(ext, ".js")) 423 | return "application/javascript"; 424 | if (iequals(ext, ".json")) 425 | return "application/json"; 426 | if (iequals(ext, ".xml")) 427 | return "application/xml"; 428 | if (iequals(ext, ".swf")) 429 | return "application/x-shockwave-flash"; 430 | if (iequals(ext, ".flv")) 431 | return "video/x-flv"; 432 | if (iequals(ext, ".png")) 433 | return "image/png"; 434 | if (iequals(ext, ".jpe")) 435 | return "image/jpeg"; 436 | if (iequals(ext, ".jpeg")) 437 | return "image/jpeg"; 438 | if (iequals(ext, ".jpg")) 439 | return "image/jpeg"; 440 | if (iequals(ext, ".gif")) 441 | return "image/gif"; 442 | if (iequals(ext, ".bmp")) 443 | return "image/bmp"; 444 | if (iequals(ext, ".ico")) 445 | return "image/vnd.microsoft.icon"; 446 | if (iequals(ext, ".tiff")) 447 | return "image/tiff"; 448 | if (iequals(ext, ".tif")) 449 | return "image/tiff"; 450 | if (iequals(ext, ".svg")) 451 | return "image/svg+xml"; 452 | if (iequals(ext, ".svgz")) 453 | return "image/svg+xml"; 454 | if (iequals(ext, ".wasm")) 455 | return "application/wasm"; 456 | return "application/text"; 457 | } 458 | 459 | template 460 | void GetSocket(TCallback&& callback) { 461 | socket_.dispatch([ 462 | this, self = this->shared_from_this(), callback = std::move(callback) 463 | ](auto& sockets) { callback(sockets.socket); }); 464 | } 465 | 466 | void SetCloseCallback(std::function&& close_callback) { 467 | close_callback_ = close_callback; 468 | } 469 | 470 | protected: 471 | virtual void OnPreConnect() { 472 | socket_.dispatch([this, self=this->shared_from_this()](auto& sockets) { 473 | sockets.websocket.async_accept_ex( 474 | parser_.get(), 475 | [](beast::websocket::response_type& res) { 476 | res.set(beast::http::field::server, 477 | "collab-vm-server"); 478 | }, 479 | socket_.wrap([this, self = std::move(self)]( 480 | auto& sockets, 481 | const boost::system::error_code ec) mutable { 482 | if (ec) { 483 | Close(); 484 | return; 485 | } 486 | OnConnect(); 487 | sockets.websocket.binary(true); 488 | sockets.websocket.auto_fragment(false); 489 | CreateMessageBuffer()->StartRead(std::move(self)); 490 | })); 491 | }); 492 | } 493 | virtual void OnConnect() = 0; 494 | virtual void OnMessage(std::shared_ptr&& buffer) = 0; 495 | virtual void OnDisconnect() = 0; 496 | 497 | private: 498 | struct SocketsWrapper { 499 | SocketsWrapper(boost::asio::io_context& io_context) 500 | : socket(io_context), websocket(socket) {} 501 | SocketsWrapper(const SocketsWrapper& io_context) = delete; 502 | asio::ip::tcp::socket socket; 503 | beast::websocket::stream websocket; 504 | // asio::ssl::stream stream_; 505 | }; 506 | 507 | static std::string_view GetIpAddressFromHeader(const boost::beast::http::fields& fields) { 508 | if (const auto header = fields.find(beast::http::field::forwarded); 509 | header != fields.end()) { 510 | auto value = header->value(); 511 | value.remove_prefix(boost::algorithm::ifind_first(value, "for=").end() - value.begin()); 512 | if (!value.empty()) { 513 | value.remove_prefix(boost::range::mismatch(value, "\"[").first - value.begin()); 514 | value.remove_suffix(value.end() - boost::range::find_first_of(value, ";,]\"")); 515 | return value; 516 | } 517 | } 518 | if (const auto header = fields.find("X-Forwarded-For"); 519 | header != fields.end()) { 520 | return *boost::beast::http::token_list(header->value()).begin(); 521 | } 522 | if (const auto header = fields.find("X-Real-IP"); 523 | header != fields.end()) { 524 | return header->value(); 525 | } 526 | return ""; 527 | } 528 | 529 | StrandGuard socket_; 530 | 531 | beast::flat_static_buffer<8192> buffer_; 532 | 533 | boost::asio::steady_timer request_deadline_; 534 | 535 | std::variant, 536 | beast::http::response> 537 | response_; 538 | 539 | std::variant< 540 | std::monostate, 541 | beast::http::response_serializer, 542 | beast::http::response_serializer> 543 | serializer_; 544 | 545 | using request_body_t = 546 | beast::http::basic_dynamic_body>; 547 | beast::http::request_parser parser_; 548 | 549 | const std::filesystem::path& doc_root_; 550 | IpAddress ip_address_; 551 | 552 | std::function close_callback_; 553 | }; 554 | 555 | class WebServer { 556 | public: 557 | WebServer(const std::string& doc_root) 558 | : sockets_(io_context_), 559 | acceptor_(io_context_), 560 | doc_root_(doc_root), 561 | interrupt_signal_(io_context_, SIGINT, SIGTERM) {} 562 | 563 | void Start(std::uint8_t threads, 564 | const std::string& host, 565 | const std::uint16_t port) { 566 | { 567 | auto ec = std::error_code(); 568 | CreateDocRoot(doc_root_, ec); 569 | if (ec) { 570 | return; 571 | } 572 | } 573 | 574 | interrupt_signal_.async_wait([this](const auto error, 575 | const auto signal_number) { Stop(); }); 576 | 577 | auto resolver = asio::ip::tcp::resolver(io_context_); 578 | auto error_code = boost::system::error_code(); 579 | auto resolver_results = resolver.resolve(host, std::to_string(port), error_code); 580 | if (error_code || resolver_results.empty()) { 581 | std::cout << "Could not resolve hostname \"" << host << "\"\n"; 582 | std::cout << error_code.message() << std::endl; 583 | return; 584 | } 585 | 586 | // TODO: Listen on all resolved endpoints 587 | const auto endpoint = asio::ip::tcp::endpoint(*resolver_results.begin()); 588 | 589 | try { 590 | acceptor_.open(endpoint.protocol()); 591 | acceptor_.bind(endpoint); 592 | acceptor_.listen(); 593 | 594 | std::cout << "Listening on " << acceptor_.local_endpoint() << std::endl; 595 | 596 | DoAccept(); 597 | 598 | // Decrement because the current thread will also become a worker 599 | --threads; 600 | std::vector threads_; 601 | threads_.reserve(threads); 602 | for (auto i = 0u; i < threads; i++) { 603 | threads_.emplace_back([&] { io_context_.run(); }); 604 | } 605 | 606 | io_context_.run(); 607 | 608 | for (auto&& thread : threads_) { 609 | thread.join(); 610 | } 611 | } catch (const boost::system::system_error& exception) { 612 | std::cout << "Failed to start server\n"; 613 | std::cout << exception.what() << std::endl; 614 | error_code = exception.code(); 615 | if (port < 1024 616 | && error_code.category() == boost::asio::error::get_system_category() 617 | && error_code.value() == EACCES) { 618 | std::cout << "Elevated permissions may be required to listen on ports below 1024" << std::endl; 619 | } 620 | } 621 | } 622 | 623 | virtual void Stop() { 624 | auto ec = boost::system::error_code(); 625 | interrupt_signal_.cancel(ec); 626 | acceptor_.close(ec); 627 | 628 | sockets_.dispatch([this](auto& sockets) { 629 | if (stopping_) { 630 | return; 631 | } 632 | stopping_ = true; 633 | for (auto&& socket : sockets) { 634 | socket->Close(); 635 | } 636 | }); 637 | } 638 | 639 | boost::asio::io_context& GetContext() { 640 | return io_context_; 641 | } 642 | protected: 643 | boost::asio::io_context io_context_; 644 | using TSocket = WebServerSocket; 645 | 646 | virtual std::shared_ptr CreateSocket( 647 | boost::asio::io_context& io_context, 648 | const std::filesystem::path& doc_root) = 0; 649 | 650 | private: 651 | static void CreateDocRoot(std::filesystem::path& path, 652 | std::error_code& ec) { 653 | auto status = std::filesystem::status(path, ec); 654 | if (ec) { 655 | if (status.type() == std::filesystem::file_type::not_found) { 656 | if (std::filesystem::create_directories(path, ec)) { 657 | std::cout << "The path " << path << " has been created" << std::endl; 658 | } else { 659 | std::cout << "Failed to create directory " << path << std::endl; 660 | return; 661 | } 662 | } else { 663 | std::cout << ec.category().message(ec.value()) << std::endl; 664 | return; 665 | } 666 | } else if (status.type() != std::filesystem::file_type::directory) { 667 | std::cout << "The doc root should be a directory, but it's a file" 668 | << std::endl; 669 | ec = std::make_error_code(std::errc::no_such_file_or_directory); 670 | return; 671 | } 672 | 673 | path = std::filesystem::canonical(path, ec); 674 | if (ec) { 675 | std::cout << ec.category().message(ec.value()) << std::endl; 676 | } 677 | } 678 | 679 | void DoAccept() { 680 | sockets_.dispatch([this](auto& sockets) { 681 | if (stopping_) { 682 | return; 683 | } 684 | const auto socket_ptr = sockets.emplace_front( 685 | CreateSocket(io_context_, doc_root_)); 686 | const auto socket_it = sockets.cbegin(); 687 | socket_ptr->SetCloseCallback( 688 | [this, socket_it] { RemoveSocket(socket_it); }); 689 | 690 | socket_ptr->GetSocket([this, socket_ptr](auto& socket) { 691 | acceptor_.async_accept( 692 | socket, 693 | [this, socket_ptr](const boost::system::error_code ec) { 694 | if (ec || !acceptor_.is_open()) { 695 | socket_ptr->Close(); 696 | return; 697 | } 698 | socket_ptr->Start(); 699 | DoAccept(); 700 | }); 701 | }); 702 | }); 703 | } 704 | 705 | void RemoveSocket( 706 | typename std::list>::const_iterator it) { 707 | sockets_.dispatch([this, it](auto& sockets) { 708 | if (!stopping_) { 709 | sockets.erase(it); 710 | } 711 | }); 712 | } 713 | 714 | bool stopping_ = false; 715 | StrandGuard< 716 | boost::asio::io_context::strand, 717 | std::list>> sockets_; 718 | 719 | boost::asio::ip::tcp::acceptor acceptor_; 720 | std::filesystem::path doc_root_; 721 | boost::asio::signal_set interrupt_signal_; 722 | }; 723 | } // namespace CollabVm::Server 724 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2019 2 | platform: x64 3 | skip_tags: true 4 | environment: 5 | # Always save the cache because there isn't enough time 6 | # to build both the dependencies and the collab-vm-server 7 | APPVEYOR_SAVE_CACHE_ON_ERROR: true 8 | access_token: 9 | secure: /1wKmhBRo1sCNw9MG237fBihMhCJIMVajWI4BgTKQm39r7J6StfkJFg26TVkK/Qv 10 | pull_requests: 11 | do_not_increment_build_number: true 12 | install: 13 | # Clone submodules 14 | - git submodule update --init --recursive 15 | # Update vcpkg 16 | - git -C C:\Tools\vcpkg\ checkout tags/2019.12 17 | - C:\Tools\vcpkg\bootstrap-vcpkg.bat 18 | # Build only release libraries, not debug 19 | - ps: Add-Content C:\Tools\vcpkg\triplets\$env:PLATFORM-windows-static.cmake "set(VCPKG_BUILD_TYPE release)" 20 | # GLib is a dependency of cairo that must be dynamically linked 21 | - ps: Add-Content C:\Tools\vcpkg\triplets\$env:PLATFORM-windows-static.cmake "if(PORT MATCHES ""glib"")`n`tset(VCPKG_LIBRARY_LINKAGE dynamic)`n`tset(VCPKG_CRT_LINKAGE dynamic)`nendif()" 22 | # Install dependencies 23 | - vcpkg.exe install --triplet %PLATFORM%-windows-static cairo libjpeg-turbo sqlite3 libpng openssl pthreads 24 | # Upgrade dependencies if they were cached 25 | - vcpkg.exe upgrade --triplet %PLATFORM%-windows-static --no-dry-run 26 | cache: 27 | - C:\Tools\vcpkg\installed\ 28 | build_script: 29 | - ps: Add-Content cmake/MSVCStaticToolchain.cmake 'include("C:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake")' 30 | - mkdir build 31 | - cd build 32 | - cmake -G "Visual Studio 16 2019" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DCMAKE_INSTALL_PREFIX=%APPVEYOR_BUILD_FOLDER%/install/ -DCMAKE_TOOLCHAIN_FILE=%APPVEYOR_BUILD_FOLDER%/cmake/MSVCStaticToolchain.cmake -DVCPKG_TARGET_TRIPLET=%PLATFORM%-windows-static .. 33 | - cmake --build . --target install --config RelWithDebInfo 34 | # Find and copy VNC demo into install directory 35 | - ps: ls -rec -incl example.exe "$env:APPVEYOR_BUILD_FOLDER/build/" | select -first 1 | cp -dest "$env:APPVEYOR_BUILD_FOLDER/install/vnc-demo.exe" 36 | # Download and extract web-app 37 | - cd .. 38 | - wsl wget https://github.com/Cosmic-Sans/collab-vm-web-app/releases/download/prerelease/web-app.tar.gz 39 | - wsl tar xfz web-app.tar.gz --directory=install/ 40 | artifacts: 41 | - path: install/ 42 | name: collab-vm-win64 43 | before_deploy: 44 | # Move the tag to the most recent commit, but delete it first 45 | # so GitHub updates the timestamp of the release 46 | - git config --global credential.helper store 47 | - ps: Add-Content "$HOME\.git-credentials" "https://$($env:access_token):x-oauth-basic@github.com`n" 48 | - git push --delete origin prerelease 49 | - git tag --force prerelease HEAD 50 | - git push --force --tags origin prerelease 51 | deploy: 52 | provider: GitHub 53 | tag: prerelease 54 | artifact: collab-vm-win64 55 | prerelease: true 56 | draft: false 57 | force_update: true 58 | on: 59 | branch: master 60 | auth_token: 61 | secure: /1wKmhBRo1sCNw9MG237fBihMhCJIMVajWI4BgTKQm39r7J6StfkJFg26TVkK/Qv 62 | -------------------------------------------------------------------------------- /capnp-list.hpp: -------------------------------------------------------------------------------- 1 | // Cap'n Proto appears to have support for compatiblity with STL iterators 2 | // by defining the macro KJ_STD_COMPAT, but it doesn't work with MSVC unless 3 | // this specialization is defined. 4 | //#ifdef WIN32 5 | ////#include 6 | //#undef VOID 7 | //#undef CONST 8 | //#undef SendMessage 9 | //#endif 10 | //#include 11 | #include 12 | #include 13 | namespace std 14 | { 15 | template 16 | struct iterator_traits> 17 | { 18 | using iterator_category = random_access_iterator_tag; 19 | using value_type = Element; 20 | using difference_type = int; 21 | 22 | using pointer = Element*; 23 | using reference = Element&; 24 | }; 25 | } // namespace std 26 | -------------------------------------------------------------------------------- /cmake/FindFilesystem.cmake: -------------------------------------------------------------------------------- 1 | # Distributed under the OSI-approved BSD 3-Clause License. See accompanying 2 | # file Copyright.txt or https://cmake.org/licensing for details. 3 | 4 | #[=======================================================================[.rst: 5 | 6 | FindFilesystem 7 | ############## 8 | 9 | This module supports the C++17 standard library's filesystem utilities. Use the 10 | :imp-target:`std::filesystem` imported target to 11 | 12 | Options 13 | ******* 14 | 15 | The ``COMPONENTS`` argument to this module supports the following values: 16 | 17 | .. find-component:: Experimental 18 | :name: fs.Experimental 19 | 20 | Allows the module to find the "experimental" Filesystem TS version of the 21 | Filesystem library. This is the library that should be used with the 22 | ``std::experimental::filesystem`` namespace. 23 | 24 | .. find-component:: Final 25 | :name: fs.Final 26 | 27 | Finds the final C++17 standard version of the filesystem library. 28 | 29 | If no components are provided, behaves as if the 30 | :find-component:`fs.Final` component was specified. 31 | 32 | If both :find-component:`fs.Experimental` and :find-component:`fs.Final` are 33 | provided, first looks for ``Final``, and falls back to ``Experimental`` in case 34 | of failure. If ``Final`` is found, :imp-target:`std::filesystem` and all 35 | :ref:`variables ` will refer to the ``Final`` version. 36 | 37 | 38 | Imported Targets 39 | **************** 40 | 41 | .. imp-target:: std::filesystem 42 | 43 | The ``std::filesystem`` imported target is defined when any requested 44 | version of the C++ filesystem library has been found, whether it is 45 | *Experimental* or *Final*. 46 | 47 | If no version of the filesystem library is available, this target will not 48 | be defined. 49 | 50 | .. note:: 51 | This target has ``cxx_std_17`` as an ``INTERFACE`` 52 | :ref:`compile language standard feature `. Linking 53 | to this target will automatically enable C++17 if no later standard 54 | version is already required on the linking target. 55 | 56 | 57 | .. _fs.variables: 58 | 59 | Variables 60 | ********* 61 | 62 | .. variable:: CXX_FILESYSTEM_IS_EXPERIMENTAL 63 | 64 | Set to ``TRUE`` when the :find-component:`fs.Experimental` version of C++ 65 | filesystem library was found, otherwise ``FALSE``. 66 | 67 | .. variable:: CXX_FILESYSTEM_HAVE_FS 68 | 69 | Set to ``TRUE`` when a filesystem header was found. 70 | 71 | .. variable:: CXX_FILESYSTEM_HEADER 72 | 73 | Set to either ``filesystem`` or ``experimental/filesystem`` depending on 74 | whether :find-component:`fs.Final` or :find-component:`fs.Experimental` was 75 | found. 76 | 77 | .. variable:: CXX_FILESYSTEM_NAMESPACE 78 | 79 | Set to either ``std::filesystem`` or ``std::experimental::filesystem`` 80 | depending on whether :find-component:`fs.Final` or 81 | :find-component:`fs.Experimental` was found. 82 | 83 | 84 | Examples 85 | ******** 86 | 87 | Using `find_package(Filesystem)` with no component arguments: 88 | 89 | .. code-block:: cmake 90 | 91 | find_package(Filesystem REQUIRED) 92 | 93 | add_executable(my-program main.cpp) 94 | target_link_libraries(my-program PRIVATE std::filesystem) 95 | 96 | 97 | #]=======================================================================] 98 | 99 | 100 | if(TARGET std::filesystem) 101 | # This module has already been processed. Don't do it again. 102 | return() 103 | endif() 104 | 105 | include(CMakePushCheckState) 106 | include(CheckIncludeFileCXX) 107 | include(CheckCXXSourceCompiles) 108 | 109 | cmake_push_check_state() 110 | 111 | set(CMAKE_REQUIRED_QUIET ${Filesystem_FIND_QUIETLY}) 112 | 113 | # All of our tests required C++17 or later 114 | set(CMAKE_CXX_STANDARD 17) 115 | 116 | # Normalize and check the component list we were given 117 | set(want_components ${Filesystem_FIND_COMPONENTS}) 118 | if(Filesystem_FIND_COMPONENTS STREQUAL "") 119 | set(want_components Final) 120 | endif() 121 | 122 | # Warn on any unrecognized components 123 | set(extra_components ${want_components}) 124 | list(REMOVE_ITEM extra_components Final Experimental) 125 | foreach(component IN LISTS extra_components) 126 | message(WARNING "Extraneous find_package component for Filesystem: ${component}") 127 | endforeach() 128 | 129 | # Detect which of Experimental and Final we should look for 130 | set(find_experimental TRUE) 131 | set(find_final TRUE) 132 | if(NOT "Final" IN_LIST want_components) 133 | set(find_final FALSE) 134 | endif() 135 | if(NOT "Experimental" IN_LIST want_components) 136 | set(find_experimental FALSE) 137 | endif() 138 | 139 | if(find_final) 140 | check_include_file_cxx("filesystem" _CXX_FILESYSTEM_HAVE_HEADER) 141 | mark_as_advanced(_CXX_FILESYSTEM_HAVE_HEADER) 142 | if(_CXX_FILESYSTEM_HAVE_HEADER) 143 | # We found the non-experimental header. Don't bother looking for the 144 | # experimental one. 145 | set(find_experimental FALSE) 146 | endif() 147 | else() 148 | set(_CXX_FILESYSTEM_HAVE_HEADER FALSE) 149 | endif() 150 | 151 | if(find_experimental) 152 | check_include_file_cxx("experimental/filesystem" _CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER) 153 | mark_as_advanced(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER) 154 | else() 155 | set(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER FALSE) 156 | endif() 157 | 158 | if(_CXX_FILESYSTEM_HAVE_HEADER) 159 | set(_have_fs TRUE) 160 | set(_fs_header filesystem) 161 | set(_fs_namespace std::filesystem) 162 | elseif(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER) 163 | set(_have_fs TRUE) 164 | set(_fs_header experimental/filesystem) 165 | set(_fs_namespace std::experimental::filesystem) 166 | else() 167 | set(_have_fs FALSE) 168 | endif() 169 | 170 | set(CXX_FILESYSTEM_HAVE_FS ${_have_fs} CACHE BOOL "TRUE if we have the C++ filesystem headers") 171 | set(CXX_FILESYSTEM_HEADER ${_fs_header} CACHE STRING "The header that should be included to obtain the filesystem APIs") 172 | set(CXX_FILESYSTEM_NAMESPACE ${_fs_namespace} CACHE STRING "The C++ namespace that contains the filesystem APIs") 173 | 174 | set(_found FALSE) 175 | 176 | if(CXX_FILESYSTEM_HAVE_FS) 177 | # We have some filesystem library available. Do link checks 178 | string(CONFIGURE [[ 179 | #include <@CXX_FILESYSTEM_HEADER@> 180 | 181 | int main() { 182 | auto cwd = @CXX_FILESYSTEM_NAMESPACE@::current_path(); 183 | return static_cast(cwd.string().size()); 184 | } 185 | ]] code @ONLY) 186 | 187 | # This shouldn't be necessary 188 | set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -std=c++17") 189 | 190 | # Try to compile a simple filesystem program without any linker flags 191 | check_cxx_source_compiles("${code}" CXX_FILESYSTEM_NO_LINK_NEEDED) 192 | 193 | set(can_link ${CXX_FILESYSTEM_NO_LINK_NEEDED}) 194 | 195 | if(NOT CXX_FILESYSTEM_NO_LINK_NEEDED) 196 | set(prev_libraries ${CMAKE_REQUIRED_LIBRARIES}) 197 | # Add the libstdc++ flag 198 | set(CMAKE_REQUIRED_LIBRARIES ${prev_libraries} -lstdc++fs) 199 | check_cxx_source_compiles("${code}" CXX_FILESYSTEM_STDCPPFS_NEEDED) 200 | set(can_link ${CXX_FILESYSTEM_STDCPPFS_NEEDED}) 201 | if(NOT CXX_FILESYSTEM_STDCPPFS_NEEDED) 202 | # Try the libc++ flag 203 | set(CMAKE_REQUIRED_LIBRARIES ${prev_libraries} -lc++fs) 204 | check_cxx_source_compiles("${code}" CXX_FILESYSTEM_CPPFS_NEEDED) 205 | set(can_link ${CXX_FILESYSTEM_CPPFS_NEEDED}) 206 | endif() 207 | endif() 208 | 209 | if(can_link) 210 | add_library(std::filesystem INTERFACE IMPORTED) 211 | set(_found TRUE) 212 | 213 | if(CXX_FILESYSTEM_NO_LINK_NEEDED) 214 | # Nothing to add... 215 | elseif(CXX_FILESYSTEM_STDCPPFS_NEEDED) 216 | target_link_libraries(std::filesystem INTERFACE -lstdc++fs) 217 | elseif(CXX_FILESYSTEM_CPPFS_NEEDED) 218 | target_link_libraries(std::filesystem INTERFACE -lc++fs) 219 | endif() 220 | endif() 221 | endif() 222 | 223 | cmake_pop_check_state() 224 | 225 | set(Filesystem_FOUND ${_found} CACHE BOOL "TRUE if we can compile and link a program using std::filesystem" FORCE) 226 | 227 | if(Filesystem_FIND_REQUIRED AND NOT Filesystem_FOUND) 228 | message(FATAL_ERROR "Cannot Compile simple program using std::filesystem") 229 | endif() 230 | -------------------------------------------------------------------------------- /cmake/FindOpenSSL.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_MODULE_PATH_OLD ${CMAKE_MODULE_PATH}) 2 | set(CMAKE_MODULE_PATH "") 3 | find_package(OpenSSL) 4 | set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH_OLD}) 5 | unset(CMAKE_MODULE_PATH_OLD) 6 | if (OPENSSL_FOUND) 7 | # FreeRDP has a bug in makecert CMakeLists.txt 8 | # OPENSSL_CRYPTO_LIBRARIES should be OPENSSL_CRYPTO_LIBRARY 9 | message(STATUS "OPENSSL_LIBRARIES: ${OPENSSL_LIBRARIES}") 10 | list(APPEND OPENSSL_CRYPTO_LIBRARY "-ldl") 11 | message(STATUS "OPENSSL_CRYPTO_LIBRARY: ${OPENSSL_CRYPTO_LIBRARY}") 12 | set(OPENSSL_CRYPTO_LIBRARIES ${OPENSSL_CRYPTO_LIBRARY}) 13 | message(STATUS "OPENSSL_CRYPTO_LIBRARY: ${OPENSSL_CRYPTO_LIBRARY}") 14 | #set(OPENSSL_LIBRARIES ${OPENSSL_LIBRARIES} -ldl) 15 | endif() 16 | -------------------------------------------------------------------------------- /cmake/FindPNG.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_MODULE_PATH_OLD ${CMAKE_MODULE_PATH}) 2 | # TODO: Use list(FILTER ...) to remove CMAKE_CURRENT_LIST_DIR 3 | set(CMAKE_MODULE_PATH "") 4 | find_package(PNG) 5 | set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH_OLD}) 6 | unset(CMAKE_MODULE_PATH_OLD) 7 | 8 | if (PNG_FOUND AND NOT WIN32) 9 | find_library(PNG_M_LIBRARY NAMES m) 10 | list(APPEND PNG_LIBRARIES "m") 11 | if(TARGET PNG::PNG) 12 | set_property(TARGET PNG::PNG APPEND PROPERTY INTERFACE_LINK_LIBRARIES "m") 13 | endif() 14 | endif() 15 | -------------------------------------------------------------------------------- /cmake/MSVCStaticToolchain.cmake: -------------------------------------------------------------------------------- 1 | foreach(flag_var IN ITEMS 2 | CMAKE_C_FLAGS_INIT 3 | CMAKE_C_FLAGS_DEBUG_INIT 4 | CMAKE_C_FLAGS_MINSIZEREL_INIT 5 | CMAKE_C_FLAGS_RELEASE_INIT 6 | CMAKE_C_FLAGS_RELWITHDEBINFO_INIT 7 | CMAKE_CXX_FLAGS_INIT 8 | CMAKE_CXX_FLAGS_DEBUG_INIT 9 | CMAKE_CXX_FLAGS_MINSIZEREL_INIT 10 | CMAKE_CXX_FLAGS_RELEASE_INIT 11 | CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT) 12 | string(REGEX REPLACE "[/-]M[DT]d?" "/MT" ${flag_var} "${${flag_var}} /MT") 13 | endforeach() 14 | -------------------------------------------------------------------------------- /file_body.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2013-2017 Vinnie Falco (vinnie dot falco at gmail dot com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | #ifndef BEAST_EXAMPLE_COMMON_FILE_BODY_HPP 9 | #define BEAST_EXAMPLE_COMMON_FILE_BODY_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | namespace beast = boost::beast; 22 | 23 | //[example_http_FileBody_1 24 | 25 | /** A message body represented by a file on the filesystem. 26 | 27 | Messages with this type have bodies represented by a 28 | file on the file system. When parsing a message using 29 | this body type, the data is stored in the file pointed 30 | to by the path, which must be writable. When serializing, 31 | the implementation will read the file and present those 32 | octets as the body content. This may be used to serve 33 | content from a directory as part of a web service. 34 | */ 35 | struct FileBody 36 | { 37 | /** The type of the @ref message::body member. 38 | 39 | Messages declared using `FileBody` will have this 40 | type for the body member. We use a path indicating 41 | the location on the file system for which the data 42 | will be read or written. 43 | */ 44 | using value_type = boost::filesystem::path; 45 | 46 | /** Returns the content length of the body in a message. 47 | 48 | This optional static function returns the size of the 49 | body in bytes. It is called from @ref message::size to 50 | return the payload size, and from @ref message::prepare 51 | to automatically set the Content-Length field. If this 52 | function is omitted from a body type, calls to 53 | @ref message::prepare will set the chunked transfer 54 | encoding. 55 | 56 | @param m The message containing a file body to check. 57 | 58 | @return The size of the file in bytes. 59 | */ 60 | static 61 | std::uint64_t 62 | size(value_type const& v); 63 | 64 | /** Algorithm for retrieving buffers when serializing. 65 | 66 | Objects of this type are created during serialization 67 | to extract the buffers representing the body. 68 | */ 69 | class reader 70 | { 71 | std::streamsize remain_; // The number of unread bytes 72 | const value_type& path_; 73 | std::ifstream stream_; 74 | char buf_[4096]; 75 | 76 | public: 77 | // The type of buffer sequence returned by `get`. 78 | // 79 | using const_buffers_type = 80 | boost::asio::const_buffer; 81 | 82 | // Constructor. 83 | // 84 | // `m` holds the message we are sending, which will 85 | // always have the `FileBody` as the body type. 86 | // 87 | template 88 | reader(beast::http::message const& m, 89 | beast::error_code& ec) 90 | : path_(m.body) 91 | { 92 | stream_.exceptions(std::ifstream::failbit | std::ifstream::badbit); 93 | try 94 | { 95 | stream_.open(path_.string(), std::ifstream::in | std::ifstream::binary); 96 | remain_ = boost::filesystem::file_size(path_, ec); 97 | } 98 | catch (std::system_error& e) 99 | { 100 | ec = boost::system::error_code(e.code().value(), boost::system::system_category()); 101 | } 102 | } 103 | 104 | // This function is called zero or more times to 105 | // retrieve buffers. A return value of `boost::none` 106 | // means there are no more buffers. Otherwise, 107 | // the contained pair will have the next buffer 108 | // to serialize, and a `bool` indicating whether 109 | // or not there may be additional buffers. 110 | boost::optional> 111 | get(beast::error_code& ec) 112 | { 113 | const std::streamsize amount = std::min(remain_, static_cast(sizeof(buf_))); 114 | 115 | // Check for an empty file 116 | if (amount == 0) 117 | { 118 | ec = {}; 119 | return boost::none; 120 | } 121 | try 122 | { 123 | stream_.read(buf_, amount); 124 | assert(stream_.gcount() == amount); 125 | } 126 | catch (std::system_error& e) 127 | { 128 | ec = boost::system::error_code(e.code().value(), boost::system::system_category()); 129 | return boost::none; 130 | } 131 | 132 | // Update the amount remaining based on what we got 133 | remain_ -= amount; 134 | 135 | // Return the buffer to the caller. 136 | // 137 | // The second element of the pair indicates whether or 138 | // not there is more data. As long as there is some 139 | // unread bytes, there will be more data. Otherwise, 140 | // we set this bool to `false` so we will not be called 141 | // again. 142 | // 143 | ec = {}; 144 | return {{ 145 | const_buffers_type{buf_, static_cast(amount)}, // buffer to return. 146 | remain_ > 0 // `true` if there are more buffers. 147 | }}; 148 | } 149 | }; 150 | 151 | /** Algorithm for storing buffers when parsing. 152 | 153 | Objects of this type are created during parsing 154 | to store incoming buffers representing the body. 155 | */ 156 | class writer; 157 | }; 158 | 159 | //] 160 | 161 | //[example_http_FileBody_2 162 | 163 | inline 164 | std::uint64_t 165 | FileBody:: 166 | size(value_type const& v) 167 | { 168 | return boost::filesystem::file_size(v); 169 | } 170 | 171 | class FileBody::writer 172 | { 173 | value_type const& path_; // A path to the file 174 | FILE* file_ = nullptr; // The file handle 175 | 176 | public: 177 | // Constructor. 178 | // 179 | // This is called after the header is parsed and 180 | // indicates that a non-zero sized body may be present. 181 | // `m` holds the message we are receiving, which will 182 | // always have the `FileBody` as the body type. 183 | // 184 | template 185 | explicit 186 | writer(beast::http::message& m, 187 | boost::optional const& content_length, 188 | beast::error_code& ec); 189 | 190 | // This function is called one or more times to store 191 | // buffer sequences corresponding to the incoming body. 192 | // 193 | template 194 | void 195 | put(ConstBufferSequence const& buffers, beast::error_code& ec); 196 | 197 | // This function is called when writing is complete. 198 | // It is an opportunity to perform any final actions 199 | // which might fail, in order to return an error code. 200 | // Operations that might fail should not be attemped in 201 | // destructors, since an exception thrown from there 202 | // would terminate the program. 203 | // 204 | void 205 | finish(beast::error_code& ec); 206 | 207 | // Destructor. 208 | // 209 | // Avoid calling anything that might fail here. 210 | // 211 | ~writer(); 212 | }; 213 | 214 | //] 215 | 216 | //[example_http_FileBody_6 217 | 218 | // Just stash a reference to the path so we can open the file later. 219 | template 220 | FileBody::writer:: 221 | writer(beast::http::message& m, 222 | boost::optional const& content_length, 223 | beast::error_code& ec) 224 | : path_(m.body) 225 | { 226 | boost::ignore_unused(content_length); 227 | 228 | // Attempt to open the file for writing 229 | file_ = fopen(path_.string().c_str(), "wb"); 230 | 231 | if(! file_) 232 | { 233 | // Convert the old-school `errno` into 234 | // an error code using the generic category. 235 | ec = beast::error_code{errno, beast::generic_category()}; 236 | return; 237 | } 238 | 239 | // This is required by the error_code specification 240 | ec = {}; 241 | } 242 | 243 | // This will get called one or more times with body buffers 244 | // 245 | template 246 | void 247 | FileBody::writer:: 248 | put(ConstBufferSequence const& buffers, beast::error_code& ec) 249 | { 250 | // Loop over all the buffers in the sequence, 251 | // and write each one to the file. 252 | for(boost::asio::const_buffer buffer : buffers) 253 | { 254 | // Write this buffer to the file 255 | fwrite( 256 | boost::asio::buffer_cast(buffer), 1, 257 | boost::asio::buffer_size(buffer), 258 | file_); 259 | 260 | // Handle any errors 261 | if(ferror(file_)) 262 | { 263 | // Convert the old-school `errno` into 264 | // an error code using the generic category. 265 | ec = beast::error_code{errno, beast::generic_category()}; 266 | return; 267 | } 268 | } 269 | 270 | // Indicate success 271 | ec = {}; 272 | } 273 | 274 | // Called after writing is done when there's no error. 275 | inline 276 | void 277 | FileBody::writer:: 278 | finish(beast::error_code& ec) 279 | { 280 | // This has to be cleared before returning, to 281 | // indicate no error. The specification requires it. 282 | ec = {}; 283 | } 284 | 285 | // The destructor is always invoked if construction succeeds 286 | // 287 | inline 288 | FileBody::writer:: 289 | ~writer() 290 | { 291 | // Just close the file if its open 292 | if(file_) 293 | fclose(file_); 294 | 295 | // In theory fclose() can fail but how would we handle it? 296 | } 297 | 298 | //] 299 | 300 | #endif 301 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | enable_testing() 2 | 3 | add_executable(recaptcha Captcha.cpp) 4 | target_include_directories(recaptcha PUBLIC ${COLLAB_VM_COMMON_BINARY_DIR} ${PROJECT_SOURCE_DIR} ${OPENSSL_INCLUDE_DIR} ${PROJECT_SOURCE_DIR}/submodules/beast/include ${Boost_INCLUDE_DIRS}) 5 | target_link_libraries(recaptcha CapnProto::capnp OpenSSL::SSL) 6 | add_test(recaptcha recaptcha) 7 | 8 | add_executable(totp Totp.cpp) 9 | target_include_directories(totp PUBLIC ${PROJECT_SOURCE_DIR} ${OPENSSL_INCLUDE_DIR} ${Boost_INCLUDE_DIRS} ${CMAKE_SOURCE_DIR}/submodules/GSL/include) 10 | target_link_libraries(totp OpenSSL::Crypto) 11 | add_test(totp totp) 12 | 13 | add_executable(guac-test Guacamole.cpp) 14 | target_include_directories(guac-test PUBLIC ${CMAKE_BINARY_DIR} ${PROJECT_SOURCE_DIR} ${Cairo_INCLUDE_DIR} ${GUACAMOLE_INCLUDE_DIRS} ${OPENSSL_INCLUDE_DIR} ${Boost_INCLUDE_DIRS} ${CMAKE_SOURCE_DIR}/submodules/GSL/include) 15 | target_link_libraries(guac-test CapnProto::capnp ${Cairo_LIBRARY} guacamole) 16 | add_test(guac-test guac-test) 17 | add_dependencies(guac-test guacamole) 18 | 19 | add_executable(turn-test TurnTest.cpp) 20 | target_include_directories(turn-test PUBLIC ${PROJECT_SOURCE_DIR} ${Boost_INCLUDE_DIRS}) 21 | add_test(turn-test turn-test) 22 | -------------------------------------------------------------------------------- /tests/Captcha.cpp: -------------------------------------------------------------------------------- 1 | #include "CaptchaVerifier.hpp" 2 | 3 | int main() { 4 | namespace ssl = boost::asio::ssl; 5 | ssl::context ssl_ctx(ssl::context::sslv23); 6 | ssl_ctx.set_verify_mode(ssl::context::verify_none); 7 | boost::asio::io_context io_context(1); 8 | auto verifier = CollabVm::Server::CaptchaVerifier(io_context, ssl_ctx); 9 | capnp::MallocMessageBuilder message_builder; 10 | auto settings = message_builder.initRoot(); 11 | settings.setEnabled(true); 12 | settings.setHttps(true); 13 | settings.setUrlHost("google.com"); 14 | settings.setUrlPort(443); 15 | settings.setUrlPath("/recaptcha/api/siteverify"); 16 | settings.setPostParams("secret=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe&response=$TOKEN&remoteip=$IP"); 17 | settings.setValidJSONVariableName("success"); 18 | verifier.SetSettings([settings]() { return settings; }); 19 | 20 | auto success = false; 21 | verifier.Verify("asdf", [&success](bool is_valid) { success = is_valid; }); 22 | verifier.Verify("1234", [&success](bool is_valid) { success &= is_valid; }); 23 | io_context.run(); 24 | return !success; 25 | } 26 | -------------------------------------------------------------------------------- /tests/Guacamole.cpp: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #include 3 | #include 4 | #ifdef WIN32 5 | #include 6 | #undef VOID 7 | #undef CONST 8 | #undef SendMessage 9 | #endif 10 | #include "Guacamole.capnp.h" 11 | #include "StrandGuard.hpp" 12 | #include 13 | // 14 | #include 15 | // 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include "CapnpMessageFrameBuilder.hpp" 21 | 22 | 23 | #include "GuacamoleClient.hpp" 24 | #ifdef __cplusplus 25 | extern "C" { 26 | #endif 27 | #include 28 | #include 29 | #include 30 | #include "protocols/vnc/client.h" 31 | #ifdef __cplusplus 32 | } 33 | #endif 34 | 35 | #include 36 | #include 37 | #include 38 | #include "freerdp/client/channels.h" 39 | #include 40 | #include "protocols/rdp/client.h" 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | #include 53 | 54 | template 55 | using TestStrandGuard = StrandGuard; 56 | 57 | template 58 | struct TestWebSocket 59 | { 60 | TestWebSocket(boost::asio::io_context& io_context, 61 | boost::asio::ip::tcp::socket&& socket) 62 | : data_(io_context, std::move(socket)) 63 | { 64 | } 65 | 66 | template 67 | void Accept(TCallback&& callback) 68 | { 69 | data_.dispatch( 70 | [this, callback = std::move(callback)](auto& data) mutable 71 | { 72 | data.socket.binary(true); 73 | data.socket.async_accept(std::move(callback)); 74 | }); 75 | } 76 | 77 | void Read() 78 | { 79 | data_.dispatch( 80 | [this](auto& data) mutable 81 | { 82 | data.receiving = true; 83 | auto buffer_ptr = std::make_shared>(); 84 | auto& buffer = *buffer_ptr; 85 | data.socket.async_read(buffer, 86 | data_.wrap([this, buffer_ptr=std::move(buffer_ptr)] 87 | (auto& data, const auto error_code, const auto bytes_transferred) 88 | { 89 | if (error_code) 90 | { 91 | data.receiving = false; 92 | data.joined = false; 93 | Close(); 94 | return; 95 | } 96 | static_cast(*this).OnMessage(*buffer_ptr); 97 | Read(); 98 | })); 99 | }); 100 | } 101 | 102 | template 103 | void SendBatch(TCollectorCallback&& callback) 104 | { 105 | data_.dispatch( 106 | [this, callback = std::move(callback)](auto& data) mutable 107 | { 108 | callback([this, &data](std::shared_ptr> socket_message) 109 | { 110 | if (data.sending) 111 | { 112 | data.send_queue.push(socket_message); 113 | } 114 | else 115 | { 116 | data.sending = true; 117 | SendMessage(data.socket, std::move(socket_message)); 118 | } 119 | }); 120 | }); 121 | } 122 | 123 | void SetJoined() { 124 | data_.dispatch( 125 | [this](auto& data) 126 | { 127 | data.joined = true; 128 | }); 129 | } 130 | 131 | void Send(std::shared_ptr> socket_message) 132 | { 133 | data_.dispatch( 134 | [this, socket_message = std::move(socket_message)](auto& data) mutable 135 | { 136 | if (!data.joined) 137 | { 138 | return; 139 | } 140 | 141 | if (data.sending) 142 | { 143 | data.send_queue.push(socket_message); 144 | } 145 | else 146 | { 147 | data.sending = true; 148 | SendMessage(data.socket, std::move(socket_message)); 149 | } 150 | }); 151 | } 152 | 153 | void Close() 154 | { 155 | data_.dispatch([this](auto& data) 156 | { 157 | auto error_code = boost::system::error_code(); 158 | data.socket.next_layer().close(error_code); 159 | 160 | if (!data.sending && !data.receiving) 161 | { 162 | static_cast(*this).OnClose(); 163 | } 164 | }); 165 | } 166 | private: 167 | void SendMessage( 168 | boost::beast::websocket::stream& socket, 169 | std::shared_ptr>&& socket_message) 170 | { 171 | const auto buffers = boost::asio::const_buffer(socket_message->asBytes().begin(), 172 | socket_message->asBytes().size()); 173 | socket.async_write(buffers, 174 | data_.wrap([this, socket_message = std::move(socket_message)]( 175 | auto& data, const auto error_code, const auto bytes_transferred) mutable 176 | { 177 | if (error_code) 178 | { 179 | data.sending = false; 180 | data.joined = false; 181 | Close(); 182 | return; 183 | } 184 | if (data.send_queue.empty()) 185 | { 186 | data.sending = false; 187 | return; 188 | } 189 | SendMessage(data.socket, std::move(data.send_queue.front())); 190 | data.send_queue.pop(); 191 | })); 192 | } 193 | 194 | struct GuardedData 195 | { 196 | GuardedData(boost::asio::ip::tcp::socket&& socket) 197 | : socket(std::move(socket)), 198 | sending(false), 199 | receiving(false), 200 | joined(false) 201 | { 202 | } 203 | boost::beast::websocket::stream socket; 204 | std::queue>> send_queue; 205 | bool sending; 206 | bool receiving; 207 | bool joined; 208 | }; 209 | 210 | boost::asio::io_context io_context_; 211 | TestStrandGuard data_; 212 | }; 213 | 214 | struct TestGuacamoleClient 215 | final : CollabVm::Server::GuacamoleClient 216 | { 217 | struct TestGuacamoleWebSocket 218 | final : TestWebSocket 219 | { 220 | TestGuacamoleWebSocket( 221 | boost::asio::io_context& io_context, 222 | boost::asio::ip::tcp::socket&& socket, 223 | TestGuacamoleClient& guacamole_client) 224 | : TestWebSocket(io_context, std::move(socket)), 225 | guacamole_client_(guacamole_client) 226 | { 227 | } 228 | 229 | void SetIterator(const std::list::iterator iterator) 230 | { 231 | list_iterator_ = iterator; 232 | } 233 | 234 | void OnMessage(boost::beast::flat_static_buffer<1024>& buffer) 235 | { 236 | const auto buffer_data = buffer.data(); 237 | const auto array_ptr = kj::ArrayPtr( 238 | static_cast(buffer_data.data()), 239 | buffer_data.size() / sizeof(capnp::word)); 240 | auto reader = capnp::FlatArrayMessageReader(array_ptr); 241 | auto instr = reader.getRoot(); 242 | 243 | static auto created_screenshot = false; 244 | if (instr.isKey() && !created_screenshot) 245 | { 246 | auto png = std::ofstream("preview.png", std::ios::out | std::ios::binary); 247 | using file_char_type = std::remove_reference::type::char_type; 248 | guacamole_client_.CreateScreenshot([&png](const auto png_bytes) 249 | { 250 | std::cout << "png_bytes.size(): " << png_bytes.size() << std::endl; 251 | png.write( 252 | reinterpret_cast(png_bytes.data()), 253 | png_bytes.size()); 254 | }); 255 | created_screenshot = true; 256 | return; 257 | } 258 | 259 | guacamole_client_.ReadInstruction(instr); 260 | } 261 | 262 | void OnClose() 263 | { 264 | guacamole_client_.RemoveSocket(list_iterator_); 265 | } 266 | 267 | std::list::iterator list_iterator_; 268 | TestGuacamoleClient& guacamole_client_; 269 | }; 270 | 271 | TestGuacamoleClient(boost::asio::io_context& io_context, 272 | TestStrandGuard>& websockets) 273 | : GuacamoleClient(boost::asio::io_context::strand(io_context)), 274 | state_(io_context, State::kDisconnected), 275 | websockets_(websockets) 276 | { 277 | } 278 | 279 | void OnStart() 280 | { 281 | state_.dispatch([this](auto& state) 282 | { 283 | state = State::kConnected; 284 | websockets_.dispatch( 285 | [this](auto& websockets) 286 | { 287 | for (auto it = websockets.begin(); it != websockets.end(); it++) 288 | { 289 | AddUser(*it); 290 | } 291 | }); 292 | }); 293 | } 294 | 295 | void Add(TestGuacamoleWebSocket& websocket) 296 | { 297 | state_.dispatch([this, &websocket](const auto state) 298 | { 299 | if (state != State::kConnected) 300 | { 301 | return; 302 | } 303 | AddUser(websocket); 304 | }); 305 | } 306 | 307 | void RemoveSocket(std::list::iterator list_iterator) 308 | { 309 | websockets_.post([list_iterator](auto& sockets) 310 | { 311 | sockets.erase(list_iterator); 312 | // TestGuacamoleWebSocket is destructed 313 | }); 314 | } 315 | 316 | void Stop() 317 | { 318 | state_.dispatch([this](auto& state) 319 | { 320 | state = State::kStopped; 321 | }); 322 | } 323 | 324 | void OnStop() 325 | { 326 | state_.dispatch([this](auto& state) 327 | { 328 | if (state == State::kStopped) 329 | { 330 | websockets_.dispatch( 331 | [](auto& websockets) 332 | { 333 | for (auto& websocket : websockets) 334 | { 335 | websocket.Close(); 336 | } 337 | }); 338 | } 339 | else 340 | { 341 | state = State::kDisconnected; 342 | StartRDP(); 343 | // StartVNC(); 344 | } 345 | }); 346 | } 347 | 348 | void OnLog(const std::string_view message) 349 | { 350 | std::cout << message << std::endl; 351 | } 352 | 353 | void OnInstruction(capnp::MallocMessageBuilder& message_builder) 354 | { 355 | auto message = std::make_shared>( 356 | capnp::messageToFlatArray(message_builder)); 357 | websockets_.dispatch( 358 | [message = std::move(message)](auto& websockets) 359 | { 360 | for (auto& websocket : websockets) 361 | { 362 | websocket.Send(message); 363 | } 364 | }); 365 | } 366 | 367 | void OnFlush() 368 | { 369 | } 370 | 371 | private: 372 | void AddUser(TestGuacamoleWebSocket& websocket) 373 | { 374 | websocket.SendBatch([this, &websocket](auto send) 375 | { 376 | websocket.SetJoined(); 377 | GuacamoleClient::AddUser([send = std::move(send)]( 378 | capnp::MallocMessageBuilder&& message_builder) 379 | { 380 | send(std::make_shared>( 381 | capnp::messageToFlatArray(message_builder))); 382 | }); 383 | }); 384 | } 385 | 386 | enum class State : std::uint8_t 387 | { 388 | kStopped, 389 | kConnected, 390 | kDisconnected 391 | }; 392 | 393 | TestStrandGuard state_; 394 | TestStrandGuard>& websockets_; 395 | }; 396 | 397 | void AcceptSocket( 398 | boost::asio::io_context& io_context, 399 | TestGuacamoleClient& guacamole_client, 400 | boost::asio::ip::tcp::acceptor& acceptor, 401 | TestStrandGuard>& websockets) 402 | { 403 | acceptor.async_accept(websockets.wrap( 404 | [&io_context, &guacamole_client, &acceptor, &websockets_guard = websockets] 405 | (auto& websockets, const auto error_code, auto&& socket) 406 | { 407 | if (error_code) 408 | { 409 | return; 410 | } 411 | auto& websocket = 412 | websockets.emplace_back(io_context, std::move(socket), guacamole_client); 413 | websocket.SetIterator(std::prev(websockets.end())); 414 | websocket.Accept( 415 | [&websocket, &guacamole_client](const auto error_code) 416 | { 417 | if (!error_code) 418 | { 419 | guacamole_client.Add(websocket); 420 | websocket.Read(); 421 | } 422 | }); 423 | AcceptSocket(io_context, guacamole_client, acceptor, websockets_guard); 424 | })); 425 | } 426 | 427 | int main() 428 | { 429 | auto io_context = boost::asio::io_context(1); 430 | using CollabVm::Server::GuacamoleClient; 431 | auto websockets = TestStrandGuard>(io_context); 432 | auto guacamole_client = TestGuacamoleClient(io_context, websockets); 433 | const auto args = std::unordered_map{ 434 | {"hostname", "localhost"}, 435 | // {"port", "5900"} 436 | {"port", "3389"} 437 | }; 438 | guacamole_client.SetArguments(args); 439 | // guacamole_client.StartVNC(args); 440 | guacamole_client.StartRDP(); 441 | 442 | auto websockets_strand = boost::asio::io_context::strand(io_context); 443 | 444 | auto acceptor = boost::asio::ip::tcp::acceptor(io_context, { 445 | boost::asio::ip::make_address("127.0.0.1"), 8081 446 | }); 447 | AcceptSocket(io_context, guacamole_client, acceptor, websockets); 448 | 449 | auto interrupt_signal = boost::asio::signal_set(io_context, SIGINT, SIGTERM); 450 | interrupt_signal.async_wait( 451 | [&](auto error_code, const auto signal) 452 | { 453 | guacamole_client.Stop(); 454 | acceptor.close(error_code); 455 | }); 456 | 457 | io_context.run(); 458 | 459 | return 0; 460 | } 461 | 462 | -------------------------------------------------------------------------------- /tests/Totp.cpp: -------------------------------------------------------------------------------- 1 | #include "Totp.hpp" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | template 8 | constexpr auto make_byte_array(T... t) 9 | { 10 | return std::array{std::byte(t)...}; 11 | } 12 | 13 | int main() { 14 | constexpr auto byte_array = 15 | make_byte_array(61, 198, 202, 164, 130, 74, 109, 16 | 40, 135, 103, 178, 51, 30, 32, 17 | 180, 49, 102, 203, 133, 217); 18 | constexpr auto digits = 6; 19 | 20 | std::cout << std::dec << std::setfill('0') << std::setw(digits) 21 | << CollabVm::Server::Totp::GenerateTotp(byte_array, digits) 22 | << std::endl; 23 | 24 | return 0; 25 | } 26 | -------------------------------------------------------------------------------- /tests/TurnTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "TurnController.hpp" 6 | 7 | class TestUser; 8 | using TestUserPtr = std::shared_ptr; 9 | using UserTurnController = CollabVm::Server::TurnController; 10 | class TestUser final : public UserTurnController::UserTurnData 11 | { 12 | std::string name_; 13 | public: 14 | TestUser(std::string&& name) : name_(std::move(name)){} 15 | }; 16 | 17 | class TestUserTurnController final : public UserTurnController { 18 | using UserTurnController::UserTurnController; 19 | void OnCurrentUserChanged( 20 | const std::deque& users, 21 | std::chrono::milliseconds time_remaining) override 22 | { 23 | } 24 | void OnUserAdded( 25 | const std::deque& users, 26 | std::chrono::milliseconds time_remaining) override 27 | { 28 | } 29 | void OnUserRemoved( 30 | const std::deque& users, 31 | std::chrono::milliseconds time_remaining) override 32 | { 33 | } 34 | }; 35 | 36 | int main(int argc, char** args) 37 | { 38 | auto io_context = boost::asio::io_context(); 39 | auto turn_controller = TestUserTurnController(io_context); 40 | turn_controller.SetTurnTime(std::chrono::seconds(5)); 41 | 42 | const auto user1 = std::make_shared("user1"); 43 | turn_controller.RequestTurn(user1); 44 | 45 | const auto current_user = turn_controller.GetCurrentUser(); 46 | 47 | const auto user2 = std::make_shared("user2"); 48 | turn_controller.RequestTurn(user2); 49 | 50 | const auto user3 = std::make_shared("user3"); 51 | turn_controller.RequestTurn(user3); 52 | 53 | turn_controller.RemoveUser(user1); 54 | turn_controller.Clear(); 55 | 56 | io_context.run(); 57 | 58 | return 0; 59 | } 60 | --------------------------------------------------------------------------------