├── .circleci └── config.yml ├── .clang-format ├── .codespellrc ├── .devcontainer ├── Dockerfile ├── devcontainer.json └── reinstall-cmake.sh ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── workflows │ ├── clang-format-check.yml │ ├── codecov.yml │ ├── codespell.yml │ ├── linux_ci.yml │ ├── mac_ci.yml │ ├── novcpkg_build_master.yml │ ├── novcpkg_build_release.yml │ ├── vcpkg_build_master.yml │ └── vcpkg_build_release.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── build_static.sh ├── cmake ├── FindGFlags.cmake ├── FindSELinux.cmake ├── FindUTempter.cmake ├── FindUnwind.cmake └── Findsodium.cmake ├── codecov.yml ├── coverage.sh ├── debian ├── postinst ├── postrm └── prerm ├── docker ├── Dockerfile.client ├── Dockerfile.server ├── README.md └── container-entrypoint ├── docs ├── creating_release.md ├── images │ ├── connection_overview.png │ ├── jumphost_architecture.png │ └── port_forwarding.png └── protocol.md ├── etc └── et.cfg ├── format.sh ├── init └── launchd │ └── homebrew.mxcl.et.plist ├── make_changelog.sh ├── make_rpm_changelog.sh ├── patches └── easylogging.p0 ├── proto ├── ET.proto └── ETerminal.proto ├── rc.d └── etserver ├── renovate.json ├── scripts └── ssh-et ├── src ├── base │ ├── BackedReader.cpp │ ├── BackedReader.hpp │ ├── BackedWriter.cpp │ ├── BackedWriter.hpp │ ├── ClientConnection.cpp │ ├── ClientConnection.hpp │ ├── Connection.cpp │ ├── Connection.hpp │ ├── CryptoHandler.cpp │ ├── CryptoHandler.hpp │ ├── DaemonCreator.cpp │ ├── DaemonCreator.hpp │ ├── Globals.hpp │ ├── Headers.hpp │ ├── JsonLib.hpp │ ├── LogHandler.cpp │ ├── LogHandler.hpp │ ├── Packet.hpp │ ├── PipeSocketHandler.cpp │ ├── PipeSocketHandler.hpp │ ├── RawSocketUtils.cpp │ ├── RawSocketUtils.hpp │ ├── ServerClientConnection.cpp │ ├── ServerClientConnection.hpp │ ├── ServerConnection.cpp │ ├── ServerConnection.hpp │ ├── SocketHandler.cpp │ ├── SocketHandler.hpp │ ├── SubprocessToString.cpp │ ├── SubprocessToString.hpp │ ├── TcpSocketHandler.cpp │ ├── TcpSocketHandler.hpp │ ├── TunnelUtils.cpp │ ├── TunnelUtils.hpp │ ├── UnixSocketHandler.cpp │ ├── UnixSocketHandler.hpp │ └── WinsockContext.hpp ├── htm │ ├── HtmClient.cpp │ ├── HtmClient.hpp │ ├── HtmClientMain.cpp │ ├── HtmHeaderCodes.hpp │ ├── HtmServer.cpp │ ├── HtmServer.hpp │ ├── HtmServerMain.cpp │ ├── IpcPairClient.cpp │ ├── IpcPairClient.hpp │ ├── IpcPairEndpoint.cpp │ ├── IpcPairEndpoint.hpp │ ├── IpcPairServer.cpp │ ├── IpcPairServer.hpp │ ├── MultiplexerState.cpp │ ├── MultiplexerState.hpp │ ├── TerminalHandler.cpp │ └── TerminalHandler.hpp └── terminal │ ├── Console.hpp │ ├── ParseConfigFile.hpp │ ├── ProcessHelper.hpp │ ├── PsuedoTerminalConsole.hpp │ ├── PsuedoUserTerminal.hpp │ ├── ServerFifoPath.cpp │ ├── ServerFifoPath.hpp │ ├── SshSetupHandler.cpp │ ├── SshSetupHandler.hpp │ ├── TelemetryService.cpp │ ├── TelemetryService.hpp │ ├── TerminalClient.cpp │ ├── TerminalClient.hpp │ ├── TerminalClientMain.cpp │ ├── TerminalMain.cpp │ ├── TerminalServer.cpp │ ├── TerminalServer.hpp │ ├── TerminalServerMain.cpp │ ├── UserJumphostHandler.cpp │ ├── UserJumphostHandler.hpp │ ├── UserTerminal.hpp │ ├── UserTerminalHandler.cpp │ ├── UserTerminalHandler.hpp │ ├── UserTerminalRouter.cpp │ ├── UserTerminalRouter.hpp │ └── forwarding │ ├── ForwardDestinationHandler.cpp │ ├── ForwardDestinationHandler.hpp │ ├── ForwardSourceHandler.cpp │ ├── ForwardSourceHandler.hpp │ ├── PortForwardHandler.cpp │ └── PortForwardHandler.hpp ├── systemctl └── et.service ├── test ├── BackedTest.cpp ├── ConnectionTest.cpp ├── CryptoHandlerTest.cpp ├── FakeConsole.hpp ├── FlakySocketHandler.hpp ├── FuzzableTerminalServer.hpp ├── JumphostTest.cpp ├── Main.cpp ├── ServerFifoPathTest.cpp ├── TerminalServerFuzzer.cpp ├── TerminalServerRouterFuzzer.cpp ├── TerminalTest.cpp ├── TestHeaders.hpp ├── system_tests │ └── connect_with_jumphost.sh └── test_tsan.suppression └── vcpkg.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | connect_to_initial_version: 4 | machine: 5 | image: ubuntu-2004:202101-01 6 | steps: 7 | - run: 8 | name: Avoid hosts unknown for github 9 | command: mkdir -p ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 10 | - run: 11 | name: Install system dependencies 12 | command: sudo DEBIAN_FRONTEND=noninteractive ACCEPT_EULA=Y apt-get -y update; sudo DEBIAN_FRONTEND=noninteractive ACCEPT_EULA=Y apt-get install -y curl zip unzip tar libssl-dev libcurl4-openssl-dev libunwind-dev git cmake ninja-build libutempter-dev build-essential openssh-server protobuf-compiler libsodium-dev libgflags-dev libprotobuf-dev libtool libtool-bin autoconf 13 | - checkout 14 | - run: 15 | name: Set up ssh & known_hosts 16 | command: sudo sed -i -e 's/#ListenAddress/ListenAddress/' /etc/ssh/sshd_config; sudo sed -i -e 's/AddressFamily inet/AddressFamily any/' /etc/ssh/sshd_config; sudo /etc/init.d/ssh restart; 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; ssh -o "StrictHostKeyChecking no" 0:0:0:0:0:0:0:1 ls 17 | 18 | - run: 19 | name: Init submodules 20 | command: if [ $CIRCLE_BRANCH != "release" ]; then git submodule update --init --recursive; fi 21 | - run: 22 | name: vcpkg 23 | command: if [ $CIRCLE_BRANCH != "release" ]; then external/vcpkg/bootstrap-vcpkg.sh; mkdir build; ./external/vcpkg/vcpkg --x-install-root=$PWD/build/vcpkg_installed --triplet=x64-linux --feature-flags="manifests" install; fi 24 | - run: 25 | name: Build the root version of the project 26 | command: mkdir ../root_version; cp -Rf .git * ../root_version/; cd ../root_version; git checkout v6; git submodule update --init --recursive; cd build; cmake ../; make -j4 27 | no_output_timeout: 60m 28 | - run: 29 | name: Build the project 30 | command: cd build; cmake ../; make -j4 31 | no_output_timeout: 60m 32 | - run: 33 | name: Connect new -> old 34 | command: sudo ../root_version/build/etserver --daemon; sudo cp ../root_version/build/etterminal /usr/bin/etterminal; sleep 3; build/et -c "ls" localhost --logtostdout --verbose=9 35 | - run: 36 | name: Connect new -> old (ipv6) 37 | command: build/et -c "ls" 0:0:0:0:0:0:0:1 --logtostdout --verbose=9 38 | - run: 39 | name: Connect new -> old (ipv6 with port in host) 40 | command: build/et -c "ls" 0:0:0:0:0:0:0:1:2022 --logtostdout --verbose=9 41 | - run: 42 | name: Connect new -> old (ipv6 abbreviated) 43 | command: build/et -c "ls" ::1 --logtostdout --verbose=9 44 | - run: 45 | name: Connect new -> old (ipv6 abbreviated with port arg) 46 | command: build/et -c "ls" ::1 --port 2022 --logtostdout --verbose=9 47 | - run: 48 | name: Kill server 49 | command: sudo pkill etserver 50 | - run: 51 | name: Connect old -> new 52 | command: export TERM=xterm-256color; sudo build/etserver --daemon; sudo cp build/etterminal /usr/bin/etterminal; sleep 3; ../root_version/build/et -c "ls" localhost --logtostdout --verbose=9 53 | - run: 54 | name: Kill server 55 | command: sudo pkill etserver 56 | - run: 57 | name: Debug info/logs if failed 58 | when: always 59 | command: ls -la /tmp/et*; sudo awk 'FNR==1 {print "XXXXXX", FILENAME, "XXXXXX"}{print}' /tmp/et*.log 60 | 61 | connect_with_jumphost: 62 | machine: 63 | image: ubuntu-2004:202101-01 64 | steps: 65 | - run: 66 | name: Avoid hosts unknown for github 67 | command: mkdir -p ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 68 | - run: 69 | name: Install system dependencies 70 | command: sudo DEBIAN_FRONTEND=noninteractive ACCEPT_EULA=Y apt-get -y update; sudo DEBIAN_FRONTEND=noninteractive ACCEPT_EULA=Y apt-get install -y curl zip unzip tar libssl-dev libcurl4-openssl-dev libunwind-dev git cmake ninja-build libutempter-dev build-essential openssh-server libtool libtool-bin autoconf 71 | - checkout 72 | - run: 73 | name: Set up ssh & known_hosts 74 | 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 75 | - run: 76 | name: Init submodules 77 | command: if [ $CIRCLE_BRANCH != "release" ]; then git submodule update --init --recursive; fi 78 | - run: 79 | name: vcpkg 80 | command: if [ $CIRCLE_BRANCH != "release" ]; then external/vcpkg/bootstrap-vcpkg.sh; mkdir build; ./external/vcpkg/vcpkg --x-install-root=$PWD/build/vcpkg_installed --triplet=x64-linux --feature-flags="manifests" install; fi 81 | - run: 82 | name: Build the project 83 | command: cd build; cmake -DCMAKE_INSTALL_PREFIX=/usr ../; make -j4; sudo make install 84 | no_output_timeout: 60m 85 | - run: 86 | name: Start Servers 87 | command: sudo build/etserver --daemon; sudo build/etserver --port 2023 --serverfifo=/tmp/etserver.idpasskey.fifo2 --daemon; sleep 3; build/et -c "ls" --serverfifo=/tmp/etserver.idpasskey.fifo2 --logtostdout --verbose=9 --jumphost localhost --jport 2022 localhost:2023 88 | - run: 89 | name: Kill servers 90 | command: sudo pkill etserver 91 | - run: 92 | name: Debug info/logs if failed 93 | when: always 94 | command: ls -la /tmp/et*; sudo awk 'FNR==1 {print "XXXXXX", FILENAME, "XXXXXX"}{print}' /tmp/et*.log 95 | 96 | workflows: 97 | version: 2 98 | build_and_test: 99 | jobs: 100 | - connect_with_jumphost 101 | - connect_to_initial_version: 102 | filters: 103 | branches: 104 | ignore: 105 | - release 106 | - deployment 107 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Google 3 | --- 4 | -------------------------------------------------------------------------------- /.codespellrc: -------------------------------------------------------------------------------- 1 | [codespell] 2 | skip = .git,*.pdf,*.svg,.codespellrc 3 | check-hidden = true 4 | ignore-regex = \bhenrik@gassmann.onl\b 5 | ignore-words-list = te 6 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.224.3/containers/cpp/.devcontainer/base.Dockerfile 2 | 3 | # [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 4 | ARG VARIANT="bullseye" 5 | FROM mcr.microsoft.com/vscode/devcontainers/cpp:0-${VARIANT} 6 | 7 | ARG USERNAME=vscode 8 | 9 | # [Optional] Install CMake version different from what base image has already installed. 10 | # CMake reinstall choices: none, 3.21.5, 3.22.2, or versions from https://cmake.org/download/ 11 | ARG REINSTALL_CMAKE_VERSION_FROM_SOURCE="3.23.1" 12 | 13 | # Use installed binaries from the system. 14 | # Do not download latest version of CMake and Ninja during vcpkg bootstrap! 15 | ENV VCPKG_FORCE_SYSTEM_BINARIES=1 16 | ENV VCPKG_USE_SYSTEM_BINARIES=1 17 | 18 | # Optionally install the cmake for vcpkg 19 | COPY ./reinstall-cmake.sh /tmp/ 20 | RUN if [ "${REINSTALL_CMAKE_VERSION_FROM_SOURCE}" != "none" ]; then \ 21 | chmod +x /tmp/reinstall-cmake.sh && /tmp/reinstall-cmake.sh ${REINSTALL_CMAKE_VERSION_FROM_SOURCE}; \ 22 | fi \ 23 | && rm -f /tmp/reinstall-cmake.sh 24 | 25 | # Install dependencies. 26 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 27 | && apt-get -y install --no-install-recommends \ 28 | libboost-dev \ 29 | libsodium-dev \ 30 | libprotobuf-dev \ 31 | protobuf-compiler \ 32 | libgflags-dev \ 33 | libutempter-dev \ 34 | build-essential \ 35 | ninja-build \ 36 | # Note that in Ubuntu 21.04, there is no libcurl-dev, use libcurl4-openssl-dev 37 | libcurl4-openssl-dev 38 | 39 | # 40 | # Set up command history volume 41 | # See https://code.visualstudio.com/remote/advancedcontainers/persist-bash-history 42 | # 43 | RUN mkdir /commandhistory \ 44 | && chown -R $USERNAME /commandhistory 45 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.224.3/containers/cpp 3 | { 4 | "name": "C++", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick an Debian / Ubuntu OS version: debian-11, debian-10, debian-9, ubuntu-21.04, ubuntu-20.04, ubuntu-18.04 8 | // Use Debian 11, Debian 9, Ubuntu 18.04 or Ubuntu 21.04 on local arm64/Apple Silicon 9 | "args": { "VARIANT": "debian-11" } 10 | }, 11 | "runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"], 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": {}, 15 | 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": [ 18 | "ms-vscode.cpptools", 19 | "ms-vscode.cmake-tools" 20 | ], 21 | 22 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 23 | // "forwardPorts": [], 24 | 25 | // Use 'postCreateCommand' to run commands after the container is created. 26 | "postCreateCommand": "git submodule update --init --recursive", 27 | 28 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 29 | "remoteUser": "vscode", 30 | 31 | // For sharing shell history out of the container. 32 | "mounts": [ 33 | "source=eternalterminal-history,target=/commandhistory,type=volume" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.devcontainer/reinstall-cmake.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | set -e 8 | 9 | CMAKE_VERSION=${1:-"none"} 10 | 11 | if [ "${CMAKE_VERSION}" = "none" ]; then 12 | echo "No CMake version specified, skipping CMake reinstallation" 13 | exit 0 14 | fi 15 | 16 | # Cleanup temporary directory and associated files when exiting the script. 17 | cleanup() { 18 | EXIT_CODE=$? 19 | set +e 20 | if [[ -n "${TMP_DIR}" ]]; then 21 | echo "Executing cleanup of tmp files" 22 | rm -Rf "${TMP_DIR}" 23 | fi 24 | exit $EXIT_CODE 25 | } 26 | trap cleanup EXIT 27 | 28 | 29 | echo "Installing CMake..." 30 | apt-get -y purge --auto-remove cmake 31 | mkdir -p /opt/cmake 32 | 33 | architecture=$(dpkg --print-architecture) 34 | case "${architecture}" in 35 | arm64) 36 | ARCH=aarch64 ;; 37 | amd64) 38 | ARCH=x86_64 ;; 39 | *) 40 | echo "Unsupported architecture ${architecture}." 41 | exit 1 42 | ;; 43 | esac 44 | 45 | CMAKE_BINARY_NAME="cmake-${CMAKE_VERSION}-linux-${ARCH}.sh" 46 | CMAKE_CHECKSUM_NAME="cmake-${CMAKE_VERSION}-SHA-256.txt" 47 | TMP_DIR=$(mktemp -d -t cmake-XXXXXXXXXX) 48 | 49 | echo "${TMP_DIR}" 50 | cd "${TMP_DIR}" 51 | 52 | curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_BINARY_NAME}" -O 53 | curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_CHECKSUM_NAME}" -O 54 | 55 | sha256sum -c --ignore-missing "${CMAKE_CHECKSUM_NAME}" 56 | sh "${TMP_DIR}/${CMAKE_BINARY_NAME}" --prefix=/opt/cmake --skip-license 57 | 58 | ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake 59 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: MisterTea 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | If you have set up ET correctly and are having an issue connecting/maintaining a session, please consider running in verbose mode and adding client & server logs to your issue. 2 | 3 | To run in verbose mode, pass the --verbose=9 flag to et. 4 | 5 | To collect logs, run the following on your client: 6 | 7 | tar -cvzPhf /tmp/etclientLogs.tar.gz /tmp/etclient_err /tmp/etclient.INFO 8 | 9 | Then run this on your server: 10 | 11 | tar -cvzPhf /tmp/etserverLogs.tar.gz /tmp/etserver_err /tmp/etserver.INFO 12 | 13 | The logs will contain the IP addresses & username of the client and server, but will not contain any of the data transmitted. 14 | 15 | If you are experiencing a crash, please also post a backtrace. To do this, replace this line in your et script: 16 | 17 | CLIENT_BINARY="etclient" 18 | 19 | to this: 20 | 21 | CLIENT_BINARY="lldb -- etclient" 22 | 23 | then rerun with lldb and when it crashes, type "bt" to give me the stack trace. If you are running the client under linux, replace with: 24 | 25 | CLIENT_BINARY="gdb --args etclient" 26 | -------------------------------------------------------------------------------- /.github/workflows/clang-format-check.yml: -------------------------------------------------------------------------------- 1 | name: clang-format Check 2 | on: [pull_request] 3 | jobs: 4 | formatting-check: 5 | name: Formatting Check 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | path: 10 | - 'src' 11 | - 'test' 12 | - 'proto' 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Run clang-format style check for C/C++/Protobuf source code. 16 | uses: jidicula/clang-format-action@v4.13.0 17 | with: 18 | clang-format-version: '18' 19 | check-path: ${{ matrix.path }} 20 | fallback-style: 'Google' 21 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | codecov: 11 | runs-on: ubuntu-22.04 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup 16 | shell: bash 17 | run: | 18 | mkdir -p ~/.ssh/ 19 | echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 20 | sudo apt-get update 21 | sudo DEBIAN_FRONTEND=noninteractive ACCEPT_EULA=Y apt-get install -y curl zip unzip tar libssl-dev libcurl4-openssl-dev libunwind-dev git cmake ninja-build gdb protobuf-compiler libsodium-dev libgflags-dev libprotobuf-dev libutempter-dev g++ lcov libtool libtool-bin autoconf 22 | 23 | echo "Host localhost\n Port 2222\n\n" >> ~/.ssh/config 24 | 25 | sudo /usr/sbin/sshd -p 2222 26 | 27 | ssh-keygen -t rsa -f ~/.ssh/id_rsa -P "" -N "" 28 | cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys 29 | cat ~/.ssh/id_rsa.pub >> ~/.ssh/known_hosts 30 | ssh -vvvvvvv -o "StrictHostKeyChecking no" -o 'PreferredAuthentications=publickey' localhost "echo foobar" # Fails if we can't ssh into localhost without a password 31 | if [[ -z "${ACT}" ]]; then auth_header="$(git config --local --get http.https://github.com/.extraheader)"; fi 32 | 33 | git submodule sync --recursive 34 | git submodule update --init --force --recursive 35 | 36 | # Restore both vcpkg and its artifacts from the GitHub cache service. 37 | - name: Restore vcpkg and its artifacts. 38 | uses: actions/cache@v4 39 | with: 40 | # The first path is where vcpkg generates artifacts while consuming the vcpkg.json manifest file. 41 | # The second path is the location of vcpkg (it contains the vcpkg executable and data files). 42 | # The other paths starting with '!' are exclusions: they contain temporary files generated during the build of the installed packages. 43 | path: | 44 | ${{ env.CMAKE_BUILD_DIR }}/vcpkg_installed/ 45 | ${{ env.VCPKG_ROOT }} 46 | !${{ env.VCPKG_ROOT }}/buildtrees 47 | !${{ env.VCPKG_ROOT }}/packages 48 | !${{ env.VCPKG_ROOT }}/downloads 49 | # The key is composed in a way that it gets properly invalidated: this must happen whenever vcpkg's Git commit id changes, or the list of packages changes. In this case a cache miss must happen and a new entry with a new key with be pushed to GitHub the cache service. 50 | # The key includes: hash of the vcpkg.json file, the hash of the vcpkg Git commit id, and the used vcpkg's triplet. The vcpkg's commit id would suffice, but computing an hash out it does not harm. 51 | # Note: given a key, the cache content is immutable. If a cache entry has been created improperly, in order the recreate the right content the key must be changed as well, and it must be brand new (i.e. not existing already). 52 | key: | 53 | et-vcpkg-${{ hashFiles( 'vcpkg.json' ) }}-${{ hashFiles( '.git/modules/external/vcpkg/HEAD' )}}-linux-codecov-1 54 | 55 | - name: Build 56 | run: | 57 | mkdir build 58 | pushd build 59 | cmake -DCODE_COVERAGE=ON ../ 60 | make -j`nproc` 61 | popd 62 | 63 | - name: Build Test with code coverage 64 | run: | 65 | ./build/et-test 66 | ./test/system_tests/connect_with_jumphost.sh 67 | lcov --capture --directory . --output-file ./coverage.info 68 | lcov --remove ./coverage.info '/usr/*' --output-file coverage.info # filter system-files 69 | lcov --list ./coverage.info # debug info 70 | 71 | - name: Upload coverage to Codecov 72 | uses: codecov/codecov-action@v4 73 | with: 74 | directory: ./ 75 | fail_ci_if_error: true 76 | files: ./coverage.info 77 | token: ${{ secrets.CODECOV_TOKEN }} 78 | verbose: true 79 | -------------------------------------------------------------------------------- /.github/workflows/codespell.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Codespell 3 | 4 | on: 5 | push: 6 | branches: [master] 7 | pull_request: 8 | branches: [master] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | codespell: 15 | name: Check for spelling errors 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Codespell 22 | uses: codespell-project/actions-codespell@v2 23 | -------------------------------------------------------------------------------- /.github/workflows/linux_ci.yml: -------------------------------------------------------------------------------- 1 | name: Linux CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | linux_ci: 11 | runs-on: ubuntu-22.04 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | sanitize: [ubsan, asan, tsan, msan] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Setup 19 | shell: bash 20 | run: | 21 | mkdir -p ~/.ssh/ 22 | echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 23 | sudo apt-get update 24 | sudo DEBIAN_FRONTEND=noninteractive ACCEPT_EULA=Y apt-get install -y curl zip unzip tar libssl-dev libcurl4-openssl-dev libunwind-dev git cmake ninja-build gdb protobuf-compiler libsodium-dev libgflags-dev libprotobuf-dev libutempter-dev g++ libtool libtool-bin autoconf 25 | 26 | echo "Host localhost\n Port 2222\n\n" >> ~/.ssh/config 27 | 28 | sudo /usr/sbin/sshd -p 2222 29 | 30 | ssh-keygen -t rsa -f ~/.ssh/id_rsa -P "" -N "" 31 | cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys 32 | cat ~/.ssh/id_rsa.pub >> ~/.ssh/known_hosts 33 | ssh -vvvvvvv -o "StrictHostKeyChecking no" -o 'PreferredAuthentications=publickey' localhost "echo foobar" # Fails if we can't ssh into localhost without a password 34 | if [[ -z "${ACT}" ]]; then auth_header="$(git config --local --get http.https://github.com/.extraheader)"; fi 35 | 36 | git submodule sync --recursive 37 | git submodule update --init --force --recursive 38 | 39 | # Restore both vcpkg and its artifacts from the GitHub cache service. 40 | - name: Restore vcpkg and its artifacts. 41 | uses: actions/cache@v4 42 | with: 43 | # The first path is where vcpkg generates artifacts while consuming the vcpkg.json manifest file. 44 | # The second path is the location of vcpkg (it contains the vcpkg executable and data files). 45 | # The other paths starting with '!' are exclusions: they contain temporary files generated during the build of the installed packages. 46 | path: | 47 | ${{ env.CMAKE_BUILD_DIR }}/vcpkg_installed/ 48 | ${{ env.VCPKG_ROOT }} 49 | !${{ env.VCPKG_ROOT }}/buildtrees 50 | !${{ env.VCPKG_ROOT }}/packages 51 | !${{ env.VCPKG_ROOT }}/downloads 52 | # The key is composed in a way that it gets properly invalidated: this must happen whenever vcpkg's Git commit id changes, or the list of packages changes. In this case a cache miss must happen and a new entry with a new key with be pushed to GitHub the cache service. 53 | # The key includes: hash of the vcpkg.json file, the hash of the vcpkg Git commit id, and the used vcpkg's triplet. The vcpkg's commit id would suffice, but computing an hash out it does not harm. 54 | # Note: given a key, the cache content is immutable. If a cache entry has been created improperly, in order the recreate the right content the key must be changed as well, and it must be brand new (i.e. not existing already). 55 | key: | 56 | et-vcpkg-${{ hashFiles( 'vcpkg.json' ) }}-${{ hashFiles( '.git/modules/external/vcpkg/HEAD' )}}-linux-${{ matrix.sanitize }}-1 57 | 58 | - name: Build with ubsan 59 | run: | 60 | mkdir build 61 | pushd build 62 | cmake -DSANITIZE_UNDEFINED=ON ../ 63 | make -j`nproc` 64 | popd 65 | ./test/system_tests/connect_with_jumphost.sh 66 | TSAN_OPTIONS="suppressions=../test/test_tsan.suppression" ./build/et-test 67 | if: matrix.sanitize == 'ubsan' 68 | 69 | - name: Build with asan 70 | run: | 71 | mkdir build 72 | pushd build 73 | cmake -DSANITIZE_ADDRESS=ON ../ 74 | make -j`nproc` 75 | popd 76 | ./test/system_tests/connect_with_jumphost.sh 77 | TSAN_OPTIONS="suppressions=../test/test_tsan.suppression" ./build/et-test 78 | if: matrix.sanitize == 'asan' 79 | 80 | - name: Build with msan 81 | run: | 82 | mkdir build 83 | pushd build 84 | cmake -DSANITIZE_MEMORY=ON ../ 85 | make -j`nproc` 86 | popd 87 | ./test/system_tests/connect_with_jumphost.sh 88 | TSAN_OPTIONS="suppressions=../test/test_tsan.suppression" ./build/et-test 89 | if: matrix.sanitize == 'msan' 90 | 91 | - name: Build with tsan 92 | run: | 93 | mkdir build 94 | pushd build 95 | cmake -DSANITIZE_THREAD=ON -DSANITIZE_LINK_STATIC=ON ../ 96 | make -j`nproc` 97 | popd 98 | ./test/system_tests/connect_with_jumphost.sh 99 | TSAN_OPTIONS="suppressions=../test/test_tsan.suppression" ./build/et-test 100 | if: matrix.sanitize == 'tsan' 101 | -------------------------------------------------------------------------------- /.github/workflows/novcpkg_build_master.yml: -------------------------------------------------------------------------------- 1 | name: NoVcpkg Build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | jobs: 9 | build_novcpkg: 10 | name: build-novcpkg-${{ matrix.os }}-gcc${{ matrix.gcc }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | os: [ubuntu-latest, macos-latest] 16 | gcc: [11, 12, 13] 17 | exclude: 18 | #- os: macos-latest # Allow one gcc variant for mac/windows, since they don't use gcc 19 | #gcc: 11 20 | - os: macos-latest 21 | gcc: 12 22 | - os: macos-latest 23 | gcc: 13 24 | #- os: windows-latest 25 | #gcc: 11 26 | - os: windows-latest 27 | gcc: 12 28 | - os: windows-latest 29 | gcc: 13 30 | env: 31 | # Indicates the CMake build directory where project files and binaries are being produced. 32 | CMAKE_BUILD_DIR: ${{ github.workspace }}/build 33 | 34 | steps: 35 | - name: Install Dependencies (Linux) 36 | run: | 37 | sudo apt-get update && \ 38 | sudo apt-get install --no-install-recommends \ 39 | libboost-dev \ 40 | libsodium-dev \ 41 | libprotobuf-dev \ 42 | protobuf-compiler \ 43 | libgflags-dev \ 44 | libutempter-dev \ 45 | build-essential \ 46 | ninja-build \ 47 | libcurl4-openssl-dev \ 48 | curl \ 49 | zip \ 50 | unzip \ 51 | tar \ 52 | cmake \ 53 | libutempter-dev \ 54 | libunwind-dev 55 | if: matrix.os == 'ubuntu-latest' 56 | - name: Install Dependencies (macOS) 57 | run: brew update && brew install ninja cmake pkg-config curl openssl protobuf libsodium automake autoconf libtool 58 | if: matrix.os == 'macos-latest' 59 | 60 | - uses: actions/checkout@v4 61 | with: 62 | submodules: recursive 63 | 64 | - name: Set up GCC (Ubuntu) 65 | uses: egor-tensin/setup-gcc@v1 66 | with: 67 | version: ${{ matrix.gcc }} 68 | platform: x64 69 | if: matrix.os == 'ubuntu-latest' 70 | 71 | # Run CMake to generate Ninja project files. 72 | - name: Install dependencies and generate project files (mac) 73 | run: | 74 | OPENSSL_ROOT_DIR=`brew --prefix openssl` cmake -S "${{ github.workspace }}" -B "${{ env.CMAKE_BUILD_DIR }}" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDISABLE_VCPKG=ON -DOPENSSL_ROOT_DIR=$OPENSSL_ROOT_DIR 75 | if: matrix.os == 'macos-latest' 76 | - name: Install dependencies and generate project files (linux) 77 | env: 78 | CC: gcc-${{ matrix.gcc }} 79 | CXX: g++-${{ matrix.gcc }} 80 | run: | 81 | cmake -S "${{ github.workspace }}" -B "${{ env.CMAKE_BUILD_DIR }}" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDISABLE_VCPKG=ON -DOPENSSL_ROOT_DIR=$OPENSSL_ROOT_DIR 82 | if: matrix.os == 'ubuntu-latest' 83 | - name: Install dependencies and generate project files (windows) 84 | run: | 85 | cmake -S "${{ github.workspace }}" -B "${{ env.CMAKE_BUILD_DIR }}" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDISABLE_VCPKG=ON -DOPENSSL_ROOT_DIR=$OPENSSL_ROOT_DIR 86 | if: matrix.os == 'windows-latest' 87 | # Build the whole project with Ninja (which is spawn by CMake). 88 | - name: Build 89 | run: | 90 | cmake --build "${{ env.CMAKE_BUILD_DIR }}" 91 | 92 | - uses: actions/upload-artifact@v4 93 | with: 94 | name: et-client-${{matrix.os}}-gcc${{matrix.gcc}} 95 | path: ${{ env.CMAKE_BUILD_DIR }}/et${{matrix.extension}} 96 | -------------------------------------------------------------------------------- /.github/workflows/novcpkg_build_release.yml: -------------------------------------------------------------------------------- 1 | name: NoVcpkg Build Release 2 | on: 3 | push: 4 | branches: 5 | - release 6 | pull_request: 7 | branches: 8 | - release 9 | 10 | jobs: 11 | build_novcpkg: 12 | name: build-novcpkg-${{ matrix.os }}-gcc${{ matrix.gcc }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: true 16 | matrix: 17 | os: [ubuntu-latest, macos-latest] 18 | gcc: [11, 12, 13] 19 | exclude: 20 | #- os: macos-latest # Allow one gcc variant for mac/windows, since they don't use gcc 21 | #gcc: 11 22 | - os: macos-latest 23 | gcc: 12 24 | - os: macos-latest 25 | gcc: 13 26 | #- os: windows-latest 27 | #gcc: 11 28 | - os: windows-latest 29 | gcc: 12 30 | - os: windows-latest 31 | gcc: 13 32 | env: 33 | # Indicates the CMake build directory where project files and binaries are being produced. 34 | CMAKE_BUILD_DIR: ${{ github.workspace }}/build 35 | 36 | steps: 37 | - name: Install Dependencies (Linux) 38 | run: | 39 | sudo apt-get update && \ 40 | sudo apt-get install --no-install-recommends \ 41 | libboost-dev \ 42 | libsodium-dev \ 43 | libprotobuf-dev \ 44 | protobuf-compiler \ 45 | libgflags-dev \ 46 | libutempter-dev \ 47 | build-essential \ 48 | ninja-build \ 49 | libcurl4-openssl-dev \ 50 | curl \ 51 | zip \ 52 | unzip \ 53 | tar \ 54 | cmake \ 55 | libutempter-dev \ 56 | libunwind-dev 57 | if: matrix.os == 'ubuntu-latest' 58 | - name: Install Dependencies (macOS) 59 | run: brew update && brew install ninja cmake pkg-config curl openssl protobuf libsodium automake autoconf libtool 60 | if: matrix.os == 'macos-latest' 61 | 62 | - uses: actions/checkout@v4 63 | with: 64 | submodules: false 65 | 66 | - name: Set up GCC (Ubuntu) 67 | uses: egor-tensin/setup-gcc@v1 68 | with: 69 | version: ${{ matrix.gcc }} 70 | platform: x64 71 | if: matrix.os == 'ubuntu-latest' 72 | 73 | # Run CMake to generate Ninja project files. 74 | - name: Install dependencies and generate project files (mac) 75 | run: | 76 | OPENSSL_ROOT_DIR=`brew --prefix openssl` cmake -S "${{ github.workspace }}" -B "${{ env.CMAKE_BUILD_DIR }}" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDISABLE_VCPKG=ON -DOPENSSL_ROOT_DIR=$OPENSSL_ROOT_DIR 77 | if: matrix.os == 'macos-latest' 78 | - name: Install dependencies and generate project files (linux) 79 | env: 80 | CC: gcc-${{ matrix.gcc }} 81 | CXX: g++-${{ matrix.gcc }} 82 | run: | 83 | cmake -S "${{ github.workspace }}" -B "${{ env.CMAKE_BUILD_DIR }}" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDISABLE_VCPKG=ON -DOPENSSL_ROOT_DIR=$OPENSSL_ROOT_DIR 84 | if: matrix.os == 'ubuntu-latest' 85 | - name: Install dependencies and generate project files (windows) 86 | run: | 87 | cmake -S "${{ github.workspace }}" -B "${{ env.CMAKE_BUILD_DIR }}" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDISABLE_VCPKG=ON -DOPENSSL_ROOT_DIR=$OPENSSL_ROOT_DIR 88 | if: matrix.os == 'windows-latest' 89 | # Build the whole project with Ninja (which is spawn by CMake). 90 | - name: Build 91 | run: | 92 | cmake --build "${{ env.CMAKE_BUILD_DIR }}" 93 | 94 | - uses: actions/upload-artifact@v4 95 | with: 96 | name: et-client-${{matrix.os}}-gcc${{matrix.gcc}} 97 | path: ${{ env.CMAKE_BUILD_DIR }}/et${{matrix.extension}} 98 | -------------------------------------------------------------------------------- /.github/workflows/vcpkg_build_master.yml: -------------------------------------------------------------------------------- 1 | name: Vcpkg Build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | jobs: 9 | build_vcpkg: 10 | name: build-vcpkg-${{ matrix.os }}-${{ matrix.gcc }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | gcc: [11, 12, 13] 17 | include: 18 | - os: windows-latest 19 | extension: .exe 20 | exclude: 21 | #- os: macos-latest # Allow one gcc variant for mac/windows, since they don't use gcc 22 | #gcc: 11 23 | - os: macos-latest 24 | gcc: 12 25 | - os: macos-latest 26 | gcc: 13 27 | #- os: windows-latest 28 | #gcc: 11 29 | - os: windows-latest 30 | gcc: 12 31 | - os: windows-latest 32 | gcc: 13 33 | env: 34 | # Indicates the CMake build directory where project files and binaries are being produced. 35 | CMAKE_BUILD_DIR: ${{ github.workspace }}/build 36 | # Indicates the location of the vcpkg as a Git submodule of the project repository. 37 | VCPKG_ROOT: ${{ github.workspace }}/external/vcpkg 38 | 39 | steps: 40 | - name: Install Dependencies (Linux) 41 | run: | 42 | sudo apt-get update && \ 43 | sudo apt-get install --no-install-recommends \ 44 | libboost-dev \ 45 | libsodium-dev \ 46 | libprotobuf-dev \ 47 | protobuf-compiler \ 48 | libgflags-dev \ 49 | libutempter-dev \ 50 | build-essential \ 51 | ninja-build \ 52 | libcurl4-openssl-dev \ 53 | curl \ 54 | zip \ 55 | unzip \ 56 | tar \ 57 | cmake \ 58 | libutempter-dev \ 59 | libunwind-dev 60 | if: matrix.os == 'ubuntu-latest' 61 | - name: Install Dependencies (Windows) 62 | run: choco install -y ninja 63 | if: matrix.os == 'windows-latest' 64 | - name: Install Dependencies (macOS) 65 | run: brew update && brew install ninja cmake automake autoconf libtool 66 | if: matrix.os == 'macos-latest' 67 | 68 | - uses: actions/checkout@v4 69 | with: 70 | submodules: recursive 71 | 72 | - name: Set up GCC (Ubuntu) 73 | uses: egor-tensin/setup-gcc@v1 74 | with: 75 | version: ${{ matrix.gcc }} 76 | platform: x64 77 | if: matrix.os == 'ubuntu-latest' 78 | 79 | # Restore both vcpkg and its artifacts from the GitHub cache service. 80 | - name: Restore vcpkg and its artifacts. 81 | uses: actions/cache@v4 82 | with: 83 | path: | 84 | ${{ env.VCPKG_ROOT }} 85 | !${{ env.VCPKG_ROOT }}/buildtrees 86 | !${{ env.VCPKG_ROOT }}/packages 87 | !${{ env.VCPKG_ROOT }}/downloads 88 | # The key is composed in a way that it gets properly invalidated: this must happen whenever vcpkg's Git commit id changes, or the list of packages changes. In this case a cache miss must happen and a new entry with a new key with be pushed to GitHub the cache service. 89 | # The key includes: hash of the vcpkg.json file, the hash of the vcpkg Git commit id, and the used vcpkg's triplet. The vcpkg's commit id would suffice, but computing an hash out it does not harm. 90 | # Note: given a key, the cache content is immutable. If a cache entry has been created improperly, in order the recreate the right content the key must be changed as well, and it must be brand new (i.e. not existing already). 91 | key: | 92 | et-vcpkg-${{ hashFiles( 'vcpkg.json' ) }}-${{ hashFiles( '.git/modules/external/vcpkg/HEAD' )}}-${{ matrix.os }}-${{ matrix.gcc }}-master-1 93 | 94 | - name: Show content of workspace after cache has been restored 95 | run: find $RUNNER_WORKSPACE 96 | shell: bash 97 | 98 | # On Windows runners, let's ensure to have the Developer Command Prompt environment setup correctly. As used here the Developer Command Prompt created is targeting x64 and using the default the Windows SDK. 99 | - uses: ilammy/msvc-dev-cmd@v1 100 | # Run CMake to generate Ninja project files, using the vcpkg's toolchain file to resolve and install the dependencies as specified in vcpkg.json. 101 | - name: Install dependencies and generate project files (linux) 102 | env: 103 | CC: gcc-${{ matrix.gcc }} 104 | CXX: g++-${{ matrix.gcc }} 105 | run: | 106 | cmake -S "${{ github.workspace }}" -B "${{ env.CMAKE_BUILD_DIR }}" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo 107 | if: matrix.os == 'ubuntu-latest' 108 | 109 | - name: Install dependencies and generate project files (mac) 110 | run: | 111 | cmake -S "${{ github.workspace }}" -B "${{ env.CMAKE_BUILD_DIR }}" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo 112 | if: matrix.os == 'macos-latest' 113 | 114 | - name: Install dependencies and generate project files (windows) 115 | run: | 116 | cmake -S "${{ github.workspace }}" -B "${{ env.CMAKE_BUILD_DIR }}" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo 117 | if: matrix.os == 'windows-latest' 118 | 119 | # Build the whole project with Ninja (which is spawn by CMake). 120 | - name: Build 121 | run: | 122 | cmake --build "${{ env.CMAKE_BUILD_DIR }}" 123 | 124 | - uses: actions/upload-artifact@v4 125 | with: 126 | name: et-client-${{matrix.os}}-gcc${{matrix.gcc}} 127 | path: ${{ env.CMAKE_BUILD_DIR }}/et${{matrix.extension}} 128 | -------------------------------------------------------------------------------- /.github/workflows/vcpkg_build_release.yml: -------------------------------------------------------------------------------- 1 | name: Vcpkg Build Release 2 | on: 3 | push: 4 | branches: 5 | - release 6 | pull_request: 7 | branches: 8 | - release 9 | 10 | jobs: 11 | build_vcpkg: 12 | name: build-vcpkg-${{ matrix.os }}-${{ matrix.gcc }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: true 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | gcc: [11, 12, 13] 19 | include: 20 | - os: windows-latest 21 | extension: .exe 22 | exclude: 23 | #- os: macos-latest # Allow one gcc variant for mac/windows, since they don't use gcc 24 | #gcc: 11 25 | - os: macos-latest 26 | gcc: 12 27 | - os: macos-latest 28 | gcc: 13 29 | #- os: windows-latest 30 | #gcc: 11 31 | - os: windows-latest 32 | gcc: 12 33 | - os: windows-latest 34 | gcc: 13 35 | env: 36 | # Indicates the CMake build directory where project files and binaries are being produced. 37 | CMAKE_BUILD_DIR: ${{ github.workspace }}/build 38 | # Indicates the location of the vcpkg as a Git submodule of the project repository. 39 | VCPKG_ROOT: ${{ github.workspace }}/external/vcpkg 40 | 41 | steps: 42 | - name: Install Dependencies (Linux) 43 | run: | 44 | sudo apt-get update && \ 45 | sudo apt-get install --no-install-recommends \ 46 | libboost-dev \ 47 | libsodium-dev \ 48 | libprotobuf-dev \ 49 | protobuf-compiler \ 50 | libgflags-dev \ 51 | libutempter-dev \ 52 | build-essential \ 53 | ninja-build \ 54 | libcurl4-openssl-dev \ 55 | curl \ 56 | zip \ 57 | unzip \ 58 | tar \ 59 | cmake \ 60 | libutempter-dev \ 61 | libunwind-dev 62 | if: matrix.os == 'ubuntu-latest' 63 | - name: Install Dependencies (Windows) 64 | run: choco install -y ninja 65 | if: matrix.os == 'windows-latest' 66 | - name: Install Dependencies (macOS) 67 | run: brew update && brew install ninja cmake automake autoconf libtool 68 | if: matrix.os == 'macos-latest' 69 | 70 | - uses: actions/checkout@v4 71 | with: 72 | submodules: false 73 | 74 | - name: Set up GCC (Ubuntu) 75 | uses: egor-tensin/setup-gcc@v1 76 | with: 77 | version: ${{ matrix.gcc }} 78 | platform: x64 79 | if: matrix.os == 'ubuntu-latest' 80 | 81 | # Restore both vcpkg and its artifacts from the GitHub cache service. 82 | - name: Restore vcpkg and its artifacts. 83 | uses: actions/cache@v4 84 | with: 85 | path: | 86 | ${{ env.VCPKG_ROOT }} 87 | !${{ env.VCPKG_ROOT }}/buildtrees 88 | !${{ env.VCPKG_ROOT }}/packages 89 | !${{ env.VCPKG_ROOT }}/downloads 90 | # The key is composed in a way that it gets properly invalidated: this must happen whenever vcpkg's Git commit id changes, or the list of packages changes. In this case a cache miss must happen and a new entry with a new key with be pushed to GitHub the cache service. 91 | # The key includes: hash of the vcpkg.json file, the hash of the vcpkg Git commit id, and the used vcpkg's triplet. The vcpkg's commit id would suffice, but computing an hash out it does not harm. 92 | # Note: given a key, the cache content is immutable. If a cache entry has been created improperly, in order the recreate the right content the key must be changed as well, and it must be brand new (i.e. not existing already). 93 | key: | 94 | et-vcpkg-${{ hashFiles( 'vcpkg.json' ) }}-${{ hashFiles( '.git/modules/external/vcpkg/HEAD' )}}-${{ matrix.os }}-${{ matrix.gcc }}-release-1 95 | 96 | - name: Show content of workspace after cache has been restored 97 | run: find $RUNNER_WORKSPACE 98 | shell: bash 99 | 100 | # On Windows runners, let's ensure to have the Developer Command Prompt environment setup correctly. As used here the Developer Command Prompt created is targeting x64 and using the default the Windows SDK. 101 | - uses: ilammy/msvc-dev-cmd@v1 102 | # Run CMake to generate Ninja project files, using the vcpkg's toolchain file to resolve and install the dependencies as specified in vcpkg.json. 103 | - name: Install dependencies and generate project files (linux) 104 | env: 105 | CC: gcc-${{ matrix.gcc }} 106 | CXX: g++-${{ matrix.gcc }} 107 | run: | 108 | cmake -S "${{ github.workspace }}" -B "${{ env.CMAKE_BUILD_DIR }}" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo 109 | if: matrix.os == 'ubuntu-latest' 110 | - name: Install dependencies and generate project files (mac) 111 | env: 112 | CC: gcc-${{ matrix.gcc }} 113 | CXX: g++-${{ matrix.gcc }} 114 | run: | 115 | cmake -S "${{ github.workspace }}" -B "${{ env.CMAKE_BUILD_DIR }}" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo 116 | if: matrix.os == 'macos-latest' 117 | - name: Install dependencies and generate project files (windows) 118 | run: | 119 | cmake -S "${{ github.workspace }}" -B "${{ env.CMAKE_BUILD_DIR }}" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo 120 | if: matrix.os == 'windows-latest' 121 | # Build the whole project with Ninja (which is spawn by CMake). 122 | - name: Build 123 | run: | 124 | cmake --build "${{ env.CMAKE_BUILD_DIR }}" 125 | 126 | - uses: actions/upload-artifact@v4 127 | with: 128 | name: et-client-${{matrix.os}}-gcc${{matrix.gcc}} 129 | path: ${{ env.CMAKE_BUILD_DIR }}/et${{matrix.extension}} 130 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | deps 3 | out 4 | .vscode 5 | *PVS-Studio* 6 | .dccache 7 | /*build/ 8 | 9 | # Public/private keys 10 | id_rsa* 11 | .gnupg 12 | 13 | # Emacs backups 14 | *~ 15 | 16 | # Vagrant 17 | .vagrant 18 | 19 | # Code Coverage 20 | code-coverage.info 21 | code_coverage_report 22 | 23 | # OS/X Junk file 24 | .DS_Store 25 | 26 | # Cmake build dir 27 | build 28 | 29 | # Generated by cmake protobuf 30 | Makefile 31 | 32 | # GTags 33 | GPATH 34 | GRTAGS 35 | GTAGS 36 | 37 | # Compiled Object files 38 | *.slo 39 | *.lo 40 | *.o 41 | *.obj 42 | 43 | # Precompiled Headers 44 | *.gch 45 | *.pch 46 | 47 | # Compiled Dynamic libraries 48 | *.so 49 | *.dylib 50 | *.dll 51 | 52 | # Fortran module files 53 | *.mod 54 | *.smod 55 | 56 | # Compiled Static libraries 57 | *.lai 58 | *.la 59 | *.a 60 | *.lib 61 | 62 | # Executables 63 | *.exe 64 | *.out 65 | *.app 66 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/cxxopts"] 2 | path = external/cxxopts 3 | url = https://github.com/jarro2783/cxxopts.git 4 | [submodule "external/Catch2"] 5 | path = external/Catch2 6 | url = https://github.com/catchorg/Catch2.git 7 | [submodule "external/msgpack-c"] 8 | path = external/msgpack-c 9 | url = https://github.com/msgpack/msgpack-c.git 10 | [submodule "external/ThreadPool"] 11 | path = external/ThreadPool 12 | url = https://github.com/progschj/ThreadPool.git 13 | [submodule "external/easyloggingpp"] 14 | path = external/easyloggingpp 15 | url = https://github.com/MisterTea/easyloggingpp.git 16 | [submodule "external/sanitizers-cmake"] 17 | path = external/sanitizers-cmake 18 | url = https://github.com/arsenm/sanitizers-cmake.git 19 | [submodule "external/cotire"] 20 | path = external/cotire 21 | url = https://github.com/sakra/cotire.git 22 | [submodule "external/UniversalStacktrace"] 23 | path = external/UniversalStacktrace 24 | url = https://github.com/MisterTea/UniversalStacktrace.git 25 | [submodule "external/vcpkg"] 26 | path = external/vcpkg 27 | url = https://github.com/microsoft/vcpkg.git 28 | [submodule "external/sentry-native"] 29 | path = external/sentry-native 30 | url = https://github.com/getsentry/sentry-native.git 31 | [submodule "external/cpp-httplib"] 32 | path = external/cpp-httplib 33 | url = https://github.com/yhirose/cpp-httplib.git 34 | [submodule "external/PlatformFolders"] 35 | path = external/PlatformFolders 36 | url = https://github.com/sago007/PlatformFolders.git 37 | [submodule "external/sole"] 38 | path = external/sole 39 | url = https://github.com/r-lyeh-archived/sole.git 40 | [submodule "external/simpleini"] 41 | path = external/simpleini 42 | url = https://github.com/brofield/simpleini.git 43 | [submodule "external/json"] 44 | path = external/json 45 | url = https://github.com/nlohmann/json.git 46 | [submodule "external/base64"] 47 | path = external/base64 48 | url = https://github.com/tkislan/base64.git 49 | -------------------------------------------------------------------------------- /build_static.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | set -x 3 | 4 | rm -rf build deps out 5 | mkdir -p build 6 | mkdir -p deps/out/bin 7 | mkdir -p out 8 | cd deps || exit 9 | export PATH="$PWD"/out/bin:/usr/sbin:/usr/bin:/sbin:/bin 10 | curl -OL https://cmake.org/files/v3.9/cmake-3.9.4.tar.gz 11 | tar xvzf cmake-3.9.4.tar.gz 12 | cd cmake-3.9.4 || exit 13 | ./configure --prefix="$PWD"/../out 14 | make -j8 install 15 | cd .. || exit 16 | curl -OL http://ftp.gnu.org/gnu/autoconf/autoconf-2.69.tar.gz 17 | tar xvzf autoconf-2.69.tar.gz 18 | cd autoconf-2.69 19 | ./configure --prefix="$PWD"/../out 20 | make -j8 install 21 | cd .. || exit 22 | curl -OL http://ftp.gnu.org/gnu/automake/automake-1.15.1.tar.gz 23 | tar xvzf automake-1.15.1.tar.gz 24 | cd automake-1.15.1 25 | ./configure --prefix="$PWD"/../out 26 | make -j8 install 27 | cd .. || exit 28 | curl -OL http://ftpmirror.gnu.org/libtool/libtool-2.4.6.tar.gz 29 | tar xvzf libtool-2.4.6.tar.gz 30 | cd libtool-2.4.6 31 | ./configure --prefix="$PWD"/../out --disable-shared --enable-static 32 | make -j8 install 33 | cd .. || exit 34 | git clone https://github.com/jedisct1/libsodium.git 35 | git clone https://github.com/google/protobuf.git 36 | cd protobuf || exit 37 | git checkout v3.4.1 38 | cmake -DCMAKE_INSTALL_PREFIX="$PWD"/../out -Dprotobuf_BUILD_TESTS=OFF ./cmake 39 | make -j8 install 40 | cd ../libsodium || exit 41 | git checkout 1.0.12 42 | ./autogen.sh 43 | ./configure --prefix="$PWD"/../out --disable-shared --enable-static 44 | make -j8 install 45 | cd ../../build || exit 46 | if [[ -f "$PWD/../deps/out/lib64/libprotobuf.a" ]]; then 47 | PROTO_LIB_DIR="$PWD/../deps/out/lib64" 48 | else 49 | PROTO_LIB_DIR="$PWD/../deps/out/lib" 50 | fi 51 | DISABLE_CRASH_LOG=${DISABLE_CRASH_LOG:-OFF} 52 | cmake \ 53 | -DProtobuf_INCLUDE_DIR="$PWD"/../deps/out/include \ 54 | -DProtobuf_LIBRARY_DEBUG="$PROTO_LIB_DIR"/libprotobuf.a \ 55 | -DProtobuf_LIBRARY_RELEASE="$PROTO_LIB_DIR"/libprotobuf.a \ 56 | -DProtobuf_LITE_LIBRARY_DEBUG="$PROTO_LIB_DIR"/libprotobuf-lite.a \ 57 | -DProtobuf_LITE_LIBRARY_RELEASE="$PROTO_LIB_DIR"/libprotobuf-lite.a \ 58 | -DProtobuf_PROTOC_EXECUTABLE="$PWD"/../deps/out/bin/protoc \ 59 | -DProtobuf_PROTOC_LIBRARY_DEBUG="$PROTO_LIB_DIR"/libprotoc.a \ 60 | -DProtobuf_PROTOC_LIBRARY_RELEASE="$PROTO_LIB_DIR"/libprotoc.a \ 61 | -Dsodium_INCLUDE_DIR="$PWD"/../deps/out/include \ 62 | -Dsodium_LIBRARY_DEBUG="$PWD"/../deps/out/lib/libsodium.a \ 63 | -Dsodium_LIBRARY_RELEASE="$PWD"/../deps/out/lib/libsodium.a \ 64 | -Dsodium_USE_STATIC_LIBS=ON \ 65 | -DCMAKE_INSTALL_PREFIX="$PWD"/../out \ 66 | -DDISABLE_CRASH_LOG="$DISABLE_CRASH_LOG" \ 67 | ../ 68 | make -j8 install 69 | cd ../out || exit 70 | echo "Done! Static binaries of et and etserver are in the out/bin directory" 71 | -------------------------------------------------------------------------------- /cmake/FindGFlags.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find GFLAGS 2 | # 3 | # The following variables are optionally searched for defaults 4 | # GFLAGS_ROOT_DIR: Base directory where all GFLAGS components are found 5 | # 6 | # The following are set after configuration is done: 7 | # GFLAGS_FOUND 8 | # GFLAGS_INCLUDE_DIRS 9 | # GFLAGS_LIBRARIES 10 | # GFLAGS_LIBRARYRARY_DIRS 11 | 12 | include(FindPackageHandleStandardArgs) 13 | 14 | set(GFLAGS_ROOT_DIR "" CACHE PATH "Folder contains Gflags") 15 | 16 | # We are testing only a couple of files in the include directories 17 | if(WIN32) 18 | find_path(GFLAGS_INCLUDE_DIR gflags/gflags.h 19 | PATHS ${GFLAGS_ROOT_DIR}/src/windows) 20 | else() 21 | find_path(GFLAGS_INCLUDE_DIR gflags/gflags.h 22 | PATHS ${GFLAGS_ROOT_DIR}) 23 | endif() 24 | 25 | if(MSVC) 26 | find_library(GFLAGS_LIBRARY_RELEASE 27 | NAMES libgflags 28 | PATHS ${GFLAGS_ROOT_DIR} 29 | PATH_SUFFIXES Release) 30 | 31 | find_library(GFLAGS_LIBRARY_DEBUG 32 | NAMES libgflags-debug 33 | PATHS ${GFLAGS_ROOT_DIR} 34 | PATH_SUFFIXES Debug) 35 | 36 | set(GFLAGS_LIBRARY optimized ${GFLAGS_LIBRARY_RELEASE} debug ${GFLAGS_LIBRARY_DEBUG}) 37 | else() 38 | find_library(GFLAGS_LIBRARY gflags) 39 | endif() 40 | 41 | find_package_handle_standard_args(GFLAGS DEFAULT_MSG 42 | GFLAGS_INCLUDE_DIR GFLAGS_LIBRARY) 43 | 44 | 45 | if(GFLAGS_FOUND) 46 | set(GFLAGS_INCLUDE_DIRS ${GFLAGS_INCLUDE_DIR}) 47 | set(GFLAGS_LIBRARIES ${GFLAGS_LIBRARY}) 48 | endif() 49 | -------------------------------------------------------------------------------- /cmake/FindSELinux.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find the SELinux library 2 | # Once done this will define 3 | # 4 | # SELINUX_FOUND - System has selinux 5 | # SELINUX_INCLUDE_DIR - The selinux include directory 6 | # SELINUX_LIBRARIES - The libraries needed to use selinux 7 | # SELINUX_DEFINITIONS - Compiler switches required for using selinux 8 | 9 | IF (UNIX) 10 | # use pkg-config to get the directories 11 | FIND_PACKAGE (PkgConfig) 12 | PKG_CHECK_MODULES (PC_SELINUX libselinux) 13 | SET (SELINUX_DEFINITIONS ${PC_SELINUX_CFLAGS_OTHER}) 14 | 15 | FIND_PATH (SELINUX_INCLUDE_DIR selinux/selinux.h 16 | HINTS 17 | ${PC_SELINUX_INCLUDEDIR} 18 | ${PC_SELINUX_INCLUDE_DIRS} 19 | ) 20 | 21 | FIND_LIBRARY (SELINUX_LIBRARIES selinux libselinux 22 | HINTS 23 | ${PC_SELINUX_LIBDIR} 24 | ${PC_SELINUX_LIBRARY_DIRS} 25 | ) 26 | 27 | INCLUDE (FindPackageHandleStandardArgs) 28 | 29 | # handle the QUIETLY and REQUIRED arguments and set SELINUX_FOUND 30 | # to TRUE if all listed variables are TRUE 31 | FIND_PACKAGE_HANDLE_STANDARD_ARGS (SELinux DEFAULT_MSG 32 | SELINUX_INCLUDE_DIR 33 | SELINUX_LIBRARIES 34 | ) 35 | 36 | MARK_AS_ADVANCED (SELINUX_INCLUDE_DIR SELINUX_LIBRARIES) 37 | 38 | ENDIF (UNIX) 39 | -------------------------------------------------------------------------------- /cmake/FindUTempter.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find the UTEMPTER directory notification library 2 | # Once done this will define 3 | # 4 | # UTEMPTER_FOUND - system has UTEMPTER 5 | # UTEMPTER_INCLUDE_DIR - the UTEMPTER include directory 6 | # UTEMPTER_LIBRARIES - The libraries needed to use UTEMPTER 7 | 8 | # Copyright (c) 2015, Hrvoje Senjan, 9 | # 10 | # Redistribution and use is allowed according to the terms of the BSD license. 11 | # For details see the accompanying COPYING-CMAKE-SCRIPTS file. 12 | 13 | 14 | find_path (UTEMPTER_INCLUDE_DIR utempter.h) 15 | 16 | find_library (UTEMPTER_LIBRARIES NAMES utempter ) 17 | 18 | include (FindPackageHandleStandardArgs) 19 | find_package_handle_standard_args (UTempter DEFAULT_MSG UTEMPTER_INCLUDE_DIR UTEMPTER_LIBRARIES) 20 | 21 | mark_as_advanced (UTEMPTER_INCLUDE_DIR UTEMPTER_LIBRARIES) 22 | -------------------------------------------------------------------------------- /cmake/FindUnwind.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find libunwind 2 | # Once done this will define 3 | # 4 | # Unwind_FOUND - system has libunwind 5 | # unwind::unwind - cmake target for libunwind 6 | 7 | include (FindPackageHandleStandardArgs) 8 | 9 | find_path (Unwind_INCLUDE_DIR NAMES unwind.h libunwind.h DOC "unwind include directory") 10 | find_library (Unwind_LIBRARY NAMES unwind DOC "unwind library") 11 | 12 | if (CMAKE_SYSTEM_PROCESSOR MATCHES "^arm") 13 | set (Unwind_ARCH "unwind-arm") 14 | elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^aarch64") 15 | set (Unwind_ARCH "unwind-aarch64") 16 | elseif (CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" OR 17 | CMAKE_SYSTEM_PROCESSOR STREQUAL "amd64" OR 18 | CMAKE_SYSTEM_PROCESSOR STREQUAL "corei7-64") 19 | set (Unwind_ARCH "unwind-x86_64" "unwind-x86") 20 | elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^i.86$") 21 | set (Unwind_ARCH "unwind-x86" "unwind-x86_64") 22 | elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(powerpc64|ppc64)") 23 | set (Unwind_ARCH "unwind-ppc64") 24 | elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(powerpc|ppc)") 25 | set (Unwind_ARCH "unwind-ppc32") 26 | elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^mips") 27 | set (Unwind_ARCH "unwind-mips") 28 | elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^hppa") 29 | set (Unwind_ARCH "unwind-hppa") 30 | elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^ia64") 31 | set (Unwind_ARCH "unwind-ia64") 32 | endif (CMAKE_SYSTEM_PROCESSOR MATCHES "^arm") 33 | 34 | find_library (Unwind_PLATFORM_LIBRARY NAMES ${Unwind_ARCH} 35 | DOC "unwind library platform") 36 | 37 | mark_as_advanced (Unwind_INCLUDE_DIR Unwind_LIBRARY Unwind_PLATFORM_LIBRARY) 38 | 39 | # Extract version information 40 | if (Unwind_LIBRARY) 41 | set (_Unwind_VERSION_HEADER ${Unwind_INCLUDE_DIR}/libunwind-common.h) 42 | 43 | if (EXISTS ${_Unwind_VERSION_HEADER}) 44 | file (READ ${_Unwind_VERSION_HEADER} _Unwind_VERSION_CONTENTS) 45 | 46 | string (REGEX REPLACE ".*#define UNW_VERSION_MAJOR[ \t]+([0-9]+).*" "\\1" 47 | Unwind_VERSION_MAJOR "${_Unwind_VERSION_CONTENTS}") 48 | string (REGEX REPLACE ".*#define UNW_VERSION_MINOR[ \t]+([0-9]+).*" "\\1" 49 | Unwind_VERSION_MINOR "${_Unwind_VERSION_CONTENTS}") 50 | string (REGEX REPLACE ".*#define UNW_VERSION_EXTRA[ \t]+([0-9]+).*" "\\1" 51 | Unwind_VERSION_PATCH "${_Unwind_VERSION_CONTENTS}") 52 | 53 | set (Unwind_VERSION ${Unwind_VERSION_MAJOR}.${Unwind_VERSION_MINOR}) 54 | 55 | if (CMAKE_MATCH_1) 56 | # Third version component may be empty 57 | set (Unwind_VERSION ${Unwind_VERSION}.${Unwind_VERSION_PATCH}) 58 | set (Unwind_VERSION_COMPONENTS 3) 59 | else (CMAKE_MATCH_1) 60 | set (Unwind_VERSION_COMPONENTS 2) 61 | endif (CMAKE_MATCH_1) 62 | endif (EXISTS ${_Unwind_VERSION_HEADER}) 63 | endif (Unwind_LIBRARY) 64 | 65 | # handle the QUIETLY and REQUIRED arguments and set Unwind_FOUND to TRUE 66 | # if all listed variables are TRUE 67 | find_package_handle_standard_args (Unwind REQUIRED_VARS Unwind_INCLUDE_DIR 68 | Unwind_LIBRARY Unwind_PLATFORM_LIBRARY VERSION_VAR Unwind_VERSION) 69 | 70 | if (Unwind_FOUND) 71 | if (NOT TARGET unwind::unwind) 72 | add_library (unwind::unwind INTERFACE IMPORTED) 73 | 74 | set_property (TARGET unwind::unwind PROPERTY 75 | INTERFACE_INCLUDE_DIRECTORIES ${Unwind_INCLUDE_DIR} 76 | ) 77 | set_property (TARGET unwind::unwind PROPERTY 78 | INTERFACE_LINK_LIBRARIES ${Unwind_LIBRARY} ${Unwind_PLATFORM_LIBRARY} 79 | ) 80 | set_property (TARGET unwind::unwind PROPERTY 81 | IMPORTED_CONFIGURATIONS RELEASE 82 | ) 83 | endif (NOT TARGET unwind::unwind) 84 | endif (Unwind_FOUND) 85 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "external" 3 | - "external_imported" 4 | - "src/base/WinsockContext.hpp" 5 | - "src/htm" # HTM not tested yet 6 | - "src/terminal/ParseConfigFile.hpp" 7 | - "src/terminal/TelemetryService*" 8 | - "src/terminal/PsuedoTerminalConsole.hpp" 9 | - "src/terminal/PsuedoUserTerminal.hpp" 10 | - "src/terminal/*Main.cpp" 11 | - "src/base/Headers.hpp" 12 | - "src/base/TcpSocketHandler*" 13 | - "src/base/SubprocessToString*" 14 | coverage: 15 | status: 16 | project: 17 | default: 18 | target: 50% 19 | -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | rm -Rf build 2 | mkdir build 3 | pushd ./build 4 | cmake ../ -DBUILD_TEST=ON -DBUILD_GTEST=ON -DCODE_COVERAGE=ON -G Ninja 5 | ninja 6 | find . -name "*.gcda" -print0 | xargs -0 rm 7 | popd 8 | ./build/test/et-test 9 | lcov --directory ./build --capture --output-file ./code-coverage.info -rc lcov_branch_coverage=1 10 | genhtml code-coverage.info --branch-coverage --output-directory ./code_coverage_report/ 11 | echo "Report generated in code_coverage_report" 12 | open code_coverage_report/index.html 13 | rm -Rf build 14 | mkdir build 15 | pushd ./build 16 | cmake ../ -DBUILD_TEST=ON -DBUILD_GTEST=ON -DCODE_COVERAGE=OFF -G Ninja 17 | popd 18 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | # Automatically added by dh_installsystemd/13.6ubuntu1 4 | if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then 5 | # This will only remove masks created by d-s-h on package removal. 6 | deb-systemd-helper unmask 'et.service' >/dev/null || true 7 | 8 | # was-enabled defaults to true, so new installations run enable. 9 | if deb-systemd-helper --quiet was-enabled 'et.service'; then 10 | # Enables the unit on first installation, creates new 11 | # symlinks on upgrades if the unit file has changed. 12 | deb-systemd-helper enable 'et.service' >/dev/null || true 13 | else 14 | # Update the statefile to add new symlinks (if any), which need to be 15 | # cleaned up on purge. Also remove old symlinks. 16 | deb-systemd-helper update-state 'et.service' >/dev/null || true 17 | fi 18 | fi 19 | # End automatically added section 20 | # Automatically added by dh_installsystemd/13.6ubuntu1 21 | if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then 22 | if [ -d /run/systemd/system ]; then 23 | systemctl --system daemon-reload >/dev/null || true 24 | if [ -n "$2" ]; then 25 | _dh_action=restart 26 | else 27 | _dh_action=start 28 | fi 29 | deb-systemd-invoke $_dh_action 'et.service' >/dev/null || true 30 | fi 31 | fi 32 | # End automatically added section 33 | -------------------------------------------------------------------------------- /debian/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | # Automatically added by dh_installsystemd/13.6ubuntu1 4 | if [ "$1" = remove ] && [ -d /run/systemd/system ] ; then 5 | systemctl --system daemon-reload >/dev/null || true 6 | fi 7 | # End automatically added section 8 | # Automatically added by dh_installsystemd/13.6ubuntu1 9 | if [ "$1" = "remove" ]; then 10 | if [ -x "/usr/bin/deb-systemd-helper" ]; then 11 | deb-systemd-helper mask 'et.service' >/dev/null || true 12 | fi 13 | fi 14 | 15 | if [ "$1" = "purge" ]; then 16 | if [ -x "/usr/bin/deb-systemd-helper" ]; then 17 | deb-systemd-helper purge 'et.service' >/dev/null || true 18 | deb-systemd-helper unmask 'et.service' >/dev/null || true 19 | fi 20 | fi 21 | # End automatically added section 22 | -------------------------------------------------------------------------------- /debian/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | # Automatically added by dh_installsystemd/13.6ubuntu1 4 | if [ -z "${DPKG_ROOT:-}" ] && [ "$1" = remove ] && [ -d /run/systemd/system ] ; then 5 | deb-systemd-invoke stop 'et.service' >/dev/null || true 6 | fi 7 | # End automatically added section 8 | -------------------------------------------------------------------------------- /docker/Dockerfile.client: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim 2 | 3 | # Use ADD to avoid having to install curl 4 | ADD --chmod=644 https://github.com/MisterTea/debian-et/raw/master/et.gpg /etc/apt/trusted.gpg.d/et.gpg 5 | 6 | # Use a run cache to speed up rebuilding and avoid having to remove the cache when we're done 7 | RUN --mount=type=cache,mode=0755,target=/var/lib/apt/lists,sharing=locked \ 8 | --mount=type=cache,mode=0755,target=/var/cache/apt,sharing=locked \ 9 | rm -f /etc/apt/apt.conf.d/docker-clean && \ 10 | echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache && \ 11 | apt-get update -qq && \ 12 | apt-get install -y ca-certificates && \ 13 | echo "deb https://github.com/MisterTea/debian-et/raw/master/debian-source/ bookworm main" > /etc/apt/sources.list.d/et.list && \ 14 | apt-get update -qq && \ 15 | apt-get install -y et openssh-server 16 | 17 | COPY --chmod=755 container-entrypoint /bin/container-entrypoint 18 | 19 | ENTRYPOINT ["/bin/container-entrypoint", "client"] 20 | -------------------------------------------------------------------------------- /docker/Dockerfile.server: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim 2 | 3 | # Use ADD to avoid having to install curl 4 | ADD --chmod=644 https://github.com/MisterTea/debian-et/raw/master/et.gpg /etc/apt/trusted.gpg.d/et.gpg 5 | 6 | # Use a run cache to speed up rebuilding and avoid having to remove the cache when we're done 7 | RUN --mount=type=cache,mode=0755,target=/var/lib/apt/lists,sharing=locked \ 8 | --mount=type=cache,mode=0755,target=/var/cache/apt,sharing=locked \ 9 | rm -f /etc/apt/apt.conf.d/docker-clean && \ 10 | echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache && \ 11 | apt-get update -qq && \ 12 | apt-get install -y ca-certificates && \ 13 | echo "deb https://github.com/MisterTea/debian-et/raw/master/debian-source/ bookworm main" > /etc/apt/sources.list.d/et.list && \ 14 | apt-get update -qq && \ 15 | apt-get install -y et openssh-server 16 | 17 | COPY --chmod=755 container-entrypoint /bin/container-entrypoint 18 | 19 | ENTRYPOINT ["/bin/container-entrypoint", "server"] 20 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Docker container for et-server 2 | 3 | ## Build 4 | 5 | ``` 6 | $ make 7 | $ docker images |grep et-.* 8 | et-client latest 54c495fe34dc 11 minutes ago 422MB 9 | et-server latest 1bf233faf414 11 minutes ago 425MB 10 | ``` 11 | 12 | ## Run 13 | 14 | 15 | ``` 16 | $ docker run -it --rm -p 2022:2022 -p 2222:22 \ 17 | -v /etc/ssh:/etc/ssh \ 18 | -v /etc/passwd:/etc/passwd \ 19 | -v /etc/shadow:/etc/shadow \ 20 | -v /etc/group:/etc/group \ 21 | -v /home:/home \ 22 | et-server 23 | ``` 24 | 25 | ## Notice 26 | 27 | - Both ports 2022 and 2222 must be open at the server host (per example above); 28 | - The container starts an sshd server to initiate et-server's handshake. 29 | - You ssh client must be able to connect to container's sshd, not host's sshd; 30 | - Running `ssh -p 2222 user@host` must work out-of-the box; 31 | - Tip: add below to your client's `~/.ssh/config`: 32 | 33 | ``` 34 | Host myhost 35 | Port 2222 36 | ``` 37 | -------------------------------------------------------------------------------- /docker/container-entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | if [ "$1" == server ]; then 6 | if [ ! -d /run/sshd ]; then 7 | mkdir /run/sshd 8 | chmod 0755 /run/sshd 9 | fi 10 | 11 | /usr/sbin/sshd 12 | shift 13 | exec etserver --logtostdout -v 1 "$@" 14 | else 15 | shift 16 | exec et "$@" 17 | fi 18 | -------------------------------------------------------------------------------- /docs/creating_release.md: -------------------------------------------------------------------------------- 1 | # Instructions to creating a new ET Release 2 | 3 | 1. Increment the project(...) version in CMakeLists.txt and push to master. 4 | 2. Switch to the release branch 5 | 3. Pull in the latest changes: `git merge master` 6 | 4. Run `git submodule update --recursive --init` to ensure that the submodules are updated to the latest master/release commit. 7 | 5. Run `import_submodules.sh` to create an in-repo copy of the submodules 8 | 6. `git add external_imported` and commit/push any changes 9 | 7. Create a github release using the latest commit and tag it with `et-vA.B.C` where A/B/C is the major/minor/patch version. 10 | 8. Switch to the deployment branch 11 | 9. Edit the `deployment/debian_SOURCE/changelog` file and add a entry for the new release. 12 | 10. Copy id_rsa / id_rsa.pub / .gnupg to the deployment directory. The rsa key needs to have write access to the debian-et github repo and the gnupg key needs to have write access to launchpad 13 | 11. Build and run a docker image interactively from ubuntu.Dockerfile 14 | 12. inside the docker image, run deploy_ubuntu_ppa.sh 15 | 13. Exit the docker image and create a vagrant VM for debian 16 | 14. inside the vagrant vm, run build_all_deb.sh 17 | 15. Follow the instructions at the end of the build_all_deb command to push the new debian artifacts. Do not push the dbgsym (debug symbols) because github cannot handle files that large 18 | -------------------------------------------------------------------------------- /docs/images/connection_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisterTea/EternalTerminal/46fbe72cf896e54eb9d48886c05855935c8c36f8/docs/images/connection_overview.png -------------------------------------------------------------------------------- /docs/images/jumphost_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisterTea/EternalTerminal/46fbe72cf896e54eb9d48886c05855935c8c36f8/docs/images/jumphost_architecture.png -------------------------------------------------------------------------------- /docs/images/port_forwarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MisterTea/EternalTerminal/46fbe72cf896e54eb9d48886c05855935c8c36f8/docs/images/port_forwarding.png -------------------------------------------------------------------------------- /etc/et.cfg: -------------------------------------------------------------------------------- 1 | ; et.cfg : Config file for Eternal Terminal 2 | ; 3 | 4 | [Networking] 5 | port = 2022 6 | # bind_ip = 0.0.0.0 7 | 8 | [Debug] 9 | verbose = 0 10 | silent = 0 11 | logsize = 20971520 12 | telemetry = true 13 | logdirectory = /tmp 14 | -------------------------------------------------------------------------------- /format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | find ./ -type f | grep "\.[hc]pp" | grep -v /ext/ | grep -v /external/ | grep -v /build/ | xargs clang-format --style=Google -i 3 | -------------------------------------------------------------------------------- /init/launchd/homebrew.mxcl.et.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | homebrew.mxcl.et 7 | ProgramArguments 8 | 9 | /usr/local/bin/etserver 10 | --cfgFile 11 | /usr/local/etc/et.conf 12 | 13 | RunAtLoad 14 | 15 | KeepAlive 16 | 17 | WorkingDirectory 18 | /usr/local 19 | StandardErrorPath 20 | /tmp/et_err 21 | StandardOutPath 22 | /tmp/et_err 23 | HardResourceLimits 24 | 25 | NumberOfFiles 26 | 4096 27 | 28 | SoftResourceLimits 29 | 30 | NumberOfFiles 31 | 4096 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /make_changelog.sh: -------------------------------------------------------------------------------- 1 | github_changelog_generator -u MisterTea -p EternalTCP 2 | -------------------------------------------------------------------------------- /make_rpm_changelog.sh: -------------------------------------------------------------------------------- 1 | git log --format="* %cd %aN%n- (%h) %s%d%n" --date=local | gsed -r 's/[0-9]+:[0-9]+:[0-9]+ //' 2 | -------------------------------------------------------------------------------- /patches/easylogging.p0: -------------------------------------------------------------------------------- 1 | 2524c2524,2525 2 | < << "Aborting application. Reason: Fatal log at [" << m_file << ":" << m_line << "]"; 3 | --- 4 | > << "Aborting application. Reason: Fatal log at [" << m_file << ":" << m_line << "]" 5 | > << std::endl << " ======= Backtrace: =========" << std::endl << base::debug::StackTrace(); 6 | -------------------------------------------------------------------------------- /proto/ET.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package et; 3 | option optimize_for = LITE_RUNTIME; 4 | 5 | enum EtPacketType { 6 | // Count down from 254 to avoid collisions 7 | HEARTBEAT = 254; 8 | INITIAL_PAYLOAD = 253; 9 | INITIAL_RESPONSE = 252; 10 | } 11 | 12 | message ConnectRequest { 13 | optional string clientId = 1; 14 | optional int32 version = 2; 15 | } 16 | 17 | enum ConnectStatus { 18 | NEW_CLIENT = 1; 19 | RETURNING_CLIENT = 2; 20 | INVALID_KEY = 3; 21 | MISMATCHED_PROTOCOL = 4; 22 | } 23 | 24 | message ConnectResponse { 25 | optional ConnectStatus status = 1; 26 | optional string error = 2; 27 | } 28 | 29 | message SequenceHeader { 30 | optional int32 sequenceNumber = 1; 31 | } 32 | 33 | message CatchupBuffer { 34 | repeated bytes buffer = 1; 35 | } 36 | 37 | message SocketEndpoint { 38 | optional string name = 1; 39 | optional int32 port = 2; 40 | } 41 | -------------------------------------------------------------------------------- /proto/ETerminal.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package et; 3 | option optimize_for = LITE_RUNTIME; 4 | 5 | import "ET.proto"; 6 | 7 | enum TerminalPacketType { 8 | KEEP_ALIVE = 0; 9 | TERMINAL_BUFFER = 1; 10 | TERMINAL_INFO = 2; 11 | PORT_FORWARD_DESTINATION_REQUEST = 5; 12 | PORT_FORWARD_DESTINATION_RESPONSE = 6; 13 | PORT_FORWARD_DATA = 7; 14 | TERMINAL_USER_INFO = 8; 15 | TERMINAL_INIT = 9; 16 | JUMPHOST_INIT = 10; 17 | } 18 | 19 | message TerminalBuffer { 20 | optional bytes buffer = 1; 21 | } 22 | 23 | message TerminalInfo { 24 | optional string id = 1; 25 | optional int32 row = 2; 26 | optional int32 column = 3; 27 | optional int32 width = 4; 28 | optional int32 height = 5; 29 | } 30 | 31 | message PortForwardSourceRequest { 32 | optional SocketEndpoint source = 1; 33 | optional SocketEndpoint destination = 2; 34 | optional string environmentvariable = 3; 35 | } 36 | 37 | message PortForwardSourceResponse { 38 | optional string error = 1; 39 | } 40 | 41 | message PortForwardDestinationRequest { 42 | optional SocketEndpoint destination = 1; 43 | optional int32 fd = 2; 44 | } 45 | 46 | message PortForwardDestinationResponse { 47 | optional int32 clientfd = 1; 48 | optional int32 socketid = 2; 49 | optional string error = 3; 50 | } 51 | 52 | message PortForwardData { 53 | optional bool sourcetodestination = 1; 54 | optional int32 socketid = 2; 55 | optional bytes buffer = 3; 56 | optional string error = 4; 57 | optional bool closed = 5; 58 | } 59 | 60 | message InitialPayload { 61 | optional bool jumphost = 1 [default = false]; 62 | repeated PortForwardSourceRequest reversetunnels = 2; 63 | } 64 | 65 | message InitialResponse { 66 | optional string error = 1; 67 | } 68 | 69 | message ConfigParams { 70 | optional int32 vlevel = 1; 71 | optional int32 minloglevel = 2; 72 | } 73 | 74 | message TermInit { 75 | repeated string environmentnames = 1; 76 | repeated string environmentvalues = 2; 77 | } 78 | 79 | message TerminalUserInfo { 80 | optional string id = 1; 81 | optional string passkey = 2; 82 | optional int64 uid = 3; 83 | optional int64 gid = 4; 84 | optional int64 fd = 5; 85 | } -------------------------------------------------------------------------------- /rc.d/etserver: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # FreeBSD rc.d service file 4 | # 5 | # PROVIDE: etserver 6 | # REQUIRE: DAEMON 7 | # KEYWORD: shutdown 8 | # 9 | # Add the following lines to /etc/rc.conf.local or /etc/rc.conf 10 | # to enable this service: 11 | # 12 | # etserver_enable (bool): Set to NO by default. 13 | # Set it to YES to enable etserver. 14 | 15 | . /etc/rc.subr 16 | 17 | name=etserver 18 | rcvar="${name}_enable" 19 | 20 | command="/usr/local/bin/etserver" 21 | pidfile=${etserver_pidfile:="/var/run/etserver.pid"} 22 | 23 | load_rc_config $name 24 | 25 | : ${etserver_enable:="NO"} 26 | 27 | command_args="--daemon --cfgfile /usr/local/etc/etserver/et.cfg --pidfile ${pidfile}" 28 | 29 | run_rc_command "$1" 30 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "includeForks": true, 7 | "lockFileMaintenance": { 8 | "enabled": true, 9 | "automerge": true, 10 | "automergeType": "pr", 11 | "platformAutomerge": true 12 | }, 13 | "platformAutomerge": true, 14 | "packageRules": [ 15 | { 16 | "description": "Automerge non-major updates", 17 | "matchUpdateTypes": [ 18 | "minor", 19 | "patch" 20 | ], 21 | "enabled": true, 22 | "automerge": true, 23 | "automergeType": "pr", 24 | "platformAutomerge": true 25 | }, 26 | { 27 | "matchDepTypes": [ 28 | "devDependencies" 29 | ], 30 | "matchPackagePatterns": [ 31 | "lint", 32 | "prettier" 33 | ], 34 | "enabled": true, 35 | "automerge": true, 36 | "automergeType": "pr", 37 | "platformAutomerge": true 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /scripts/ssh-et: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Usage: 3 | # ssh-et [ssh_options] 4 | 5 | # See https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ 6 | set -o errexit -o errtrace -o nounset -o pipefail 7 | 8 | readonly MAX_RETRIES=10 9 | 10 | _command_exists() { 11 | command -v -- "$1" &> /dev/null 12 | } 13 | 14 | _log_info() { 15 | printf 'ssh-et: %s\n' "$*" 16 | } 17 | 18 | _error() { 19 | local error normal 20 | # Red color 21 | error="$(tput setaf 1 2> /dev/null)" || true 22 | normal="$(tput sgr0 2> /dev/null)" || true 23 | printf >&2 '%s\n' "${error}${*}${normal}" 24 | } 25 | 26 | _log_error() { 27 | _error "$(printf 'ssh-et: %s' "$*")" 28 | } 29 | 30 | _print_usage_and_die() { 31 | printf >&2 'Usage: ssh-et [ssh_options] \n' 32 | exit 1 33 | } 34 | 35 | _check_deps() { 36 | local s=0 37 | if ((BASH_VERSINFO[0] < 4)); then 38 | _log_error 'Bash version must be at least 4.0' 39 | s=1 40 | fi 41 | for cmd in et ssh; do 42 | if ! _command_exists "${cmd}"; then 43 | _log_error "Missing dependency: ${cmd}" 44 | s=1 45 | fi 46 | done 47 | return $s 48 | } 49 | 50 | _check_args() { 51 | if (($# < 1)); then 52 | _print_usage_and_die 53 | fi 54 | if [[ "${*: -1}" == -* ]]; then 55 | _log_error 'Last arg must be a remote, not an SSH option' 56 | _print_usage_and_die 57 | fi 58 | } 59 | 60 | # https://stackoverflow.com/a/6943581 61 | _is_port_open() { 62 | local port="$1" 63 | ! { printf '' > /dev/tcp/127.0.0.1/"${port}"; } &> /dev/null 64 | } 65 | 66 | # The range 49152–65535 contains ephemeral/dynamic ports. We scan 200 ports 67 | # that start with the prefixes "522" or "622" (the 22 part of the prefix is 68 | # useful to remember it's used for SSH). 69 | _find_ephemeral_port() { 70 | for port in {52200..52299} {62200..62299}; do 71 | if _is_port_open "${port}"; then 72 | echo "${port}" 73 | return 0 74 | fi 75 | done 76 | return 1 77 | } 78 | 79 | main() { 80 | _check_deps || return 1 81 | _check_args "$@" || return 1 82 | local remote="${*: -1}" 83 | local ssh_args=("${@:1:(($# - 1))}") 84 | local tmpdir 85 | tmpdir="$(mktemp -d -t "ssh-et-fifo-$$_XXX")" 86 | # NOTE: The path variable in trap must be expanded here because it may not be 87 | # defined when the trap is ran. 88 | # shellcheck disable=SC2064 89 | trap "rm -rf -- '${tmpdir}' &> /dev/null || true" EXIT ERR INT HUP TERM 90 | local et_fifo="${tmpdir}/et_fifo" 91 | mkfifo "${et_fifo}" || return $? 92 | local port num_retries=0 success=0 has_stdout=0 93 | while ((!success && num_retries < MAX_RETRIES)); do 94 | if ! port="$(_find_ephemeral_port)"; then 95 | _log_error 'Could not find an ephemeral port' 96 | return 1 97 | fi 98 | _log_info "Found open port: ${port}" 99 | local et_cmd=(et -t "${port}":22 -N "${remote}") 100 | _log_info "Running: ${et_cmd[*]}" 101 | "${et_cmd[@]}" > "${et_fifo}" & 102 | et_pid=$! 103 | success=0 104 | has_stdout=0 105 | while IFS='' read -r line; do 106 | has_stdout=1 107 | printf 'et: %s\n' "${line}" 108 | if [[ $line == *"Address already in use"* ]]; then 109 | wait "${et_pid}" || true 110 | if ((num_retries < MAX_RETRIES - 1)); then 111 | _log_info 'Port became used, finding another one...' 112 | sleep 0.$((RANDOM)) 113 | fi 114 | ((num_retries += 1)) 115 | break 116 | fi 117 | if [[ $line == *"feel free to background"* ]]; then 118 | success=1 119 | break 120 | fi 121 | return 1 122 | done < "${et_fifo}" 123 | ((has_stdout)) || return 1 124 | done 125 | ((success)) || return 1 126 | # We use the localhost loopback address for all remote hosts, but we still 127 | # want to use the SSH options set for the remote in the client 128 | # config. Therefore, we use the original remote hostname, but override two SSH 129 | # options: 130 | # - HostName: to connect to the loopback address 131 | # - HostKeyAlias: to avoid warnings about the host key file 132 | # NOTE: We must put the user provided SSH args first, since SSH gives priority 133 | # to the *first* args it encounters. 134 | # TODO: This doesn't work correctly when using the "%h" token in the ssh 135 | # config, since it will be set to the loopback address instead of the remote 136 | # name given on the command line. One possible workaround to explore is to 137 | # create a temporary SSH config that includes the real config, and makes sure 138 | # to only set HostName after all other config is passed. 139 | local ssh_cmd=( 140 | ssh -p "${port}" "${ssh_args[@]}" -oHostName='127.0.0.1' 141 | -oHostKeyAlias="${remote}" "${remote}" 142 | ) 143 | _log_info "Running: ${ssh_cmd[*]}" 144 | "${ssh_cmd[@]}" 145 | kill "${et_pid}" 146 | # wait "${et_pid}" 147 | rm -rf -- "${tmpdir}" 148 | } 149 | 150 | main "$@" 151 | -------------------------------------------------------------------------------- /src/base/BackedReader.cpp: -------------------------------------------------------------------------------- 1 | #include "BackedReader.hpp" 2 | 3 | namespace et { 4 | BackedReader::BackedReader(std::shared_ptr socketHandler_, // 5 | std::shared_ptr cryptoHandler_, // 6 | int socketFd_) 7 | : socketHandler(socketHandler_), 8 | cryptoHandler(cryptoHandler_), 9 | socketFd(socketFd_), 10 | sequenceNumber(0) {} 11 | 12 | bool BackedReader::hasData() { 13 | lock_guard guard(recoverMutex); 14 | if (socketFd < 0) { 15 | return false; 16 | } 17 | 18 | if (localBuffer.size() > 0) { 19 | return true; 20 | } 21 | 22 | return socketHandler->hasData(socketFd); 23 | } 24 | 25 | int BackedReader::read(Packet* packet) { 26 | lock_guard guard(recoverMutex); 27 | if (socketFd < 0) { 28 | // The socket is dead, return 0 bytes until it returns 29 | VLOG(1) << "Tried to read from a dead socket"; 30 | return 0; 31 | } 32 | 33 | if (localBuffer.size() > 0) { 34 | VLOG(1) << "Reading from local buffer"; 35 | *packet = Packet(localBuffer.front()); 36 | localBuffer.pop_front(); 37 | VLOG(1) << "New local buffer size: " << localBuffer.size(); 38 | packet->decrypt(cryptoHandler); 39 | return 1; 40 | } 41 | 42 | // Read from the socket 43 | if (partialMessage.length() < 4) { 44 | // Read the header 45 | char tmpBuf[4]; 46 | ssize_t bytesRead = 47 | socketHandler->read(socketFd, tmpBuf, 4 - partialMessage.length()); 48 | if (bytesRead == 0) { 49 | // Connection is closed. Instead of closing the socket, set EPIPE. 50 | // In EternalTCP, the server needs to explicitly tell the client that 51 | // the session is over. 52 | SetErrno(EPIPE); 53 | return -1; 54 | } else if (bytesRead > 0) { 55 | partialMessage.append(tmpBuf, bytesRead); 56 | } else if (bytesRead == -1) { 57 | // Read error 58 | return -1; 59 | } else { 60 | STFATAL << "Read returned value outside of [-1,inf): " << bytesRead; 61 | } 62 | } 63 | if (partialMessage.length() < 4) { 64 | // We didn't get the full header yet. 65 | return 0; 66 | } 67 | int messageLength = getPartialMessageLength(); 68 | VLOG(2) << "Reading message of length: " << messageLength; 69 | int messageRemainder = messageLength - (partialMessage.length() - 4); 70 | if (messageRemainder) { 71 | VLOG(2) << "bytes remaining: " << messageRemainder; 72 | string s(messageRemainder, '\0'); 73 | ssize_t bytesRead = socketHandler->read(socketFd, &s[0], s.length()); 74 | if (bytesRead == 0) { 75 | // Connection is closed. Instead of closing the socket, set EPIPE. 76 | // In EternalTCP, the server needs to explicitly tell the client that 77 | // the session is over. 78 | SetErrno(EPIPE); 79 | return -1; 80 | } else if (bytesRead == -1) { 81 | VLOG(2) << "Error while reading"; 82 | return bytesRead; 83 | } else if (bytesRead > 0) { 84 | partialMessage.append(&s[0], bytesRead); 85 | messageRemainder -= bytesRead; 86 | } else { 87 | STFATAL << "Invalid value from read: " << bytesRead; 88 | } 89 | } 90 | if (!messageRemainder) { 91 | constructPartialMessage(packet); 92 | return 1; 93 | } 94 | 95 | return 0; 96 | } 97 | 98 | void BackedReader::revive(int newSocketFd, 99 | const vector& newLocalEntries) { 100 | partialMessage = ""; 101 | localBuffer.insert(localBuffer.end(), newLocalEntries.begin(), 102 | newLocalEntries.end()); 103 | sequenceNumber += newLocalEntries.size(); 104 | socketFd = newSocketFd; 105 | } 106 | 107 | int BackedReader::getPartialMessageLength() { 108 | if (partialMessage.length() < 4) { 109 | STFATAL << "Tried to construct a message header that wasn't complete"; 110 | } 111 | int messageSize; 112 | memcpy(&messageSize, &partialMessage[0], sizeof(int)); 113 | messageSize = ntohl(messageSize); 114 | return messageSize; 115 | } 116 | 117 | void BackedReader::constructPartialMessage(Packet* packet) { 118 | int messageSize = getPartialMessageLength(); 119 | if (int(partialMessage.length()) - 4 != messageSize) { 120 | STFATAL 121 | << "Tried to construct a message that wasn't complete or over-filled: " 122 | << (int(partialMessage.length()) - 4) << " != " << messageSize; 123 | } 124 | string serializedPacket = partialMessage.substr(4, messageSize); 125 | *packet = Packet(serializedPacket); 126 | packet->decrypt(cryptoHandler); 127 | partialMessage.clear(); 128 | sequenceNumber++; 129 | } 130 | } // namespace et 131 | -------------------------------------------------------------------------------- /src/base/BackedReader.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_BACKED_READER__ 2 | #define __ET_BACKED_READER__ 3 | 4 | #include "CryptoHandler.hpp" 5 | #include "Headers.hpp" 6 | #include "Packet.hpp" 7 | #include "SocketHandler.hpp" 8 | 9 | namespace et { 10 | class BackedReader { 11 | public: 12 | BackedReader(shared_ptr socketHandler, 13 | shared_ptr cryptoHandler, int socketFd); 14 | 15 | bool hasData(); 16 | int read(Packet* packet); 17 | 18 | mutex& getRecoverMutex() { return recoverMutex; } 19 | void revive(int newSocketFd, const vector& newLocalEntries); 20 | 21 | inline void invalidateSocket() { 22 | lock_guard guard(recoverMutex); 23 | socketFd = -1; 24 | } 25 | 26 | inline int64_t getSequenceNumber() { return sequenceNumber; } 27 | 28 | protected: 29 | mutex recoverMutex; 30 | shared_ptr socketHandler; 31 | shared_ptr cryptoHandler; 32 | volatile int socketFd; 33 | int64_t sequenceNumber; 34 | deque localBuffer; 35 | 36 | string partialMessage; 37 | 38 | void init(int64_t firstSequenceNumber); 39 | int getPartialMessageLength(); 40 | void constructPartialMessage(Packet* packet); 41 | }; 42 | } // namespace et 43 | 44 | #endif // __ET_BACKED_READER__ 45 | -------------------------------------------------------------------------------- /src/base/BackedWriter.cpp: -------------------------------------------------------------------------------- 1 | #include "BackedWriter.hpp" 2 | 3 | namespace et { 4 | BackedWriter::BackedWriter(std::shared_ptr socketHandler_, // 5 | std::shared_ptr cryptoHandler_, // 6 | int socketFd_) 7 | : socketHandler(socketHandler_), 8 | cryptoHandler(cryptoHandler_), 9 | socketFd(socketFd_), 10 | backupSize(0), 11 | sequenceNumber(0) {} 12 | 13 | BackedWriterWriteState BackedWriter::write(Packet packet) { 14 | // If recover started, Wait until finished 15 | lock_guard guard(recoverMutex); 16 | { 17 | if (socketFd < 0) { 18 | // We have no socket to write to, don't bother trying to write 19 | return BackedWriterWriteState::SKIPPED; 20 | } 21 | 22 | // Once we encrypt and the encryption state is updated, there's no 23 | // going back. 24 | packet.encrypt(cryptoHandler); 25 | 26 | // Backup the buffer 27 | backupBuffer.push_front(packet); 28 | backupSize += packet.length(); 29 | sequenceNumber++; 30 | // Cleanup old values 31 | while (backupSize > MAX_BACKUP_BYTES) { 32 | backupSize -= backupBuffer.back().length(); 33 | backupBuffer.pop_back(); 34 | } 35 | } 36 | 37 | // Size before we add the header 38 | int messageSize = packet.length(); 39 | 40 | messageSize = htonl(messageSize); 41 | string s = string("0000") + packet.serialize(); 42 | if (int64_t(packet.length()) != int64_t(s.length()) - 4) { 43 | STFATAL << "Packet header size is invalid: " << packet.length() 44 | << " != " << (s.length() - 4); 45 | } 46 | memcpy(&s[0], &messageSize, sizeof(int)); 47 | 48 | size_t bytesWritten = 0; 49 | size_t count = s.length(); 50 | VLOG(2) << "Message length with header: " << count; 51 | 52 | while (true) { 53 | // We have a socket, let's try to use it. 54 | if (socketFd < 0) { 55 | return BackedWriterWriteState::WROTE_WITH_FAILURE; 56 | } 57 | ssize_t result = socketHandler->write( 58 | socketFd, ((char*)&s[0]) + bytesWritten, count - bytesWritten); 59 | if (result >= 0) { 60 | bytesWritten += result; 61 | if (bytesWritten == count) { 62 | return BackedWriterWriteState::SUCCESS; 63 | } 64 | std::this_thread::sleep_for(std::chrono::microseconds(1000)); 65 | } else { 66 | // Error, we don't know how many bytes were written but it 67 | // doesn't matter because the reader is going to have to 68 | // reconnect anyways. The important thing is for the caller to 69 | // think that the bytes were written and not call again. 70 | return BackedWriterWriteState::WROTE_WITH_FAILURE; 71 | } 72 | } 73 | } 74 | 75 | vector BackedWriter::recover(int64_t lastValidSequenceNumber) { 76 | if (socketFd >= 0) { 77 | throw std::runtime_error("Can't recover when the fd is still alive"); 78 | } 79 | VLOG(1) << int64_t(this) << ": Manually locking recover mutex!"; 80 | 81 | int64_t messagesToRecover = sequenceNumber - lastValidSequenceNumber; 82 | if (messagesToRecover < 0) { 83 | STFATAL << "Something went really wrong, client is ahead of server"; 84 | } 85 | if (messagesToRecover == 0) { 86 | return vector(); 87 | } 88 | VLOG(1) << int64_t(this) << ": Recovering " << messagesToRecover 89 | << " Messages"; 90 | int64_t messagesSeen = 0; 91 | vector retval; 92 | for (auto it = backupBuffer.begin(); it != backupBuffer.end(); ++it) { 93 | retval.push_back(it->serialize()); 94 | messagesSeen++; 95 | if (messagesSeen == messagesToRecover) { 96 | reverse(retval.begin(), retval.end()); 97 | return retval; 98 | } 99 | } 100 | throw std::runtime_error("Client is too far behind server."); 101 | } 102 | 103 | void BackedWriter::revive(int newSocketFd) { socketFd = newSocketFd; } 104 | } // namespace et 105 | -------------------------------------------------------------------------------- /src/base/BackedWriter.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_BACKED_WRITER__ 2 | #define __ET_BACKED_WRITER__ 3 | 4 | #include "CryptoHandler.hpp" 5 | #include "Headers.hpp" 6 | #include "Packet.hpp" 7 | #include "SocketHandler.hpp" 8 | 9 | namespace et { 10 | enum class BackedWriterWriteState { 11 | SKIPPED = 0, // 12 | SUCCESS = 1, 13 | WROTE_WITH_FAILURE = 2 14 | }; 15 | 16 | class BackedWriter { 17 | public: 18 | BackedWriter(shared_ptr socketHandler, 19 | shared_ptr cryptoHandler, int socketFd); 20 | 21 | BackedWriterWriteState write(Packet packet); 22 | 23 | vector recover(int64_t lastValidSequenceNumber); 24 | 25 | void revive(int newSocketFd); 26 | mutex& getRecoverMutex() { return recoverMutex; } 27 | 28 | inline int getSocketFd() { return socketFd; } 29 | 30 | inline void invalidateSocket() { 31 | lock_guard guard(recoverMutex); 32 | socketFd = -1; 33 | } 34 | 35 | inline int64_t getSequenceNumber() { return sequenceNumber; } 36 | 37 | protected: 38 | mutex recoverMutex; 39 | shared_ptr socketHandler; 40 | shared_ptr cryptoHandler; 41 | int socketFd; 42 | 43 | static const int MAX_BACKUP_BYTES = 64 * 1024 * 1024; 44 | std::deque backupBuffer; 45 | int64_t backupSize; 46 | int64_t sequenceNumber; 47 | }; 48 | } // namespace et 49 | 50 | #endif // __ET_BACKED_WRITER__ 51 | -------------------------------------------------------------------------------- /src/base/ClientConnection.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_CLIENT_CONNECTION__ 2 | #define __ET_CLIENT_CONNECTION__ 3 | 4 | #include "Connection.hpp" 5 | #include "Headers.hpp" 6 | 7 | namespace et { 8 | extern const int NULL_CLIENT_ID; 9 | 10 | class ClientConnection : public Connection { 11 | public: 12 | ClientConnection(std::shared_ptr _socketHandler, 13 | const SocketEndpoint& _endpoint, const string& _id, 14 | const string& _key); 15 | 16 | virtual ~ClientConnection(); 17 | 18 | bool connect(); 19 | 20 | virtual void closeSocketAndMaybeReconnect(); 21 | 22 | void waitReconnect(); 23 | 24 | protected: 25 | void pollReconnect(); 26 | 27 | SocketEndpoint remoteEndpoint; 28 | std::shared_ptr reconnectThread; 29 | }; 30 | } // namespace et 31 | 32 | #endif // __ET_SERVER_CONNECTION__ 33 | -------------------------------------------------------------------------------- /src/base/Connection.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_CONNECTION__ 2 | #define __ET_CONNECTION__ 3 | 4 | #include "BackedReader.hpp" 5 | #include "BackedWriter.hpp" 6 | #include "Headers.hpp" 7 | #include "SocketHandler.hpp" 8 | 9 | namespace et { 10 | class Connection { 11 | public: 12 | Connection(shared_ptr _socketHandler, const string& _id, 13 | const string& _key); 14 | 15 | virtual ~Connection(); 16 | 17 | virtual bool readPacket(Packet* packet); 18 | virtual void writePacket(const Packet& packet); 19 | 20 | virtual bool read(Packet* packet); 21 | virtual bool write(const Packet& packet); 22 | 23 | inline shared_ptr getReader() { return reader; } 24 | inline shared_ptr getWriter() { return writer; } 25 | 26 | int getSocketFd() { return socketFd; } 27 | 28 | inline shared_ptr getSocketHandler() { return socketHandler; } 29 | 30 | bool isDisconnected() { return socketFd == -1; } 31 | 32 | string getId() { return id; } 33 | 34 | inline bool hasData() { return reader->hasData(); } 35 | 36 | virtual void closeSocket(); 37 | virtual void closeSocketAndMaybeReconnect() { 38 | // By default, don't try to reconnect. Subclasses can override. 39 | closeSocket(); 40 | } 41 | 42 | void shutdown(); 43 | 44 | inline bool isShuttingDown() { 45 | lock_guard guard(connectionMutex); 46 | return shuttingDown; 47 | } 48 | 49 | protected: 50 | bool recover(int newSocketFd); 51 | 52 | shared_ptr socketHandler; 53 | string id; 54 | string key; 55 | std::shared_ptr reader; 56 | std::shared_ptr writer; 57 | int socketFd; 58 | bool shuttingDown; 59 | recursive_mutex connectionMutex; 60 | }; 61 | } // namespace et 62 | 63 | #endif // __ET_CONNECTION__ 64 | -------------------------------------------------------------------------------- /src/base/CryptoHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "CryptoHandler.hpp" 2 | 3 | #define SODIUM_FAIL(X) \ 4 | { \ 5 | int rc = (X); \ 6 | if ((rc) == -1) STFATAL << "Crypto Error: (" << rc << ")"; \ 7 | } 8 | namespace et { 9 | 10 | CryptoHandler::CryptoHandler(const string& _key, unsigned char nonceMSB) { 11 | lock_guard guard(cryptoMutex); 12 | if (-1 == sodium_init()) { 13 | STFATAL << "libsodium init failed"; 14 | } 15 | if (_key.length() != crypto_secretbox_KEYBYTES) { 16 | STFATAL << "Invalid key length"; 17 | } 18 | memcpy(key, &_key[0], _key.length()); 19 | memset(nonce, 0, crypto_secretbox_NONCEBYTES); 20 | nonce[crypto_secretbox_NONCEBYTES - 1] = nonceMSB; 21 | } 22 | 23 | CryptoHandler::~CryptoHandler() {} 24 | 25 | string CryptoHandler::encrypt(const string& buffer) { 26 | lock_guard guard(cryptoMutex); 27 | incrementNonce(); 28 | string retval(buffer.length() + crypto_secretbox_MACBYTES, '\0'); 29 | SODIUM_FAIL(crypto_secretbox_easy((unsigned char*)&retval[0], 30 | (const unsigned char*)buffer.c_str(), 31 | buffer.length(), nonce, key)); 32 | return retval; 33 | } 34 | 35 | string CryptoHandler::decrypt(const string& buffer) { 36 | lock_guard guard(cryptoMutex); 37 | incrementNonce(); 38 | string retval(buffer.length() - crypto_secretbox_MACBYTES, '\0'); 39 | if (crypto_secretbox_open_easy((unsigned char*)&retval[0], 40 | (const unsigned char*)buffer.c_str(), 41 | buffer.length(), nonce, key) == -1) { 42 | STFATAL << "Decrypt failed. Possible key mismatch?"; 43 | } 44 | return retval; 45 | } 46 | 47 | void CryptoHandler::incrementNonce() { 48 | // Increment nonce 49 | for (int a = 0; a < int(crypto_secretbox_NONCEBYTES); a++) { 50 | nonce[a]++; 51 | if (nonce[a]) { 52 | // When nonce[a]==0, it means we rolled over to the next digit; 53 | break; 54 | } 55 | } 56 | } 57 | } // namespace et 58 | -------------------------------------------------------------------------------- /src/base/CryptoHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_CRYPTO_HANDLER__ 2 | #define __ET_CRYPTO_HANDLER__ 3 | 4 | #include 5 | 6 | #include "Headers.hpp" 7 | 8 | namespace et { 9 | 10 | class CryptoHandler { 11 | public: 12 | explicit CryptoHandler(const string& key, unsigned char nonceMSB); 13 | ~CryptoHandler(); 14 | 15 | string encrypt(const string& buffer); 16 | string decrypt(const string& buffer); 17 | 18 | protected: 19 | void incrementNonce(); 20 | unsigned char nonce[crypto_secretbox_NONCEBYTES]; 21 | unsigned char key[crypto_secretbox_KEYBYTES]; 22 | 23 | private: 24 | mutex cryptoMutex; 25 | }; 26 | } // namespace et 27 | 28 | #endif // __ET_CRYPTO_HANDLER__ 29 | -------------------------------------------------------------------------------- /src/base/DaemonCreator.cpp: -------------------------------------------------------------------------------- 1 | #ifndef WIN32 2 | #include "DaemonCreator.hpp" 3 | 4 | namespace et { 5 | int DaemonCreator::createSessionLeader() { return ::daemon(0, 0); } 6 | 7 | int DaemonCreator::create(bool parentExit, string childPidFile) { 8 | pid_t pid; 9 | 10 | /* Fork off the parent process */ 11 | pid = fork(); 12 | 13 | /* An error occurred */ 14 | if (pid < 0) exit(EXIT_FAILURE); 15 | 16 | /* Success: Return so the parent can continue */ 17 | if (pid > 0) { 18 | if (parentExit) { 19 | exit(EXIT_SUCCESS); 20 | } 21 | return PARENT; 22 | } 23 | 24 | /* On success: The child process becomes session leader */ 25 | if (setsid() < 0) exit(EXIT_FAILURE); 26 | 27 | /* Catch, ignore and handle signals */ 28 | signal(SIGHUP, SIG_IGN); 29 | 30 | /* Fork off for the second time*/ 31 | pid = fork(); 32 | 33 | /* An error occurred */ 34 | if (pid < 0) exit(EXIT_FAILURE); 35 | 36 | /* Success: Let the parent terminate */ 37 | if (pid > 0) exit(EXIT_SUCCESS); 38 | 39 | /* Child process, write pid file */ 40 | if (childPidFile != "") { 41 | int pidFilehandle = open(childPidFile.c_str(), O_RDWR | O_CREAT, 0600); 42 | if (pidFilehandle == -1) { 43 | STFATAL << "Error opening pidfile for writing: " << childPidFile; 44 | } 45 | 46 | // Max pid length for x86_64 is 2^22 ~ 4000000 47 | std::stringstream pid_ss; 48 | pid_ss << getpid() << "\n"; 49 | std::string pid_str = pid_ss.str(); 50 | write(pidFilehandle, pid_str.c_str(), pid_str.length()); 51 | close(pidFilehandle); 52 | } 53 | 54 | /* Change the working directory to the root directory */ 55 | /* or another appropriated directory */ 56 | chdir("/"); 57 | 58 | auto fd = open("/dev/null", O_WRONLY | O_CREAT, 0666); 59 | dup2(fd, STDOUT_FILENO); 60 | dup2(fd, STDERR_FILENO); 61 | 62 | auto fd2 = open("/dev/null", O_RDONLY); 63 | dup2(fd2, STDIN_FILENO); 64 | 65 | return CHILD; 66 | } 67 | } // namespace et 68 | #endif 69 | -------------------------------------------------------------------------------- /src/base/DaemonCreator.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __DAEMON_CREATOR_H__ 2 | #define __DAEMON_CREATOR_H__ 3 | 4 | #include "Headers.hpp" 5 | 6 | namespace et { 7 | class DaemonCreator { 8 | public: 9 | static int createSessionLeader(); 10 | static int create(bool terminateParent, string childPidFile); 11 | static const int PARENT = 1; 12 | static const int CHILD = 2; 13 | }; 14 | } // namespace et 15 | 16 | #endif // __DAEMON_CREATOR_H__ 17 | -------------------------------------------------------------------------------- /src/base/Globals.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_GLOBALS__ 2 | #define __ET_GLOBALS__ 3 | 4 | namespace et { 5 | void fatalOnWriteError(ssize_t expected, ssize_t actual); 6 | } 7 | 8 | #endif // __ET_GLOBALS__ 9 | -------------------------------------------------------------------------------- /src/base/JsonLib.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "nlohmann/json.hpp" 4 | 5 | using json = nlohmann::json; 6 | -------------------------------------------------------------------------------- /src/base/LogHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "LogHandler.hpp" 2 | 3 | INITIALIZE_EASYLOGGINGPP 4 | 5 | namespace et { 6 | el::Configurations LogHandler::setupLogHandler(int *argc, char ***argv) { 7 | // easylogging parses verbose arguments, see [Application Arguments] 8 | // in https://github.com/muflihun/easyloggingpp/blob/master/README.md 9 | // but it is non-intuitive so we explicitly set verbosity based on cxxopts 10 | START_EASYLOGGINGPP(*argc, *argv); 11 | 12 | // Easylogging configurations 13 | el::Configurations defaultConf; 14 | defaultConf.setToDefault(); 15 | // doc says %thread_name, but %thread is the right one 16 | defaultConf.setGlobally(el::ConfigurationType::Format, 17 | "[%level %datetime %thread %fbase:%line] %msg"); 18 | defaultConf.setGlobally(el::ConfigurationType::Enabled, "true"); 19 | defaultConf.setGlobally(el::ConfigurationType::SubsecondPrecision, "3"); 20 | defaultConf.setGlobally(el::ConfigurationType::PerformanceTracking, "false"); 21 | defaultConf.setGlobally(el::ConfigurationType::LogFlushThreshold, "1"); 22 | defaultConf.set(el::Level::Verbose, el::ConfigurationType::Format, 23 | "[%levshort%vlevel %datetime %thread %fbase:%line] %msg"); 24 | return defaultConf; 25 | } 26 | 27 | void LogHandler::setupLogFiles(el::Configurations *defaultConf, 28 | const string &path, const string &filenamePrefix, 29 | bool logToStdout, bool redirectStderrToFile, 30 | bool appendPid, string maxlogsize) { 31 | time_t rawtime; 32 | struct tm *timeinfo; 33 | char buffer[80]; 34 | time(&rawtime); 35 | timeinfo = localtime(&rawtime); 36 | strftime(buffer, sizeof(buffer), "%Y-%m-%d_%H-%M-%S", timeinfo); 37 | string current_time(buffer); 38 | string logFilename = filenamePrefix + "-" + current_time; 39 | string stderrFilename = filenamePrefix + "-stderr-" + current_time; 40 | if (appendPid) { 41 | string pid = std::to_string(getpid()); 42 | logFilename.append("_" + pid); 43 | stderrFilename.append("_" + pid); 44 | } 45 | logFilename.append(".log"); 46 | stderrFilename.append(".log"); 47 | string fullFname = createLogFile(path, logFilename); 48 | 49 | // Enable strict log file size check 50 | el::Loggers::addFlag(el::LoggingFlag::StrictLogFileSizeCheck); 51 | defaultConf->setGlobally(el::ConfigurationType::Filename, fullFname); 52 | defaultConf->setGlobally(el::ConfigurationType::ToFile, "true"); 53 | defaultConf->setGlobally(el::ConfigurationType::MaxLogFileSize, maxlogsize); 54 | 55 | if (logToStdout) { 56 | defaultConf->setGlobally(el::ConfigurationType::ToStandardOutput, "true"); 57 | } else { 58 | defaultConf->setGlobally(el::ConfigurationType::ToStandardOutput, "false"); 59 | } 60 | 61 | if (redirectStderrToFile) { 62 | stderrToFile(path, stderrFilename); 63 | } 64 | } 65 | 66 | void LogHandler::rolloutHandler(const char *filename, std::size_t size) { 67 | // SHOULD NOT LOG ANYTHING HERE BECAUSE LOG FILE IS CLOSED! 68 | // REMOVE OLD LOG 69 | remove(filename); 70 | } 71 | 72 | void LogHandler::setupStdoutLogger() { 73 | el::Logger *stdoutLogger = el::Loggers::getLogger("stdout"); 74 | // Easylogging configurations 75 | el::Configurations stdoutConf; 76 | stdoutConf.setToDefault(); 77 | // Values are always std::string 78 | stdoutConf.setGlobally(el::ConfigurationType::Format, "%msg"); 79 | stdoutConf.setGlobally(el::ConfigurationType::ToStandardOutput, "true"); 80 | stdoutConf.setGlobally(el::ConfigurationType::ToFile, "false"); 81 | el::Loggers::reconfigureLogger(stdoutLogger, stdoutConf); 82 | } 83 | 84 | string LogHandler::createLogFile(const string &path, const string &filename) { 85 | string fullFname = path + "/" + filename; 86 | try { 87 | fs::create_directories(path); 88 | } catch (const fs::filesystem_error &fse) { 89 | CLOG(ERROR, "stdout") << "Cannot create logfile directory: " << fse.what() 90 | << endl; 91 | exit(1); 92 | } 93 | #ifdef WIN32 94 | // O_NOFOLLOW does not exist on windows 95 | FATAL_FAIL(::open(fullFname.c_str(), O_EXCL | O_CREAT, 0600)); 96 | #else 97 | FATAL_FAIL(::open(fullFname.c_str(), O_NOFOLLOW | O_EXCL | O_CREAT, 0600)); 98 | #endif 99 | return fullFname; 100 | } 101 | 102 | void LogHandler::stderrToFile(const string &path, 103 | const string &stderrFilename) { 104 | string fullFname = createLogFile(path, stderrFilename); 105 | FILE *stderr_stream = freopen(fullFname.c_str(), "w", stderr); 106 | if (!stderr_stream) { 107 | STFATAL << "Invalid filename " << stderrFilename; 108 | } 109 | setvbuf(stderr_stream, NULL, _IOLBF, BUFSIZ); // set to line buffering 110 | } 111 | 112 | } // namespace et 113 | -------------------------------------------------------------------------------- /src/base/LogHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_LOG_HANDLER__ 2 | #define __ET_LOG_HANDLER__ 3 | 4 | #include "Headers.hpp" 5 | 6 | namespace et { 7 | class LogHandler { 8 | public: 9 | static el::Configurations setupLogHandler(int *argc, char ***argv); 10 | static void setupLogFiles(el::Configurations *defaultConf, const string &path, 11 | const string &filenamePrefix, 12 | bool logToStdout = false, 13 | bool redirectStderrToFile = false, 14 | bool appendPid = false, 15 | string maxlogsize = "20971520"); 16 | static void rolloutHandler(const char *filename, std::size_t size); 17 | static void setupStdoutLogger(); 18 | 19 | private: 20 | static void stderrToFile(const string &path, const string &stderrFilename); 21 | static string createLogFile(const string &path, const string &filename); 22 | }; 23 | } // namespace et 24 | #endif // __ET_LOG_HANDLER__ 25 | -------------------------------------------------------------------------------- /src/base/Packet.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_PACKET_H__ 2 | #define __ET_PACKET_H__ 3 | 4 | #include "CryptoHandler.hpp" 5 | #include "Headers.hpp" 6 | 7 | namespace et { 8 | class Packet { 9 | public: 10 | Packet() : encrypted(false), header(255) {} 11 | Packet(uint8_t _header, const string& _payload) 12 | : encrypted(false), header(_header), payload(_payload) {} 13 | Packet(bool _encrypted, uint8_t _header, const string& _payload) 14 | : encrypted(_encrypted), header(_header), payload(_payload) {} 15 | explicit Packet(const string& serializedPacket) { 16 | encrypted = serializedPacket[0]; 17 | header = serializedPacket[1]; 18 | payload = serializedPacket.substr(2); 19 | } 20 | 21 | void decrypt(shared_ptr cryptoHandler) { 22 | if (encrypted) { 23 | encrypted = false; 24 | payload = cryptoHandler->decrypt(payload); 25 | } else { 26 | STFATAL << "Tried to decrypt a packet that wasn't encrypted"; 27 | } 28 | } 29 | 30 | void encrypt(shared_ptr cryptoHandler) { 31 | if (encrypted) { 32 | STFATAL << "Tried to encrypt a packet that was already encrypted"; 33 | } else { 34 | encrypted = true; 35 | payload = cryptoHandler->encrypt(payload); 36 | } 37 | } 38 | 39 | bool isEncrypted() const { return encrypted; } 40 | uint8_t getHeader() const { return header; } 41 | string getPayload() const { return payload; } 42 | 43 | ssize_t length() const { return HEADER_SIZE + payload.length(); } 44 | 45 | string serialize() const { 46 | string s = "00" + payload; 47 | s[0] = uint8_t(encrypted); 48 | s[1] = header; 49 | return s; 50 | } 51 | 52 | protected: 53 | static const int HEADER_SIZE = 2; 54 | bool encrypted; 55 | uint8_t header; 56 | 57 | string payload; 58 | }; 59 | } // namespace et 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /src/base/PipeSocketHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "PipeSocketHandler.hpp" 2 | 3 | namespace et { 4 | PipeSocketHandler::PipeSocketHandler() {} 5 | 6 | int PipeSocketHandler::connect(const SocketEndpoint& endpoint) { 7 | lock_guard mutexGuard(globalMutex); 8 | 9 | string pipePath = endpoint.name(); 10 | sockaddr_un remote; 11 | 12 | int sockFd = ::socket(AF_UNIX, SOCK_STREAM, 0); 13 | FATAL_FAIL(sockFd); 14 | initSocket(sockFd); 15 | remote.sun_family = AF_UNIX; 16 | strncpy(remote.sun_path, pipePath.c_str(), sizeof(remote.sun_path)); 17 | 18 | VLOG(3) << "Connecting to " << endpoint << " with fd " << sockFd; 19 | int result = 20 | ::connect(sockFd, (struct sockaddr*)&remote, sizeof(sockaddr_un)); 21 | auto localErrno = GetErrno(); 22 | if (result < 0 && localErrno != EINPROGRESS) { 23 | VLOG(3) << "Connection result: " << result << " (" << strerror(localErrno) 24 | << ")"; 25 | #ifdef WIN32 26 | ::shutdown(sockFd, SD_BOTH); 27 | #else 28 | ::shutdown(sockFd, SHUT_RDWR); 29 | #endif 30 | #ifdef _MSC_VER 31 | FATAL_FAIL(::closesocket(sockFd)); 32 | #else 33 | FATAL_FAIL(::close(sockFd)); 34 | #endif 35 | sockFd = -1; 36 | SetErrno(localErrno); 37 | return sockFd; 38 | } 39 | 40 | fd_set fdset; 41 | FD_ZERO(&fdset); 42 | FD_SET(sockFd, &fdset); 43 | timeval tv; 44 | tv.tv_sec = 3; /* 3 second timeout */ 45 | tv.tv_usec = 0; 46 | VLOG(4) << "Before selecting sockFd"; 47 | select(sockFd + 1, NULL, &fdset, NULL, &tv); 48 | 49 | if (FD_ISSET(sockFd, &fdset)) { 50 | VLOG(4) << "sockFd " << sockFd << " is selected"; 51 | int so_error; 52 | socklen_t len = sizeof so_error; 53 | 54 | FATAL_FAIL( 55 | ::getsockopt(sockFd, SOL_SOCKET, SO_ERROR, (char*)&so_error, &len)); 56 | 57 | if (so_error == 0) { 58 | LOG(INFO) << "Connected to endpoint " << endpoint; 59 | // Initialize the socket again once it's blocking to make sure timeouts 60 | // are set 61 | initSocket(sockFd); 62 | 63 | // if we get here, we must have connected successfully 64 | } else { 65 | LOG(INFO) << "Error connecting to " << endpoint << ": " << so_error << " " 66 | << strerror(so_error); 67 | #ifdef _MSC_VER 68 | FATAL_FAIL(::closesocket(sockFd)); 69 | #else 70 | FATAL_FAIL(::close(sockFd)); 71 | #endif 72 | sockFd = -1; 73 | } 74 | } else { 75 | auto localErrno = GetErrno(); 76 | LOG(INFO) << "Error connecting to " << endpoint << ": " << localErrno << " " 77 | << strerror(localErrno); 78 | #ifdef _MSC_VER 79 | FATAL_FAIL(::closesocket(sockFd)); 80 | #else 81 | FATAL_FAIL(::close(sockFd)); 82 | #endif 83 | sockFd = -1; 84 | } 85 | 86 | LOG(INFO) << sockFd << " is a good socket"; 87 | if (sockFd >= 0) { 88 | addToActiveSockets(sockFd); 89 | } 90 | return sockFd; 91 | } 92 | 93 | set PipeSocketHandler::listen(const SocketEndpoint& endpoint) { 94 | lock_guard guard(globalMutex); 95 | 96 | string pipePath = endpoint.name(); 97 | if (pipeServerSockets.find(pipePath) != pipeServerSockets.end()) { 98 | throw runtime_error("Tried to listen twice on the same path"); 99 | } 100 | 101 | sockaddr_un local; 102 | 103 | int fd = socket(AF_UNIX, SOCK_STREAM, 0); 104 | FATAL_FAIL(fd); 105 | initServerSocket(fd); 106 | local.sun_family = AF_UNIX; /* local is declared before socket() ^ */ 107 | strncpy(local.sun_path, pipePath.c_str(), sizeof(local.sun_path)); 108 | unlink(local.sun_path); 109 | 110 | FATAL_FAIL(::bind(fd, (struct sockaddr*)&local, sizeof(sockaddr_un))); 111 | ::listen(fd, 5); 112 | #ifndef WIN32 113 | FATAL_FAIL(::chmod(local.sun_path, S_IRUSR | S_IWUSR | S_IXUSR)); 114 | #endif 115 | 116 | pipeServerSockets[pipePath] = set({fd}); 117 | return pipeServerSockets[pipePath]; 118 | } 119 | 120 | set PipeSocketHandler::getEndpointFds(const SocketEndpoint& endpoint) { 121 | lock_guard guard(globalMutex); 122 | 123 | string pipePath = endpoint.name(); 124 | if (pipeServerSockets.find(pipePath) == pipeServerSockets.end()) { 125 | STFATAL << "Tried to getPipeFd on a pipe without calling listen() first: " 126 | << pipePath; 127 | } 128 | return pipeServerSockets[pipePath]; 129 | } 130 | 131 | void PipeSocketHandler::stopListening(const SocketEndpoint& endpoint) { 132 | lock_guard guard(globalMutex); 133 | 134 | string pipePath = endpoint.name(); 135 | auto it = pipeServerSockets.find(pipePath); 136 | if (it == pipeServerSockets.end()) { 137 | STFATAL << "Tried to stop listening to a pipe that we weren't listening on:" 138 | << pipePath; 139 | } 140 | int sockFd = *(it->second.begin()); 141 | #ifdef _MSC_VER 142 | FATAL_FAIL(::closesocket(sockFd)); 143 | #else 144 | FATAL_FAIL(::close(sockFd)); 145 | #endif 146 | } 147 | } // namespace et 148 | -------------------------------------------------------------------------------- /src/base/PipeSocketHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_PIPE_SOCKET_HANDLER__ 2 | #define __ET_PIPE_SOCKET_HANDLER__ 3 | 4 | #include "UnixSocketHandler.hpp" 5 | 6 | namespace et { 7 | class PipeSocketHandler : public UnixSocketHandler { 8 | public: 9 | PipeSocketHandler(); 10 | virtual ~PipeSocketHandler() {} 11 | 12 | virtual int connect(const SocketEndpoint& endpoint); 13 | virtual set listen(const SocketEndpoint& endpoint); 14 | virtual set getEndpointFds(const SocketEndpoint& endpoint); 15 | virtual void stopListening(const SocketEndpoint& endpoint); 16 | 17 | protected: 18 | map> pipeServerSockets; 19 | }; 20 | } // namespace et 21 | 22 | #endif // __ET_TCP_SOCKET_HANDLER__ 23 | -------------------------------------------------------------------------------- /src/base/RawSocketUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "RawSocketUtils.hpp" 2 | 3 | namespace et { 4 | void RawSocketUtils::writeAll(int fd, const char* buf, size_t count) { 5 | size_t bytesWritten = 0; 6 | do { 7 | #ifdef WIN32 8 | int rc = ::send(fd, buf + bytesWritten, count - bytesWritten, 0); 9 | #else 10 | int rc = ::write(fd, buf + bytesWritten, count - bytesWritten); 11 | #endif 12 | if (rc < 0) { 13 | auto localErrno = GetErrno(); 14 | if (localErrno == EAGAIN || localErrno == EWOULDBLOCK) { 15 | // This is fine, just keep retrying 16 | std::this_thread::sleep_for(std::chrono::microseconds(100 * 1000)); 17 | continue; 18 | } 19 | STERROR << "Cannot write to raw socket: " << strerror(localErrno); 20 | throw std::runtime_error("Cannot write to raw socket"); 21 | } 22 | if (rc == 0) { 23 | throw std::runtime_error("Cannot write to raw socket: socket closed"); 24 | } 25 | bytesWritten += rc; 26 | } while (bytesWritten != count); 27 | } 28 | 29 | void RawSocketUtils::readAll(int fd, char* buf, size_t count) { 30 | size_t bytesRead = 0; 31 | do { 32 | if (!waitOnSocketData(fd)) { 33 | continue; 34 | } 35 | #ifdef WIN32 36 | int rc = ::recv(fd, buf + bytesRead, count - bytesRead, 0); 37 | #else 38 | int rc = ::read(fd, buf + bytesRead, count - bytesRead); 39 | #endif 40 | if (rc < 0) { 41 | auto localErrno = GetErrno(); 42 | if (localErrno == EAGAIN || localErrno == EWOULDBLOCK) { 43 | // This is fine, just keep retrying 44 | continue; 45 | } 46 | STERROR << "Cannot write to raw socket: " << strerror(localErrno); 47 | throw std::runtime_error("Cannot read from raw socket"); 48 | } 49 | if (rc == 0) { 50 | throw std::runtime_error("Socket has closed abruptly."); 51 | } 52 | bytesRead += rc; 53 | } while (bytesRead != count); 54 | } 55 | } // namespace et 56 | -------------------------------------------------------------------------------- /src/base/RawSocketUtils.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_RAW_SOCKET_UTILS__ 2 | #define __ET_RAW_SOCKET_UTILS__ 3 | 4 | #include "Headers.hpp" 5 | 6 | namespace et { 7 | class RawSocketUtils { 8 | public: 9 | static void writeAll(int fd, const char* buf, size_t count); 10 | 11 | static void readAll(int fd, char* buf, size_t count); 12 | }; 13 | } // namespace et 14 | #endif // __ET_RAW_SOCKET_UTILS__ 15 | -------------------------------------------------------------------------------- /src/base/ServerClientConnection.cpp: -------------------------------------------------------------------------------- 1 | #include "ServerClientConnection.hpp" 2 | 3 | namespace et { 4 | ServerClientConnection::ServerClientConnection( 5 | const std::shared_ptr& _socketHandler, 6 | const string& clientId, int _socketFd, const string& key) 7 | : Connection(_socketHandler, clientId, key) { 8 | socketFd = _socketFd; 9 | reader = shared_ptr( 10 | new BackedReader(socketHandler, 11 | shared_ptr( 12 | new CryptoHandler(key, CLIENT_SERVER_NONCE_MSB)), 13 | _socketFd)); 14 | writer = shared_ptr( 15 | new BackedWriter(socketHandler, 16 | shared_ptr( 17 | new CryptoHandler(key, SERVER_CLIENT_NONCE_MSB)), 18 | _socketFd)); 19 | } 20 | 21 | ServerClientConnection::~ServerClientConnection() { 22 | if (socketFd != -1) { 23 | closeSocket(); 24 | } 25 | } 26 | 27 | bool ServerClientConnection::recoverClient(int newSocketFd) { 28 | { 29 | lock_guard guard(connectionMutex); 30 | if (socketFd != -1) { 31 | closeSocket(); 32 | } 33 | } 34 | return recover(newSocketFd); 35 | } 36 | 37 | bool ServerClientConnection::verifyPasskey(const string& targetKey) { 38 | // Do a string comparison without revealing timing information if an early 39 | // character mismatches, always loop through the entire string. 40 | const size_t commonSize = 41 | key.size() < targetKey.size() ? key.size() : targetKey.size(); 42 | 43 | bool matchFailed = key.size() != targetKey.size(); 44 | for (size_t i = 0; i < commonSize; ++i) { 45 | matchFailed |= key[i] != targetKey[i]; 46 | } 47 | 48 | return !matchFailed; 49 | } 50 | 51 | } // namespace et 52 | -------------------------------------------------------------------------------- /src/base/ServerClientConnection.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_SERVER_CLIENT_CONNECTION__ 2 | #define __ET_SERVER_CLIENT_CONNECTION__ 3 | 4 | #include "Connection.hpp" 5 | #include "Headers.hpp" 6 | 7 | namespace et { 8 | class ServerClientConnection : public Connection { 9 | public: 10 | explicit ServerClientConnection( 11 | const std::shared_ptr& _socketHandler, 12 | const string& clientId, int _socketFd, const string& key); 13 | 14 | virtual ~ServerClientConnection(); 15 | 16 | bool recoverClient(int newSocketFd); 17 | 18 | bool verifyPasskey(const string& targetKey); 19 | 20 | protected: 21 | }; 22 | } // namespace et 23 | 24 | #endif // __ET_SERVER_CLIENT_CONNECTION__ 25 | -------------------------------------------------------------------------------- /src/base/ServerConnection.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_SERVER_CONNECTION__ 2 | #define __ET_SERVER_CONNECTION__ 3 | 4 | #include "Headers.hpp" 5 | #include "ServerClientConnection.hpp" 6 | #include "SocketHandler.hpp" 7 | 8 | namespace et { 9 | struct IdKeyPair { 10 | string id; 11 | string key; 12 | }; 13 | 14 | class ServerConnection { 15 | public: 16 | explicit ServerConnection(std::shared_ptr socketHandler, 17 | const SocketEndpoint& _serverEndpoint); 18 | 19 | ~ServerConnection(); 20 | 21 | inline bool clientKeyExists(const string& clientId) { 22 | lock_guard guard(classMutex); 23 | return clientKeys.find(clientId) != clientKeys.end(); 24 | } 25 | 26 | inline bool clientConnectionExists(const string& clientId) { 27 | lock_guard guard(classMutex); 28 | return clientConnections.find(clientId) != clientConnections.end(); 29 | } 30 | 31 | inline shared_ptr getSocketHandler() { return socketHandler; } 32 | 33 | bool acceptNewConnection(int fd); 34 | 35 | void shutdown(); 36 | 37 | inline void addClientKey(const string& id, const string& passkey) { 38 | lock_guard guard(classMutex); 39 | clientKeys[id] = passkey; 40 | } 41 | 42 | void clientHandler(int clientSocketFd); 43 | 44 | bool removeClient(const string& id); 45 | 46 | shared_ptr getClientConnection( 47 | const string& clientId) { 48 | lock_guard guard(classMutex); 49 | auto it = clientConnections.find(clientId); 50 | if (it == clientConnections.end()) { 51 | STFATAL << "Error: Tried to get a client connection that doesn't exist"; 52 | } 53 | return it->second; 54 | } 55 | 56 | virtual bool newClient( 57 | shared_ptr serverClientState) = 0; 58 | 59 | protected: 60 | void destroyPartialConnection(const string& clientId); 61 | 62 | shared_ptr socketHandler; 63 | SocketEndpoint serverEndpoint; 64 | std::unordered_map clientKeys; 65 | std::unordered_map> 66 | clientConnections; 67 | std::unique_ptr clientHandlerThreadPool; 68 | recursive_mutex classMutex; 69 | mutex connectMutex; 70 | }; 71 | } // namespace et 72 | 73 | #endif // __ET_SERVER_CONNECTION__ 74 | -------------------------------------------------------------------------------- /src/base/SocketHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "SocketHandler.hpp" 2 | 3 | #include "base64.h" 4 | 5 | namespace et { 6 | #define SOCKET_DATA_TRANSFER_TIMEOUT (30) 7 | 8 | void SocketHandler::readAll(int fd, void* buf, size_t count, bool timeout) { 9 | time_t startTime = time(NULL); 10 | size_t pos = 0; 11 | while (pos < count) { 12 | if (!waitOnSocketData(fd)) { 13 | time_t currentTime = time(NULL); 14 | if (timeout && currentTime > startTime + SOCKET_DATA_TRANSFER_TIMEOUT) { 15 | throw std::runtime_error("Socket Timeout"); 16 | } 17 | continue; 18 | } 19 | 20 | ssize_t bytesRead = read(fd, ((char*)buf) + pos, count - pos); 21 | if (bytesRead == 0) { 22 | // Connection is closed. Instead of closing the socket, set EPIPE. 23 | // In EternalTCP, the server needs to explicitly tell the client that 24 | // the session is over. 25 | SetErrno(EPIPE); 26 | bytesRead = -1; 27 | } 28 | if (bytesRead < 0) { 29 | auto localErrno = GetErrno(); 30 | if (localErrno == EAGAIN || localErrno == EWOULDBLOCK) { 31 | // This is fine, just keep retrying 32 | LOG(INFO) << "Got EAGAIN, waiting..."; 33 | } else { 34 | VLOG(1) << "Failed a call to readAll: " << strerror(localErrno); 35 | throw std::runtime_error("Failed a call to readAll"); 36 | } 37 | } else { 38 | pos += bytesRead; 39 | startTime = time(NULL); 40 | } 41 | } 42 | } 43 | 44 | int SocketHandler::writeAllOrReturn(int fd, const void* buf, size_t count) { 45 | size_t pos = 0; 46 | time_t startTime = time(NULL); 47 | while (pos < count) { 48 | time_t currentTime = time(NULL); 49 | if (currentTime > startTime + SOCKET_DATA_TRANSFER_TIMEOUT) { 50 | return -1; 51 | } 52 | ssize_t bytesWritten = write(fd, ((const char*)buf) + pos, count - pos); 53 | auto localErrno = GetErrno(); 54 | if (bytesWritten < 0) { 55 | if (localErrno == EAGAIN || localErrno == EWOULDBLOCK) { 56 | LOG(INFO) << "Got EAGAIN, waiting..."; 57 | // This is fine, just keep retrying at 10hz 58 | std::this_thread::sleep_for(std::chrono::microseconds(100 * 1000)); 59 | } else { 60 | VLOG(1) << "Failed a call to writeAll: " << strerror(localErrno); 61 | return -1; 62 | } 63 | } else if (bytesWritten == 0) { 64 | return 0; 65 | } else { 66 | pos += bytesWritten; 67 | // Reset the timeout as long as we are writing bytes 68 | startTime = currentTime; 69 | } 70 | } 71 | return count; 72 | } 73 | 74 | void SocketHandler::writeAllOrThrow(int fd, const void* buf, size_t count, 75 | bool timeout) { 76 | time_t startTime = time(NULL); 77 | size_t pos = 0; 78 | while (pos < count) { 79 | time_t currentTime = time(NULL); 80 | if (timeout && currentTime > startTime + SOCKET_DATA_TRANSFER_TIMEOUT) { 81 | throw std::runtime_error("Socket Timeout"); 82 | } 83 | ssize_t bytesWritten = write(fd, ((const char*)buf) + pos, count - pos); 84 | auto localErrno = GetErrno(); 85 | if (bytesWritten < 0) { 86 | if (localErrno == EAGAIN || localErrno == EWOULDBLOCK) { 87 | LOG(INFO) << "Got EAGAIN, waiting..."; 88 | // This is fine, just keep retrying at 10hz 89 | std::this_thread::sleep_for(std::chrono::microseconds(100 * 1000)); 90 | } else { 91 | LOG(WARNING) << "Failed a call to writeAll: " << strerror(localErrno); 92 | throw std::runtime_error("Failed a call to writeAll"); 93 | } 94 | } else if (bytesWritten == 0) { 95 | throw std::runtime_error("Socket closed during writeAll"); 96 | } else { 97 | pos += bytesWritten; 98 | // Reset the timeout as long as we are writing bytes 99 | startTime = currentTime; 100 | } 101 | } 102 | } 103 | 104 | void SocketHandler::writeB64(int fd, const char* buf, size_t count) { 105 | size_t encodedLength = Base64::EncodedLength(count); 106 | string s(encodedLength, '\0'); 107 | if (!Base64::Encode(buf, count, &s[0], s.length())) { 108 | throw runtime_error("b64 decode failed"); 109 | } 110 | writeAllOrThrow(fd, &s[0], s.length(), false); 111 | } 112 | 113 | void SocketHandler::readB64(int fd, char* buf, size_t count) { 114 | size_t encodedLength = Base64::EncodedLength(count); 115 | string s(encodedLength, '\0'); 116 | readAll(fd, &s[0], s.length(), false); 117 | if (!Base64::Decode((const char*)&s[0], s.length(), buf, count)) { 118 | throw runtime_error("b64 decode failed"); 119 | } 120 | } 121 | 122 | void SocketHandler::readB64EncodedLength(int fd, string* out, 123 | size_t encodedLength) { 124 | string s(encodedLength, '\0'); 125 | readAll(fd, &s[0], s.length(), false); 126 | if (!Base64::Decode(s, out)) { 127 | throw runtime_error("b64 decode failed"); 128 | } 129 | } 130 | } // namespace et 131 | -------------------------------------------------------------------------------- /src/base/SocketHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_SOCKET_HANDLER__ 2 | #define __ET_SOCKET_HANDLER__ 3 | 4 | #include "Headers.hpp" 5 | #include "Packet.hpp" 6 | 7 | namespace et { 8 | class SocketHandler { 9 | public: 10 | virtual ~SocketHandler() {} 11 | 12 | virtual bool hasData(int fd) = 0; 13 | virtual ssize_t read(int fd, void* buf, size_t count) = 0; 14 | virtual ssize_t write(int fd, const void* buf, size_t count) = 0; 15 | 16 | void readAll(int fd, void* buf, size_t count, bool timeout); 17 | int writeAllOrReturn(int fd, const void* buf, size_t count); 18 | void writeAllOrThrow(int fd, const void* buf, size_t count, bool timeout); 19 | 20 | template 21 | inline T readProto(int fd, bool timeout) { 22 | T t; 23 | int64_t length; 24 | readAll(fd, &length, sizeof(int64_t), timeout); 25 | if (length < 0 || length > 128 * 1024 * 1024) { 26 | // If the message is <= 0 or too big, assume this is a bad packet and 27 | // throw 28 | string s = string("Invalid size (<0 or >128 MB): ") + to_string(length); 29 | throw std::runtime_error(s.c_str()); 30 | } 31 | if (length == 0) { 32 | return t; 33 | } 34 | string s(length, '\0'); 35 | readAll(fd, &s[0], length, timeout); 36 | if (!t.ParseFromString(s)) { 37 | throw std::runtime_error("Invalid proto"); 38 | } 39 | return t; 40 | } 41 | 42 | template 43 | inline void writeProto(int fd, const T& t, bool timeout) { 44 | string s; 45 | if (!t.SerializeToString(&s)) { 46 | STFATAL << "Serialization of " << t.GetTypeName() << " failed!"; 47 | } 48 | int64_t length = s.length(); 49 | if (length < 0 || length > 128 * 1024 * 1024) { 50 | STFATAL << "Invalid proto length: " << length << " For proto " 51 | << t.GetTypeName(); 52 | } 53 | writeAllOrThrow(fd, &length, sizeof(int64_t), timeout); 54 | if (length > 0) { 55 | writeAllOrThrow(fd, &s[0], length, timeout); 56 | } 57 | } 58 | 59 | inline bool readPacket(int fd, Packet* packet) { 60 | int64_t length; 61 | readAll(fd, (char*)&length, sizeof(int64_t), false); 62 | if (length < 0 || length > 128 * 1024 * 1024) { 63 | // If the message is < 0 or too big, assume this is a bad packet and throw 64 | string s("Invalid size (<0 or >128 MB): "); 65 | s += std::to_string(length); 66 | throw std::runtime_error(s.c_str()); 67 | } 68 | if (length == 0) { 69 | return false; 70 | } 71 | string s(length, '\0'); 72 | readAll(fd, &s[0], length, false); 73 | *packet = Packet(s); 74 | return true; 75 | } 76 | 77 | inline void writePacket(int fd, const Packet& packet) { 78 | string s = packet.serialize(); 79 | int64_t length = s.length(); 80 | if (length < 0 || length > 128 * 1024 * 1024) { 81 | STFATAL << "Invalid message length: " << length; 82 | } 83 | writeAllOrThrow(fd, (const char*)&length, sizeof(int64_t), false); 84 | if (length) { 85 | writeAllOrThrow(fd, &s[0], length, false); 86 | } 87 | } 88 | 89 | void writeB64(int fd, const char* buf, size_t count); 90 | void readB64(int fd, char* buf, size_t count); 91 | void readB64EncodedLength(int fd, string* out, size_t encodedLength); 92 | 93 | virtual int connect(const SocketEndpoint& endpoint) = 0; 94 | virtual set listen(const SocketEndpoint& endpoint) = 0; 95 | virtual set getEndpointFds(const SocketEndpoint& endpoint) = 0; 96 | virtual int accept(int fd) = 0; 97 | virtual void stopListening(const SocketEndpoint& endpoint) = 0; 98 | virtual void close(int fd) = 0; 99 | virtual vector getActiveSockets() = 0; 100 | 101 | protected: 102 | }; 103 | } // namespace et 104 | 105 | #endif // __ET_SOCKET_HANDLER__ 106 | -------------------------------------------------------------------------------- /src/base/SubprocessToString.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_SUBPROCESS_TO_STRING__ 2 | #define __ET_SUBPROCESS_TO_STRING__ 3 | 4 | #include "Headers.hpp" 5 | 6 | namespace et { 7 | inline std::string SystemToStr(const char* cmd) { 8 | std::array buffer; 9 | std::string result; 10 | std::shared_ptr pipe(popen(cmd, "r"), pclose); 11 | if (!pipe) throw std::runtime_error("popen() failed!"); 12 | while (!feof(pipe.get())) { 13 | if (fgets(buffer.data(), 128, pipe.get()) != nullptr) 14 | result += buffer.data(); 15 | } 16 | return result; 17 | } 18 | 19 | string SubprocessToStringInteractive(const string& command, 20 | const vector& args); 21 | } // namespace et 22 | 23 | #endif // __ET_SUBPROCESS_TO_STRING__ 24 | -------------------------------------------------------------------------------- /src/base/TcpSocketHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_TCP_SOCKET_HANDLER__ 2 | #define __ET_TCP_SOCKET_HANDLER__ 3 | 4 | #include "UnixSocketHandler.hpp" 5 | 6 | namespace et { 7 | class TcpSocketHandler : public UnixSocketHandler { 8 | public: 9 | TcpSocketHandler(); 10 | virtual ~TcpSocketHandler() {} 11 | 12 | virtual int connect(const SocketEndpoint& endpoint); 13 | virtual set listen(const SocketEndpoint& endpoint); 14 | virtual set getEndpointFds(const SocketEndpoint& endpoint); 15 | virtual void stopListening(const SocketEndpoint& endpoint); 16 | 17 | protected: 18 | map> portServerSockets; 19 | 20 | virtual void initSocket(int fd); 21 | }; 22 | } // namespace et 23 | 24 | #endif // __ET_TCP_SOCKET_HANDLER__ 25 | -------------------------------------------------------------------------------- /src/base/TunnelUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "TunnelUtils.hpp" 2 | 3 | namespace et { 4 | vector parseRangesToRequests(const string& input) { 5 | vector pfsrs; 6 | auto j = split(input, ','); 7 | for (auto& pair : j) { 8 | vector sourceDestination = split(pair, ':'); 9 | if (sourceDestination.size() < 2) { 10 | throw TunnelParseException( 11 | "Tunnel argument must have source and destination between a ':'"); 12 | } 13 | try { 14 | if (sourceDestination[0].find_first_not_of("0123456789-") != 15 | string::npos && 16 | sourceDestination[1].find_first_not_of("0123456789-") != 17 | string::npos) { 18 | PortForwardSourceRequest pfsr; 19 | pfsr.set_environmentvariable(sourceDestination[0]); 20 | pfsr.mutable_destination()->set_name(sourceDestination[1]); 21 | pfsrs.push_back(pfsr); 22 | } else if (sourceDestination[0].find('-') != string::npos && 23 | sourceDestination[1].find('-') != string::npos) { 24 | vector sourcePortRange = split(sourceDestination[0], '-'); 25 | int sourcePortStart = stoi(sourcePortRange[0]); 26 | int sourcePortEnd = stoi(sourcePortRange[1]); 27 | 28 | vector destinationPortRange = split(sourceDestination[1], '-'); 29 | int destinationPortStart = stoi(destinationPortRange[0]); 30 | int destinationPortEnd = stoi(destinationPortRange[1]); 31 | 32 | if (sourcePortEnd - sourcePortStart != 33 | destinationPortEnd - destinationPortStart) { 34 | throw TunnelParseException( 35 | "source/destination port range must have same length"); 36 | } else { 37 | int portRangeLength = sourcePortEnd - sourcePortStart + 1; 38 | for (int i = 0; i < portRangeLength; ++i) { 39 | PortForwardSourceRequest pfsr; 40 | pfsr.mutable_source()->set_name("localhost"); 41 | pfsr.mutable_source()->set_port(sourcePortStart + i); 42 | pfsr.mutable_destination()->set_port(destinationPortStart + i); 43 | pfsrs.push_back(pfsr); 44 | } 45 | } 46 | } else if (sourceDestination[0].find('-') != string::npos || 47 | sourceDestination[1].find('-') != string::npos) { 48 | throw TunnelParseException( 49 | "Invalid port range syntax: if source is a range, " 50 | "destination must be a range (and vice versa)"); 51 | } else { 52 | PortForwardSourceRequest pfsr; 53 | pfsr.mutable_source()->set_name("localhost"); 54 | pfsr.mutable_source()->set_port(stoi(sourceDestination[0])); 55 | pfsr.mutable_destination()->set_port(stoi(sourceDestination[1])); 56 | pfsrs.push_back(pfsr); 57 | } 58 | } catch (const TunnelParseException& e) { 59 | throw e; 60 | } catch (const std::logic_error& lr) { 61 | throw TunnelParseException("Invalid tunnel argument '" + input + 62 | "': " + lr.what()); 63 | } 64 | } 65 | return pfsrs; 66 | } 67 | 68 | } // namespace et 69 | -------------------------------------------------------------------------------- /src/base/TunnelUtils.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_TUNNEL_UTILS__ 2 | #define __ET_TUNNEL_UTILS__ 3 | 4 | #include "Headers.hpp" 5 | 6 | namespace et { 7 | 8 | vector parseRangesToRequests(const string& input); 9 | 10 | class TunnelParseException : public std::exception { 11 | public: 12 | TunnelParseException(const string& msg) : message(msg) {} 13 | const char* what() const noexcept override { return message.c_str(); } 14 | 15 | private: 16 | std::string message = " "; 17 | }; 18 | 19 | } // namespace et 20 | #endif // __ET_TUNNEL_UTILS__ 21 | -------------------------------------------------------------------------------- /src/base/UnixSocketHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_UNIX_SOCKET_HANDLER__ 2 | #define __ET_UNIX_SOCKET_HANDLER__ 3 | 4 | #include "SocketHandler.hpp" 5 | 6 | namespace et { 7 | class UnixSocketHandler : public SocketHandler { 8 | public: 9 | UnixSocketHandler(); 10 | virtual ~UnixSocketHandler() {} 11 | 12 | virtual bool waitForData(int fd, int64_t sec, int64_t usec); 13 | virtual bool hasData(int fd); 14 | virtual ssize_t read(int fd, void* buf, size_t count); 15 | virtual ssize_t write(int fd, const void* buf, size_t count); 16 | virtual int accept(int fd); 17 | virtual void close(int fd); 18 | virtual vector getActiveSockets(); 19 | 20 | protected: 21 | void addToActiveSockets(int fd); 22 | virtual void initSocket(int fd); 23 | virtual void initServerSocket(int fd); 24 | void setBlocking(int sockFd, bool blocking); 25 | 26 | map> activeSocketMutexes; 27 | recursive_mutex globalMutex; 28 | }; 29 | } // namespace et 30 | 31 | #endif // __ET_UNIX_SOCKET_HANDLER__ 32 | -------------------------------------------------------------------------------- /src/base/WinsockContext.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_WINSOCK_CONTEXT__ 2 | #define __ET_WINSOCK_CONTEXT__ 3 | 4 | #include "Headers.hpp" 5 | 6 | namespace et { 7 | class WinsockContext { 8 | public: 9 | WinsockContext() { 10 | #ifdef WIN32 11 | WORD wVersionRequested; 12 | WSADATA wsaData; 13 | int err; 14 | 15 | /* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */ 16 | wVersionRequested = MAKEWORD(2, 2); 17 | 18 | err = WSAStartup(wVersionRequested, &wsaData); 19 | if (err != 0) { 20 | /* Tell the user that we could not find a usable */ 21 | /* Winsock DLL. */ 22 | printf("WSAStartup failed with error: %d\n", err); 23 | exit(1); 24 | } 25 | 26 | /* Confirm that the WinSock DLL supports 2.2.*/ 27 | /* Note that if the DLL supports versions greater */ 28 | /* than 2.2 in addition to 2.2, it will still return */ 29 | /* 2.2 in wVersion since that is the version we */ 30 | /* requested. */ 31 | 32 | if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { 33 | /* Tell the user that we could not find a usable */ 34 | /* WinSock DLL. */ 35 | printf("Could not find a usable version of Winsock.dll\n"); 36 | WSACleanup(); 37 | exit(1); 38 | } 39 | #endif 40 | } 41 | 42 | ~WinsockContext() {} 43 | }; 44 | } // namespace et 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /src/htm/HtmClient.cpp: -------------------------------------------------------------------------------- 1 | #include "HtmClient.hpp" 2 | 3 | #include "HtmServer.hpp" 4 | #include "IpcPairClient.hpp" 5 | #include "LogHandler.hpp" 6 | #include "MultiplexerState.hpp" 7 | #include "RawSocketUtils.hpp" 8 | 9 | namespace et { 10 | HtmClient::HtmClient(shared_ptr _socketHandler, 11 | const SocketEndpoint& endpoint) 12 | : IpcPairClient(_socketHandler, endpoint) {} 13 | 14 | void HtmClient::run() { 15 | const int BUF_SIZE = 1024; 16 | char buf[BUF_SIZE]; 17 | while (true) { 18 | // Data structures needed for select() and 19 | // non-blocking I/O. 20 | fd_set rfd; 21 | timeval tv; 22 | 23 | FD_ZERO(&rfd); 24 | FD_SET(endpointFd, &rfd); 25 | FD_SET(STDIN_FILENO, &rfd); 26 | tv.tv_sec = 0; 27 | tv.tv_usec = 10000; 28 | select(max(STDIN_FILENO, endpointFd) + 1, &rfd, NULL, NULL, &tv); 29 | 30 | if (FD_ISSET(STDIN_FILENO, &rfd)) { 31 | VLOG(1) << "STDIN -> " << endpointFd; 32 | int rc = ::read(STDIN_FILENO, buf, BUF_SIZE); 33 | if (rc < 0) { 34 | throw std::runtime_error("Cannot read from raw socket"); 35 | } 36 | if (rc == 0) { 37 | throw std::runtime_error("stdin has closed abruptly."); 38 | } 39 | socketHandler->writeAllOrThrow(endpointFd, buf, rc, false); 40 | } 41 | 42 | if (FD_ISSET(endpointFd, &rfd)) { 43 | int rc = socketHandler->read(endpointFd, buf, BUF_SIZE); 44 | VLOG(1) << endpointFd << " -> STDOUT (" << rc << ")"; 45 | if (rc < 0) { 46 | throw std::runtime_error("Cannot read from raw socket"); 47 | } 48 | if (rc == 0) { 49 | LOG(INFO) << "htmd has closed"; 50 | endpointFd = -1; 51 | return; 52 | } 53 | 54 | // HACK: In the future we should use heartbeats to detect a dead server. 55 | // For now, just listen for session end. 56 | if (rc == 1 && buf[0] == SESSION_END) { 57 | LOG(INFO) << "htmd has closed"; 58 | endpointFd = -1; 59 | return; 60 | } 61 | 62 | RawSocketUtils::writeAll(STDOUT_FILENO, buf, rc); 63 | } 64 | } 65 | } 66 | } // namespace et 67 | -------------------------------------------------------------------------------- /src/htm/HtmClient.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __HTM_CLIENT_H__ 2 | #define __HTM_CLIENT_H__ 3 | 4 | #include "Headers.hpp" 5 | #include "IpcPairClient.hpp" 6 | 7 | namespace et { 8 | class HtmClient : public IpcPairClient { 9 | public: 10 | HtmClient(shared_ptr _socketHandler, 11 | const SocketEndpoint& endpoint); 12 | void run(); 13 | }; 14 | } // namespace et 15 | 16 | #endif // __HTM_CLIENT_H__ -------------------------------------------------------------------------------- /src/htm/HtmClientMain.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "DaemonCreator.hpp" 4 | #include "HtmClient.hpp" 5 | #include "HtmServer.hpp" 6 | #include "IpcPairClient.hpp" 7 | #include "LogHandler.hpp" 8 | #include "MultiplexerState.hpp" 9 | #include "PipeSocketHandler.hpp" 10 | #include "RawSocketUtils.hpp" 11 | #include "SubprocessToString.hpp" 12 | 13 | using namespace et; 14 | 15 | termios terminal_backup; 16 | 17 | void term(int signum) { 18 | char buf[] = { 19 | 0x1b, 0x5b, '$', '$', '$', 'q', 20 | }; 21 | RawSocketUtils::writeAll(STDOUT_FILENO, buf, sizeof(buf)); 22 | fflush(stdout); 23 | tcsetattr(0, TCSANOW, &terminal_backup); 24 | exit(1); 25 | } 26 | 27 | int main(int argc, char** argv) { 28 | GOOGLE_PROTOBUF_VERIFY_VERSION; 29 | srand(1); 30 | // Parse command line arguments 31 | cxxopts::Options options("htm", "Headless terminal multiplexer"); 32 | options.allow_unrecognised_options(); 33 | 34 | options.add_options() // 35 | ("help", "Print help") // 36 | ("x,kill-other-sessions", 37 | "kill all old sessions belonging to the user") // 38 | ; 39 | 40 | auto result = options.parse(argc, argv); 41 | if (result.count("help")) { 42 | CLOG(INFO, "stdout") << options.help({}) << endl; 43 | exit(0); 44 | } 45 | 46 | setvbuf(stdin, NULL, _IONBF, 0); // turn off buffering 47 | setvbuf(stdout, NULL, _IONBF, 0); // turn off buffering 48 | 49 | // Turn on raw terminal mode 50 | termios terminal_local; 51 | tcgetattr(0, &terminal_local); 52 | memcpy(&terminal_backup, &terminal_local, sizeof(struct termios)); 53 | cfmakeraw(&terminal_local); 54 | tcsetattr(0, TCSANOW, &terminal_local); 55 | 56 | // Catch sigterm and send exit control code 57 | struct sigaction action; 58 | memset(&action, 0, sizeof(struct sigaction)); 59 | action.sa_handler = term; 60 | sigaction(SIGTERM, &action, NULL); 61 | 62 | // Setup easylogging configurations 63 | el::Configurations defaultConf = LogHandler::setupLogHandler(&argc, &argv); 64 | el::Loggers::setVerboseLevel(3); 65 | LogHandler::setupLogFiles(&defaultConf, GetTempDirectory(), "htm", false, 66 | true); 67 | 68 | // Reconfigure default logger to apply settings above 69 | el::Loggers::reconfigureLogger("default", defaultConf); 70 | 71 | et::HandleTerminate(); 72 | 73 | // Override easylogging handler for sigint 74 | ::signal(SIGINT, et::InterruptSignalHandler); 75 | 76 | uid_t myuid = getuid(); 77 | if (result.count("x")) { 78 | LOG(INFO) << "Killing previous htmd"; 79 | // Kill previous htm daemon 80 | string command = 81 | string("pkill -x -U ") + to_string(myuid) + string(" htmd"); 82 | system(command.c_str()); 83 | } 84 | 85 | // Check if daemon exists 86 | string command = string("pgrep -x -U ") + to_string(myuid) + string(" htmd"); 87 | string pgrepOutput = SystemToStr(command.c_str()); 88 | 89 | if (pgrepOutput.length() == 0) { 90 | // Fork to create the daemon 91 | int result = DaemonCreator::create(false, ""); 92 | if (result == DaemonCreator::CHILD) { 93 | // This means we are the daemon 94 | exit(system("htmd")); 95 | } 96 | } 97 | 98 | // This means we are the client to the daemon 99 | std::this_thread::sleep_for(std::chrono::microseconds( 100 | 10 * 1000)); // Sleep for 10ms to let the daemon come alive 101 | shared_ptr socketHandler(new PipeSocketHandler()); 102 | SocketEndpoint pipeEndpoint; 103 | pipeEndpoint.set_name(HtmServer::getPipeName()); 104 | HtmClient htmClient(socketHandler, pipeEndpoint); 105 | htmClient.run(); 106 | 107 | char buf[] = { 108 | 0x1b, 0x5b, '$', '$', '$', 'q', 109 | }; 110 | RawSocketUtils::writeAll(STDOUT_FILENO, buf, sizeof(buf)); 111 | fflush(stdout); 112 | tcsetattr(0, TCSANOW, &terminal_backup); 113 | 114 | return 0; 115 | } 116 | -------------------------------------------------------------------------------- /src/htm/HtmHeaderCodes.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __HTM_HEADER_CODES_H__ 2 | #define __HTM_HEADER_CODES_H__ 3 | 4 | const char INSERT_KEYS = '1'; 5 | const char INIT_STATE = '2'; 6 | const char CLIENT_CLOSE_PANE = '3'; 7 | const char APPEND_TO_PANE = '4'; 8 | const char NEW_TAB = '5'; 9 | const char SERVER_CLOSE_PANE = '8'; 10 | const char NEW_SPLIT = '9'; 11 | const char RESIZE_PANE = 'A'; 12 | const char DEBUG_LOG = 'B'; 13 | const char INSERT_DEBUG_KEYS = 'C'; 14 | const char SESSION_END = 'D'; 15 | 16 | #endif -------------------------------------------------------------------------------- /src/htm/HtmServer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __HTM_SERVER_H__ 2 | #define __HTM_SERVER_H__ 3 | 4 | #include "Headers.hpp" 5 | #include "IpcPairServer.hpp" 6 | #include "MultiplexerState.hpp" 7 | 8 | namespace et { 9 | class HtmServer : public IpcPairServer { 10 | public: 11 | HtmServer(shared_ptr _socketHandler, 12 | const SocketEndpoint& endpoint); 13 | void run(); 14 | static string getPipeName(); 15 | virtual void recover(); 16 | void sendDebug(const string& msg); 17 | 18 | protected: 19 | MultiplexerState state; 20 | bool running; 21 | }; 22 | } // namespace et 23 | 24 | #endif // __HTM_SERVER_H__ -------------------------------------------------------------------------------- /src/htm/HtmServerMain.cpp: -------------------------------------------------------------------------------- 1 | #include "HtmServer.hpp" 2 | #include "LogHandler.hpp" 3 | #include "MultiplexerState.hpp" 4 | #include "PipeSocketHandler.hpp" 5 | 6 | using namespace et; 7 | 8 | int main(int argc, char **argv) { 9 | // Version string need to be set before GFLAGS parse arguments 10 | GOOGLE_PROTOBUF_VERIFY_VERSION; 11 | srand(1); 12 | 13 | // Setup easylogging configurations 14 | el::Configurations defaultConf = 15 | et::LogHandler::setupLogHandler(&argc, &argv); 16 | el::Loggers::setVerboseLevel(3); 17 | LogHandler::setupLogFiles(&defaultConf, GetTempDirectory(), "htmd", false, 18 | true); 19 | 20 | // Reconfigure default logger to apply settings above 21 | el::Loggers::reconfigureLogger("default", defaultConf); 22 | 23 | et::HandleTerminate(); 24 | 25 | // Override easylogging handler for sigint 26 | ::signal(SIGINT, et::InterruptSignalHandler); 27 | 28 | shared_ptr socketHandler(new PipeSocketHandler()); 29 | SocketEndpoint endpoint; 30 | endpoint.set_name(HtmServer::getPipeName()); 31 | HtmServer htm(socketHandler, endpoint); 32 | htm.run(); 33 | LOG(INFO) << "Server is shutting down"; 34 | 35 | return 0; 36 | } 37 | -------------------------------------------------------------------------------- /src/htm/IpcPairClient.cpp: -------------------------------------------------------------------------------- 1 | #include "IpcPairClient.hpp" 2 | 3 | namespace et { 4 | IpcPairClient::IpcPairClient(shared_ptr _socketHandler, 5 | const SocketEndpoint& endpoint) 6 | : IpcPairEndpoint(_socketHandler, -1) { 7 | for (int retry = 0; retry < 5; retry++) { 8 | endpointFd = socketHandler->connect(endpoint); 9 | if (endpointFd < 0) { 10 | std::this_thread::sleep_for(std::chrono::seconds(1)); 11 | } else { 12 | return; 13 | } 14 | } 15 | throw std::runtime_error("Connect to IPC failed"); 16 | } 17 | 18 | } // namespace et -------------------------------------------------------------------------------- /src/htm/IpcPairClient.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __IPC_PAIR_CLIENT_H__ 2 | #define __IPC_PAIR_CLIENT_H__ 3 | 4 | #include "Headers.hpp" 5 | #include "IpcPairEndpoint.hpp" 6 | #include "SocketHandler.hpp" 7 | 8 | namespace et { 9 | class IpcPairClient : public IpcPairEndpoint { 10 | public: 11 | IpcPairClient(shared_ptr _socketHandler, 12 | const SocketEndpoint& endpoint); 13 | virtual ~IpcPairClient() {} 14 | 15 | protected: 16 | }; 17 | } // namespace et 18 | 19 | #endif // __IPC_PAIR_CLIENT_H__ -------------------------------------------------------------------------------- /src/htm/IpcPairEndpoint.cpp: -------------------------------------------------------------------------------- 1 | #include "IpcPairEndpoint.hpp" 2 | 3 | namespace et { 4 | IpcPairEndpoint::IpcPairEndpoint(shared_ptr _socketHandler, 5 | int _endpointFd) 6 | : socketHandler(_socketHandler), endpointFd(_endpointFd) {} 7 | 8 | IpcPairEndpoint::~IpcPairEndpoint() { 9 | if (endpointFd >= 0) { 10 | closeEndpoint(); 11 | } 12 | } 13 | 14 | } // namespace et -------------------------------------------------------------------------------- /src/htm/IpcPairEndpoint.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __IPC_PAIR_ENDPOINT_H__ 2 | #define __IPC_PAIR_ENDPOINT_H__ 3 | 4 | #include "Headers.hpp" 5 | #include "HtmHeaderCodes.hpp" 6 | #include "SocketHandler.hpp" 7 | 8 | namespace et { 9 | class IpcPairEndpoint { 10 | public: 11 | IpcPairEndpoint(shared_ptr _socketHandler, int _endpointFd); 12 | virtual ~IpcPairEndpoint(); 13 | inline int getEndpointFd() { return endpointFd; } 14 | virtual void closeEndpoint() { 15 | LOG(INFO) << "SENDING SESSION END"; 16 | unsigned char header = SESSION_END; 17 | socketHandler->writeAllOrThrow(endpointFd, (const char *)&header, 1, false); 18 | socketHandler->close(endpointFd); 19 | endpointFd = -1; 20 | } 21 | 22 | protected: 23 | shared_ptr socketHandler; 24 | int endpointFd; 25 | }; 26 | } // namespace et 27 | 28 | #endif // __IPC_PAIR_SERVER_H__ 29 | -------------------------------------------------------------------------------- /src/htm/IpcPairServer.cpp: -------------------------------------------------------------------------------- 1 | #include "IpcPairServer.hpp" 2 | 3 | namespace et { 4 | IpcPairServer::IpcPairServer(shared_ptr _socketHandler, 5 | const SocketEndpoint &_endpoint) 6 | : IpcPairEndpoint(_socketHandler, -1), endpoint(_endpoint) { 7 | serverFd = *(socketHandler->listen(endpoint).begin()); 8 | } 9 | 10 | IpcPairServer::~IpcPairServer() { ::close(serverFd); } 11 | 12 | void IpcPairServer::pollAccept() { 13 | int fd = socketHandler->accept(serverFd); 14 | if (fd < 0) { 15 | // Nothing to accept 16 | return; 17 | } 18 | 19 | if (endpointFd >= 0) { 20 | // Need to disconnect the current client 21 | closeEndpoint(); 22 | } 23 | 24 | endpointFd = fd; 25 | recover(); 26 | } 27 | } // namespace et 28 | -------------------------------------------------------------------------------- /src/htm/IpcPairServer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __IPC_PAIR_SERVER_H__ 2 | #define __IPC_PAIR_SERVER_H__ 3 | 4 | #include "Headers.hpp" 5 | #include "IpcPairEndpoint.hpp" 6 | 7 | namespace et { 8 | class IpcPairServer : public IpcPairEndpoint { 9 | public: 10 | IpcPairServer(shared_ptr _socketHandler, 11 | const SocketEndpoint &_endpoint); 12 | virtual ~IpcPairServer(); 13 | virtual void pollAccept(); 14 | inline int getServerFd() { return serverFd; } 15 | 16 | virtual void recover() = 0; 17 | 18 | protected: 19 | int serverFd; 20 | SocketEndpoint endpoint; 21 | }; 22 | } // namespace et 23 | 24 | #endif // __IPC_PAIR_SERVER_H__ -------------------------------------------------------------------------------- /src/htm/MultiplexerState.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __MULTIPLEXER_STATE_HPP__ 2 | #define __MULTIPLEXER_STATE_HPP__ 3 | 4 | #include "Headers.hpp" 5 | #include "SocketHandler.hpp" 6 | #include "TerminalHandler.hpp" 7 | 8 | namespace et { 9 | const int UUID_LENGTH = 36; 10 | 11 | class MultiplexerState { 12 | protected: 13 | struct Pane; 14 | struct Split; 15 | struct Tab; 16 | 17 | public: 18 | MultiplexerState(shared_ptr _socketHandler); 19 | string toJsonString(); 20 | void appendData(const string& uid, const string& data); 21 | void newTab(const string& tabId, const string& paneId); 22 | void newSplit(const string& sourceId, const string& paneId, bool vertical); 23 | void closePane(const string& paneId); 24 | void update(int endpointFd); 25 | void sendTerminalBuffers(int endpointFd); 26 | void resizePane(const string& paneId, int cols, int rows); 27 | inline int numPanes() { return int(panes.size()); } 28 | 29 | protected: 30 | shared_ptr socketHandler; 31 | map> tabs; 32 | map> panes; 33 | map> splits; 34 | set closed; 35 | 36 | inline shared_ptr getTab(const string& id) { 37 | auto it = tabs.find(id); 38 | if (it == tabs.end()) { 39 | STFATAL << "Tried to get a pane that doesn't exist: " << id; 40 | } 41 | return it->second; 42 | } 43 | inline shared_ptr getPane(const string& id) { 44 | auto it = panes.find(id); 45 | if (it == panes.end()) { 46 | STFATAL << "Tried to get a pane that doesn't exist: " << id; 47 | } 48 | return it->second; 49 | } 50 | inline shared_ptr getSplit(const string& id) { 51 | auto it = splits.find(id); 52 | if (it == splits.end()) { 53 | STFATAL << "Tried to get a pane that doesn't exist: " << id; 54 | } 55 | return it->second; 56 | } 57 | 58 | void fatalIfFound(const string& id); 59 | }; 60 | } // namespace et 61 | 62 | #endif // __MULTIPLEXER_STATE_HPP__ 63 | -------------------------------------------------------------------------------- /src/htm/TerminalHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "TerminalHandler.hpp" 2 | 3 | #include "ETerminal.pb.h" 4 | #include "RawSocketUtils.hpp" 5 | #include "ServerConnection.hpp" 6 | #include "UserTerminalRouter.hpp" 7 | 8 | namespace et { 9 | TerminalHandler::TerminalHandler() : run(true), bufferLength(0) {} 10 | 11 | void TerminalHandler::start() { 12 | pid_t pid = forkpty(&masterFd, NULL, NULL, NULL); 13 | switch (pid) { 14 | case -1: 15 | FATAL_FAIL(pid); 16 | case 0: { 17 | passwd* pwd = getpwuid(getuid()); 18 | if (pwd == NULL) { 19 | LOG(FATAL) 20 | << "Not able to fork a terminal because getpwuid returns null"; 21 | } 22 | chdir(pwd->pw_dir); 23 | string terminal = string(::getenv("SHELL")); 24 | setenv("HTM_VERSION", ET_VERSION, 1); 25 | execl(terminal.c_str(), terminal.c_str(), "-l", NULL); 26 | exit(0); 27 | break; 28 | } 29 | default: { 30 | // parent 31 | VLOG(1) << "pty opened " << masterFd << endl; 32 | childPid = pid; 33 | #ifdef WITH_UTEMPTER 34 | { 35 | char buf[1024]; 36 | sprintf(buf, "htm [%lld]", (long long)getpid()); 37 | utempter_add_record(masterFd, buf); 38 | } 39 | #endif 40 | break; 41 | } 42 | } 43 | } 44 | 45 | #define MAX_BUFFER_LINES (1024) 46 | #define MAX_BUFFER_CHARS (128 * MAX_BUFFER_LINES) 47 | 48 | string TerminalHandler::pollUserTerminal() { 49 | if (!run) { 50 | return string(); 51 | } 52 | 53 | #define BUF_SIZE (16 * 1024) 54 | char b[BUF_SIZE]; 55 | 56 | // Data structures needed for select() and 57 | // non-blocking I/O. 58 | fd_set rfd; 59 | timeval tv; 60 | 61 | FD_ZERO(&rfd); 62 | FD_SET(masterFd, &rfd); 63 | tv.tv_sec = 0; 64 | tv.tv_usec = 10000; 65 | select(masterFd + 1, &rfd, NULL, NULL, &tv); 66 | 67 | try { 68 | // Check for data to receive; the received 69 | // data includes also the data previously sent 70 | // on the same master descriptor (line 90). 71 | if (FD_ISSET(masterFd, &rfd)) { 72 | // Read from terminal and write to client 73 | memset(b, 0, BUF_SIZE); 74 | int rc = read(masterFd, b, BUF_SIZE); 75 | if (rc < 0) { 76 | // Terminal failed for some reason, bail. 77 | throw std::runtime_error("Terminal Failure"); 78 | } 79 | if (rc > 0) { 80 | string newChars(b, rc); 81 | vector tokens = split(newChars, '\n'); 82 | for (auto& it : tokens) { 83 | bufferLength += it.length(); 84 | } 85 | if (buffer.empty()) { 86 | buffer.insert(buffer.end(), tokens.begin(), tokens.end()); 87 | } else { 88 | buffer.back().append(tokens.front()); 89 | if (tokens.size() > 1) { 90 | buffer.insert(buffer.end(), tokens.begin() + 1, tokens.end()); 91 | } 92 | } 93 | if (buffer.size() > MAX_BUFFER_LINES) { 94 | int amountToErase = buffer.size() - MAX_BUFFER_LINES; 95 | for (auto it = buffer.begin(); 96 | it != buffer.end() && it != (buffer.begin() + amountToErase); 97 | it++) { 98 | bufferLength -= it->length(); 99 | } 100 | buffer.erase(buffer.begin(), buffer.begin() + amountToErase); 101 | } 102 | while (bufferLength > MAX_BUFFER_CHARS) { 103 | bufferLength -= buffer.begin()->length(); 104 | buffer.pop_front(); 105 | } 106 | LOG(INFO) << "BUFFER LINES: " << buffer.size() << " " << tokens.size() 107 | << endl; 108 | return newChars; 109 | } else { 110 | LOG(INFO) << "Terminal session ended"; 111 | #if __NetBSD__ 112 | // this unfortunateness seems to be fixed in NetBSD-8 (or at 113 | // least -CURRENT) sadness for now :/ 114 | int throwaway; 115 | FATAL_FAIL(waitpid(childPid, &throwaway, WUNTRACED)); 116 | #else 117 | siginfo_t childInfo; 118 | int rc = waitid(P_PID, childPid, &childInfo, WEXITED); 119 | if (rc < 0 && GetErrno() != ECHILD) { 120 | FATAL_FAIL(rc); 121 | } 122 | #endif 123 | run = false; 124 | #ifdef WITH_UTEMPTER 125 | utempter_remove_record(masterFd); 126 | #endif 127 | return string(); 128 | } 129 | } 130 | } catch (const std::exception& ex) { 131 | LOG(INFO) << ex.what(); 132 | run = false; 133 | #ifdef WITH_UTEMPTER 134 | utempter_remove_record(masterFd); 135 | #endif 136 | } 137 | 138 | return string(); 139 | } 140 | 141 | void TerminalHandler::appendData(const string& data) { 142 | RawSocketUtils::writeAll(masterFd, &data[0], data.length()); 143 | } 144 | 145 | void TerminalHandler::updateTerminalSize(int col, int row) { 146 | winsize tmpwin; 147 | tmpwin.ws_row = row; 148 | tmpwin.ws_col = col; 149 | tmpwin.ws_xpixel = 0; 150 | tmpwin.ws_ypixel = 0; 151 | ioctl(masterFd, TIOCSWINSZ, &tmpwin); 152 | } 153 | 154 | void TerminalHandler::stop() { 155 | kill(childPid, SIGKILL); 156 | run = false; 157 | } 158 | 159 | } // namespace et 160 | -------------------------------------------------------------------------------- /src/htm/TerminalHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __HTM_TERMINAL_HANDLER__ 2 | #define __HTM_TERMINAL_HANDLER__ 3 | 4 | #include "Headers.hpp" 5 | 6 | namespace et { 7 | class TerminalHandler { 8 | public: 9 | TerminalHandler(); 10 | void start(); 11 | string pollUserTerminal(); 12 | void updateTerminalSize(int col, int row); 13 | void appendData(const string &data); 14 | inline bool isRunning() { return run; } 15 | void stop(); 16 | const deque &getBuffer() { return buffer; } 17 | 18 | protected: 19 | int masterFd; 20 | int childPid; 21 | bool run; 22 | deque buffer; 23 | int64_t bufferLength; 24 | }; 25 | } // namespace et 26 | 27 | #endif // __HTM_TERMINAL_HANDLER__ 28 | -------------------------------------------------------------------------------- /src/terminal/Console.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __CONSOLE_HPP__ 2 | #define __CONSOLE_HPP__ 3 | 4 | #include "ETerminal.pb.h" 5 | #include "Headers.hpp" 6 | #include "RawSocketUtils.hpp" 7 | 8 | namespace et { 9 | class Console { 10 | public: 11 | virtual TerminalInfo getTerminalInfo() = 0; 12 | virtual void setup() = 0; 13 | virtual void teardown() = 0; 14 | virtual int getFd() = 0; 15 | 16 | virtual void write(const string& s) { 17 | #ifdef WIN32 18 | std::wstring_convert> converter; 19 | std::wstring wide = converter.from_bytes(s); 20 | 21 | auto hstdout = GetStdHandle(STD_OUTPUT_HANDLE); 22 | DWORD numWritten; 23 | WriteConsole(hstdout, wide.c_str(), wide.length(), &numWritten, NULL); 24 | #else 25 | RawSocketUtils::writeAll(getFd(), &s[0], s.length()); 26 | #endif 27 | } 28 | }; 29 | } // namespace et 30 | 31 | #endif -------------------------------------------------------------------------------- /src/terminal/ProcessHelper.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_PROCESS_HELPER__ 2 | #define __ET_PROCESS_HELPER__ 3 | 4 | #include "Headers.hpp" 5 | 6 | class ProcessHelper { 7 | public: 8 | static void daemonize(); 9 | }; 10 | 11 | #endif // __ET_PROCESS_HELPER__ 12 | -------------------------------------------------------------------------------- /src/terminal/PsuedoTerminalConsole.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __PSUEDO_TERMINAL_CONSOLE_HPP__ 2 | #define __PSUEDO_TERMINAL_CONSOLE_HPP__ 3 | 4 | #include "Console.hpp" 5 | #include "ETerminal.pb.h" 6 | #include "RawSocketUtils.hpp" 7 | 8 | namespace et { 9 | class PsuedoTerminalConsole : public Console { 10 | public: 11 | PsuedoTerminalConsole() { 12 | #ifdef WIN32 13 | auto hstdin = GetStdHandle(STD_INPUT_HANDLE); 14 | GetConsoleMode(hstdin, &inputMode); 15 | auto hstdout = GetStdHandle(STD_OUTPUT_HANDLE); 16 | GetConsoleMode(hstdout, &outputMode); 17 | #else 18 | termios terminal_local; 19 | tcgetattr(0, &terminal_local); 20 | memcpy(&terminal_backup, &terminal_local, sizeof(struct termios)); 21 | #endif 22 | } 23 | 24 | virtual ~PsuedoTerminalConsole() {} 25 | 26 | virtual void setup() { 27 | #ifdef WIN32 28 | auto hstdin = GetStdHandle(STD_INPUT_HANDLE); 29 | SetConsoleMode(hstdin, ENABLE_VIRTUAL_TERMINAL_INPUT); 30 | // auto hstdout = GetStdHandle(STD_OUTPUT_HANDLE); 31 | // SetConsoleMode(hstdout, 0/*ENABLE_VIRTUAL_TERMINAL_PROCESSING*/); 32 | #else 33 | termios terminal_local; 34 | tcgetattr(0, &terminal_local); 35 | memcpy(&terminal_backup, &terminal_local, sizeof(struct termios)); 36 | cfmakeraw(&terminal_local); 37 | tcsetattr(0, TCSANOW, &terminal_local); 38 | #endif 39 | } 40 | 41 | virtual void teardown() { 42 | #ifdef WIN32 43 | auto hstdin = GetStdHandle(STD_INPUT_HANDLE); 44 | SetConsoleMode(hstdin, inputMode); 45 | auto hstdout = GetStdHandle(STD_OUTPUT_HANDLE); 46 | SetConsoleMode(hstdout, outputMode); 47 | #else 48 | tcsetattr(0, TCSANOW, &terminal_backup); 49 | #endif 50 | } 51 | 52 | virtual TerminalInfo getTerminalInfo() { 53 | #ifdef WIN32 54 | CONSOLE_SCREEN_BUFFER_INFO csbi; 55 | int columns, rows; 56 | 57 | if (!GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi)) { 58 | STFATAL << "Error getting console info: " << GetLastError(); 59 | } 60 | TerminalInfo ti; 61 | ti.set_column(csbi.srWindow.Right - csbi.srWindow.Left + 1); 62 | ti.set_row(csbi.srWindow.Bottom - csbi.srWindow.Top + 1); 63 | 64 | /* TODO: Find out why this does not work 65 | HWND myconsole = GetConsoleWindow(); 66 | HDC mydc = GetDC(myconsole); 67 | RECT rect; 68 | GetClientRect(myconsole, &rect); 69 | ti.set_height(rect.bottom - rect.top); 70 | ti.set_width(rect.right - rect.left); 71 | */ 72 | 73 | return ti; 74 | #else 75 | winsize win; 76 | ioctl(1, TIOCGWINSZ, &win); 77 | TerminalInfo ti; 78 | ti.set_row(win.ws_row); 79 | ti.set_column(win.ws_col); 80 | ti.set_width(win.ws_xpixel); 81 | ti.set_height(win.ws_ypixel); 82 | return ti; 83 | #endif 84 | } 85 | 86 | virtual int getFd() { 87 | #ifdef WIN32 88 | return _fileno(stdout); 89 | #else 90 | return STDOUT_FILENO; 91 | #endif 92 | } 93 | 94 | protected: 95 | #ifdef WIN32 96 | DWORD inputMode; 97 | DWORD outputMode; 98 | #else 99 | termios terminal_backup; 100 | #endif 101 | 102 | }; // namespace et 103 | } // namespace et 104 | 105 | #endif 106 | -------------------------------------------------------------------------------- /src/terminal/PsuedoUserTerminal.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __PSUEDO_USER_TERMINAL_HPP__ 2 | #define __PSUEDO_USER_TERMINAL_HPP__ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #if __APPLE__ 9 | #include 10 | #include 11 | #elif __FreeBSD__ 12 | #include 13 | #elif __NetBSD__ // do not need pty.h on NetBSD 14 | #else 15 | #include 16 | #endif 17 | 18 | #ifdef WITH_UTEMPTER 19 | #include 20 | #endif 21 | 22 | #include "UserTerminal.hpp" 23 | 24 | namespace et { 25 | class PsuedoUserTerminal : public UserTerminal { 26 | public: 27 | virtual ~PsuedoUserTerminal() {} 28 | 29 | virtual int setup(int routerFd) { 30 | pid_t pid = forkpty(&masterFd, NULL, NULL, NULL); 31 | switch (pid) { 32 | case -1: 33 | FATAL_FAIL(pid); 34 | break; 35 | case 0: { 36 | close(routerFd); 37 | runTerminal(); 38 | // only get here if execl fails so a break is not needed since we exit 39 | exit(0); 40 | } 41 | default: { 42 | // parent 43 | } 44 | } 45 | 46 | #ifdef WITH_UTEMPTER 47 | { 48 | char buf[1024]; 49 | sprintf(buf, "et [%lld]", (long long)getpid()); 50 | utempter_add_record(masterFd, buf); 51 | } 52 | #endif 53 | return masterFd; 54 | } 55 | 56 | virtual void runTerminal() { 57 | passwd* pwd = getpwuid(getuid()); 58 | chdir(pwd->pw_dir); 59 | string terminal = string(::getenv("SHELL")); 60 | VLOG(1) << "Child process launching terminal " << terminal; 61 | setenv("ET_VERSION", ET_VERSION, 1); 62 | // bash will not reset SIGCHLD to SIG_DFL when run, remembering the current 63 | // SIGCHLD disposition as the "original value" and allowing the user to 64 | // "reset" the signal handler to it's "original value" (trap --help). 65 | // 66 | // If our current SIGCHLD is SIG_IGN then it will be impossible, from 67 | // within bash, to set it to SIG_DFL by issuing "trap -- - SIGCHLD". This 68 | // in turn means that innocent implementations assuming they receive 69 | // SIGCHLD without anything special required on their part, break. 70 | // An example is Python2's popen(), which will fail with 71 | // "IOError: [Errno 10] No child processes". 72 | // 73 | // Such processes *could* help themselves by setting SIGCHLD to SIG_DFL 74 | // from within the process, but this is an esoteric requirement from the 75 | // process and many don't. And as mentioned, the shell user can't help 76 | // with "trap -- - SIGCHLD" either. 77 | // 78 | // Let's help everyone by setting SIGCHLD to SIG_DFL here, right before 79 | // exec'ing the shell. By doing it here, and not somewhere before, we add 80 | // no requirements for any wait(2) on our part. 81 | // 82 | signal(SIGCHLD, SIG_DFL); 83 | FATAL_FAIL(execl(terminal.c_str(), terminal.c_str(), "-l", NULL)); 84 | } 85 | 86 | virtual void cleanup() { 87 | #ifdef WITH_UTEMPTER 88 | utempter_remove_record(masterFd); 89 | #endif 90 | } 91 | 92 | virtual void handleSessionEnd() { 93 | #if __NetBSD__ // this unfortunateness seems to be fixed in NetBSD-8 (or at 94 | // least -CURRENT) sadness for now :/ 95 | int throwaway; 96 | FATAL_FAIL(waitpid(getPid(), &throwaway, WUNTRACED)); 97 | #else 98 | siginfo_t childInfo; 99 | FATAL_FAIL(waitid(P_PID, getPid(), &childInfo, WEXITED)); 100 | #endif 101 | } 102 | 103 | virtual void setInfo(const winsize& tmpwin) { 104 | ioctl(masterFd, TIOCSWINSZ, &tmpwin); 105 | } 106 | 107 | pid_t getPid() { return pid; } 108 | 109 | virtual int getFd() { return masterFd; } 110 | 111 | protected: 112 | pid_t pid; 113 | int masterFd; 114 | }; 115 | } // namespace et 116 | 117 | #endif 118 | -------------------------------------------------------------------------------- /src/terminal/ServerFifoPath.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_SERVER_FIFO_PATH__ 2 | #define __ET_SERVER_FIFO_PATH__ 3 | 4 | #include 5 | 6 | #include "Headers.hpp" 7 | #include "SocketHandler.hpp" 8 | 9 | namespace et { 10 | 11 | /** 12 | * A helper class to handle creating and detecting the server fifo path. 13 | * 14 | * The default fifo path location varies based on which user the etserver 15 | * process is running as, and it may also be overridden from a command line 16 | * flag. 17 | * 18 | * This class aggregates that logic, both on the server and client side. 19 | * 20 | * To use: 21 | * - Create the class, and optionally call \ref setPathOverride. 22 | * - On the creation side, call \ref createDirectoriesIfRequired and \ref 23 | * getPathForCreation. 24 | * - On the client side, call \ref getEndpointForConnect and \ref 25 | * detectAndConnect, which will either connect to the overridden path or try 26 | * both the root location, followed by the non-root location of the fifo to 27 | * connect. Since a broken fifo file can be left behind when the process 28 | * exits, this tries to connect to each pipe in sequence and performs a 29 | * graceful fallback. 30 | * 31 | * For root, the fifo is placed in the root-accessible directory /var/run. 32 | * 33 | * For non-root, this is placed in the user directory, under $HOME/.local/share, 34 | * following the XDG spec. This class contains logic to create the 35 | * $HOME/.local/share directory structure if required. This means that if the 36 | * server runs as a non-root user, it may only be connected by the same user. 37 | */ 38 | class ServerFifoPath { 39 | public: 40 | ServerFifoPath(); 41 | 42 | /** 43 | * Overrides the fifo path to a user-specified location. Note that this 44 | * disables the auto-detection behavior. 45 | * 46 | * @param path User-specified path to the serverfifo. 47 | */ 48 | void setPathOverride(string path); 49 | 50 | /** 51 | * Based on the current uid, create the directory structure required to store 52 | * the fifo once it is created. If XDG_DATA_HOME is not set and the processes 53 | * user cannot access /var/run, this will ensure that $HOME/.local/share 54 | * exists. 55 | */ 56 | void createDirectoriesIfRequired(); 57 | 58 | /** 59 | * Get the computed fifo path to use when creating the fifo. This will return 60 | * the override path, or a location in either /var/run as root or 61 | * $HOME/.local/share otherwise. 62 | */ 63 | string getPathForCreation(); 64 | 65 | /** 66 | * Return an SocketEndpoint or nullopt based on the current configuration, 67 | * which may later be passed to \ref detectAndConnect to connect to the 68 | * relevant endpoint. 69 | */ 70 | optional getEndpointForConnect(); 71 | 72 | /** 73 | * Either connect to the specific router endpoint, if provided, or detect and 74 | * connect to the default root or non-root location of the endpoint. 75 | * 76 | * @return fd of the connected pipe, always valid. Exits internally if the 77 | * pipe cannot be connected. 78 | */ 79 | static int detectAndConnect( 80 | const optional specificRouterEndpoint, 81 | const shared_ptr& socketHandler); 82 | 83 | private: 84 | optional pathOverride; 85 | }; 86 | 87 | } // namespace et 88 | 89 | #endif // __ET_SERVER_FIFO_PATH__ 90 | -------------------------------------------------------------------------------- /src/terminal/SshSetupHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "SshSetupHandler.hpp" 2 | 3 | #include "SubprocessToString.hpp" 4 | 5 | namespace et { 6 | const string SshSetupHandler::ETTERMINAL_BIN = "etterminal"; 7 | 8 | string genCommand(const string& passkey, const string& id, 9 | const string& clientTerm, const string& user, bool kill, 10 | const string& etterminal_path, const string& options) { 11 | string ssh_script_prefix; 12 | string etterminal_bin = etterminal_path.empty() 13 | ? SshSetupHandler::ETTERMINAL_BIN 14 | : etterminal_path; 15 | 16 | string command = "echo '" + id + "/" + passkey + "_" + clientTerm + "' | " + 17 | etterminal_bin + " " + options; 18 | 19 | // Kill old ET sessions of the user 20 | if (kill) { 21 | ssh_script_prefix = 22 | "pkill etterminal -u " + user + "; sleep 0.5; " + ssh_script_prefix; 23 | } 24 | 25 | return ssh_script_prefix + command; 26 | } 27 | 28 | string SshSetupHandler::SetupSsh(const string& user, const string& host, 29 | const string& host_alias, int port, 30 | const string& jumphost, 31 | const string& jServerFifo, bool kill, 32 | int vlevel, const string& cmd_prefix, 33 | const string& serverFifo, 34 | const std::vector& ssh_options) { 35 | string clientTerm("xterm-256color"); 36 | auto envString = getenv("TERM"); 37 | if (envString != NULL) { 38 | // Default to xterm-256color 39 | clientTerm = envString; 40 | } 41 | string passkey = genRandomAlphaNum(32); 42 | string id = genRandomAlphaNum(16); 43 | 44 | id[0] = id[1] = id[2] = 'X'; // For compatibility with old servers that do 45 | // not generate their own keys 46 | 47 | string cmdoptions{"--verbose=" + std::to_string(vlevel)}; 48 | if (!serverFifo.empty()) { 49 | cmdoptions += " --serverfifo=" + serverFifo; 50 | } 51 | 52 | string SSH_SCRIPT_DST = 53 | genCommand(passkey, id, clientTerm, user, kill, cmd_prefix, cmdoptions); 54 | 55 | string SSH_USER_PREFIX = ""; 56 | if (!user.empty()) { 57 | SSH_USER_PREFIX += user + "@"; 58 | } 59 | 60 | std::vector ssh_args; 61 | if (!jumphost.empty()) { 62 | ssh_args = { 63 | "-J", 64 | jumphost, 65 | }; 66 | } 67 | 68 | ssh_args.push_back(SSH_USER_PREFIX + host_alias); 69 | 70 | for (auto& arg : ssh_options) { 71 | ssh_args.push_back("-o" + arg); 72 | } 73 | 74 | ssh_args.push_back(SSH_SCRIPT_DST); 75 | 76 | std::string ssh_concat; 77 | for (const auto& piece : ssh_args) ssh_concat += piece + " "; 78 | VLOG(1) << "Trying ssh with args: " << ssh_concat << endl; 79 | auto sshBuffer = SubprocessToStringInteractive("ssh", ssh_args); 80 | 81 | try { 82 | if (sshBuffer.length() <= 0) { 83 | // Ssh failed 84 | CLOG(INFO, "stdout") 85 | << "Error starting ET process through ssh, please make sure your " 86 | "ssh works first" 87 | << endl; 88 | exit(1); 89 | } 90 | auto passKeyIndex = sshBuffer.find(string("IDPASSKEY:")); 91 | if (passKeyIndex == string::npos) { 92 | // Returned value not contain "IDPASSKEY:" 93 | CLOG(INFO, "stdout") 94 | << "Error in authentication with etserver: " << sshBuffer 95 | << ", please make sure you don't print anything in server's " 96 | ".bashrc/.zshrc" 97 | << endl; 98 | exit(1); 99 | } 100 | auto idpasskey = sshBuffer.substr(passKeyIndex + 10, 16 + 1 + 32); 101 | auto idpasskey_splited = split(idpasskey, '/'); 102 | id = idpasskey_splited[0]; 103 | passkey = idpasskey_splited[1]; 104 | LOG(INFO) << "etserver started"; 105 | } catch (const runtime_error& err) { 106 | CLOG(INFO, "stdout") << "Error initializing connection" << err.what() 107 | << endl; 108 | } 109 | 110 | // start jumpclient daemon on jumphost. 111 | if (!jumphost.empty()) { 112 | /* If jumphost is set, we need to pass dst host and port to jumphost 113 | * and connect to jumphost here */ 114 | string jump_cmdoptions{"--verbose=" + std::to_string(vlevel)}; 115 | if (!jServerFifo.empty()) { 116 | jump_cmdoptions += " --serverfifo=" + jServerFifo; 117 | } 118 | jump_cmdoptions = jump_cmdoptions + " --jump --dsthost=" + host + 119 | " --dstport=" + to_string(port); 120 | string SSH_SCRIPT_JUMP = genCommand(passkey, id, clientTerm, user, kill, 121 | cmd_prefix, jump_cmdoptions); 122 | 123 | string sshLinkBuffer = 124 | SubprocessToStringInteractive("ssh", {jumphost, SSH_SCRIPT_JUMP}); 125 | if (sshLinkBuffer.length() <= 0) { 126 | // At this point "ssh -J jumphost dst" already works. 127 | CLOG(INFO, "stdout") << "etserver jumpclient failed to start" << endl; 128 | exit(1); 129 | } 130 | try { 131 | auto idpasskey = split(sshLinkBuffer, ':')[1]; 132 | idpasskey.erase(idpasskey.find_last_not_of(" \n\r\t") + 1); 133 | idpasskey = idpasskey.substr(0, 16 + 1 + 32); 134 | auto idpasskey_splited = split(idpasskey, '/'); 135 | id = idpasskey_splited[0]; 136 | passkey = idpasskey_splited[1]; 137 | } catch (const runtime_error& err) { 138 | CLOG(INFO, "stdout") << "Error initializing connection" << err.what() 139 | << endl; 140 | } 141 | } 142 | 143 | if (id.length() == 0 || passkey.length() == 0) { 144 | STFATAL << "Somehow missing id or passkey: " << id.length() << " " 145 | << passkey.length(); 146 | } 147 | return id + "/" + passkey; 148 | } 149 | } // namespace et 150 | -------------------------------------------------------------------------------- /src/terminal/SshSetupHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_SSH_SETUP_HANDLER__ 2 | #define __ET_SSH_SETUP_HANDLER__ 3 | 4 | #include "Headers.hpp" 5 | 6 | namespace et { 7 | class SshSetupHandler { 8 | public: 9 | static string SetupSsh(const string &user, const string &host, 10 | const string &host_alias, int port, 11 | const string &jumphost, const string &jServerFifo, 12 | bool kill, int vlevel, const string &etterminal_path, 13 | const string &serverFifo, 14 | const std::vector &ssh_options); 15 | static const string ETTERMINAL_BIN; 16 | }; 17 | } // namespace et 18 | #endif // __ET_SSH_SETUP_HANDLER__ 19 | -------------------------------------------------------------------------------- /src/terminal/TelemetryService.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Headers.hpp" 4 | 5 | namespace httplib { 6 | class Client; 7 | } 8 | 9 | namespace et { 10 | class TelemetryService { 11 | public: 12 | TelemetryService(const bool _allow, const string& databasePath, 13 | const string& environment); 14 | 15 | virtual ~TelemetryService(); 16 | 17 | void logToSentry(el::Level level, const std::string& message); 18 | 19 | void logToDatadog(const string& logText, el::Level logLevel, 20 | const string& filename, const int line); 21 | 22 | static void create(bool _allow, const string& databasePath, 23 | const string& environment) { 24 | telemetryServiceInstance.reset( 25 | new TelemetryService(_allow, databasePath, environment)); 26 | } 27 | 28 | static void destroy() { telemetryServiceInstance.reset(); } 29 | 30 | void shutdown(); 31 | 32 | static bool exists() { return telemetryServiceInstance.get() != NULL; } 33 | 34 | static shared_ptr get() { 35 | if (telemetryServiceInstance) { 36 | return telemetryServiceInstance; 37 | } 38 | STFATAL << "Tried to get a singleton before it was created!"; 39 | return NULL; 40 | } 41 | 42 | protected: 43 | static shared_ptr telemetryServiceInstance; 44 | bool allowed; 45 | string environment; 46 | unique_ptr logHttpClient; 47 | recursive_mutex logMutex; 48 | vector> logBuffer; 49 | bool shuttingDown; 50 | unique_ptr logSendingThread; 51 | sole::uuid telemetryId; 52 | }; 53 | 54 | } // namespace et 55 | -------------------------------------------------------------------------------- /src/terminal/TerminalClient.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_TERMINAL_CLIENT__ 2 | #define __ET_TERMINAL_CLIENT__ 3 | 4 | #include "ClientConnection.hpp" 5 | #include "Console.hpp" 6 | #include "CryptoHandler.hpp" 7 | #include "ETerminal.pb.h" 8 | #include "ForwardSourceHandler.hpp" 9 | #include "Headers.hpp" 10 | #include "LogHandler.hpp" 11 | #include "PortForwardHandler.hpp" 12 | #include "RawSocketUtils.hpp" 13 | #include "ServerConnection.hpp" 14 | #include "SshSetupHandler.hpp" 15 | #include "TcpSocketHandler.hpp" 16 | 17 | namespace et { 18 | class TerminalClient { 19 | public: 20 | TerminalClient(std::shared_ptr _socketHandler, 21 | std::shared_ptr _pipeSocketHandler, 22 | const SocketEndpoint& _socketEndpoint, const string& id, 23 | const string& passkey, shared_ptr _console, 24 | bool jumphost, const string& tunnels, 25 | const string& reverseTunnels, bool forwardSshAgent, 26 | const string& identityAgent, int _keepaliveDuration); 27 | virtual ~TerminalClient(); 28 | void run(const string& command, const bool noexit); 29 | void shutdown() { 30 | lock_guard guard(shutdownMutex); 31 | shuttingDown = true; 32 | } 33 | 34 | protected: 35 | shared_ptr console; 36 | shared_ptr connection; 37 | shared_ptr portForwardHandler; 38 | bool shuttingDown; 39 | recursive_mutex shutdownMutex; 40 | int keepaliveDuration; 41 | }; 42 | 43 | } // namespace et 44 | #endif // __ET_TERMINAL_CLIENT__ 45 | -------------------------------------------------------------------------------- /src/terminal/TerminalServer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_TERMINAL_SERVER__ 2 | #define __ET_TERMINAL_SERVER__ 3 | 4 | #include "ClientConnection.hpp" 5 | #include "CryptoHandler.hpp" 6 | #include "DaemonCreator.hpp" 7 | #include "ETerminal.pb.h" 8 | #include "Headers.hpp" 9 | #include "LogHandler.hpp" 10 | #include "PortForwardHandler.hpp" 11 | #include "ServerConnection.hpp" 12 | #include "TcpSocketHandler.hpp" 13 | #include "UserTerminalHandler.hpp" 14 | #include "UserTerminalRouter.hpp" 15 | 16 | namespace et { 17 | 18 | class TerminalServer : public ServerConnection { 19 | public: 20 | TerminalServer(std::shared_ptr _socketHandler, 21 | const SocketEndpoint &_serverEndpoint, 22 | std::shared_ptr _pipeSocketHandler, 23 | const SocketEndpoint &_routerEndpoint); 24 | virtual ~TerminalServer(); 25 | void runJumpHost(shared_ptr serverClientState, 26 | const InitialPayload &payload); 27 | void runTerminal(shared_ptr serverClientState, 28 | const InitialPayload &payload); 29 | void handleConnection(shared_ptr serverClientState); 30 | virtual bool newClient(shared_ptr serverClientState); 31 | 32 | void run(); 33 | void shutdown() { 34 | lock_guard guard(terminalThreadMutex); 35 | halt = true; 36 | } 37 | 38 | shared_ptr terminalRouter; 39 | 40 | vector> terminalThreads; 41 | bool halt = false; 42 | 43 | protected: 44 | mutex terminalThreadMutex; 45 | SocketEndpoint routerEndpoint; 46 | }; 47 | } // namespace et 48 | 49 | #endif // __ET_TERMINAL_SERVER__ 50 | -------------------------------------------------------------------------------- /src/terminal/UserJumphostHandler.hpp: -------------------------------------------------------------------------------- 1 | #include "ClientConnection.hpp" 2 | #include "Headers.hpp" 3 | #include "SocketHandler.hpp" 4 | 5 | namespace et { 6 | class UserJumphostHandler { 7 | public: 8 | UserJumphostHandler(shared_ptr _jumpClientSocketHandler, 9 | const string &_idpasskey, 10 | const SocketEndpoint &_dstSocketEndpoint, 11 | shared_ptr routerSocketHandler, 12 | const optional routerEndpoint); 13 | 14 | void run(); 15 | void shutdown() { 16 | lock_guard guard(shutdownMutex); 17 | shuttingDown = true; 18 | } 19 | 20 | protected: 21 | shared_ptr routerSocketHandler; 22 | int routerFd; 23 | shared_ptr jumpclient; 24 | shared_ptr jumpClientSocketHandler; 25 | string idpasskey; 26 | SocketEndpoint dstSocketEndpoint; 27 | bool shuttingDown; 28 | recursive_mutex shutdownMutex; 29 | }; 30 | } // namespace et 31 | -------------------------------------------------------------------------------- /src/terminal/UserTerminal.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __USER_TERMINAL_HPP__ 2 | #define __USER_TERMINAL_HPP__ 3 | 4 | #include "ETerminal.pb.h" 5 | #include "Headers.hpp" 6 | 7 | namespace et { 8 | class UserTerminal { 9 | public: 10 | virtual ~UserTerminal() {} 11 | 12 | virtual int setup(int routerFd) = 0; 13 | virtual void runTerminal() = 0; 14 | virtual void handleSessionEnd() = 0; 15 | virtual void cleanup() = 0; 16 | virtual int getFd() = 0; 17 | virtual void setInfo(const winsize& tmpwin) = 0; 18 | }; 19 | } // namespace et 20 | 21 | #endif -------------------------------------------------------------------------------- /src/terminal/UserTerminalHandler.cpp: -------------------------------------------------------------------------------- 1 | #ifndef WIN32 2 | #include "UserTerminalHandler.hpp" 3 | 4 | #include "ETerminal.pb.h" 5 | #include "RawSocketUtils.hpp" 6 | #include "ServerConnection.hpp" 7 | #include "ServerFifoPath.hpp" 8 | #include "UserTerminalRouter.hpp" 9 | 10 | namespace et { 11 | UserTerminalHandler::UserTerminalHandler( 12 | shared_ptr _socketHandler, shared_ptr _term, 13 | bool _noratelimit, const optional routerEndpoint, 14 | const string &idPasskey) 15 | : socketHandler(_socketHandler), 16 | term(_term), 17 | noratelimit(_noratelimit), 18 | shuttingDown(false) { 19 | auto idpasskey_splited = split(idPasskey, '/'); 20 | string id = idpasskey_splited[0]; 21 | string passkey = idpasskey_splited[1]; 22 | TerminalUserInfo tui; 23 | tui.set_id(id); 24 | tui.set_passkey(passkey); 25 | tui.set_uid(getuid()); 26 | tui.set_gid(getgid()); 27 | 28 | routerFd = ServerFifoPath::detectAndConnect(routerEndpoint, socketHandler); 29 | 30 | try { 31 | socketHandler->writePacket( 32 | routerFd, 33 | Packet(TerminalPacketType::TERMINAL_USER_INFO, protoToString(tui))); 34 | 35 | } catch (const std::runtime_error &re) { 36 | STFATAL << "Error connecting to router: " << re.what(); 37 | } 38 | } 39 | 40 | void UserTerminalHandler::run() { 41 | while (true) { 42 | Packet termInitPacket; 43 | if (!socketHandler->readPacket(routerFd, &termInitPacket)) { 44 | continue; 45 | } 46 | if (termInitPacket.getHeader() != TerminalPacketType::TERMINAL_INIT) { 47 | STFATAL << "Invalid terminal init packet header: " 48 | << termInitPacket.getHeader(); 49 | } 50 | TermInit ti = stringToProto(termInitPacket.getPayload()); 51 | for (int a = 0; a < ti.environmentnames_size(); a++) { 52 | setenv(ti.environmentnames(a).c_str(), ti.environmentvalues(a).c_str(), 53 | true); 54 | } 55 | break; 56 | } 57 | 58 | int masterfd = term->setup(routerFd); 59 | VLOG(1) << "pty opened " << masterfd; 60 | runUserTerminal(masterfd); 61 | close(routerFd); 62 | } 63 | 64 | void UserTerminalHandler::runUserTerminal(int masterFd) { 65 | #define BUF_SIZE (16 * 1024) 66 | char b[BUF_SIZE]; 67 | 68 | time_t lastSecond = time(NULL); 69 | int64_t outputPerSecond = 0; 70 | 71 | while (true) { 72 | { 73 | lock_guard guard(shutdownMutex); 74 | if (shuttingDown) { 75 | break; 76 | } 77 | } 78 | // Data structures needed for select() and 79 | // non-blocking I/O. 80 | fd_set rfd; 81 | timeval tv; 82 | 83 | FD_ZERO(&rfd); 84 | FD_SET(masterFd, &rfd); 85 | FD_SET(routerFd, &rfd); 86 | int maxfd = max(masterFd, routerFd); 87 | tv.tv_sec = 0; 88 | tv.tv_usec = 10000; 89 | select(maxfd + 1, &rfd, NULL, NULL, &tv); 90 | VLOG(4) << "select is done"; 91 | 92 | time_t currentSecond = time(NULL); 93 | if (lastSecond != currentSecond) { 94 | outputPerSecond = 0; 95 | lastSecond = currentSecond; 96 | } 97 | 98 | try { 99 | // Check for data to receive; the received 100 | // data includes also the data previously sent 101 | // on the same master descriptor (line 90). 102 | if (FD_ISSET(masterFd, &rfd) && (noratelimit || outputPerSecond < 1024)) { 103 | // Read from terminal and write to client, with a limit in rows/sec 104 | memset(b, 0, BUF_SIZE); 105 | int rc = read(masterFd, b, BUF_SIZE); 106 | VLOG(4) << "Read from terminal"; 107 | FATAL_FAIL(rc); 108 | if (rc > 0) { 109 | string s(b, rc); 110 | outputPerSecond += std::count(s.begin(), s.end(), '\n'); 111 | socketHandler->writeAllOrThrow(routerFd, b, rc, false); 112 | VLOG(4) << "Write to client: " 113 | << std::count(s.begin(), s.end(), '\n'); 114 | } else { 115 | LOG(INFO) << "Terminal session ended"; 116 | term->handleSessionEnd(); 117 | lock_guard guard(shutdownMutex); 118 | shuttingDown = true; 119 | break; 120 | } 121 | } 122 | 123 | if (FD_ISSET(routerFd, &rfd)) { 124 | char packetType; 125 | int rc = read(routerFd, &packetType, 1); 126 | FATAL_FAIL(rc); 127 | if (rc == 0) { 128 | throw std::runtime_error( 129 | "Router has ended abruptly. Killing terminal session."); 130 | } 131 | switch (packetType) { 132 | case TERMINAL_BUFFER: { 133 | TerminalBuffer tb = 134 | socketHandler->readProto(routerFd, false); 135 | VLOG(4) << "Read from router"; 136 | const string &buffer = tb.buffer(); 137 | RawSocketUtils::writeAll(masterFd, &buffer[0], buffer.length()); 138 | VLOG(4) << "Write to terminal"; 139 | break; 140 | } 141 | case TERMINAL_INFO: { 142 | TerminalInfo ti = 143 | socketHandler->readProto(routerFd, false); 144 | winsize tmpwin; 145 | tmpwin.ws_row = ti.row(); 146 | tmpwin.ws_col = ti.column(); 147 | tmpwin.ws_xpixel = ti.width(); 148 | tmpwin.ws_ypixel = ti.height(); 149 | term->setInfo(tmpwin); 150 | break; 151 | } 152 | } 153 | } 154 | } catch (const std::exception &ex) { 155 | LOG(INFO) << ex.what(); 156 | lock_guard guard(shutdownMutex); 157 | shuttingDown = true; 158 | break; 159 | } 160 | } 161 | 162 | term->cleanup(); 163 | } 164 | } // namespace et 165 | #endif 166 | -------------------------------------------------------------------------------- /src/terminal/UserTerminalHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_USER_TERMINAL_HANDLER__ 2 | #define __ET_USER_TERMINAL_HANDLER__ 3 | 4 | #include "Headers.hpp" 5 | #include "SocketHandler.hpp" 6 | #include "UserTerminal.hpp" 7 | 8 | namespace et { 9 | class UserTerminalHandler { 10 | public: 11 | UserTerminalHandler(shared_ptr _socketHandler, 12 | shared_ptr _term, bool noratelimit, 13 | const optional _routerEndpoint, 14 | const string &idPasskey); 15 | void run(); 16 | void shutdown() { 17 | lock_guard guard(shutdownMutex); 18 | shuttingDown = true; 19 | } 20 | 21 | protected: 22 | int routerFd; 23 | shared_ptr socketHandler; 24 | shared_ptr term; 25 | bool noratelimit; 26 | bool shuttingDown; 27 | recursive_mutex shutdownMutex; 28 | 29 | void runUserTerminal(int masterFd); 30 | }; 31 | } // namespace et 32 | 33 | #endif // __ET_ID_PASSKEY_HANDLER__ 34 | -------------------------------------------------------------------------------- /src/terminal/UserTerminalRouter.cpp: -------------------------------------------------------------------------------- 1 | #ifndef WIN32 2 | #include "UserTerminalRouter.hpp" 3 | 4 | #include "ETerminal.pb.h" 5 | 6 | namespace et { 7 | UserTerminalRouter::UserTerminalRouter( 8 | shared_ptr _socketHandler, 9 | const SocketEndpoint &_routerEndpoint) 10 | : socketHandler(_socketHandler) { 11 | serverFd = *(socketHandler->listen(_routerEndpoint).begin()); 12 | FATAL_FAIL(::chown(_routerEndpoint.name().c_str(), getuid(), getgid())); 13 | FATAL_FAIL(::chmod(_routerEndpoint.name().c_str(), 14 | S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | 15 | S_IROTH | S_IWOTH | S_IXOTH)); 16 | } 17 | 18 | IdKeyPair UserTerminalRouter::acceptNewConnection() { 19 | lock_guard guard(routerMutex); 20 | LOG(INFO) << "Listening to id/key FIFO"; 21 | const int terminalFd = socketHandler->accept(serverFd); 22 | if (terminalFd < 0) { 23 | if (GetErrno() != EAGAIN && GetErrno() != EWOULDBLOCK) { 24 | FATAL_FAIL(-1); // STFATAL with the error 25 | } else { 26 | return IdKeyPair({"", ""}); // Nothing to accept this time 27 | } 28 | } 29 | 30 | LOG(INFO) << "Connected"; 31 | 32 | try { 33 | Packet packet; 34 | if (!socketHandler->readPacket(terminalFd, &packet)) { 35 | STFATAL << "Missing user info packet"; 36 | } 37 | if (packet.getHeader() != TerminalPacketType::TERMINAL_USER_INFO) { 38 | STFATAL << "Got an invalid packet header: " << int(packet.getHeader()); 39 | } 40 | TerminalUserInfo tui = stringToProto(packet.getPayload()); 41 | tui.set_fd(terminalFd); 42 | 43 | const bool inserted = 44 | idInfoMap.insert(std::make_pair(tui.id(), tui)).second; 45 | if (!inserted) { 46 | LOG(ERROR) << "Rejecting duplicate terminal connection for " << tui.id(); 47 | socketHandler->close(terminalFd); 48 | return IdKeyPair({"", ""}); 49 | } 50 | 51 | return IdKeyPair({tui.id(), tui.passkey()}); 52 | } catch (const std::runtime_error &re) { 53 | LOG(ERROR) << "Router can't talk to terminal: " << re.what(); 54 | socketHandler->close(terminalFd); 55 | return IdKeyPair({"", ""}); 56 | } 57 | 58 | STFATAL << "Should never get here"; 59 | return IdKeyPair({"", ""}); 60 | } 61 | 62 | std::optional UserTerminalRouter::tryGetInfoForConnection( 63 | const shared_ptr &serverClientState) { 64 | lock_guard guard(routerMutex); 65 | auto it = idInfoMap.find(serverClientState->getId()); 66 | if (it == idInfoMap.end()) { 67 | STFATAL << " Tried to read from an id that no longer exists"; 68 | } 69 | 70 | // While both the id and passkey are randomly generated, do an extra 71 | // verification that the passkey matches to ensure that this is the intended 72 | // serverClientState. 73 | if (!serverClientState->verifyPasskey(it->second.passkey())) { 74 | LOG(ERROR) << "Failed to verify passkey for client id: " << it->second.id(); 75 | return std::nullopt; 76 | } 77 | 78 | return it->second; 79 | } 80 | 81 | } // namespace et 82 | #endif 83 | -------------------------------------------------------------------------------- /src/terminal/UserTerminalRouter.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_USER_TERMINAL_ROUTER__ 2 | #define __ET_USER_TERMINAL_ROUTER__ 3 | 4 | #include 5 | 6 | #include "Headers.hpp" 7 | #include "PipeSocketHandler.hpp" 8 | #include "ServerConnection.hpp" 9 | 10 | namespace et { 11 | 12 | class UserTerminalRouter { 13 | public: 14 | UserTerminalRouter(shared_ptr _socketHandler, 15 | const SocketEndpoint& _routerEndpoint); 16 | inline int getServerFd() { return serverFd; } 17 | IdKeyPair acceptNewConnection(); 18 | 19 | std::optional tryGetInfoForConnection( 20 | const shared_ptr& serverClientState); 21 | 22 | inline shared_ptr getSocketHandler() { 23 | return socketHandler; 24 | } 25 | 26 | protected: 27 | int serverFd; 28 | unordered_map idInfoMap; 29 | shared_ptr socketHandler; 30 | recursive_mutex routerMutex; 31 | }; 32 | } // namespace et 33 | 34 | #endif // __ET_ID_PASSKEY_ROUTER__ 35 | -------------------------------------------------------------------------------- /src/terminal/forwarding/ForwardDestinationHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "ForwardDestinationHandler.hpp" 2 | 3 | namespace et { 4 | ForwardDestinationHandler::ForwardDestinationHandler( 5 | shared_ptr _socketHandler, int _fd, int _socketId) 6 | : socketHandler(_socketHandler), fd(_fd), socketId(_socketId) {} 7 | 8 | void ForwardDestinationHandler::close() { socketHandler->close(fd); } 9 | 10 | void ForwardDestinationHandler::write(const string& s) { 11 | VLOG(1) << "Writing " << s.length() << " bytes to port destination"; 12 | socketHandler->writeAllOrReturn(fd, s.c_str(), s.length()); 13 | } 14 | 15 | void ForwardDestinationHandler::update(vector* retval) { 16 | if (fd == -1) { 17 | return; 18 | } 19 | 20 | while (socketHandler->hasData(fd)) { 21 | char buf[1024]; 22 | int bytesRead = socketHandler->read(fd, buf, 1024); 23 | auto readErrno = GetErrno(); 24 | if (bytesRead == -1 && (readErrno == EAGAIN || readErrno == EWOULDBLOCK)) { 25 | // Bail for now 26 | break; 27 | } 28 | PortForwardData pwd; 29 | pwd.set_socketid(socketId); 30 | pwd.set_sourcetodestination(false); 31 | if (bytesRead == -1) { 32 | VLOG(1) << "Got error reading socket " << socketId << " " 33 | << strerror(readErrno); 34 | pwd.set_error(strerror(readErrno)); 35 | } else if (bytesRead == 0) { 36 | VLOG(1) << "Got close reading socket " << socketId; 37 | pwd.set_closed(true); 38 | } else { 39 | VLOG(1) << "Reading " << bytesRead << " bytes from socket " << socketId; 40 | pwd.set_buffer(string(buf, bytesRead)); 41 | } 42 | retval->push_back(pwd); 43 | if (bytesRead < 1) { 44 | LOG(INFO) << "Socket " << socketId << " closed"; 45 | if (bytesRead < 0) { 46 | STERROR << "Socket " << socketId << " closed with error " << readErrno 47 | << ' ' << strerror(readErrno); 48 | } 49 | socketHandler->close(fd); 50 | fd = -1; 51 | break; 52 | } 53 | } 54 | } 55 | } // namespace et 56 | -------------------------------------------------------------------------------- /src/terminal/forwarding/ForwardDestinationHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __PORT_FORWARD_DESTINATION_HANDLER_H__ 2 | #define __PORT_FORWARD_DESTINATION_HANDLER_H__ 3 | 4 | #include "ETerminal.pb.h" 5 | #include "Headers.hpp" 6 | #include "SocketHandler.hpp" 7 | 8 | namespace et { 9 | class ForwardDestinationHandler { 10 | public: 11 | ForwardDestinationHandler(shared_ptr _socketHandler, int _fd, 12 | int _socketId); 13 | void write(const string& s); 14 | 15 | void update(vector* retval); 16 | 17 | void close(); 18 | 19 | inline int getFd() { return fd; } 20 | 21 | protected: 22 | shared_ptr socketHandler; 23 | int fd; 24 | int socketId; 25 | }; 26 | } // namespace et 27 | 28 | #endif // __PORT_FORWARD_DESTINATION_HANDLER_H__ 29 | -------------------------------------------------------------------------------- /src/terminal/forwarding/ForwardSourceHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "ForwardSourceHandler.hpp" 2 | 3 | namespace et { 4 | ForwardSourceHandler::ForwardSourceHandler( 5 | shared_ptr _socketHandler, const SocketEndpoint& _source, 6 | const SocketEndpoint& _destination) 7 | : socketHandler(_socketHandler), 8 | source(_source), 9 | destination(_destination) { 10 | socketHandler->listen(source); 11 | } 12 | 13 | ForwardSourceHandler::~ForwardSourceHandler() { 14 | socketHandler->stopListening(source); 15 | } 16 | 17 | int ForwardSourceHandler::listen() { 18 | // TODO: Replace with select 19 | for (int i : socketHandler->getEndpointFds(source)) { 20 | int fd = socketHandler->accept(i); 21 | if (fd > -1) { 22 | LOG(INFO) << "Tunnel " << source << " -> " << destination 23 | << " socket created with fd " << fd; 24 | unassignedFds.insert(fd); 25 | return fd; 26 | } 27 | } 28 | return -1; 29 | } 30 | 31 | void ForwardSourceHandler::update(vector* data) { 32 | vector socketsToRemove; 33 | 34 | for (auto& it : socketFdMap) { 35 | int socketId = it.first; 36 | int fd = it.second; 37 | 38 | while (socketHandler->hasData(fd)) { 39 | char buf[1024]; 40 | int bytesRead = socketHandler->read(fd, buf, 1024); 41 | auto readErrno = GetErrno(); 42 | if (bytesRead == -1 && 43 | (readErrno == EAGAIN || readErrno == EWOULDBLOCK)) { 44 | // Bail for now 45 | break; 46 | } 47 | PortForwardData pwd; 48 | pwd.set_socketid(socketId); 49 | pwd.set_sourcetodestination(true); 50 | if (bytesRead == -1) { 51 | VLOG(1) << "Got error reading socket " << socketId << " " 52 | << strerror(readErrno); 53 | pwd.set_error(strerror(readErrno)); 54 | } else if (bytesRead == 0) { 55 | VLOG(1) << "Got close reading socket " << socketId; 56 | pwd.set_closed(true); 57 | } else { 58 | VLOG(1) << "Reading " << bytesRead << " bytes from socket " << socketId; 59 | pwd.set_buffer(string(buf, bytesRead)); 60 | } 61 | data->push_back(pwd); 62 | if (bytesRead < 1) { 63 | socketHandler->close(fd); 64 | socketsToRemove.push_back(socketId); 65 | break; 66 | } 67 | } 68 | } 69 | for (auto& it : socketsToRemove) { 70 | socketFdMap.erase(it); 71 | } 72 | } 73 | 74 | bool ForwardSourceHandler::hasUnassignedFd(int fd) { 75 | return unassignedFds.find(fd) != unassignedFds.end(); 76 | } 77 | 78 | void ForwardSourceHandler::closeUnassignedFd(int fd) { 79 | if (unassignedFds.find(fd) == unassignedFds.end()) { 80 | STERROR << "Tried to close an unassigned fd that doesn't exist"; 81 | return; 82 | } 83 | socketHandler->close(fd); 84 | unassignedFds.erase(fd); 85 | } 86 | 87 | void ForwardSourceHandler::addSocket(int socketId, int sourceFd) { 88 | if (unassignedFds.find(sourceFd) == unassignedFds.end()) { 89 | STERROR << "Tried to close an unassigned fd that doesn't exist " 90 | << sourceFd; 91 | return; 92 | } 93 | LOG(INFO) << "Adding socket: " << socketId << " " << sourceFd; 94 | unassignedFds.erase(sourceFd); 95 | socketFdMap[socketId] = sourceFd; 96 | } 97 | void ForwardSourceHandler::sendDataOnSocket(int socketId, const string& data) { 98 | if (socketFdMap.find(socketId) == socketFdMap.end()) { 99 | LOG(INFO) << "Tried to write to a socket that no longer exists!"; 100 | return; 101 | } 102 | 103 | int fd = socketFdMap[socketId]; 104 | const char* buf = data.c_str(); 105 | int count = data.length(); 106 | socketHandler->writeAllOrReturn(fd, buf, count); 107 | } 108 | 109 | void ForwardSourceHandler::closeSocket(int socketId) { 110 | auto it = socketFdMap.find(socketId); 111 | if (it == socketFdMap.end()) { 112 | LOG(WARNING) << "Tried to remove a socket that no longer exists!"; 113 | } else { 114 | socketHandler->close(it->second); 115 | socketFdMap.erase(it); 116 | } 117 | } 118 | } // namespace et 119 | -------------------------------------------------------------------------------- /src/terminal/forwarding/ForwardSourceHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __FORWARD_SOURCE_HANDLER_H__ 2 | #define __FORWARD_SOURCE_HANDLER_H__ 3 | 4 | #include "Headers.hpp" 5 | #include "SocketHandler.hpp" 6 | 7 | namespace et { 8 | class ForwardSourceHandler { 9 | public: 10 | ForwardSourceHandler(shared_ptr _socketHandler, 11 | const SocketEndpoint& _source, 12 | const SocketEndpoint& _destination); 13 | 14 | ~ForwardSourceHandler(); 15 | 16 | int listen(); 17 | 18 | void update(vector* data); 19 | 20 | bool hasUnassignedFd(int fd); 21 | 22 | void closeUnassignedFd(int fd); 23 | 24 | void addSocket(int socketId, int sourceFd); 25 | 26 | void closeSocket(int socketId); 27 | 28 | void sendDataOnSocket(int socketId, const string& data); 29 | 30 | inline SocketEndpoint getDestination() { return destination; } 31 | 32 | protected: 33 | shared_ptr socketHandler; 34 | SocketEndpoint source; 35 | SocketEndpoint destination; 36 | unordered_set unassignedFds; 37 | unordered_map socketFdMap; 38 | }; 39 | } // namespace et 40 | 41 | #endif // __FORWARD_SOURCE_HANDLER_H__ 42 | -------------------------------------------------------------------------------- /src/terminal/forwarding/PortForwardHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __PORT_FORWARD_HANDLER_H__ 2 | #define __PORT_FORWARD_HANDLER_H__ 3 | 4 | #include "Connection.hpp" 5 | #include "ETerminal.pb.h" 6 | #include "ForwardDestinationHandler.hpp" 7 | #include "ForwardSourceHandler.hpp" 8 | #include "SocketHandler.hpp" 9 | 10 | namespace et { 11 | class PortForwardHandler { 12 | public: 13 | explicit PortForwardHandler(shared_ptr _networkSocketHandler, 14 | shared_ptr _pipeSocketHandler); 15 | void update(vector* requests, 16 | vector* dataToSend); 17 | void handlePacket(const Packet& packet, shared_ptr connection); 18 | #ifndef WIN32 19 | PortForwardSourceResponse createSource(const PortForwardSourceRequest& pfsr, 20 | string* sourceName, uid_t userid, 21 | gid_t groupid); 22 | #endif 23 | PortForwardDestinationResponse createDestination( 24 | const PortForwardDestinationRequest& pfdr); 25 | 26 | void closeSourceFd(int fd); 27 | void addSourceSocketId(int socketId, int sourceFd); 28 | void closeSourceSocketId(int socketId); 29 | void sendDataToSourceOnSocket(int socketId, const string& data); 30 | 31 | protected: 32 | shared_ptr networkSocketHandler; 33 | shared_ptr pipeSocketHandler; 34 | unordered_map> destinationHandlers; 35 | 36 | vector> sourceHandlers; 37 | unordered_map> socketIdSourceHandlerMap; 38 | }; 39 | } // namespace et 40 | 41 | #endif // __PORT_FORWARD_HANDLER_H__ 42 | -------------------------------------------------------------------------------- /systemctl/et.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Eternal Terminal 3 | After=syslog.target network.target 4 | 5 | [Service] 6 | Type=simple 7 | Restart=on-failure 8 | ExecStart=/usr/bin/etserver --cfgfile=/etc/et.cfg 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /test/BackedTest.cpp: -------------------------------------------------------------------------------- 1 | #include "BackedReader.hpp" 2 | #include "BackedWriter.hpp" 3 | #include "CryptoHandler.hpp" 4 | #include "FlakySocketHandler.hpp" 5 | #include "LogHandler.hpp" 6 | #include "PipeSocketHandler.hpp" 7 | #include "TestHeaders.hpp" 8 | 9 | using namespace et; 10 | 11 | class BackedCollector { 12 | public: 13 | BackedCollector(shared_ptr _reader, 14 | shared_ptr _writer) 15 | : reader(_reader), writer(_writer), done(false) { 16 | collectorThread = std::thread(&BackedCollector::run, this); 17 | } 18 | 19 | ~BackedCollector() { finish(); } 20 | 21 | void run() { 22 | while (true) { 23 | { 24 | lock_guard guard(collectorMutex); 25 | if (done) { 26 | break; 27 | } 28 | } 29 | Packet packet; 30 | if (reader->read(&packet) > 0) { 31 | lock_guard guard(collectorMutex); 32 | fifo.push_back(packet.getPayload()); 33 | } else { 34 | std::this_thread::sleep_for(std::chrono::microseconds(1000)); 35 | } 36 | } 37 | } 38 | 39 | bool hasData() { 40 | lock_guard guard(collectorMutex); 41 | return !fifo.empty(); 42 | } 43 | 44 | string pop() { 45 | lock_guard guard(collectorMutex); 46 | if (fifo.empty()) { 47 | STFATAL << "Tried to pop an empty fifo"; 48 | } 49 | string s = fifo.front(); 50 | fifo.pop_front(); 51 | return s; 52 | } 53 | 54 | string read() { 55 | while (!hasData()) { 56 | std::this_thread::sleep_for(std::chrono::microseconds(1000)); 57 | } 58 | return pop(); 59 | } 60 | 61 | void finish() { 62 | { 63 | lock_guard guard(collectorMutex); 64 | done = true; 65 | } 66 | collectorThread.join(); 67 | } 68 | 69 | BackedWriterWriteState write(string s) { return writer->write(Packet(0, s)); } 70 | 71 | protected: 72 | shared_ptr reader; 73 | shared_ptr writer; 74 | deque fifo; 75 | std::thread collectorThread; 76 | std::mutex collectorMutex; 77 | bool done; 78 | }; 79 | 80 | void listenFn(shared_ptr socketHandler, SocketEndpoint endpoint, 81 | int* serverClientFd) { 82 | // Only works when there is 1:1 mapping between endpoint and fds. Will fix in 83 | // future api 84 | int serverFd = *(socketHandler->listen(endpoint).begin()); 85 | int fd; 86 | while (true) { 87 | fd = socketHandler->accept(serverFd); 88 | auto localErrno = GetErrno(); 89 | if (fd == -1) { 90 | if (localErrno != EAGAIN) { 91 | FATAL_FAIL(fd); 92 | } else { 93 | std::this_thread::sleep_for(std::chrono::microseconds(100 * 1000)); 94 | } 95 | } else { 96 | break; 97 | } 98 | } 99 | *serverClientFd = fd; 100 | } 101 | 102 | TEST_CASE("BackedTest", "[BackedTest]") { 103 | shared_ptr serverSocketHandler; 104 | shared_ptr clientSocketHandler; 105 | serverSocketHandler.reset(new PipeSocketHandler()); 106 | clientSocketHandler.reset(new PipeSocketHandler()); 107 | 108 | shared_ptr serverCollector; 109 | shared_ptr clientCollector; 110 | string pipeDirectory; 111 | string pipePath; 112 | 113 | string tmpPath = GetTempDirectory() + string("et_test_XXXXXXXX"); 114 | pipeDirectory = string(mkdtemp(&tmpPath[0])); 115 | pipePath = string(pipeDirectory) + "/pipe"; 116 | SocketEndpoint endpoint; 117 | endpoint.set_name(pipePath); 118 | int serverClientFd = -1; 119 | std::thread serverListenThread(listenFn, serverSocketHandler, endpoint, 120 | &serverClientFd); 121 | 122 | // Wait for server to spin up 123 | std::this_thread::sleep_for(std::chrono::seconds(1)); 124 | int clientServerFd = clientSocketHandler->connect(endpoint); 125 | FATAL_FAIL(clientServerFd); 126 | serverListenThread.join(); 127 | FATAL_FAIL(serverClientFd); 128 | 129 | serverCollector.reset(new BackedCollector( 130 | shared_ptr(new BackedReader( 131 | serverSocketHandler, 132 | shared_ptr(new CryptoHandler( 133 | "12345678901234567890123456789012", CLIENT_SERVER_NONCE_MSB)), 134 | serverClientFd)), 135 | shared_ptr(new BackedWriter( 136 | serverSocketHandler, 137 | shared_ptr(new CryptoHandler( 138 | "12345678901234567890123456789012", SERVER_CLIENT_NONCE_MSB)), 139 | serverClientFd)))); 140 | 141 | clientCollector.reset(new BackedCollector( 142 | shared_ptr(new BackedReader( 143 | clientSocketHandler, 144 | shared_ptr(new CryptoHandler( 145 | "12345678901234567890123456789012", SERVER_CLIENT_NONCE_MSB)), 146 | clientServerFd)), 147 | shared_ptr(new BackedWriter( 148 | clientSocketHandler, 149 | shared_ptr(new CryptoHandler( 150 | "12345678901234567890123456789012", CLIENT_SERVER_NONCE_MSB)), 151 | clientServerFd)))); 152 | 153 | SECTION("ReliableBackedTest") { 154 | string s(64 * 1024, '\0'); 155 | for (int a = 0; a < 64 * 1024 - 1; a++) { 156 | s[a] = rand() % 26 + 'A'; 157 | } 158 | s[64 * 1024 - 1] = 0; 159 | 160 | for (int a = 0; a < 64; a++) { 161 | VLOG(1) << "Writing packet " << a; 162 | BackedWriterWriteState r = 163 | serverCollector->write(string((&s[0] + a * 1024), 1024)); 164 | if (r != BackedWriterWriteState::SUCCESS) { 165 | STFATAL << "Invalid write state: " << int(r); 166 | } 167 | } 168 | 169 | string resultConcat; 170 | string result; 171 | for (int a = 0; a < 64; a++) { 172 | result = clientCollector->read(); 173 | resultConcat = resultConcat.append(result); 174 | } 175 | REQUIRE(resultConcat == s); 176 | } 177 | 178 | FATAL_FAIL(::remove(pipePath.c_str())); 179 | FATAL_FAIL(::remove(pipeDirectory.c_str())); 180 | } 181 | -------------------------------------------------------------------------------- /test/CryptoHandlerTest.cpp: -------------------------------------------------------------------------------- 1 | #include "CryptoHandler.hpp" 2 | #include "TestHeaders.hpp" 3 | 4 | using namespace et; 5 | 6 | TEST_CASE("DoesEncryptDecrypt", "[CryptoHandler]") { 7 | string key = "12345678901234567890123456789012"; 8 | shared_ptr encryptHandler(new CryptoHandler(key, 0)); 9 | shared_ptr decryptHandler(new CryptoHandler(key, 0)); 10 | string message = "ET Phone Home"; 11 | string encryptedMessage = encryptHandler->encrypt(message); 12 | REQUIRE(message != encryptedMessage); 13 | string decryptedMessage = decryptHandler->decrypt(encryptedMessage); 14 | REQUIRE(message == decryptedMessage); 15 | } 16 | -------------------------------------------------------------------------------- /test/FlakySocketHandler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_FLAKY_SOCKET_HANDLER__ 2 | #define __ET_FLAKY_SOCKET_HANDLER__ 3 | 4 | #include "UnixSocketHandler.hpp" 5 | 6 | namespace et { 7 | class FlakySocketHandler : public SocketHandler { 8 | public: 9 | FlakySocketHandler(shared_ptr _actualSocketHandler, 10 | bool _enableFlake) 11 | : actualSocketHandler(_actualSocketHandler), enableFlake(_enableFlake) {} 12 | virtual ~FlakySocketHandler() {} 13 | 14 | inline void setFlake(bool _enableFlake) { enableFlake = _enableFlake; } 15 | 16 | virtual int connect(const SocketEndpoint& endpoint) { 17 | if (enableFlake && rand() % 2 == 0) { 18 | return -1; 19 | } 20 | return actualSocketHandler->connect(endpoint); 21 | } 22 | virtual set listen(const SocketEndpoint& endpoint) { 23 | return actualSocketHandler->listen(endpoint); 24 | } 25 | virtual set getEndpointFds(const SocketEndpoint& endpoint) { 26 | return actualSocketHandler->getEndpointFds(endpoint); 27 | } 28 | virtual void stopListening(const SocketEndpoint& endpoint) { 29 | return actualSocketHandler->stopListening(endpoint); 30 | } 31 | virtual bool hasData(int fd) { 32 | if (enableFlake && rand() % 2 == 0) { 33 | return false; 34 | } 35 | return actualSocketHandler->hasData(fd); 36 | } 37 | virtual ssize_t read(int fd, void* buf, size_t count) { 38 | auto millis = std::chrono::duration_cast( 39 | std::chrono::system_clock::now().time_since_epoch()) 40 | .count(); 41 | if (enableFlake && millis % 10 == 0) { 42 | SetErrno(EPIPE); 43 | return -1; 44 | } 45 | if (enableFlake && millis % 10 == 5) { 46 | SetErrno(EAGAIN); 47 | return -1; 48 | } 49 | return actualSocketHandler->read(fd, buf, count); 50 | } 51 | virtual ssize_t write(int fd, const void* buf, size_t count) { 52 | auto millis = std::chrono::duration_cast( 53 | std::chrono::system_clock::now().time_since_epoch()) 54 | .count(); 55 | if (enableFlake && millis % 10 == 0) { 56 | SetErrno(EPIPE); 57 | return -1; 58 | } 59 | if (enableFlake && millis % 10 == 5) { 60 | SetErrno(EAGAIN); 61 | return -1; 62 | } 63 | return actualSocketHandler->write(fd, buf, count); 64 | } 65 | virtual vector getActiveSockets() { 66 | return actualSocketHandler->getActiveSockets(); 67 | } 68 | 69 | int writeAllOrReturn(int fd, const void* buf, size_t count) { 70 | if (enableFlake && rand() % 30 == 0) { 71 | SetErrno(EPIPE); 72 | return -1; 73 | } 74 | return actualSocketHandler->writeAllOrReturn(fd, buf, count); 75 | } 76 | 77 | virtual int accept(int fd) { 78 | if (enableFlake && rand() % 2 == 0) { 79 | SetErrno(EAGAIN); 80 | return -1; 81 | } 82 | return actualSocketHandler->accept(fd); 83 | } 84 | virtual void close(int fd) { actualSocketHandler->close(fd); } 85 | 86 | protected: 87 | shared_ptr actualSocketHandler; 88 | bool enableFlake; 89 | }; 90 | } // namespace et 91 | 92 | #endif // __ET_FLAKY_SOCKET_HANDLER__ 93 | -------------------------------------------------------------------------------- /test/FuzzableTerminalServer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ET_FUZZABLE_TERMINAL_SERVER__ 2 | #define __ET_FUZZABLE_TERMINAL_SERVER__ 3 | 4 | #include "TerminalServer.hpp" 5 | 6 | namespace et { 7 | class FuzzableTerminalServer { 8 | public: 9 | FuzzableTerminalServer() { 10 | serverSocketHandler.reset(new PipeSocketHandler()); 11 | pipeSocketHandler.reset(new PipeSocketHandler()); 12 | 13 | string tmpPath = GetTempDirectory() + string("etserver_fuzzer_XXXXXXXX"); 14 | const string pipeDirectory = string(mkdtemp(&tmpPath[0])); 15 | 16 | const string serverPipePath = pipeDirectory + "/pipe_server"; 17 | SocketEndpoint serverEndpoint; 18 | serverEndpoint.set_name(serverPipePath); 19 | 20 | const string routerPath = pipeDirectory + "/pipe_router"; 21 | routerEndpoint.set_name(routerPath); 22 | 23 | terminalServer.reset(new TerminalServer(serverSocketHandler, serverEndpoint, 24 | pipeSocketHandler, routerEndpoint)); 25 | terminalServerThread = thread([this]() { terminalServer->run(); }); 26 | } 27 | 28 | ~FuzzableTerminalServer() { 29 | terminalServer->shutdown(); 30 | terminalServerThread.join(); 31 | } 32 | 33 | std::shared_ptr serverSocketHandler; 34 | std::shared_ptr pipeSocketHandler; 35 | 36 | SocketEndpoint serverEndpoint; 37 | SocketEndpoint routerEndpoint; 38 | 39 | shared_ptr terminalServer; 40 | thread terminalServerThread; 41 | }; 42 | 43 | } // namespace et 44 | 45 | #endif // __ET_FUZZABLE_TERMINAL_SERVER__ 46 | -------------------------------------------------------------------------------- /test/Main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_RUNNER 2 | 3 | #include "LogHandler.hpp" 4 | #include "TelemetryService.hpp" 5 | #include "TestHeaders.hpp" 6 | 7 | using namespace et; 8 | 9 | int main(int argc, char **argv) { 10 | srand(1); 11 | 12 | // Setup easylogging configurations 13 | el::Configurations defaultConf = 14 | et::LogHandler::setupLogHandler(&argc, &argv); 15 | et::LogHandler::setupStdoutLogger(); 16 | // el::Loggers::setVerboseLevel(9); 17 | 18 | et::HandleTerminate(); 19 | 20 | string logDirectoryPattern = GetTempDirectory() + string("et_test_XXXXXXXX"); 21 | string logDirectory = string(mkdtemp(&logDirectoryPattern[0])); 22 | CLOG(INFO, "stdout") << "Writing log to " << logDirectory << endl; 23 | et::LogHandler::setupLogFiles(&defaultConf, logDirectory, "log", true, true); 24 | 25 | // Reconfigure default logger to apply settings above 26 | el::Loggers::reconfigureLogger("default", defaultConf); 27 | 28 | TelemetryService::create(false, "", ""); 29 | 30 | int result = Catch::Session().run(argc, argv); 31 | 32 | TelemetryService::get()->shutdown(); 33 | TelemetryService::destroy(); 34 | 35 | FATAL_FAIL(fs::remove_all(logDirectory.c_str())); 36 | return result; 37 | } 38 | -------------------------------------------------------------------------------- /test/TerminalServerFuzzer.cpp: -------------------------------------------------------------------------------- 1 | #include "FuzzableTerminalServer.hpp" 2 | 3 | namespace et { 4 | 5 | static std::unique_ptr sServer; 6 | 7 | extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) { 8 | sServer.reset(new FuzzableTerminalServer()); 9 | 10 | return 0; 11 | } 12 | 13 | extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { 14 | PipeSocketHandler sockerHandler; 15 | const int fd = sockerHandler.connect(sServer->serverEndpoint); 16 | if (fd == -1) { 17 | // Ignore if we fail to connect. 18 | return 0; 19 | } 20 | 21 | sockerHandler.write(fd, data, size); 22 | 23 | // Shutdown the server, to verify that it gracefully exits. 24 | sServer->terminalServer->shutdown(); 25 | 26 | sockerHandler.close(fd); 27 | 28 | return 0; 29 | } 30 | 31 | } // namespace et 32 | -------------------------------------------------------------------------------- /test/TerminalServerRouterFuzzer.cpp: -------------------------------------------------------------------------------- 1 | #include "FuzzableTerminalServer.hpp" 2 | 3 | namespace et { 4 | 5 | static std::unique_ptr sServer; 6 | 7 | extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) { 8 | sServer.reset(new FuzzableTerminalServer()); 9 | return 0; 10 | } 11 | 12 | extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { 13 | PipeSocketHandler sockerHandler; 14 | const int fd = sockerHandler.connect(sServer->routerEndpoint); 15 | if (fd == -1) { 16 | // Ignore if we fail to connect. 17 | return 0; 18 | } 19 | 20 | sockerHandler.write(fd, data, size); 21 | 22 | // Shutdown the server, to verify that it gracefully exits. 23 | sServer->terminalServer->shutdown(); 24 | 25 | sockerHandler.close(fd); 26 | 27 | return 0; 28 | } 29 | 30 | } // namespace et 31 | -------------------------------------------------------------------------------- /test/TestHeaders.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __TEST_HEADERS_HPP__ 2 | #define __TEST_HEADERS_HPP__ 3 | 4 | #include "Headers.hpp" 5 | 6 | #undef CHECK 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /test/system_tests/connect_with_jumphost.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | ssh -o 'PreferredAuthentications=publickey' localhost "echo" || exit 1 # Fails if we can't ssh into localhost without a password 6 | 7 | # Bypass host check 8 | ssh -o "StrictHostKeyChecking no" localhost echo "Bypassing host check 1" 9 | ssh -o "StrictHostKeyChecking no" 127.0.0.1 echo "Bypassing host check 2" 10 | 11 | mkdir -p /tmp/et_test_logs/connect_with_jumphost/1 12 | build/etserver --port 9900 --serverfifo=/tmp/etserver.idpasskey.fifo1 -l /tmp/et_test_logs/connect_with_jumphost/1 & 13 | first_server_pid=$! 14 | 15 | mkdir -p /tmp/et_test_logs/connect_with_jumphost/2 16 | build/etserver --port 9901 --serverfifo=/tmp/etserver.idpasskey.fifo2 -l /tmp/et_test_logs/connect_with_jumphost/2 & 17 | second_server_pid=$! 18 | sleep 3 19 | 20 | # Make sure servers are working 21 | build/et -c "echo 'Hello World 1!'" --serverfifo=/tmp/etserver.idpasskey.fifo1 --logtostdout --terminal-path $PWD/build/etterminal localhost:9900 22 | build/et -c "echo 'Hello World 2!'" --serverfifo=/tmp/etserver.idpasskey.fifo2 --logtostdout --terminal-path $PWD/build/etterminal localhost:9901 23 | 24 | build/et -c "echo 'Hello World 3!'" --serverfifo=/tmp/etserver.idpasskey.fifo2 --logtostdout --terminal-path $PWD/build/etterminal --jumphost localhost --jport 9900 --jserverfifo=/tmp/etserver.idpasskey.fifo1 127.0.0.1:9901 # We can't use 'localhost' for both the jumphost and the destination because ssh doesn't support keeping them the same. 25 | 26 | kill -9 $first_server_pid 27 | kill -9 $second_server_pid 28 | -------------------------------------------------------------------------------- /test/test_tsan.suppression: -------------------------------------------------------------------------------- 1 | called_from_lib:libunwind.so 2 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eternal-terminal", 3 | "version-string": "6.0.13", 4 | "dependencies": [ 5 | "zlib", 6 | "openssl", 7 | "protobuf", 8 | "libsodium", 9 | "sentry-native", 10 | "nlohmann-json", 11 | "simpleini", 12 | "platform-folders", 13 | "cpp-httplib", 14 | "cxxopts", 15 | "catch2" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------