diff --git a/.circleci/config.yml b/.circleci/config.yml index cca8998f6..42eba7471 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,7 +13,7 @@ jobs: - checkout - run: name: Set up ssh & known_hosts - command: sudo /etc/init.d/ssh start; rm -f ~/.ssh/id_rsa*; ssh-keygen -q -N "" -f ~/.ssh/id_rsa; sudo rm ~/.ssh/authorized_keys; cp ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys; rm -f ~/.ssh/known_hosts; ssh -o "StrictHostKeyChecking no" localhost ls + command: sudo /etc/init.d/ssh start; rm -f ~/.ssh/id_rsa*; ssh-keygen -q -N "" -f ~/.ssh/id_rsa; sudo rm -f ~/.ssh/authorized_keys; cp ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys; rm -f ~/.ssh/known_hosts; ssh -o "StrictHostKeyChecking no" localhost ls - run: name: Init submodules command: if [ $CIRCLE_BRANCH != "release" ]; then git submodule update --init --recursive; fi @@ -54,7 +54,7 @@ jobs: - checkout - run: name: Set up ssh & known_hosts - command: sudo /etc/init.d/ssh start; rm -f ~/.ssh/id_rsa*; ssh-keygen -q -N "" -f ~/.ssh/id_rsa; sudo rm ~/.ssh/authorized_keys; cp ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys; rm -f ~/.ssh/known_hosts; ssh -o "StrictHostKeyChecking no" localhost ls + command: sudo /etc/init.d/ssh start; rm -f ~/.ssh/id_rsa*; ssh-keygen -q -N "" -f ~/.ssh/id_rsa; sudo rm -f ~/.ssh/authorized_keys; cp ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys; rm -f ~/.ssh/known_hosts; ssh -o "StrictHostKeyChecking no" localhost ls - run: name: Init submodules command: if [ $CIRCLE_BRANCH != "release" ]; then git submodule update --init --recursive; fi diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..5ab094519 --- /dev/null +++ b/.clang-format @@ -0,0 +1,3 @@ +--- +BasedOnStyle: Google +--- diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..9cc708368 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,45 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.224.3/containers/cpp/.devcontainer/base.Dockerfile + +# [Choice] Debian / Ubuntu version (use Debian 11, Ubuntu 18.04/21.04 on local arm64/Apple Silicon): debian-11, debian-10, ubuntu-21.04, ubuntu-20.04, ubuntu-18.04 +ARG VARIANT="bullseye" +FROM mcr.microsoft.com/vscode/devcontainers/cpp:0-${VARIANT} + +ARG USERNAME=vscode + +# [Optional] Install CMake version different from what base image has already installed. +# CMake reinstall choices: none, 3.21.5, 3.22.2, or versions from https://cmake.org/download/ +ARG REINSTALL_CMAKE_VERSION_FROM_SOURCE="3.23.1" + +# Use installed binaries from the system. +# Do not download latest version of CMake and Ninja during vcpkg bootstrap! +ENV VCPKG_FORCE_SYSTEM_BINARIES=1 +ENV VCPKG_USE_SYSTEM_BINARIES=1 + +# Optionally install the cmake for vcpkg +COPY ./reinstall-cmake.sh /tmp/ +RUN if [ "${REINSTALL_CMAKE_VERSION_FROM_SOURCE}" != "none" ]; then \ + chmod +x /tmp/reinstall-cmake.sh && /tmp/reinstall-cmake.sh ${REINSTALL_CMAKE_VERSION_FROM_SOURCE}; \ + fi \ + && rm -f /tmp/reinstall-cmake.sh + +# Install dependencies. +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends \ + libboost-dev \ + libsodium-dev \ + libncurses5-dev \ + libprotobuf-dev \ + protobuf-compiler \ + libgflags-dev \ + libutempter-dev \ + build-essential \ + ninja-build \ + # Note that in Ubuntu 21.04, there is no libcurl-dev, use libcurl4-openssl-dev + libcurl4-openssl-dev + +# +# Set up command history volume +# See https://code.visualstudio.com/remote/advancedcontainers/persist-bash-history +# +RUN mkdir /commandhistory \ + && chown -R $USERNAME /commandhistory diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..762900706 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,35 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.224.3/containers/cpp +{ + "name": "C++", + "build": { + "dockerfile": "Dockerfile", + // Update 'VARIANT' to pick an Debian / Ubuntu OS version: debian-11, debian-10, debian-9, ubuntu-21.04, ubuntu-20.04, ubuntu-18.04 + // Use Debian 11, Debian 9, Ubuntu 18.04 or Ubuntu 21.04 on local arm64/Apple Silicon + "args": { "VARIANT": "ubuntu-21.04" } + }, + "runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"], + + // Set *default* container specific settings.json values on container create. + "settings": {}, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "git submodule update --init --recursive", + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + + // For sharing shell history out of the container. + "mounts": [ + "source=eternalterminal-history,target=/commandhistory,type=volume" + ] +} diff --git a/.devcontainer/reinstall-cmake.sh b/.devcontainer/reinstall-cmake.sh new file mode 100644 index 000000000..c83325383 --- /dev/null +++ b/.devcontainer/reinstall-cmake.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +set -e + +CMAKE_VERSION=${1:-"none"} + +if [ "${CMAKE_VERSION}" = "none" ]; then + echo "No CMake version specified, skipping CMake reinstallation" + exit 0 +fi + +# Cleanup temporary directory and associated files when exiting the script. +cleanup() { + EXIT_CODE=$? + set +e + if [[ -n "${TMP_DIR}" ]]; then + echo "Executing cleanup of tmp files" + rm -Rf "${TMP_DIR}" + fi + exit $EXIT_CODE +} +trap cleanup EXIT + + +echo "Installing CMake..." +apt-get -y purge --auto-remove cmake +mkdir -p /opt/cmake + +architecture=$(dpkg --print-architecture) +case "${architecture}" in + arm64) + ARCH=aarch64 ;; + amd64) + ARCH=x86_64 ;; + *) + echo "Unsupported architecture ${architecture}." + exit 1 + ;; +esac + +CMAKE_BINARY_NAME="cmake-${CMAKE_VERSION}-linux-${ARCH}.sh" +CMAKE_CHECKSUM_NAME="cmake-${CMAKE_VERSION}-SHA-256.txt" +TMP_DIR=$(mktemp -d -t cmake-XXXXXXXXXX) + +echo "${TMP_DIR}" +cd "${TMP_DIR}" + +curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_BINARY_NAME}" -O +curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_CHECKSUM_NAME}" -O + +sha256sum -c --ignore-missing "${CMAKE_CHECKSUM_NAME}" +sh "${TMP_DIR}/${CMAKE_BINARY_NAME}" --prefix=/opt/cmake --skip-license + +ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake diff --git a/.github/workflows/vcpkg_build.yml b/.github/workflows/vcpkg_build.yml index 86bbc4e18..89961552e 100644 --- a/.github/workflows/vcpkg_build.yml +++ b/.github/workflows/vcpkg_build.yml @@ -13,6 +13,9 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest, macos-latest, windows-latest] + include: + - os: windows-latest + extension: .exe env: # Indicates the CMake build directory where project files and binaries are being produced. CMAKE_BUILD_DIR: ${{ github.workspace }}/build @@ -21,7 +24,26 @@ jobs: steps: - name: Install Dependencies (Linux) - run: sudo apt-get update && sudo apt-get install curl zip unzip tar cmake ninja-build libutempter-dev libunwind-dev libcurl4-openssl-dev + run: | + sudo apt-get update && \ + sudo apt-get install --no-install-recommends \ + libboost-dev \ + libsodium-dev \ + libncurses5-dev \ + libprotobuf-dev \ + protobuf-compiler \ + libgflags-dev \ + libutempter-dev \ + build-essential \ + ninja-build \ + libcurl4-openssl-dev \ + curl \ + zip \ + unzip \ + tar \ + cmake \ + libutempter-dev \ + libunwind-dev if: matrix.os == 'ubuntu-latest' - name: Install Dependencies (Windows) run: choco install ninja diff --git a/CMakeLists.txt b/CMakeLists.txt index 25e4c6bf9..60785337e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,9 +144,16 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DNO_TELEMETRY") endif() if(USE_SENTRY) +if(DISABLE_VCPKG) set(SENTRY_BUILD_RUNTIMESTATIC ON) set(BUILD_SHARED_LIBS OFF) add_subdirectory("${EXTERNAL_DIR}/sentry-native") +include_directories( + ${EXTERNAL_DIR}/sentry-native/include +) +else() +find_package(sentry CONFIG REQUIRED) +endif() endif() set(CMAKE_MODULE_PATH "${EXTERNAL_DIR}/cotire/CMake" @@ -157,6 +164,7 @@ if(POLICY CMP0058) endif() option(CODE_COVERAGE "Enable code coverage" OFF) +option(FUZZING "Enable builds for fuzz testing" OFF) option(DISABLE_CRASH_LOG "Disable installing easylogging crash handler" OFF) add_definitions(-DET_VERSION="${PROJECT_VERSION}") @@ -288,6 +296,26 @@ macro(DECORATE_TARGET TARGET_NAME) endif() endmacro() +macro(DECORATE_FUZZER TARGET_NAME) + add_sanitizers(${TARGET_NAME}) + + if(FUZZING) + # ASAN must also be enabled to build fuzzers. + if(NOT SANITIZE_ADDRESS) + message(FATAL_ERROR "Fuzzing requires SANITIZE_ADDRESS=ON to detect memory errors.") + endif() + + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set_property(TARGET ${TARGET_NAME} APPEND_STRING + PROPERTY COMPILE_FLAGS " -fsanitize=fuzzer") + set_property(TARGET ${TARGET_NAME} APPEND_STRING + PROPERTY LINK_FLAGS " -fsanitize=fuzzer") + else() + message(FATAL_ERROR "Currently fuzzing is only supported with Clang.") + endif() + endif() +endmacro() + include_directories( ${EXTERNAL_DIR}/easyloggingpp/src ${EXTERNAL_DIR}/ThreadPool @@ -295,7 +323,6 @@ include_directories( ${EXTERNAL_DIR}/Catch2/single_include ${EXTERNAL_DIR}/cxxopts/include ${EXTERNAL_DIR}/msgpack-c/include - ${EXTERNAL_DIR}/sentry-native/include ${EXTERNAL_DIR}/json/single_include/nlohmann ${EXTERNAL_DIR}/sole ${EXTERNAL_DIR}/base64 @@ -378,6 +405,8 @@ add_library( src/terminal/UserTerminalRouter.cpp src/terminal/TerminalClient.hpp src/terminal/TerminalClient.cpp + src/terminal/ServerFifoPath.hpp + src/terminal/ServerFifoPath.cpp src/terminal/SshSetupHandler.hpp src/terminal/SshSetupHandler.cpp src/terminal/UserTerminalHandler.hpp @@ -513,6 +542,46 @@ else(WIN32) add_test(et-test et-test) decorate_target(et-test) + if(FUZZING) + add_executable( + TerminalServerFuzzer + test/TerminalServerFuzzer.cpp + test/FuzzableTerminalServer.hpp + ) + add_dependencies(TerminalServerFuzzer TerminalCommon et-lib) + target_link_libraries( + TerminalServerFuzzer + TerminalCommon + et-lib + ${CMAKE_THREAD_LIBS_INIT} + ${PROTOBUF_LIBS} + ${sodium_LIBRARY_RELEASE} + ${SELINUX_LIBRARIES} + ${UTEMPTER_LIBRARIES} + ${Boost_LIBRARIES} + ${CORE_LIBRARIES}) + decorate_fuzzer(TerminalServerFuzzer) + + add_executable( + TerminalServerRouterFuzzer + test/TerminalServerRouterFuzzer.cpp + test/FuzzableTerminalServer.hpp + ) + add_dependencies(TerminalServerRouterFuzzer TerminalCommon et-lib) + target_link_libraries( + TerminalServerRouterFuzzer + TerminalCommon + et-lib + ${CMAKE_THREAD_LIBS_INIT} + ${PROTOBUF_LIBS} + ${sodium_LIBRARY_RELEASE} + ${SELINUX_LIBRARIES} + ${UTEMPTER_LIBRARIES} + ${Boost_LIBRARIES} + ${CORE_LIBRARIES}) + decorate_fuzzer(TerminalServerRouterFuzzer) + endif(FUZZING) + install( TARGETS etserver etterminal et htm htmd PERMISSIONS diff --git a/cmake/FindUnwind.cmake b/cmake/FindUnwind.cmake index 342609194..abe1a20ee 100644 --- a/cmake/FindUnwind.cmake +++ b/cmake/FindUnwind.cmake @@ -19,9 +19,9 @@ elseif (CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" OR set (Unwind_ARCH "unwind-x86_64" "unwind-x86") elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^i.86$") set (Unwind_ARCH "unwind-x86" "unwind-x86_64") -elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^ppc64") +elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(powerpc64|ppc64)") set (Unwind_ARCH "unwind-ppc64") -elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^ppc") +elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(powerpc|ppc)") set (Unwind_ARCH "unwind-ppc32") elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^mips") set (Unwind_ARCH "unwind-mips") diff --git a/etc/et.cfg b/etc/et.cfg index 25f0bb220..812adafdb 100644 --- a/etc/et.cfg +++ b/etc/et.cfg @@ -9,3 +9,4 @@ port = 2022 verbose = 0 silent = 0 logsize = 20971520 +telemetry = 1 diff --git a/external/sentry-native b/external/sentry-native index a0d789237..5500192dd 160000 --- a/external/sentry-native +++ b/external/sentry-native @@ -1 +1 @@ -Subproject commit a0d7892375e53188d1b4abdfc46888aa91c2a411 +Subproject commit 5500192dda05c82468787b2b0637e9c2688b9aed diff --git a/external/vcpkg b/external/vcpkg index 76d4836f3..e809a42f8 160000 --- a/external/vcpkg +++ b/external/vcpkg @@ -1 +1 @@ -Subproject commit 76d4836f3b1e027758044fdbdde91256b0f0955d +Subproject commit e809a42f87565e803b2178a0c11263f462d1800a diff --git a/scripts/ssh-et b/scripts/ssh-et new file mode 100755 index 000000000..a74a45578 --- /dev/null +++ b/scripts/ssh-et @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Usage: +# ssh-et [ssh_options] +# +# See also https://github.com/infokiller/ssh-et + +# See https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -o errexit -o errtrace -o nounset -o pipefail + +_command_exists() { + command -v -- "$1" &> /dev/null +} + +_log_info() { + printf 'ssh-et: %s\n' "$*" +} + +_error() { + local error normal + # Red color + error="$(tput setaf 1 2> /dev/null)" || true + normal="$(tput sgr0 2> /dev/null)" || true + printf >&2 '%s\n' "${error}${*}${normal}" +} + +_log_error() { + _error "$(printf 'ssh-et: %s' "$*")" +} + +_print_usage_and_die() { + printf >&2 'Usage: ssh-et [ssh_options] \n' + exit 1 +} + +_check_deps() { + local s=0 + if ((BASH_VERSINFO[0] < 4)); then + _log_error 'Bash version must be at least 4.0' + s=1 + fi + for cmd in et ssh; do + if ! _command_exists "${cmd}"; then + _log_error "Missing dependency: ${cmd}" + s=1 + fi + done + return $s +} + +_check_args() { + if (($# < 1)); then + _print_usage_and_die + fi + if [[ "${*: -1}" == -* ]]; then + _log_error 'Last arg must be a remote, not an SSH option' + _print_usage_and_die + fi +} + +# The range 49152–65535 contains ephemeral/dynamic ports. We scan 200 ports +# that start with the prefixes "522" or "622" (the 22 part of the prefix is +# useful to remember it's used for SSH). +_find_ephemeral_port() { + for port in {52200..52299} {62200..62299}; do + # Check if port is open. See: https://stackoverflow.com/a/6943581 + if ! { printf '' > /dev/tcp/127.0.0.1/"${port}"; } &> /dev/null; then + echo "${port}" + return 0 + fi + done + return 1 +} + +main() { + _check_deps || return 1 + _check_args "$@" || return 1 + local remote="${*: -1}" + local ssh_args=("${@:1:(($# - 1))}") + local tmpdir + tmpdir="$(mktemp -d -t "ssh-et-fifo-$$_XXX")" + # NOTE: The path variable in trap must be expanded here because it may not be + # defined when the trap is ran. + # shellcheck disable=SC2064 + trap "rm -rf -- '${tmpdir}' &> /dev/null || true" EXIT ERR INT HUP TERM + local port + if ! port="$(_find_ephemeral_port)"; then + _log_error 'Could not find an ephemeral port' + return 2 + fi + _log_info "Found open port: ${port}" + local et_fifo="${tmpdir}/et_fifo" + mkfifo "${et_fifo}" || return $? + local et_cmd=(et -t "${port}":22 -N "${remote}") + _log_info "Running: ${et_cmd[*]}" + "${et_cmd[@]}" > "${et_fifo}" & + et_pid=$! + found=0 + while IFS='' read -r line; do + printf 'et: %s\n' "${line}" + if [[ $line == *"feel free to background"* ]]; then + found=1 + break + fi + done < "${et_fifo}" + ((found)) || return 3 + # We use the localhost loopback address for all remote hosts, so we don't want + # to register it in the known hosts file or do any host auth against it (which + # will lead to errors since SSH will think the host key changed). et does its + # own host auth so this is hopefully safe. + local ssh_cmd=( + ssh -o 'UserKnownHostsFile=/dev/null' -o 'StrictHostKeyChecking=no' + '127.0.0.1' -p "${port}" "${ssh_args[@]}" + ) + _log_info "Running: ${ssh_cmd[*]}" + "${ssh_cmd[@]}" + kill "${et_pid}" + # wait "${et_pid}" + rm -rf -- "${tmpdir}" +} + +main "$@" diff --git a/src/base/Headers.hpp b/src/base/Headers.hpp index 9f94841fa..c5f9ba43f 100644 --- a/src/base/Headers.hpp +++ b/src/base/Headers.hpp @@ -251,6 +251,12 @@ inline string WindowsErrnoToString() { #define FATAL_FAIL_UNLESS_EINVAL(X) \ if (((X) == -1) && GetErrno() != EINVAL) \ STFATAL << "Error: (" << GetErrno() << "): " << strerror(GetErrno()); + +// On FreeBSD we can get EAGAIN on close if the descriptor is being +// selected. +#define FATAL_FAIL_UNLESS_EAGAIN(X) \ + if (((X) == -1) && GetErrno() != EAGAIN) \ + STFATAL << "Error: (" << GetErrno() << "): " << strerror(GetErrno()); #endif #ifndef ET_VERSION @@ -325,6 +331,12 @@ inline string protoToString(const T& t) { return s; } +/** + * Wait on a fd to have data available. + * + * @return true if the fd has data, or false if the timeout (of 1 second) is + * reached or if the call is interrupted by a syscall. + */ inline bool waitOnSocketData(int fd) { fd_set fdset; FD_ZERO(&fdset); @@ -333,8 +345,17 @@ inline bool waitOnSocketData(int fd) { tv.tv_sec = 1; tv.tv_usec = 0; VLOG(4) << "Before selecting sockFd"; - FATAL_FAIL(select(fd + 1, &fdset, NULL, NULL, &tv)); - return FD_ISSET(fd, &fdset); + const int selectResult = select(fd + 1, &fdset, NULL, NULL, &tv); + if (selectResult < 0) { + if (errno == EINTR) { + // Interrupted by the signal, the caller will retry. + return false; + } else { + FATAL_FAIL(selectResult); + } + } else { + return FD_ISSET(fd, &fdset); + } } inline string genRandomAlphaNum(int len) { diff --git a/src/base/ServerClientConnection.cpp b/src/base/ServerClientConnection.cpp index 6a3aaef80..32883eabc 100644 --- a/src/base/ServerClientConnection.cpp +++ b/src/base/ServerClientConnection.cpp @@ -33,4 +33,19 @@ bool ServerClientConnection::recoverClient(int newSocketFd) { } return recover(newSocketFd); } + +bool ServerClientConnection::verifyPasskey(const string& targetKey) { + // Do a string comparison without revealing timing information if an early + // character mismatches, always loop through the entire string. + const size_t commonSize = + key.size() < targetKey.size() ? key.size() : targetKey.size(); + + bool matchFailed = key.size() != targetKey.size(); + for (size_t i = 0; i < commonSize; ++i) { + matchFailed |= key[i] != targetKey[i]; + } + + return !matchFailed; +} + } // namespace et diff --git a/src/base/ServerClientConnection.hpp b/src/base/ServerClientConnection.hpp index 3e371f9e2..5bdc687e9 100644 --- a/src/base/ServerClientConnection.hpp +++ b/src/base/ServerClientConnection.hpp @@ -1,9 +1,8 @@ #ifndef __ET_SERVER_CLIENT_CONNECTION__ #define __ET_SERVER_CLIENT_CONNECTION__ -#include "Headers.hpp" - #include "Connection.hpp" +#include "Headers.hpp" namespace et { class ServerClientConnection : public Connection { @@ -16,6 +15,8 @@ class ServerClientConnection : public Connection { bool recoverClient(int newSocketFd); + bool verifyPasskey(const string& targetKey); + protected: }; } // namespace et diff --git a/src/base/ServerConnection.cpp b/src/base/ServerConnection.cpp index 46a758f29..74d7c6e20 100644 --- a/src/base/ServerConnection.cpp +++ b/src/base/ServerConnection.cpp @@ -39,6 +39,7 @@ void ServerConnection::clientHandler(int clientSocketFd) { el::Helpers::setThreadName("server-clientHandler"); string clientId; + bool createdClientConnection = false; try { et::ConnectRequest request = socketHandler->readProto(clientSocketFd, true); @@ -64,14 +65,24 @@ void ServerConnection::clientHandler(int clientSocketFd) { } } clientId = request.clientid(); - LOG(INFO) << "Got client with id: " << clientId; shared_ptr serverClientState = NULL; bool clientKeyExistsNow; + { lock_guard guard(classMutex); + + // Log within the mutex, so that we can guarantee that this client id wins + // the lock when this message appears. + LOG(INFO) << "Got client with id: " << clientId; + clientKeyExistsNow = clientKeyExists(clientId); if (clientConnectionExists(clientId)) { serverClientState = getClientConnection(clientId); + } else if (clientKeyExistsNow) { + createdClientConnection = true; + serverClientState.reset(new ServerClientConnection( + socketHandler, clientId, clientSocketFd, clientKeys.at(clientId))); + clientConnections.insert(std::make_pair(clientId, serverClientState)); } } if (!clientKeyExistsNow) { @@ -85,7 +96,7 @@ void ServerConnection::clientHandler(int clientSocketFd) { socketHandler->writeProto(clientSocketFd, response, true); socketHandler->close(clientSocketFd); - } else if (serverClientState.get() == NULL) { + } else if (createdClientConnection) { et::ConnectResponse response; response.set_status(NEW_CLIENT); socketHandler->writeProto(clientSocketFd, response, true); @@ -95,9 +106,6 @@ void ServerConnection::clientHandler(int clientSocketFd) { { lock_guard guard(classMutex); - serverClientState.reset(new ServerClientConnection( - socketHandler, clientId, clientSocketFd, clientKeys.at(clientId))); - clientConnections.insert(std::make_pair(clientId, serverClientState)); if (!newClient(serverClientState)) { VLOG(1) << "newClient failed"; @@ -117,9 +125,15 @@ void ServerConnection::clientHandler(int clientSocketFd) { } catch (const runtime_error& err) { // Comm failed, close the connection LOG(WARNING) << "Error handling new client: " << err.what(); + if (createdClientConnection) { + destroyPartialConnection(clientId); + } socketHandler->close(clientSocketFd); - } catch (const std::exception &e) { + } catch (const std::exception& e) { LOG(ERROR) << "Got an unexpected error handling new client: " << e.what(); + if (createdClientConnection) { + destroyPartialConnection(clientId); + } socketHandler->close(clientSocketFd); } } @@ -138,4 +152,15 @@ bool ServerConnection::removeClient(const string& id) { clientConnections.erase(id); return true; } + +void ServerConnection::destroyPartialConnection(const string& clientId) { + lock_guard guard(classMutex); + const auto it = clientConnections.find(clientId); + if (it == clientConnections.end()) { + return; + } + it->second->shutdown(); + clientConnections.erase(it); +} + } // namespace et diff --git a/src/base/ServerConnection.hpp b/src/base/ServerConnection.hpp index 9b01c6216..f7c937c9a 100644 --- a/src/base/ServerConnection.hpp +++ b/src/base/ServerConnection.hpp @@ -57,6 +57,8 @@ class ServerConnection { shared_ptr serverClientState) = 0; protected: + void destroyPartialConnection(const string& clientId); + shared_ptr socketHandler; SocketEndpoint serverEndpoint; std::unordered_map clientKeys; diff --git a/src/base/UnixSocketHandler.cpp b/src/base/UnixSocketHandler.cpp index 4bbd38814..21f742b18 100644 --- a/src/base/UnixSocketHandler.cpp +++ b/src/base/UnixSocketHandler.cpp @@ -167,8 +167,12 @@ void UnixSocketHandler::close(int fd) { setBlocking(fd, true); #ifdef _MSC_VER FATAL_FAIL_UNLESS_ZERO(::closesocket(fd)); +#else +#ifdef __FreeBSD__ + FATAL_FAIL_UNLESS_EAGAIN(::close(fd)); #else FATAL_FAIL(::close(fd)); +#endif #endif activeSocketMutexes.erase(it); } diff --git a/src/terminal/ServerFifoPath.cpp b/src/terminal/ServerFifoPath.cpp new file mode 100644 index 000000000..f487e2350 --- /dev/null +++ b/src/terminal/ServerFifoPath.cpp @@ -0,0 +1,223 @@ +#ifndef WIN32 +#include "ServerFifoPath.hpp" + +namespace et { + +/** + * @file + * + * Provides utilities for creating and finding the server fifo path, handling + * cases where etserver is running as either root or another user. + * + * When running as root, this applies the following principles to be defensive: + * - Only use "/var/run" as the fifo directory. + * - Do not query environment variables. + * - Do not create directories or change file permissions. + * + * For all users, this takes a fail-fast approach, where instead of correcting + * issues it will crash or error out. + */ + +namespace { + +// As root, prefer "/var/run" since it is not world-writeable. +const string ROUTER_FIFO_BASENAME = "etserver.idpasskey.fifo"; +const string ROOT_FIFO_DIRECTORY = "/var/run"; +const string ROOT_ROUTER_FIFO_NAME = + ROOT_FIFO_DIRECTORY + "/" + ROUTER_FIFO_BASENAME; + +struct ValueWithDefault { + string value; + bool isDefault; +}; + +bool IsRoot() { return ::geteuid() == 0; } + +bool IsAbsolutePath(const string& path) { + return (!path.empty() && path[0] == '/'); +} + +string GetHome() { + const char* home = getenv("HOME"); + CHECK_NOTNULL(home) + << "Failed to get the value of the $HOME environment variable."; + + string homeStr(home); + CHECK(IsAbsolutePath(homeStr)) + << "Unexpected relative path for $HOME environment variable: " << homeStr; + return homeStr; +} + +/** + * Get the value of XDG_RUNTIME_DIR, by following the specification defined + * here: + * https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + */ +ValueWithDefault GetXdgRuntimeDir() { + // If the env doesn't exist, or is not an absolute path, fallback to + // $HOME/.local/share since it can be created on mac as well. + // + // Per the spec: + // > If an implementation encounters a relative path in any of these variables + // > it should consider the path invalid and ignore it + if (const char* dataHome = getenv("XDG_RUNTIME_DIR")) { + if (IsAbsolutePath(dataHome)) { + return ValueWithDefault{dataHome, /*isDefault*/ false}; + } + } + + return ValueWithDefault{GetHome() + string("/.local/share"), + /*isDefault*/ true}; +} + +void TryCreateDirectory(string dir, mode_t mode) { + // Reset umask to 0 while creating subdirs, and restore after. + const mode_t oldMode = ::umask(0); + + if (::mkdir(dir.c_str(), mode) == -1) { + // Permit EEXIST if the directory already exists. + CHECK_EQ(errno, EEXIST) + << "Unexpected result creating " << dir << ": " << strerror(errno); + } + + CHECK_EQ(::umask(oldMode), 0) + << "Unexpected result when restoring umask, which should return the " + "previous overriden value (0)."; +} + +} // namespace + +ServerFifoPath::ServerFifoPath() = default; + +void ServerFifoPath::setPathOverride(string path) { + CHECK(!path.empty()) << "Server fifo path must not be empty"; + pathOverride = path; +} + +void ServerFifoPath::createDirectoriesIfRequired() { + // No action required unless we're running as non-root. + if (pathOverride || IsRoot()) { + return; + } + + const auto xdgRuntimeDir = GetXdgRuntimeDir(); + if (xdgRuntimeDir.isDefault) { + // Only create directories if the default path is returned. + // + // Create subdirectories for ~/.local/share. These may already be created + // with different permissions on different machines, so also create an + // etserver subdir to enforce 0700 permssions. + const string homeDir = GetHome(); + TryCreateDirectory(homeDir + "/.local", 0755); + TryCreateDirectory(homeDir + "/.local/share", 0755); + } + + const string etserverDir = xdgRuntimeDir.value + "/etserver"; + + // First try creating the directory. TryCreateDirectory will ignore error if + // the directory already exists. + TryCreateDirectory(etserverDir, 0700); + + struct stat etserverStat; + const int statResult = ::stat(etserverDir.c_str(), &etserverStat); + if (statResult != 0) { + LOG(FATAL) << "Failed to create server fifo directory: " << etserverDir + << "\n" + << "Error: " << strerror(errno); + } + + // Directory exists, verify that it has the appropriate permissions. + if (etserverStat.st_uid != ::geteuid()) { + LOG(FATAL) << "Server fifo directory must be owned by the current " + "user: " + << etserverDir << "\n" + << "Expected euid=" << ::geteuid() + << ", actual=" << etserverStat.st_uid; + } + + if (!S_ISDIR(etserverStat.st_mode)) { + LOG(FATAL) << "Server fifo directory must be a directory: " << etserverDir; + } + + // Fail if the folder has write permissions to group or other. + if ((etserverStat.st_mode & (S_IWGRP | S_IWOTH)) != 0) { + LOG(FATAL) << "Server fifo directory must not provide write access to " + "group/other: " + << etserverDir; + } +} + +string ServerFifoPath::getPathForCreation() { + if (pathOverride) { + return pathOverride.value(); + } else if (IsRoot()) { + return ROOT_ROUTER_FIFO_NAME; + } else { + return GetXdgRuntimeDir().value + string("/etserver/") + + ROUTER_FIFO_BASENAME; + } +} + +optional ServerFifoPath::getEndpointForConnect() { + if (pathOverride) { + SocketEndpoint endpoint; + endpoint.set_name(pathOverride.value()); + return endpoint; + } else { + return std::nullopt; + } +} + +void reportConnectionError() { + const int localErrno = GetErrno(); + + if (localErrno == ECONNREFUSED) { + CLOG(INFO, "stdout") + << "Error: The Eternal Terminal daemon is not running. Please " + "(re)start the et daemon on the server." + << endl; + } else { + CLOG(INFO, "stdout") + << "Error: Connection error communicating with et daemon: " + << strerror(localErrno) << "." << endl; + } + exit(1); +} + +int ServerFifoPath::detectAndConnect( + const optional specificRouterEndpoint, + const shared_ptr& socketHandler) { + int routerFd = -1; + if (specificRouterEndpoint) { + routerFd = socketHandler->connect(specificRouterEndpoint.value()); + if (routerFd < 0) { + reportConnectionError(); + } + } else { + SocketEndpoint rootRouterEndpoint; + rootRouterEndpoint.set_name(ROOT_ROUTER_FIFO_NAME); + routerFd = socketHandler->connect(rootRouterEndpoint); + if (routerFd >= 0) { + // Successfully connected. + return routerFd; + } + + if (!IsRoot()) { + // Fallback to trying the non-root location. + SocketEndpoint nonRootRouterEndpoint; + nonRootRouterEndpoint.set_name(GetXdgRuntimeDir().value + + string("/etserver/") + + ROUTER_FIFO_BASENAME); + routerFd = socketHandler->connect(nonRootRouterEndpoint); + } + + if (routerFd < 0) { + reportConnectionError(); + } + } + + return routerFd; +} + +} // namespace et +#endif diff --git a/src/terminal/ServerFifoPath.hpp b/src/terminal/ServerFifoPath.hpp new file mode 100644 index 000000000..0f1b4f957 --- /dev/null +++ b/src/terminal/ServerFifoPath.hpp @@ -0,0 +1,89 @@ +#ifndef __ET_SERVER_FIFO_PATH__ +#define __ET_SERVER_FIFO_PATH__ + +#include + +#include "Headers.hpp" +#include "SocketHandler.hpp" + +namespace et { + +/** + * A helper class to handle creating and detecting the server fifo path. + * + * The default fifo path location varies based on which user the etserver + * process is running as, and it may also be overridden from a command line + * flag. + * + * This class aggregates that logic, both on the server and client side. + * + * To use: + * - Create the class, and optionally call \ref setPathOverride. + * - On the creation side, call \ref createDirectoriesIfRequired and \ref + * getPathForCreation. + * - On the client side, call \ref getEndpointForConnect and \ref + * detectAndConnect, which will either connect to the overridden path or try + * both the root location, followed by the non-root location of the fifo to + * connect. Since a broken fifo file can be left behind when the process + * exits, this tries to connect to each pipe in sequence and performs a + * graceful fallback. + * + * For root, the fifo is placed in the root-accessible directory /var/run. + * + * For non-root, this is placed in the user directory, under $HOME/.local/share, + * following the XDG spec. This class contains logic to create the + * $HOME/.local/share directory structure if required. This means that if the + * server runs as a non-root user, it may only be connected by the same user. + */ +class ServerFifoPath { + public: + ServerFifoPath(); + + /** + * Overrides the fifo path to a user-specified location. Note that this + * disables the auto-detection behavior. + * + * @param path User-specified path to the serverfifo. + */ + void setPathOverride(string path); + + /** + * Based on the current uid, create the directory structure required to store + * the fifo once it is created. If XDG_DATA_HOME is not set and the processes + * user cannot access /var/run, this will ensure that $HOME/.local/share + * exists. + */ + void createDirectoriesIfRequired(); + + /** + * Get the computed fifo path to use when creating the fifo. This will return + * the override path, or a location in either /var/run as root or + * $HOME/.local/share otherwise. + */ + string getPathForCreation(); + + /** + * Return an SocketEndpoint or nullopt based on the current configuration, + * which may later be passed to \ref detectAndConnect to connect to the + * relevant endpoint. + */ + optional getEndpointForConnect(); + + /** + * Either connect to the specific router endpoint, if provided, or detect and + * connect to the default root or non-root location of the endpoint. + * + * @return fd of the connected pipe, always valid. Exits internally if the + * pipe cannot be connected. + */ + static int detectAndConnect( + const optional specificRouterEndpoint, + const shared_ptr& socketHandler); + + private: + optional pathOverride; +}; + +} // namespace et + +#endif // __ET_SERVER_FIFO_PATH__ diff --git a/src/terminal/TelemetryService.cpp b/src/terminal/TelemetryService.cpp index 397c8c161..895a4167d 100644 --- a/src/terminal/TelemetryService.cpp +++ b/src/terminal/TelemetryService.cpp @@ -76,8 +76,8 @@ class TelemetryDispatcher : public el::LogDispatchCallback { }; void shutdownTelemetry() { - cerr << "Shutting down sentry" << endl; if (TelemetryService::exists()) { + cerr << "Shutting down sentry" << endl; auto ts = TelemetryService::get(); ts->shutdown(); } @@ -158,9 +158,6 @@ TelemetryService::TelemetryService(const bool _allow, sentry_set_user(user); vector signalsToCatch = { -#ifdef SIGINT - SIGINT, -#endif #ifdef SIGILL SIGILL, #endif @@ -183,6 +180,16 @@ TelemetryService::TelemetryService(const bool _allow, for (auto it : signalsToCatch) { signal(it, sentryShutdownHandler); } + +#ifdef SIGINT + signal(SIGINT, [](int i) { + shutdownTelemetry(); + // Normally this is installed in TerminalServerMain, TerminalClientMain, + // and TerminalMain, but we need to forward the call since Sentry + // overrides it. This is important to handle SIGINT from Ctrl-C. + et::InterruptSignalHandler(i); + }); +#endif #endif atexit([] { shutdownTelemetry(); }); diff --git a/src/terminal/TerminalClient.cpp b/src/terminal/TerminalClient.cpp index c1e4c8151..178b67854 100644 --- a/src/terminal/TerminalClient.cpp +++ b/src/terminal/TerminalClient.cpp @@ -220,6 +220,8 @@ void TerminalClient::run(const string& command) { TerminalInfo lastTerminalInfo; if (!console.get()) { + // NOTE: ../../scripts/ssh-et relies on the wording of this message, so if + // you change it please update it as well. CLOG(INFO, "stdout") << "ET running, feel free to background..." << endl; } diff --git a/src/terminal/TerminalMain.cpp b/src/terminal/TerminalMain.cpp index 0ff2a7e8d..3c8c29297 100644 --- a/src/terminal/TerminalMain.cpp +++ b/src/terminal/TerminalMain.cpp @@ -5,6 +5,7 @@ #include "ParseConfigFile.hpp" #include "PipeSocketHandler.hpp" #include "PsuedoUserTerminal.hpp" +#include "ServerFifoPath.hpp" #include "TcpSocketHandler.hpp" #include "UserJumphostHandler.hpp" #include "UserTerminalHandler.hpp" @@ -55,8 +56,8 @@ int main(int argc, char** argv) { ("logtostdout", "Write log to stdout") // ("serverfifo", "If set, connects to the etserver instance listening on the matching " - "fifo name", // - cxxopts::value()->default_value(ROUTER_FIFO_NAME)) // + "fifo name", // + cxxopts::value()->default_value("")) // ; options.parse_positional({"host", "positional"}); @@ -79,6 +80,11 @@ int main(int argc, char** argv) { GOOGLE_PROTOBUF_VERIFY_VERSION; srand(1); + ServerFifoPath serverFifo; + if (!result["serverfifo"].as().empty()) { + serverFifo.setPathOverride(result["serverfifo"].as()); + } + shared_ptr ipcSocketHandler(new PipeSocketHandler()); shared_ptr term(new PsuedoUserTerminal()); @@ -93,11 +99,14 @@ int main(int argc, char** argv) { FD_SET(STDIN_FILENO, &readfds); - int res = select(1, &readfds, NULL, NULL, &timeout); - if (res < 0) { - FATAL_FAIL(res); - } - if (res == 0) { + int selectResult = 0; + do { + // Repeatedly calls when interrupted, up to the timeout. + selectResult = select(1, &readfds, NULL, NULL, &timeout); + } while (selectResult < 0 && errno == EINTR); + + FATAL_FAIL(selectResult); + if (selectResult == 0) { CLOG(INFO, "stdout") << "Call etterminal with --idpasskey or --idpasskeyfile, or feed " "this information on stdin\n"; @@ -160,15 +169,13 @@ int main(int argc, char** argv) { if (DaemonCreator::createSessionLeader() == -1) { STFATAL << "Error creating daemon: " << strerror(GetErrno()); } - SocketEndpoint routerFifoEndpoint; - routerFifoEndpoint.set_name(result["serverfifo"].as()); SocketEndpoint destinationEndpoint; destinationEndpoint.set_name(result["dsthost"].as()); destinationEndpoint.set_port(result["dstport"].as()); shared_ptr jumpClientSocketHandler(new TcpSocketHandler()); UserJumphostHandler ujh(jumpClientSocketHandler, idpasskey, destinationEndpoint, ipcSocketHandler, - routerFifoEndpoint); + serverFifo.getEndpointForConnect()); ujh.run(); // Uninstall log rotation callback @@ -190,10 +197,8 @@ int main(int argc, char** argv) { // Install log rotation callback el::Helpers::installPreRollOutCallback(LogHandler::rolloutHandler); - SocketEndpoint routerEndpoint; - routerEndpoint.set_name(result["serverfifo"].as()); - UserTerminalHandler uth(ipcSocketHandler, term, true, routerEndpoint, - idpasskey); + UserTerminalHandler uth(ipcSocketHandler, term, true, + serverFifo.getEndpointForConnect(), idpasskey); CLOG(INFO, "stdout") << "IDPASSKEY:" << idpasskey << endl; if (DaemonCreator::createSessionLeader() == -1) { STFATAL << "Error creating daemon: " << strerror(GetErrno()); diff --git a/src/terminal/TerminalServer.cpp b/src/terminal/TerminalServer.cpp index 82a543ed1..c980019d7 100644 --- a/src/terminal/TerminalServer.cpp +++ b/src/terminal/TerminalServer.cpp @@ -35,8 +35,10 @@ void TerminalServer::run() { maxCoreFd = max(maxCoreFd, terminalRouter->getServerFd()); numCoreFds++; - TelemetryService::get()->logToDatadog("Server started", el::Level::Info, - __FILE__, __LINE__); + if (TelemetryService::exists()) { + TelemetryService::get()->logToDatadog("Server started", el::Level::Info, + __FILE__, __LINE__); + } while (true) { { @@ -57,7 +59,15 @@ void TerminalServer::run() { tv.tv_sec = 0; tv.tv_usec = 10000; - int numFdsSet = select(maxFd + 1, &rfds, NULL, NULL, &tv); + + const int numFdsSet = select(maxFd + 1, &rfds, NULL, NULL, &tv); + if (numFdsSet < 0 && errno == EINTR) { + // If EINTR was returned, then the syscall was interrupted by a signal. + // This is not an error, but can be a signal that the program is being + // shutdown, so restart the loop to check for the halt condition. + continue; + } + FATAL_FAIL(numFdsSet); if (numFdsSet == 0) { continue; @@ -97,8 +107,16 @@ void TerminalServer::runJumpHost( el::Helpers::setThreadName(serverClientState->getId()); bool run = true; - int terminalFd = - terminalRouter->getInfoForId(serverClientState->getId()).fd(); + int terminalFd = -1; + if (auto maybeUserInfo = + terminalRouter->tryGetInfoForConnection(serverClientState)) { + terminalFd = maybeUserInfo->fd(); + } else { + LOG(ERROR) << "Jumphost failed to bind to terminal router"; + serverClientState->closeSocket(); + return; + } + shared_ptr terminalSocketHandler = terminalRouter->getSocketHandler(); @@ -179,7 +197,16 @@ void TerminalServer::runJumpHost( void TerminalServer::runTerminal( shared_ptr serverClientState, const InitialPayload &payload) { - auto userInfo = terminalRouter->getInfoForId(serverClientState->getId()); + auto maybeUserInfo = + terminalRouter->tryGetInfoForConnection(serverClientState); + if (!maybeUserInfo) { + LOG(ERROR) << "Terminal client failed to bind to terminal router"; + serverClientState->closeSocket(); + return; + } + + const auto userInfo = std::move(maybeUserInfo.value()); + InitialResponse response; shared_ptr serverSocketHandler = getSocketHandler(); shared_ptr pipeSocketHandler(new PipeSocketHandler()); diff --git a/src/terminal/TerminalServerMain.cpp b/src/terminal/TerminalServerMain.cpp index 346342123..b4ef770c0 100644 --- a/src/terminal/TerminalServerMain.cpp +++ b/src/terminal/TerminalServerMain.cpp @@ -1,5 +1,6 @@ #include +#include "ServerFifoPath.hpp" #include "SimpleIni.h" #include "TelemetryService.hpp" #include "TerminalServer.hpp" @@ -43,8 +44,8 @@ int main(int argc, char **argv) { ("v,verbose", "Enable verbose logging", cxxopts::value()->default_value("0"), "LEVEL") // ("serverfifo", - "If set, listens on the matching fifo name", // - cxxopts::value()->default_value(ROUTER_FIFO_NAME)) // + "If set, listens on the matching fifo name", // + cxxopts::value()->default_value("")) // ("telemetry", "Allow et to anonymously send errors to guide future improvements", cxxopts::value()) // @@ -75,7 +76,7 @@ int main(int argc, char **argv) { LogHandler::stderrToFile(GetTempDirectory() + "etserver"); } - string serverFifo = ROUTER_FIFO_NAME; + ServerFifoPath serverFifo; // default max log file size is 20MB for etserver string maxlogsize = "20971520"; @@ -112,11 +113,11 @@ int main(int argc, char **argv) { el::Loggers::setVerboseLevel(atoi(vlevel)); } - { - const char *fifoName = - ini.GetValue("Debug", "serverfifo", ROUTER_FIFO_NAME.c_str()); - if (fifoName) { - serverFifo = string(fifoName); + const char *fifoName = ini.GetValue("Debug", "serverfifo", NULL); + if (fifoName) { + const string fifoNameStr(fifoName); + if (!fifoNameStr.empty()) { + serverFifo.setPathOverride(fifoNameStr); } } @@ -138,8 +139,8 @@ int main(int argc, char **argv) { } if (result.count("serverfifo") && - result["serverfifo"].as() != ROUTER_FIFO_NAME) { - serverFifo = result["serverfifo"].as(); + !result["serverfifo"].as().empty()) { + serverFifo.setPathOverride(result["serverfifo"].as()); } if (result.count("port")) { @@ -175,6 +176,8 @@ int main(int argc, char **argv) { TelemetryService::create( telemetry, GetTempDirectory() + "/.sentry-native-etserver", "Server"); + serverFifo.createDirectoriesIfRequired(); + std::shared_ptr tcpSocketHandler(new TcpSocketHandler()); std::shared_ptr pipeSocketHandler( new PipeSocketHandler()); @@ -187,7 +190,7 @@ int main(int argc, char **argv) { serverEndpoint.set_name(bindIp); } SocketEndpoint routerFifo; - routerFifo.set_name(serverFifo); + routerFifo.set_name(serverFifo.getPathForCreation()); TerminalServer terminalServer(tcpSocketHandler, serverEndpoint, pipeSocketHandler, routerFifo); terminalServer.run(); diff --git a/src/terminal/UserJumphostHandler.cpp b/src/terminal/UserJumphostHandler.cpp index 936f838d8..30089b4b4 100644 --- a/src/terminal/UserJumphostHandler.cpp +++ b/src/terminal/UserJumphostHandler.cpp @@ -2,34 +2,21 @@ #include "UserJumphostHandler.hpp" #include "ETerminal.pb.h" +#include "ServerFifoPath.hpp" namespace et { UserJumphostHandler::UserJumphostHandler( shared_ptr _jumpClientSocketHandler, const string &_idpasskey, const SocketEndpoint &_dstSocketEndpoint, shared_ptr _routerSocketHandler, - const SocketEndpoint &routerEndpoint) + const optional routerEndpoint) : routerSocketHandler(_routerSocketHandler), jumpClientSocketHandler(_jumpClientSocketHandler), idpasskey(_idpasskey), dstSocketEndpoint(_dstSocketEndpoint), shuttingDown(false) { - routerFd = routerSocketHandler->connect(routerEndpoint); - auto localErrno = GetErrno(); - - if (routerFd < 0) { - if (localErrno == ECONNREFUSED) { - CLOG(INFO, "stdout") - << "Error: The Eternal Terminal daemon is not running. Please " - "(re)start the et daemon on the server." - << endl; - } else { - CLOG(INFO, "stdout") - << "Error: Connection error communicating with et daemon: " - << strerror(localErrno) << "." << endl; - } - exit(1); - } + routerFd = + ServerFifoPath::detectAndConnect(routerEndpoint, routerSocketHandler); } void UserJumphostHandler::run() { diff --git a/src/terminal/UserJumphostHandler.hpp b/src/terminal/UserJumphostHandler.hpp index 9392becb6..6b23921f6 100644 --- a/src/terminal/UserJumphostHandler.hpp +++ b/src/terminal/UserJumphostHandler.hpp @@ -10,7 +10,7 @@ class UserJumphostHandler { const string &_idpasskey, const SocketEndpoint &_dstSocketEndpoint, shared_ptr routerSocketHandler, - const SocketEndpoint &routerEndpoint); + const optional routerEndpoint); void run(); void shutdown() { @@ -28,4 +28,4 @@ class UserJumphostHandler { bool shuttingDown; recursive_mutex shutdownMutex; }; -} // namespace et \ No newline at end of file +} // namespace et diff --git a/src/terminal/UserTerminalHandler.cpp b/src/terminal/UserTerminalHandler.cpp index 3b0e0289c..d64ee57e3 100644 --- a/src/terminal/UserTerminalHandler.cpp +++ b/src/terminal/UserTerminalHandler.cpp @@ -4,19 +4,18 @@ #include "ETerminal.pb.h" #include "RawSocketUtils.hpp" #include "ServerConnection.hpp" +#include "ServerFifoPath.hpp" #include "UserTerminalRouter.hpp" namespace et { UserTerminalHandler::UserTerminalHandler( shared_ptr _socketHandler, shared_ptr _term, - bool _noratelimit, const SocketEndpoint &_routerEndpoint, + bool _noratelimit, const optional routerEndpoint, const string &idPasskey) : socketHandler(_socketHandler), term(_term), noratelimit(_noratelimit), - routerEndpoint(_routerEndpoint), shuttingDown(false) { - routerFd = socketHandler->connect(routerEndpoint); auto idpasskey_splited = split(idPasskey, '/'); string id = idpasskey_splited[0]; string passkey = idpasskey_splited[1]; @@ -26,19 +25,7 @@ UserTerminalHandler::UserTerminalHandler( tui.set_uid(getuid()); tui.set_gid(getgid()); - if (routerFd < 0) { - if (GetErrno() == ECONNREFUSED) { - CLOG(INFO, "stdout") - << "Error: The Eternal Terminal daemon is not running. Please " - "(re)start the et daemon on the server." - << endl; - } else { - CLOG(INFO, "stdout") - << "Error: Connection error communicating with et deamon: " - << strerror(GetErrno()) << "." << endl; - } - exit(1); - } + routerFd = ServerFifoPath::detectAndConnect(routerEndpoint, socketHandler); try { socketHandler->writePacket( diff --git a/src/terminal/UserTerminalHandler.hpp b/src/terminal/UserTerminalHandler.hpp index 8b07a4f8d..00f21711d 100644 --- a/src/terminal/UserTerminalHandler.hpp +++ b/src/terminal/UserTerminalHandler.hpp @@ -11,7 +11,7 @@ class UserTerminalHandler { public: UserTerminalHandler(shared_ptr _socketHandler, shared_ptr _term, bool noratelimit, - const SocketEndpoint &_routerEndpoint, + const optional _routerEndpoint, const string &idPasskey); void run(); void shutdown() { @@ -24,7 +24,6 @@ class UserTerminalHandler { shared_ptr socketHandler; shared_ptr term; bool noratelimit; - SocketEndpoint routerEndpoint; bool shuttingDown; recursive_mutex shutdownMutex; diff --git a/src/terminal/UserTerminalRouter.cpp b/src/terminal/UserTerminalRouter.cpp index a4513f932..788931cf8 100644 --- a/src/terminal/UserTerminalRouter.cpp +++ b/src/terminal/UserTerminalRouter.cpp @@ -18,7 +18,7 @@ UserTerminalRouter::UserTerminalRouter( IdKeyPair UserTerminalRouter::acceptNewConnection() { lock_guard guard(routerMutex); LOG(INFO) << "Listening to id/key FIFO"; - int terminalFd = socketHandler->accept(serverFd); + const int terminalFd = socketHandler->accept(serverFd); if (terminalFd < 0) { if (GetErrno() != EAGAIN && GetErrno() != EWOULDBLOCK) { FATAL_FAIL(-1); // STFATAL with the error @@ -39,23 +39,44 @@ IdKeyPair UserTerminalRouter::acceptNewConnection() { } TerminalUserInfo tui = stringToProto(packet.getPayload()); tui.set_fd(terminalFd); - idInfoMap[tui.id()] = tui; + + const bool inserted = + idInfoMap.insert(std::make_pair(tui.id(), tui)).second; + if (!inserted) { + LOG(ERROR) << "Rejecting duplicate terminal connection for " << tui.id(); + socketHandler->close(terminalFd); + return IdKeyPair({"", ""}); + } + return IdKeyPair({tui.id(), tui.passkey()}); } catch (const std::runtime_error &re) { - STFATAL << "Router can't talk to terminal: " << re.what(); + LOG(ERROR) << "Router can't talk to terminal: " << re.what(); + socketHandler->close(terminalFd); + return IdKeyPair({"", ""}); } STFATAL << "Should never get here"; return IdKeyPair({"", ""}); } -TerminalUserInfo UserTerminalRouter::getInfoForId(const string &id) { +std::optional UserTerminalRouter::tryGetInfoForConnection( + const shared_ptr &serverClientState) { lock_guard guard(routerMutex); - auto it = idInfoMap.find(id); + auto it = idInfoMap.find(serverClientState->getId()); if (it == idInfoMap.end()) { STFATAL << " Tried to read from an id that no longer exists"; } + + // While both the id and passkey are randomly generated, do an extra + // verification that the passkey matches to ensure that this is the intended + // serverClientState. + if (!serverClientState->verifyPasskey(it->second.passkey())) { + LOG(ERROR) << "Failed to verify passkey for client id: " << it->second.id(); + return std::nullopt; + } + return it->second; } + } // namespace et #endif diff --git a/src/terminal/UserTerminalRouter.hpp b/src/terminal/UserTerminalRouter.hpp index 5fd7e27df..52c4e10b2 100644 --- a/src/terminal/UserTerminalRouter.hpp +++ b/src/terminal/UserTerminalRouter.hpp @@ -1,12 +1,13 @@ #ifndef __ET_USER_TERMINAL_ROUTER__ #define __ET_USER_TERMINAL_ROUTER__ +#include + #include "Headers.hpp" #include "PipeSocketHandler.hpp" #include "ServerConnection.hpp" namespace et { -const string ROUTER_FIFO_NAME = GetTempDirectory() + "etserver.idpasskey.fifo"; class UserTerminalRouter { public: @@ -14,7 +15,10 @@ class UserTerminalRouter { const SocketEndpoint& _routerEndpoint); inline int getServerFd() { return serverFd; } IdKeyPair acceptNewConnection(); - TerminalUserInfo getInfoForId(const string& id); + + std::optional tryGetInfoForConnection( + const shared_ptr& serverClientState); + inline shared_ptr getSocketHandler() { return socketHandler; } diff --git a/test/FuzzableTerminalServer.hpp b/test/FuzzableTerminalServer.hpp new file mode 100644 index 000000000..d4b954280 --- /dev/null +++ b/test/FuzzableTerminalServer.hpp @@ -0,0 +1,45 @@ +#ifndef __ET_FUZZABLE_TERMINAL_SERVER__ +#define __ET_FUZZABLE_TERMINAL_SERVER__ + +#include "TerminalServer.hpp" + +namespace et { +class FuzzableTerminalServer { + public: + FuzzableTerminalServer() { + serverSocketHandler.reset(new PipeSocketHandler()); + pipeSocketHandler.reset(new PipeSocketHandler()); + + string tmpPath = GetTempDirectory() + string("etserver_fuzzer_XXXXXXXX"); + const string pipeDirectory = string(mkdtemp(&tmpPath[0])); + + const string serverPipePath = pipeDirectory + "/pipe_server"; + SocketEndpoint serverEndpoint; + serverEndpoint.set_name(serverPipePath); + + const string routerPath = pipeDirectory + "/pipe_router"; + routerEndpoint.set_name(routerPath); + + terminalServer.reset(new TerminalServer(serverSocketHandler, serverEndpoint, + pipeSocketHandler, routerEndpoint)); + terminalServerThread = thread([this]() { terminalServer->run(); }); + } + + ~FuzzableTerminalServer() { + terminalServer->shutdown(); + terminalServerThread.join(); + } + + std::shared_ptr serverSocketHandler; + std::shared_ptr pipeSocketHandler; + + SocketEndpoint serverEndpoint; + SocketEndpoint routerEndpoint; + + shared_ptr terminalServer; + thread terminalServerThread; +}; + +} // namespace et + +#endif // __ET_FUZZABLE_TERMINAL_SERVER__ diff --git a/test/ServerFifoPathTest.cpp b/test/ServerFifoPathTest.cpp new file mode 100644 index 000000000..12c864cb0 --- /dev/null +++ b/test/ServerFifoPathTest.cpp @@ -0,0 +1,230 @@ +#include + +#include +#include + +#include "ServerFifoPath.hpp" +#include "TestHeaders.hpp" + +using namespace et; + +namespace { + +struct FileInfo { + bool exists = false; + mode_t mode = 0; + + mode_t fileMode() const { return mode & 0777; } + + // Codespaces and similar environments may enforce additional ACLs, so verify + // that the permissions are less than a certain maximum. See + // https://github.community/t/bug-umask-does-not-seem-to-be-respected/129638 + void requireFileModeLessPrivilegedThan(mode_t highestMode) const { + INFO("fileMode()=" << fileMode() << ", highestMode=" << highestMode); + REQUIRE((fileMode() & highestMode) == fileMode()); + } +}; + +int RemoveDirectory(const char* path) { + // Use posix file tree walk to traverse the directory and remove the contents. + return nftw( + path, + [](const char* fpath, const struct stat* sb, int typeflag, + struct FTW* ftwbuf) { return ::remove(fpath); }, + 64, // Maximum open fds. + FTW_DEPTH | FTW_PHYS); +} + +class TestEnvironment { + public: + string createTempDir() { + string tmpPath = GetTempDirectory() + string("et_test_XXXXXXXX"); + const string dir = string(mkdtemp(&tmpPath[0])); + + temporaryDirs.push_back(dir); + return dir; + } + + FileInfo getFileInfo(const string& name) { + struct stat fileStat; + const int statResult = ::stat(name.c_str(), &fileStat); + if (statResult != 0) { + return FileInfo{}; + } + + FileInfo result; + result.exists = true; + result.mode = fileStat.st_mode; + return result; + } + + void setEnv(const char* name, const string& value) { + if (!savedEnvs.count(name)) { + const char* previousValue = ::getenv(name); + if (previousValue) { + savedEnvs[name] = string(previousValue); + } else { + savedEnvs[name] = std::nullopt; + } + } + + const int replace = 1; // non-zero to replace. + ::setenv(name, value.c_str(), replace); + } + + ~TestEnvironment() { + // Remove temporary dirs. + for (const string& dir : temporaryDirs) { + const int removeResult = RemoveDirectory(dir.c_str()); + if (removeResult == -1) { + LOG(ERROR) << "Error when removing dir: " << dir; + FATAL_FAIL(removeResult); + } + } + + // Restore env. + for (const auto& [key, value] : savedEnvs) { + if (value) { + const int replace = 1; // non-zero to replace. + ::setenv(key.c_str(), value->c_str(), replace); + } else { + ::unsetenv(key.c_str()); + } + } + } + + private: + vector temporaryDirs; + map> savedEnvs; +}; + +} // namespace + +TEST_CASE("Creation", "[ServerFifoPath]") { + TestEnvironment env; + + const string homeDir = env.createTempDir(); + env.setEnv("HOME", homeDir.c_str()); + INFO("homeDir = " << homeDir); + + const string expectedFifoPath = + homeDir + "/.local/share/etserver/etserver.idpasskey.fifo"; + + ServerFifoPath serverFifo; + REQUIRE(serverFifo.getPathForCreation() == expectedFifoPath); + REQUIRE(serverFifo.getEndpointForConnect() == + std::nullopt); // Expected to be null unless the path is overridden. + + SECTION("Create all directories") { + REQUIRE(!env.getFileInfo(homeDir + "/.local/share/etserver").exists); + serverFifo.createDirectoriesIfRequired(); + + // Verify the entire tree is created with the correct permissions. + env.getFileInfo(homeDir + "/.local") + .requireFileModeLessPrivilegedThan(0755); + env.getFileInfo(homeDir + "/.local/share") + .requireFileModeLessPrivilegedThan(0755); + env.getFileInfo(homeDir + "/.local/share/etserver") + .requireFileModeLessPrivilegedThan(0700); + } + + const string localDir = homeDir + "/.local"; + const mode_t localDirMode = 0777; // Create with different permissions so + // we can check that this hasn't changed. + const string shareDir = homeDir + "/.local/share"; + const mode_t shareDirMode = 0770; // Another non-default mode. + const string etserverDir = homeDir + "/.local/share/etserver"; + + SECTION(".local already exists") { + const int oldMask = ::umask(0); + FATAL_FAIL(::mkdir(localDir.c_str(), localDirMode)); + ::umask(oldMask); + + serverFifo.createDirectoriesIfRequired(); + + env.getFileInfo(homeDir + "/.local") + .requireFileModeLessPrivilegedThan(localDirMode); + env.getFileInfo(homeDir + "/.local/share") + .requireFileModeLessPrivilegedThan(0755); + env.getFileInfo(homeDir + "/.local/share/etserver") + .requireFileModeLessPrivilegedThan(0700); + } + + SECTION(".local/share already exists") { + const int oldMask = ::umask(0); + FATAL_FAIL(::mkdir(localDir.c_str(), localDirMode)); + FATAL_FAIL(::mkdir(shareDir.c_str(), shareDirMode)); + ::umask(oldMask); + + serverFifo.createDirectoriesIfRequired(); + + env.getFileInfo(homeDir + "/.local") + .requireFileModeLessPrivilegedThan(localDirMode); + env.getFileInfo(homeDir + "/.local/share") + .requireFileModeLessPrivilegedThan(shareDirMode); + env.getFileInfo(homeDir + "/.local/share/etserver") + .requireFileModeLessPrivilegedThan(0700); + } + + SECTION(".local/share/etserver already exists") { + const mode_t etserverDirMode = 0750; // Use slightly different permissions, + // but still without write access. + + const int oldMask = ::umask(0); + FATAL_FAIL(::mkdir(localDir.c_str(), localDirMode)); + FATAL_FAIL(::mkdir(shareDir.c_str(), shareDirMode)); + FATAL_FAIL(::mkdir(etserverDir.c_str(), etserverDirMode)); + ::umask(oldMask); + + serverFifo.createDirectoriesIfRequired(); + + env.getFileInfo(homeDir + "/.local") + .requireFileModeLessPrivilegedThan(localDirMode); + env.getFileInfo(homeDir + "/.local/share") + .requireFileModeLessPrivilegedThan(shareDirMode); + env.getFileInfo(homeDir + "/.local/share/etserver") + .requireFileModeLessPrivilegedThan(etserverDirMode); + } + + SECTION("Override XDG_RUNTIME_DIR") { + const string xdgRuntimeDir = env.createTempDir(); + env.setEnv("XDG_RUNTIME_DIR", xdgRuntimeDir); + + const string xdgRuntimeDirFifoPath = + xdgRuntimeDir + "/etserver/etserver.idpasskey.fifo"; + REQUIRE(serverFifo.getPathForCreation() == xdgRuntimeDirFifoPath); + + // Test creation of the etserver subdirectory. + const string xdgRuntimeDirEtserver = xdgRuntimeDir + "/etserver"; + REQUIRE(!env.getFileInfo(xdgRuntimeDirEtserver).exists); + + serverFifo.createDirectoriesIfRequired(); + + env.getFileInfo(xdgRuntimeDirEtserver) + .requireFileModeLessPrivilegedThan(0700); + } +} + +TEST_CASE("Override", "[ServerFifoPath]") { + TestEnvironment env; + + const string homeDir = env.createTempDir(); + env.setEnv("HOME", homeDir.c_str()); + + const string expectedFifoPath = + homeDir + "/.local/share/etserver/etserver.idpasskey.fifo"; + + ServerFifoPath serverFifo; + REQUIRE(serverFifo.getPathForCreation() == expectedFifoPath); + REQUIRE(serverFifo.getEndpointForConnect() == std::nullopt); + + // Override and re-test. + const string pathOverride = env.createTempDir() + "/etserver.idpasskey.fifo"; + serverFifo.setPathOverride(pathOverride); + + REQUIRE(serverFifo.getPathForCreation() == pathOverride); + + const optional endpoint = serverFifo.getEndpointForConnect(); + REQUIRE(endpoint != std::nullopt); + REQUIRE(endpoint.value().name() == pathOverride); +} diff --git a/test/TerminalServerFuzzer.cpp b/test/TerminalServerFuzzer.cpp new file mode 100644 index 000000000..6e4da939b --- /dev/null +++ b/test/TerminalServerFuzzer.cpp @@ -0,0 +1,31 @@ +#include "FuzzableTerminalServer.hpp" + +namespace et { + +static std::unique_ptr sServer; + +extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) { + sServer.reset(new FuzzableTerminalServer()); + + return 0; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + PipeSocketHandler sockerHandler; + const int fd = sockerHandler.connect(sServer->serverEndpoint); + if (fd == -1) { + // Ignore if we fail to connect. + return 0; + } + + sockerHandler.write(fd, data, size); + + // Shutdown the server, to verify that it gracefully exits. + sServer->terminalServer->shutdown(); + + sockerHandler.close(fd); + + return 0; +} + +} // namespace et diff --git a/test/TerminalServerRouterFuzzer.cpp b/test/TerminalServerRouterFuzzer.cpp new file mode 100644 index 000000000..d342e3df1 --- /dev/null +++ b/test/TerminalServerRouterFuzzer.cpp @@ -0,0 +1,30 @@ +#include "FuzzableTerminalServer.hpp" + +namespace et { + +static std::unique_ptr sServer; + +extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) { + sServer.reset(new FuzzableTerminalServer()); + return 0; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + PipeSocketHandler sockerHandler; + const int fd = sockerHandler.connect(sServer->routerEndpoint); + if (fd == -1) { + // Ignore if we fail to connect. + return 0; + } + + sockerHandler.write(fd, data, size); + + // Shutdown the server, to verify that it gracefully exits. + sServer->terminalServer->shutdown(); + + sockerHandler.close(fd); + + return 0; +} + +} // namespace et diff --git a/test/TerminalTest.cpp b/test/TerminalTest.cpp index c0b0a4ec1..b8be12173 100644 --- a/test/TerminalTest.cpp +++ b/test/TerminalTest.cpp @@ -1,3 +1,5 @@ +#include + #include "FakeConsole.hpp" #include "TerminalClient.hpp" #include "TerminalServer.hpp" @@ -81,6 +83,7 @@ TEST_CASE("FakeUserTerminalTest", "[FakeUserTerminalTest]") { } const string CRYPTO_KEY = "12345678901234567890123456789012"; +const string CRYPTO_KEY2 = "98765432109876543210987654321098"; void readWriteTest(const string& clientId, shared_ptr routerSocketHandler, @@ -135,7 +138,97 @@ void readWriteTest(const string& clientId, uth.reset(); } -TEST_CASE("EndToEndTest", "[EndToEndTest]") { +class LogInterceptHandler : public el::LogDispatchCallback { + public: + void handle(const el::LogDispatchData* data) { + const std::string& message = data->logMessage()->message(); + + std::function callback; + { + lock_guard lock(classMutex); + + if (!wasHit && message.size() >= interceptPrefix.size() && + message.substr(0, interceptPrefix.size()) == interceptPrefix) { + wasHit = true; + callback = std::move(interceptCallback); + } + } + + if (callback) { + callback(); + } + } + + void setIntercept(string prefix, std::function callback) { + lock_guard lock(classMutex); + wasHit = false; + interceptPrefix = prefix; + interceptCallback = callback; + } + + private: + mutex classMutex; + // Default to true to disable the matcher until setIntercept is called. + bool wasHit = true; + string interceptPrefix; + std::function interceptCallback; +}; + +class EndToEndTestFixture { + public: + EndToEndTestFixture() { + el::Helpers::installLogDispatchCallback( + "LogInterceptHandler"); + + srand(1); + clientSocketHandler.reset(new PipeSocketHandler()); + clientPipeSocketHandler.reset(new PipeSocketHandler()); + serverSocketHandler.reset(new PipeSocketHandler()); + routerSocketHandler.reset(new PipeSocketHandler()); + el::Helpers::setThreadName("Main"); + consoleSocketHandler.reset(new PipeSocketHandler()); + fakeConsole.reset(new FakeConsole(consoleSocketHandler)); + + userTerminalSocketHandler.reset(new PipeSocketHandler()); + fakeUserTerminal.reset(new FakeUserTerminal(userTerminalSocketHandler)); + + string tmpPath = GetTempDirectory() + string("etserver_test_XXXXXXXX"); + pipeDirectory = string(mkdtemp(&tmpPath[0])); + + routerPipePath = string(pipeDirectory) + "/pipe_router"; + routerEndpoint.set_name(routerPipePath); + + serverPipePath = string(pipeDirectory) + "/pipe_server"; + serverEndpoint.set_name(serverPipePath); + + server = shared_ptr( + new TerminalServer(serverSocketHandler, serverEndpoint, + routerSocketHandler, routerEndpoint)); + serverThread = thread([this]() { server->run(); }); + sleep(1); + } + + ~EndToEndTestFixture() { + server->shutdown(); + serverThread.join(); + + consoleSocketHandler.reset(); + userTerminalSocketHandler.reset(); + serverSocketHandler.reset(); + clientSocketHandler.reset(); + clientPipeSocketHandler.reset(); + routerSocketHandler.reset(); + FATAL_FAIL(::remove(routerPipePath.c_str())); + FATAL_FAIL(::remove(serverPipePath.c_str())); + FATAL_FAIL(::remove(pipeDirectory.c_str())); + + el::Helpers::uninstallLogDispatchCallback( + "LogInterceptHandler"); + } + + protected: + LogInterceptHandler logInterceptHandler; + shared_ptr consoleSocketHandler; shared_ptr userTerminalSocketHandler; shared_ptr routerSocketHandler; @@ -144,57 +237,162 @@ TEST_CASE("EndToEndTest", "[EndToEndTest]") { shared_ptr clientSocketHandler; shared_ptr clientPipeSocketHandler; + string pipeDirectory; + SocketEndpoint serverEndpoint; + string serverPipePath; + + SocketEndpoint routerEndpoint; + string routerPipePath; shared_ptr fakeConsole; shared_ptr fakeUserTerminal; - string pipeDirectory; + shared_ptr server; + thread serverThread; - srand(1); - clientSocketHandler.reset(new PipeSocketHandler()); - clientPipeSocketHandler.reset(new PipeSocketHandler()); - serverSocketHandler.reset(new PipeSocketHandler()); - routerSocketHandler.reset(new PipeSocketHandler()); - el::Helpers::setThreadName("Main"); - consoleSocketHandler.reset(new PipeSocketHandler()); - fakeConsole.reset(new FakeConsole(consoleSocketHandler)); - - userTerminalSocketHandler.reset(new PipeSocketHandler()); - fakeUserTerminal.reset(new FakeUserTerminal(userTerminalSocketHandler)); - fakeUserTerminal->setup(-1); + private: + bool wasShutdown = false; +}; - string tmpPath = GetTempDirectory() + string("etserver_test_XXXXXXXX"); - pipeDirectory = string(mkdtemp(&tmpPath[0])); +TEST_CASE_METHOD(EndToEndTestFixture, "EndToEndTest", "[EndToEndTest]") { + readWriteTest("1234567890123456", routerSocketHandler, fakeUserTerminal, + serverEndpoint, clientSocketHandler, clientPipeSocketHandler, + fakeConsole, routerEndpoint); +} - string routerPipePath = string(pipeDirectory) + "/pipe_router"; - SocketEndpoint routerEndpoint; - routerEndpoint.set_name(routerPipePath); +void simultaneousTerminalConnectionTest( + const string& clientId, const string& simultaneousTerminalPasskey, + LogInterceptHandler& logInterceptHandler, + shared_ptr routerSocketHandler, + shared_ptr userTerminalSocketHandler, + shared_ptr fakeUserTerminal, + SocketEndpoint serverEndpoint, + shared_ptr clientSocketHandler, + shared_ptr clientPipeSocketHandler, + shared_ptr fakeConsole, const SocketEndpoint& routerEndpoint) { + struct SimultaneousTerminalState { + const string& clientId; + const string& passkey; + shared_ptr routerSocketHandler; + shared_ptr userTerminalSocketHandler; + shared_ptr fakeUserTerminal; + const SocketEndpoint& routerEndpoint; + + shared_ptr handler; + thread handlerThread; + + SimultaneousTerminalState( + const string& _clientId, const string& _passkey, + shared_ptr _routerSocketHandler, + shared_ptr _userTerminalSocketHandler, + shared_ptr _fakeUserTerminal, + const SocketEndpoint& _routerEndpoint) + : clientId(_clientId), + passkey(_passkey), + routerSocketHandler(_routerSocketHandler), + userTerminalSocketHandler(_userTerminalSocketHandler), + fakeUserTerminal(_fakeUserTerminal), + routerEndpoint(_routerEndpoint) {} + + // Move-only. + SimultaneousTerminalState(const SimultaneousTerminalState&) = delete; + SimultaneousTerminalState(SimultaneousTerminalState&&) = default; + + void start() { + handler.reset( + new UserTerminalHandler(routerSocketHandler, fakeUserTerminal, true, + routerEndpoint, clientId + "/" + passkey)); + handlerThread = thread([this]() { CHECK_THROWS(handler->run()); }); + } - string serverPipePath = string(pipeDirectory) + "/pipe_server"; - serverEndpoint.set_name(serverPipePath); + ~SimultaneousTerminalState() { + if (handler) { + handler->shutdown(); + if (handlerThread.joinable()) { + handlerThread.join(); + } + } + } + }; - auto server = shared_ptr( - new TerminalServer(serverSocketHandler, serverEndpoint, - routerSocketHandler, routerEndpoint)); - thread t_server([server]() { server->run(); }); + auto uth = shared_ptr( + new UserTerminalHandler(routerSocketHandler, fakeUserTerminal, true, + routerEndpoint, clientId + "/" + CRYPTO_KEY)); + + constexpr int kNumSimultaneousTerminals = 4; + std::vector otherTerminals; + for (int i = 0; i < kNumSimultaneousTerminals; ++i) { + otherTerminals.emplace_back(clientId, simultaneousTerminalPasskey, + routerSocketHandler, userTerminalSocketHandler, + fakeUserTerminal, routerEndpoint); + } + + logInterceptHandler.setIntercept("Got client with id: ", [&otherTerminals]() { + // Try to create more terminals while the main terminal is connecting. + // NOTE: There must be no logging within this handler, + for (auto& terminal : otherTerminals) { + terminal.start(); + } + }); + + thread uthThread([uth]() { REQUIRE_NOTHROW(uth->run()); }); sleep(1); - readWriteTest("1234567890123456", routerSocketHandler, fakeUserTerminal, - serverEndpoint, clientSocketHandler, clientPipeSocketHandler, - fakeConsole, routerEndpoint); - server->shutdown(); - t_server.join(); - - consoleSocketHandler.reset(); - userTerminalSocketHandler.reset(); - serverSocketHandler.reset(); - clientSocketHandler.reset(); - clientPipeSocketHandler.reset(); - routerSocketHandler.reset(); - FATAL_FAIL(::remove(routerPipePath.c_str())); - FATAL_FAIL(::remove(serverPipePath.c_str())); - FATAL_FAIL(::remove(pipeDirectory.c_str())); + shared_ptr terminalClient(new TerminalClient( + clientSocketHandler, clientPipeSocketHandler, serverEndpoint, clientId, + CRYPTO_KEY, fakeConsole, false, "", "", false, "", + MAX_CLIENT_KEEP_ALIVE_DURATION)); + thread terminalClientThread([terminalClient]() { terminalClient->run(""); }); + sleep(3); + + const string s("test"); + thread typeKeysThread([s, fakeConsole]() { + for (int a = 0; a < s.size(); a++) { + VLOG(1) << "Writing packet " << a; + fakeConsole->simulateKeystrokes(string(1, s[a])); + } + }); + + string resultConcat; + string result; + for (int a = 0; a < s.size(); a++) { + result = fakeUserTerminal->getKeystrokes(1); + resultConcat = resultConcat.append(result); + LOG(INFO) << "ON MESSAGE " << a; + } + typeKeysThread.join(); + + REQUIRE(resultConcat == s); + + terminalClient->shutdown(); + terminalClientThread.join(); + terminalClient.reset(); + + uth->shutdown(); + uthThread.join(); + uth.reset(); + + otherTerminals.clear(); +} + +TEST_CASE_METHOD(EndToEndTestFixture, "TerminalConnectSimultaneous", + "[EndToEndTest]") { + SECTION("Valid passkey") { + simultaneousTerminalConnectionTest( + "1234567890123456", CRYPTO_KEY, logInterceptHandler, + routerSocketHandler, userTerminalSocketHandler, fakeUserTerminal, + serverEndpoint, clientSocketHandler, clientPipeSocketHandler, + fakeConsole, routerEndpoint); + } + + SECTION("Different passkey") { + simultaneousTerminalConnectionTest( + "1234567890123456", CRYPTO_KEY2, logInterceptHandler, + routerSocketHandler, userTerminalSocketHandler, fakeUserTerminal, + serverEndpoint, clientSocketHandler, clientPipeSocketHandler, + fakeConsole, routerEndpoint); + } } // TODO: Multiple clients diff --git a/vcpkg.json b/vcpkg.json index da0ef0007..234c792b4 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,14 +1,5 @@ { - "name": "eternal-terminal", - "version-string": "6.0.13", - "dependencies": [ - "zlib", - "openssl", - "protobuf", - "libsodium", - { - "name": "curl", - "platform": "windows" - } - ] - } + "name": "eternal-terminal", + "version-string": "6.0.13", + "dependencies": ["zlib", "openssl", "protobuf", "libsodium", "sentry-native"] +}