From fa82bce08ef6d230736dbbb13b293d6101906813 Mon Sep 17 00:00:00 2001 From: "Michaut.Francois" Date: Tue, 5 Aug 2025 19:43:13 -0300 Subject: [PATCH] Styling, Client Auth, Version Handshake - Added clang-tidy + re-styled most of the code - Split server and peer configs - Added PreAuthPeer to handle version handshake + auth step - Added a temporary in-memory KnownPeerStore for auth step - Remember Configs load filepath to re-use on save by default - FileType to/from str/streams - Fix File transfers logic that could sometimes crash --- .clang-tidy | 41 ++ .editorconfig | 11 + .github/workflows/cmake-multi-platform.yml | 138 ++--- .github/workflows/code_scanning.yml | 116 ++--- .github/workflows/windows-vcpkg/action.yml | 40 ++ .gitignore | 5 + CMakeLists.txt | 32 +- dependencies/CMakeLists.txt | 13 +- include/FileShare/Client.hpp | 107 ---- include/FileShare/Config.hpp | 94 ---- include/FileShare/Config/Config.hpp | 76 +++ include/FileShare/Config/FileMapping.hpp | 157 +++--- include/FileShare/Config/KnownPeerStore.hpp | 40 ++ include/FileShare/Config/Serialization.hpp | 137 ++--- include/FileShare/Config/ServerConfig.hpp | 73 +++ include/FileShare/MessageQueue.hpp | 25 +- include/FileShare/Peer/Peer.hpp | 106 ++++ include/FileShare/Peer/PeerBase.hpp | 70 +++ include/FileShare/Peer/PreAuthPeer.hpp | 50 ++ include/FileShare/Protocol/Definitions.hpp | 33 +- .../Handler/v0.0.0/ProtocolHandler.hpp | 48 +- include/FileShare/Protocol/Protocol.hpp | 41 +- include/FileShare/Protocol/RequestData.hpp | 77 ++- include/FileShare/Protocol/Version.hpp | 30 +- include/FileShare/Server.hpp | 148 ++++-- include/FileShare/TransferHandler.hpp | 52 +- include/FileShare/Utils/Path.hpp | 16 +- include/FileShare/Utils/Poll.hpp | 28 + include/FileShare/Utils/Serialize.hpp | 6 +- include/FileShare/Utils/StringHash.hpp | 25 - include/FileShare/Utils/Strings.hpp | 125 +++++ include/FileShare/Utils/Time.hpp | 10 +- include/FileShare/Utils/VarInt.hpp | 20 +- include/FileShare/Utils/Vector.hpp | 28 + source/Config.cpp | 85 --- source/Config/Config.cpp | 70 +++ source/Config/FileMapping.cpp | 322 +++++++----- source/Config/KnownPeerStore.cpp | 55 ++ source/Config/ServerConfig.cpp | 99 ++++ source/MessageQueue.cpp | 35 +- source/{Client.cpp => Peer/Peer.cpp} | 127 +++-- source/Peer/PeerBase.cpp | 105 ++++ .../Peer_private.cpp} | 187 ++++--- source/Peer/PreAuthPeer.cpp | 113 ++++ source/Protocol/Handler/IProtocolHandler.cpp | 122 +++++ .../Handler/v0.0.0/ProtocolHandler.cpp | 200 ++++---- source/Protocol/Protocol.cpp | 77 ++- source/Protocol/RequestData.cpp | 76 ++- source/Protocol/Version.cpp | 32 +- source/Server.cpp | 484 ++++++++++++------ source/TransferHandler.cpp | 54 +- source/Utils/FileDescriptor.cpp | 13 +- source/Utils/FileHash.cpp | 38 +- source/Utils/Path.cpp | 19 +- source/Utils/Poll.cpp | 43 ++ source/Utils/VarInt.cpp | 35 +- tests/CMakeLists.txt | 9 +- tests/Config/TestFileMapping.cpp | 87 ++-- vcpkg.json | 6 + 59 files changed, 3002 insertions(+), 1509 deletions(-) create mode 100644 .clang-tidy create mode 100644 .editorconfig create mode 100644 .github/workflows/windows-vcpkg/action.yml delete mode 100644 include/FileShare/Client.hpp delete mode 100644 include/FileShare/Config.hpp create mode 100644 include/FileShare/Config/Config.hpp create mode 100644 include/FileShare/Config/KnownPeerStore.hpp create mode 100644 include/FileShare/Config/ServerConfig.hpp create mode 100644 include/FileShare/Peer/Peer.hpp create mode 100644 include/FileShare/Peer/PeerBase.hpp create mode 100644 include/FileShare/Peer/PreAuthPeer.hpp create mode 100644 include/FileShare/Utils/Poll.hpp delete mode 100644 include/FileShare/Utils/StringHash.hpp create mode 100644 include/FileShare/Utils/Strings.hpp create mode 100644 include/FileShare/Utils/Vector.hpp delete mode 100644 source/Config.cpp create mode 100644 source/Config/Config.cpp create mode 100644 source/Config/KnownPeerStore.cpp create mode 100644 source/Config/ServerConfig.cpp rename source/{Client.cpp => Peer/Peer.cpp} (65%) create mode 100644 source/Peer/PeerBase.cpp rename source/{Client_private.cpp => Peer/Peer_private.cpp} (68%) create mode 100644 source/Peer/PreAuthPeer.cpp create mode 100644 source/Protocol/Handler/IProtocolHandler.cpp create mode 100644 source/Utils/Poll.cpp create mode 100644 vcpkg.json diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..a5ed099 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,41 @@ +Checks: ' +*, +-llvmlibc-*, +-android-*, +-altera-*, +-fuchsia*, +-google-readability-todo, +-cppcoreguidelines-init-variables, +-cppcoreguidelines-pro-type-reinterpret-cast, +-llvm-namespace-comment, +-readability-implicit-bool-conversion, +-google-explicit-constructor, +-abseil-string-find-str-contains, +-readability-avoid-return-with-void-value, +-readability-convert-member-functions-to-static, + +-google-readability-braces-around-statements, +-google-readability-namespace-comments, +-hicpp-special-member-functions, +-hicpp-braces-around-statements, +-hicpp-explicit-conversions +' + +WarningsAsErrors: 'bugprone-exception-escape' +FormatStyle: 'none' # TODO: Replace with 'file' once we have a proper .clang-format file +InheritParentConfig: true +CheckOptions: + misc-include-cleaner.MissingIncludes: 'false' + misc-include-cleaner.IgnoreHeaders: 'CppSockets/OSDetection.*;.*Version.hpp' + + bugprone-argument-comment.StrictMode: 1 + + # Readability + readability-braces-around-statements.ShortStatementLines: 2 + + readability-identifier-naming.NamespaceCase: CamelCase + + readability-identifier-length.IgnoredVariableNames: "^(fd|nb|n|ss|ec|is|os|_.*)$" + readability-identifier-length.IgnoredParameterNames: "^(fd|n|is|os|_.*)$" + + cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor: true diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..66dff6e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.{c++,cc,cpp,cxx,h,h++,hh,hpp,hxx,inl,ipp,tlh,tli}] +cpp_indent_case_contents_when_block = true +cpp_new_line_before_open_brace_namespace = same_line +indent_size = 4 +indent_style = space diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index f298dc0..75c805e 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -4,6 +4,7 @@ on: push: branches: [ "master" ] pull_request: + branches: [ "master" ] types: [ "opened", "reopened", "synchronize", "ready_for_review" ] jobs: @@ -11,83 +12,102 @@ jobs: runs-on: ${{ matrix.os }} strategy: - fail-fast: false # ensure we don't stop after 1 failure to always have a complete picture of what is failing + # ensure we don't stop after 1 failure to always have a complete picture of what is failing + fail-fast: false # Set up a matrix to run the following configurations: # - ubuntu Debug/Release clang/gcc # - windows Debug/Release cl # - macos Debug/Release clang matrix: - os: [ubuntu-latest, macos-latest] # , windows-latest + os: [ubuntu-latest, macos-latest, windows-latest] build_type: [Release, Debug] - c_compiler: [gcc-13, clang-15, cl] + c_compiler: [gcc, clang, cl] include: - # - os: windows-latest - # c_compiler: cl - # cpp_compiler: cl + - os: windows-latest + c_compiler: cl + cpp_compiler: cl - os: ubuntu-latest - c_compiler: gcc-13 - cpp_compiler: g++-13 + c_compiler: gcc + cpp_compiler: g++ - os: ubuntu-latest - c_compiler: clang-15 - cpp_compiler: clang++-15 + c_compiler: clang + cpp_compiler: clang++ - os: macos-latest - c_compiler: gcc-13 - cpp_compiler: g++-13 + c_compiler: clang + cpp_compiler: clang++ exclude: - os: windows-latest - c_compiler: gcc-13 + c_compiler: gcc - os: windows-latest - c_compiler: clang-15 + c_compiler: clang - os: ubuntu-latest c_compiler: cl - os: macos-latest c_compiler: cl - os: macos-latest - c_compiler: clang-15 + c_compiler: gcc steps: - - uses: actions/checkout@v3 - - - name: Set reusable strings - # Turn repeated input strings (such as the build output directory) into step outputs. These step outputs can be used throughout the workflow file. - id: strings - shell: bash - run: | - echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" - - - name: Cache VCPKG (Windows) - if: runner.os == 'Windows' - uses: actions/cache@v3 - with: - path: ${{ env.VCPKG_ROOT }} - key: ${{ runner.os }}-${{ matrix.build_type }}-${{ hashFiles('vcpkg.json') }} - - - name: Install OpenSSL (Windows) - if: runner.os == 'Windows' - shell: powershell - run: | - echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append - echo "CMAKE_TOOLCHAIN_FILE=${env:VCPKG_INSTALLATION_ROOT}\scripts\buildsystems\vcpkg.cmake" | Out-File -FilePath $env:GITHUB_ENV -Append - vcpkg install - - - name: Configure CMake - # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. - # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type - run: > - cmake -B ${{ steps.strings.outputs.build-output-dir }} - -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} - -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} - -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} - -DLIBFSP_BUILD_TESTS=TRUE - -S ${{ github.workspace }} - - - name: Build - # Build your program with the given configuration. Note that --config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). - run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} - - - name: Test - working-directory: ${{ steps.strings.outputs.build-output-dir }} - # Execute tests defined by the CMake configuration. Note that --build-config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). - # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail - run: ctest --build-config ${{ matrix.build_type }} --test-dir tests --output-on-failure + - uses: actions/checkout@v3 + + - name: Set Env + shell: bash + run: | + echo "BUILD_OUTPUT_DIR=${{ github.workspace }}/build" >> "$GITHUB_ENV" + + - name: VCPKG Install (Windows) + if: runner.os == 'Windows' + uses: ./.github/workflows/windows-vcpkg + with: + key: ${{ runner.os }}-${{ matrix.build_type }} + + - name: Configure CMake + # Configure CMake in a 'build' subdirectory. + # `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: > + cmake -B ${{ env.BUILD_OUTPUT_DIR }} + -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} + -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + -DLIBFSP_BUILD_TESTS=TRUE + -S ${{ github.workspace }} + + - name: Build + # Build your program with the given configuration. Note that --config is needed + # because the default Windows generator is a multi-config generator (Visual Studio generator). + run: cmake --build ${{ env.BUILD_OUTPUT_DIR }} --config ${{ matrix.build_type }} + + - uses: actions/upload-artifact@v4 + if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'Release' && matrix.c_compiler == 'clang' + with: + name: compile_commands.json + path: ${{ env.BUILD_OUTPUT_DIR }}/compile_commands.json + + - name: Test + working-directory: ${{ env.BUILD_OUTPUT_DIR }} + # Execute tests defined by the CMake configuration. Note that --build-config is needed + # because the default Windows generator is a multi-config generator (Visual Studio generator). + # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail + run: ctest --build-config ${{ matrix.build_type }} --test-dir tests --output-on-failure + + clang-tidy: + needs: 'build' + runs-on: ubuntu-latest + if: always() && github.event_name == 'pull_request' + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: compile_commands.json + + - name: clang-tidy review + uses: ZedThree/clang-tidy-review@v0.21.0 + + # If there are any comments, fail the check + - if: steps.review.outputs.total_comments > 0 + run: exit 1 diff --git a/.github/workflows/code_scanning.yml b/.github/workflows/code_scanning.yml index f841d60..38971df 100644 --- a/.github/workflows/code_scanning.yml +++ b/.github/workflows/code_scanning.yml @@ -30,7 +30,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: 'c-cpp' # If you wish to specify custom queries, you can do so here or in a config file. @@ -44,23 +44,10 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - env: - CXX: g++-13 - CC: gcc-13 - uses: github/codeql-action/autobuild@v2 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:c-cpp" @@ -76,53 +63,66 @@ jobs: uses: actions/checkout@v3 - name: flawfinder_scan - uses: david-a-wheeler/flawfinder@8e4a779ad59dbfaee5da586aa9210853b701959c + uses: david-a-wheeler/flawfinder@2.0.19 with: arguments: '--sarif ./' output: 'flawfinder_results.sarif' - name: Upload analysis results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ${{github.workspace}}/flawfinder_results.sarif - # microsoft-analyze: - # permissions: - # contents: read # for actions/checkout to fetch code - # security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - # actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status - # name: Microsoft Analyze - # runs-on: windows-latest - - # steps: - # - name: Checkout repository - # uses: actions/checkout@v3 - - # - name: Configure CMake - # run: cmake -B ./build - - # # Build is not required unless generated source files are used - # # - name: Build CMake - # # run: cmake --build ./build - - # - name: Initialize MSVC Code Analysis - # uses: microsoft/msvc-code-analysis-action@04825f6d9e00f87422d6bf04e1a38b1f3ed60d99 - # # Provide a unique ID to access the sarif output path - # id: run-analysis - # with: - # cmakeBuildDirectory: ${{ env.build }} - # # Ruleset file that will determine what checks will be run - # ruleset: NativeRecommendedRules.ruleset - - # # Upload SARIF file to GitHub Code Scanning Alerts - # - name: Upload SARIF to GitHub - # uses: github/codeql-action/upload-sarif@v2 - # with: - # sarif_file: ${{ steps.run-analysis.outputs.sarif }} - - # # Upload SARIF file as an Artifact to download and view - # # - name: Upload SARIF as an Artifact - # # uses: actions/upload-artifact@v3 - # # with: - # # name: sarif-file - # # path: ${{ steps.run-analysis.outputs.sarif }} + microsoft-analyze: + permissions: + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + name: Microsoft Analyze + runs-on: windows-latest + + env: + # Path to the CMake build directory. + build: '${{ github.workspace }}/build' + config: 'Debug' + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: VCPKG Install (Windows) + uses: ./.github/workflows/windows-vcpkg + with: + key: ${{ runner.os }}-${{ env.config }} + + - name: Configure CMake + run: cmake -B ${{ env.build }} -DCMAKE_BUILD_TYPE=${{ env.config }} + + # Build is not required unless generated source files are used + # - name: Build CMake + # run: cmake --build ${{ env.build }} --config ${{ env.config }} + + - name: Run MSVC Code Analysis + uses: microsoft/msvc-code-analysis-action@v0.1.1 + # Provide a unique ID to access the sarif output path + id: run-analysis + with: + cmakeBuildDirectory: ${{ env.build }} + buildConfiguration: ${{ env.config }} + # Ruleset file that will determine what checks will be run + ruleset: NativeRecommendedRules.ruleset + # Paths to ignore analysis of CMake targets and includes + # ignoredPaths: ${{ github.workspace }}/dependencies;${{ github.workspace }}/test + + # Upload SARIF file to GitHub Code Scanning Alerts + - name: Upload SARIF to GitHub + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ steps.run-analysis.outputs.sarif }} + + # # Upload SARIF file as an Artifact to download and view + # - name: Upload SARIF as an Artifact + # uses: actions/upload-artifact@v4 + # with: + # name: sarif-file + # path: ${{ steps.run-analysis.outputs.sarif }} diff --git a/.github/workflows/windows-vcpkg/action.yml b/.github/workflows/windows-vcpkg/action.yml new file mode 100644 index 0000000..d826148 --- /dev/null +++ b/.github/workflows/windows-vcpkg/action.yml @@ -0,0 +1,40 @@ +name: Windows VCPKG + +inputs: + key: + required: true + type: string + vcpkg_path: + type: string + default: ".\\" + +runs: + using: "composite" + steps: + - name: Set Env + shell: powershell + run: | + echo "VCPKG_ROOT=${env:VCPKG_INSTALLATION_ROOT}" | Out-File -FilePath $env:GITHUB_ENV -Append + echo "VCPKG_CACHE=${env:LOCALAPPDATA}\vcpkg\archives" | Out-File -FilePath $env:GITHUB_ENV -Append + + - name: Fetch VCPKG Cache (Windows) + id: fetch-vcpkg-cache + if: runner.os == 'Windows' + uses: actions/cache/restore@v4 + with: + key: ${{ inputs.key }}-vcpkg-${{ hashFiles(format('{0}\vcpkg.json', inputs.vcpkg_path)) }} + path: ${{ env.VCPKG_CACHE }} + + - name: VCPKG Install (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + echo "CMAKE_TOOLCHAIN_FILE=${env:VCPKG_ROOT}\scripts\buildsystems\vcpkg.cmake" | Out-File -FilePath $env:GITHUB_ENV -Append + vcpkg install --x-manifest-root="${{ inputs.vcpkg_path }}" + + - name: Always Save VCPKG Cache (Windows) + if: always() && runner.os == 'Windows' && steps.fetch-vcpkg-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + key: ${{ steps.fetch-vcpkg-cache.outputs.cache-primary-key }} + path: ${{ env.VCPKG_CACHE }} diff --git a/.gitignore b/.gitignore index 968d4c0..9ea04e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,12 @@ build/ +libfsp.so compile_commands.json *.tmp *.gch +*.pch vgcore.* + +.vscode/ +.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt index b76d2b0..6e89693 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ ## Author Francois Michaut ## ## Started on Thu May 26 23:23:59 2022 Francois Michaut -## Last update Sun Dec 10 10:17:30 2023 Francois Michaut +## Last update Mon Aug 18 17:26:23 2025 Francois Michaut ## ## CMakeLists.txt : CMake to build the FileShareProtocol library ## @@ -20,7 +20,7 @@ set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) if(!WIN32) set(CMAKE_CXX_FLAGS_DEBUG_INIT "-O3 -DDEBUG -g3") set(CMAKE_CXX_FLAGS_RELEASE_INIT "-O3") - add_compile_definitions(_GNU_SOURCE) + add_compile_definitions(_GNU_SOURCE _FILE_OFFSET_BITS=64 _TIME_BITS=64) endif() project(LibFileShareProtocol VERSION 0.1.0 LANGUAGES C CXX) @@ -29,29 +29,37 @@ configure_file(include/FileShare/Version.hpp.in FileShare/Version.hpp) add_subdirectory(dependencies) add_library(fsp + source/Config/Config.cpp + source/Config/FileMapping.cpp + source/Config/KnownPeerStore.cpp + source/Config/ServerConfig.cpp + source/Errors/TransferErrors.cpp + source/Peer/Peer.cpp + source/Peer/PeerBase.cpp + source/Peer/PreAuthPeer.cpp + source/Peer/Peer_private.cpp + + source/MessageQueue.cpp + source/Protocol/Handler/v0.0.0/ProtocolHandler.cpp + source/Protocol/Handler/IProtocolHandler.cpp source/Protocol/Protocol.cpp source/Protocol/RequestData.cpp source/Protocol/Version.cpp + source/Server.cpp + source/TransferHandler.cpp + source/Utils/DebugPerf.cpp source/Utils/FileDescriptor.cpp source/Utils/FileHash.cpp source/Utils/Path.cpp + source/Utils/Poll.cpp source/Utils/Serialize.cpp source/Utils/VarInt.cpp - - source/Config/FileMapping.cpp - - source/Client.cpp - source/Client_private.cpp - source/Config.cpp - source/MessageQueue.cpp - source/Server.cpp - source/TransferHandler.cpp ) target_include_directories(fsp PUBLIC $ $) @@ -61,7 +69,7 @@ target_compile_options(fsp PRIVATE $<$:/W4> ) -target_link_libraries(fsp cppsockets cereal) +target_link_libraries(fsp cppsockets cereal frozen) if(WIN32) target_link_libraries(fsp userenv) endif() diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index 8b50e7d..185e04d 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -4,7 +4,7 @@ ## Author Francois Michaut ## ## Started on Sun Dec 10 10:27:39 2023 Francois Michaut -## Last update Sun Dec 10 18:56:37 2023 Francois Michaut +## Last update Sat Aug 23 12:23:04 2025 Francois Michaut ## ## CMakeLists.txt : CMake to fetch and build the dependecies of the FileShareProtocol library ## @@ -14,13 +14,20 @@ include(FetchContent) FetchContent_Declare( cppsockets GIT_REPOSITORY https://github.com/FileShare-Project/libcppsockets.git - GIT_TAG 8161640e1686507cc19b069faf7610e31c19238e + GIT_TAG 9ffb4b952bceb59eac82e1f8eb2e369b79985547 ) FetchContent_MakeAvailable(cppsockets) +FetchContent_Declare( + frozen + GIT_REPOSITORY https://github.com/serge-sans-paille/frozen.git + GIT_TAG 61dce5ae18ca59931e27675c468e64118aba8744 +) +FetchContent_MakeAvailable(frozen) + FetchContent_Declare( cereal GIT_REPOSITORY https://github.com/f-michaut/cereal.git - GIT_TAG 203ba6f75047dd90e961596d6ad742b80d942014 + GIT_TAG 83cb386d0146bd5fe77f9bc5974d85b4da3cda90 ) add_subdirectory(cereal) diff --git a/include/FileShare/Client.hpp b/include/FileShare/Client.hpp deleted file mode 100644 index 242836a..0000000 --- a/include/FileShare/Client.hpp +++ /dev/null @@ -1,107 +0,0 @@ -/* -** Project LibFileShareProtocol, 2022 -** -** Author Francois Michaut -** -** Started on Sun Aug 28 09:23:07 2022 Francois Michaut -** Last update Sat Dec 9 09:01:57 2023 Francois Michaut -** -** Client.hpp : Client to communicate with peers with the FileShareProtocol -*/ - -#pragma once - -#include "FileShare/Config.hpp" -#include "FileShare/Protocol/Protocol.hpp" -#include "FileShare/MessageQueue.hpp" -#include "FileShare/TransferHandler.hpp" - -#include -#include -#include - -#include - -// TODO handle UDP -// TODO: rename Client -> Peer -namespace FileShare { - class Client { - public: - using ProgressCallback = std::function; - - Client(const CppSockets::IEndpoint &peer, std::string device_uuid, std::string public_key, Protocol::Protocol protocol, Config config = Client::default_config()); - Client(CppSockets::TlsSocket &&peer, std::string device_uuid, std::string public_key, Protocol::Protocol protocol, Config config = Client::default_config()); - - [[nodiscard]] - std::vector pull_requests(); - void respond_to_request(Protocol::Request, Protocol::StatusCode); - - // Blocking functions - Protocol::Response send_file(std::string filepath, ProgressCallback progress_callback = [](const std::string &, std::size_t, std::size_t){}); - Protocol::Response receive_file(std::string filepath, ProgressCallback progress_callback = [](const std::string &, std::size_t, std::size_t){}); - Protocol::Response> list_files(std::string folderpath = ""); - // TODO: Async non-blocking Functions - - // TODO determine params - Protocol::Response initiate_pairing(); - Protocol::Response accept_pairing(); - - [[nodiscard]] - const CppSockets::TlsSocket &get_socket() const; - void reconnect(const CppSockets::IEndpoint &peer); - void reconnect(CppSockets::TlsSocket &&peer); - - [[nodiscard]] - const Config &get_config() const; - void set_config(Config config); - - [[nodiscard]] - const Protocol::Protocol &get_protocol() const; - void set_protocol(Protocol::Protocol protocol); - - [[nodiscard]] - std::string_view get_device_uuid() const; - [[nodiscard]] - std::string_view get_public_key() const; - void set_config(std::string device_uuid); - void set_public_key(std::string public_key); - - protected: - static Config default_config(); - using UploadTransferMap = std::unordered_map; - using DownloadTransferMap = std::unordered_map; - using ListFilesTransferMap = std::unordered_map; - using FileListTransferMap = std::unordered_map; - - private: - Protocol::StatusCode wait_for_status(Protocol::MessageID message_id); - void poll_requests(); - - void authorize_request(Protocol::Request request); - - void receive_reply(Protocol::MessageID message_id, Protocol::StatusCode status); - void send_reply(Protocol::MessageID message_id, Protocol::StatusCode status); - std::uint8_t send_request(Protocol::CommandCode command, std::shared_ptr request_data); - std::uint8_t send_request(Protocol::Request request); - std::pair, Protocol::StatusCode> prepare_upload(std::filesystem::path host_filepath, std::string virtual_filepath, std::size_t packet_size, std::size_t packet_start); - UploadTransferMap::iterator create_host_upload(std::filesystem::path host_filepath); - UploadTransferMap::iterator create_upload(UploadTransferHandler handler); - DownloadTransferMap::iterator create_download(Protocol::MessageID message_id, const std::shared_ptr &data); - - private: - CppSockets::TlsSocket m_socket; - Config m_config; - Protocol::Protocol m_protocol; - std::string m_device_uuid; - std::string m_public_key; - - std::string m_buffer; - std::vector m_request_buffer; - MessageQueue m_message_queue; - - DownloadTransferMap m_download_transfers; - UploadTransferMap m_upload_transfers; - ListFilesTransferMap m_list_files_transfers; - FileListTransferMap m_file_list_transfers; - }; -} diff --git a/include/FileShare/Config.hpp b/include/FileShare/Config.hpp deleted file mode 100644 index 69998d7..0000000 --- a/include/FileShare/Config.hpp +++ /dev/null @@ -1,94 +0,0 @@ -/* -** Project LibFileShareProtocol, 2022 -** -** Author Francois Michaut -** -** Started on Tue Sep 13 11:23:57 2022 Francois Michaut -** Last update Sun Dec 10 17:37:30 2023 Francois Michaut -** -** Config.hpp : Configuration of the file sharing -*/ - -#pragma once - -#include "FileShare/Config/FileMapping.hpp" - -#include -#include - -namespace FileShare { - class Config { - public: - enum TransportMode { - UDP, - TCP, - AUTOMATIC // AUTOMATIC switches between TCP/UDP based - // on current operation and errors/latency - }; - - Config(); - ~Config() = default; - - // paths starting with '~/' will have this part replaced by the current user's home directory - static Config from_file(std::filesystem::path config_file = "~/.fsp/config"); - void to_file(std::filesystem::path config_file = "~/.fsp/config"); - - [[nodiscard]] const std::filesystem::path &get_downloads_folder() const; - Config &set_downloads_folder(const std::filesystem::path path); - - Config &set_file_mapping(FileMapping mapping); - const FileMapping &get_file_mapping() const; - FileMapping &get_file_mapping(); - - [[nodiscard]] const std::filesystem::path &get_private_keys_dir() const; - [[nodiscard]] const std::string &get_private_key_name() const; - Config &set_private_keys_dir(std::filesystem::path path); - Config &set_private_key_name(std::string name); - - [[nodiscard]] TransportMode get_transport_mode() const; - Config &set_transport_mode(TransportMode mode); - - [[nodiscard]] bool is_server_disabled() const; - Config &set_server_disabled(bool disabled); - public: - static const std::filesystem::perms secure_file_perms; - static const std::filesystem::perms secure_folder_perms; - - static const std::filesystem::path &default_private_keys_dir(); - private: - // The protocol works on both TCP and UDP. You can force one or the - // other using this option, however we recomand to keep the default. - TransportMode m_transport_mode = AUTOMATIC; - - // List of mapped directories/files that will be available to the - // remote clients for listing/download if visibility is PUBLIC. - // It does not restrict what files you can send. - // - // However, firectories/files in the forbidden_paths will NEVER be sent - // to other clients. Sensitive directories like ~/.ssh should be listed. - // It will override any path set in in m_root_nodes and will also - // apply to files you send. - // Use this to allow certain directory but exclude sub-directories, - // and to prevent against sentitives files/folders mistakely - // added to m_root_nodes or manually sent. - FileMapping m_filemap; - // Default location for downloads. Default to a 'FileShare/' folder - // in the local Downloads folder if empty string. - std::filesystem::path m_downloads_folder = ""; - - // Folder where the private key/certificate will be stored/created. - // An error will be raised if it exists with unsecure permissions. - std::filesystem::path m_private_keys_dir = Config::default_private_keys_dir(); - // Name of the key/certificate file to use - std::string m_private_key_name = "file_share"; - - // If set to true, the server will not open a socket nor listen to - // incomming connections. You will still be able to connect to other - // server but they will not be able to initiate the connection. - // Set this to true if you want an extra layer of security by - // preventing external connections. - // Note that when you initiate a connection to another server it will - // be able to send commands as well. - bool m_disable_server = false; - }; -} diff --git a/include/FileShare/Config/Config.hpp b/include/FileShare/Config/Config.hpp new file mode 100644 index 0000000..594c02c --- /dev/null +++ b/include/FileShare/Config/Config.hpp @@ -0,0 +1,76 @@ +/* +** Project LibFileShareProtocol, 2022 +** +** Author Francois Michaut +** +** Started on Tue Sep 13 11:23:57 2022 Francois Michaut +** Last update Fri Aug 22 18:59:52 2025 Francois Michaut +** +** Config.hpp : Configuration of the file sharing +*/ + +#pragma once + +#include "FileShare/Config/FileMapping.hpp" + +#include + +namespace FileShare { + class Config { + public: + enum TransportMode : std::uint8_t { + UDP, + TCP, + AUTOMATIC // AUTOMATIC switches between TCP/UDP based + // on current operation and errors/latency + }; + + Config(); + + // paths starting with '~/' will have this part replaced by the current user's home directory + static auto load(std::filesystem::path config_file = "") -> Config; + void save(std::filesystem::path config_file = "") const; + + [[nodiscard]] auto get_downloads_folder() const -> const std::filesystem::path & { return m_downloads_folder; } + auto set_downloads_folder(const std::filesystem::path &path) -> Config &; + + auto set_file_mapping(FileMapping mapping) -> Config & { m_filemap = std::move(mapping); return *this; } + auto get_file_mapping() const -> const FileMapping & { return m_filemap; } + auto get_file_mapping() -> FileMapping & { return m_filemap; } + + [[nodiscard]] auto get_transport_mode() const -> TransportMode { return m_transport_mode; } + auto set_transport_mode(TransportMode mode) -> Config & { m_transport_mode = mode; return *this; } + + private: + template + friend void serialize(Archive &archive, Config &config, std::uint32_t version); + + Config(bool); + + std::filesystem::path m_filepath; + + // Nickname for this given Peer. Allows users to override the display_name advertised by + // Peers with a custom one. Invalid on the default Config. + std::string m_nickname; + + // The protocol works on both TCP and UDP. You can force one or the + // other using this option, however we recomand to keep the default. + TransportMode m_transport_mode = AUTOMATIC; + + // List of mapped directories/files that will be available to the + // remote clients for listing/download if visibility is PUBLIC. + // It does not restrict what files you can send. + // + // However, firectories/files in the forbidden_paths will NEVER be sent + // to other clients. Sensitive directories like ~/.ssh should be listed. + // It will override any path set in in m_root_nodes and will also + // apply to files you send. + // Use this to allow certain directory but exclude sub-directories, + // and to prevent against sentitives files/folders mistakely + // added to m_root_nodes or manually sent. + FileMapping m_filemap; + // Default location for downloads. Default to a 'FileShare/' folder + // in the local Downloads folder if empty string. + std::filesystem::path m_downloads_folder = ""; + }; +} diff --git a/include/FileShare/Config/FileMapping.hpp b/include/FileShare/Config/FileMapping.hpp index 1311b46..08748bd 100644 --- a/include/FileShare/Config/FileMapping.hpp +++ b/include/FileShare/Config/FileMapping.hpp @@ -4,15 +4,18 @@ ** Author Francois Michaut ** ** Started on Sun Nov 19 11:23:07 2023 Francois Michaut -** Last update Sun Dec 10 17:33:23 2023 Francois Michaut +** Last update Sun Aug 24 11:28:31 2025 Francois Michaut ** ** FileMapping.hpp : Class to hold information about which files are available for listing/download */ #pragma once -#include "FileShare/Utils/StringHash.hpp" +#include "FileShare/Utils/Strings.hpp" +#include + +#include #include #include #include @@ -20,77 +23,108 @@ #include namespace FileShare { - // TODO: not protected against cyclic graph + class FileMapping; + class PathNode; + + template + concept IsPathNode = std::is_base_of_v; + class PathNode { public: - enum Type { HOST_FILE, HOST_FOLDER, VIRTUAL }; - enum Visibility { VISIBLE, HIDDEN }; + enum Type : std::uint8_t { HOST_FILE, HOST_FOLDER, VIRTUAL }; + enum Visibility : std::uint8_t { VISIBLE, HIDDEN }; using NodeMap = std::unordered_map>; PathNode() = default; + + PathNode(const PathNode &); + PathNode(PathNode &&) = default; + auto operator=(const PathNode &) -> PathNode &; + auto operator=(PathNode &&) -> PathNode & = default; + virtual ~PathNode() = default; - static PathNode make_virtual_node(std::string name, Visibility visibility = HIDDEN); - static PathNode make_virtual_node(std::string name, Visibility visibility, NodeMap child_nodes); - static PathNode make_virtual_node(std::string name, Visibility visibility, std::vector child_nodes); - static PathNode make_virtual_node(std::string name, NodeMap child_nodes, Visibility visibility = HIDDEN); - static PathNode make_virtual_node(std::string name, std::vector child_nodes, Visibility visibility = HIDDEN); - static PathNode make_host_node(std::string name, Type type, std::filesystem::path host_path, Visibility visibility = HIDDEN); + auto operator==(const PathNode &other) const noexcept -> bool; - bool operator==(const PathNode &other) const noexcept; + static auto make_virtual_node(std::string name, Visibility visibility = HIDDEN) -> PathNode; + static auto make_virtual_node(std::string name, Visibility visibility, NodeMap child_nodes) -> PathNode; + static auto make_virtual_node(std::string name, Visibility visibility, std::vector child_nodes) -> PathNode; + static auto make_virtual_node(std::string name, NodeMap child_nodes, Visibility visibility = HIDDEN) -> PathNode; + static auto make_virtual_node(std::string name, std::vector child_nodes, Visibility visibility = HIDDEN) -> PathNode; + static auto make_host_node(std::string name, Type type, std::filesystem::path host_path, Visibility visibility = HIDDEN) -> PathNode; - PathNode &add_child_node(PathNode node); - PathNode &remove_child_node(std::string_view name); - PathNode &clear_child_nodes(); + [[nodiscard]] auto insert_child_node(PathNode node) -> PathNode &; // Return the inserted node instead of *this + auto add_child_node(PathNode node) -> PathNode &; + auto remove_child_node(const std::string &name) -> PathNode &; + auto clear_child_nodes() -> PathNode &; - [[nodiscard]] std::string_view get_name() const; - [[nodiscard]] const std::string &get_name_str() const; - PathNode &set_name(std::string name); + [[nodiscard]] auto get_name() const -> std::string_view { return m_name; } + virtual auto set_name(std::string name) -> PathNode &; - [[nodiscard]] Type get_type() const; - PathNode &set_type(Type type); + [[nodiscard]] auto get_type() const -> Type { return m_type; } + virtual auto set_type(Type type) -> PathNode &; - bool is_virtual() const; - bool is_host() const; - bool is_host_file() const; - bool is_host_folder() const; + auto is_virtual() const -> bool { return m_type == Type::VIRTUAL; } + auto is_host() const -> bool { return !is_virtual(); } + auto is_host_file() const -> bool { return m_type == Type::HOST_FILE; } + auto is_host_folder() const -> bool { return m_type == Type::HOST_FOLDER; } - [[nodiscard]] Visibility get_visibility() const; - PathNode &set_visibility(Visibility visibility); + [[nodiscard]] auto get_visibility() const -> Visibility { return m_visibility; } + virtual auto set_visibility(Visibility visibility) -> PathNode & { m_visibility = visibility; return *this; } - [[nodiscard]] NodeMap &get_child_nodes(); - [[nodiscard]] const NodeMap &get_child_nodes() const; - PathNode &set_child_nodes(NodeMap child_nodes); - PathNode &set_child_nodes(std::vector child_nodes); + [[nodiscard]] auto get_child_nodes() const -> const NodeMap & { return m_child_nodes; } + auto set_child_nodes(NodeMap child_nodes) -> PathNode &; + auto set_child_nodes(std::vector child_nodes) -> PathNode &; - [[nodiscard]] const std::filesystem::path &get_host_path() const; - PathNode &set_host_path(std::filesystem::path host_path); + [[nodiscard]] auto get_host_path() const -> const std::filesystem::path & { return m_host_path; } + virtual auto set_host_path(std::filesystem::path host_path) -> PathNode &; + auto get_ancestor_path() const -> std::filesystem::path; protected: - PathNode(std::string name, Visibility visibility, NodeMap child_nodes); - PathNode(std::string name, Type type, std::filesystem::path host_path, Visibility visibility = HIDDEN); + PathNode(std::string name, PathNode *parent, Visibility visibility, NodeMap child_nodes); + PathNode(std::string name, PathNode *parent, Type type, std::filesystem::path host_path, Visibility visibility = HIDDEN); + + template + friend void serialize(Archive &archive, Node &node, std::uint32_t version) + requires IsPathNode; + + template class Map, typename... Args, typename mapped_type> inline + friend void cereal::CEREAL_LOAD_FUNCTION_NAME(Archive &archive, Map &map); private: static void assert_virtual_type(Type type); static void assert_not_virtual_type(Type type); virtual void assert_valid_name(std::string_view name); + void assign_parent(PathNode *); + void reassign_childs(); - private: std::string m_name; // no `/` allowed in the stem - Type m_type; - Visibility m_visibility; + PathNode *m_parent = nullptr; // Always present, except for RootPathNode where it is always NULL NodeMap m_child_nodes; // Only if type == VIRTUAL std::filesystem::path m_host_path; // Only if type != VIRTUAL + + // Safe Defaults + Type m_type = HOST_FILE; + Visibility m_visibility = HIDDEN; + + // Caching of internal data + std::unique_ptr m_cached_ancestor_path = nullptr; }; class RootPathNode : public PathNode { public: - RootPathNode(std::string root_name = default_root_name, PathNode::NodeMap root_nodes = {}); - RootPathNode(std::string root_name, std::vector root_nodes); - RootPathNode(PathNode::NodeMap root_nodes); - RootPathNode(std::vector root_nodes); + explicit RootPathNode(std::string_view root_name = DEFAULT_ROOT_NAME, PathNode::NodeMap root_nodes = {}); + explicit RootPathNode(std::string_view root_name, std::vector root_nodes); + explicit RootPathNode(PathNode::NodeMap root_nodes); + explicit RootPathNode(std::vector root_nodes); + + auto set_name(std::string name) -> PathNode & override; - static constexpr const char *default_root_name = "//fsp"; + auto set_type(Type type) -> PathNode & override; + auto set_visibility(Visibility visibility) -> PathNode & override; + auto set_host_path(std::filesystem::path host_path) -> PathNode & override; + + static constexpr const char *DEFAULT_ROOT_NAME = "//fsp"; private: void assert_valid_name(std::string_view name) override; }; @@ -102,34 +136,35 @@ namespace FileShare { FileMapping() = default; FileMapping(RootPathNode root_node, FilepathSet forbidden_paths = FileMapping::default_forbidden_paths()); - [[nodiscard]] std::string_view get_root_name() const; - void set_root_name(std::string root_name); + [[nodiscard]] auto get_root_name() const -> std::string_view { return m_root_node.get_name(); } + void set_root_name(std::string root_name) { m_root_node.set_name(std::move(root_name)); } // TODO: make sure we can't change root node visibility or type - [[nodiscard]] const RootPathNode &get_root_node() const; - [[nodiscard]] RootPathNode &get_root_node(); - void set_root_node(RootPathNode root_node); + [[nodiscard]] auto get_root_node() const -> const RootPathNode & { return m_root_node; } + [[nodiscard]] auto get_root_node() -> RootPathNode & { return m_root_node; } + void set_root_node(RootPathNode root_node) { m_root_node = std::move(root_node); } - [[nodiscard]] const PathNode::NodeMap &get_root_nodes() const; - [[nodiscard]] PathNode::NodeMap &get_root_nodes(); - void set_root_nodes(PathNode::NodeMap root_nodes); + [[nodiscard]] auto get_root_nodes() const -> const PathNode::NodeMap & { return m_root_node.get_child_nodes(); } + void set_root_nodes(PathNode::NodeMap root_nodes) { m_root_node.set_child_nodes(std::move(root_nodes)); } void set_root_nodes(std::vector root_nodes); - [[nodiscard]] const FilepathSet &get_forbidden_paths() const; - [[nodiscard]] FilepathSet get_forbidden_paths(); - void set_forbidden_paths(FilepathSet paths); + [[nodiscard]] auto get_forbidden_paths() const -> const FilepathSet & { return m_forbidden_paths; } + [[nodiscard]] auto get_forbidden_paths() -> FilepathSet { return m_forbidden_paths; } + void set_forbidden_paths(FilepathSet paths) { m_forbidden_paths = std::move(paths); } - std::filesystem::path host_to_virtual(const std::filesystem::path &path) const; - std::filesystem::path virtual_to_host(const std::filesystem::path &path) const; - std::filesystem::path virtual_to_host(const std::filesystem::path &virtual_path, const std::optional &node, std::filesystem::path::iterator iter) const; - std::optional find_virtual_node(std::filesystem::path virtual_path, std::filesystem::path::iterator &out, bool only_visible = true) const; - std::optional find_virtual_node(std::filesystem::path virtual_path, bool only_visible = true) const; + auto host_to_virtual(const std::filesystem::path &path) const -> std::filesystem::path; + auto virtual_to_host(const std::filesystem::path &path) const -> std::filesystem::path; + static auto virtual_to_host(const std::filesystem::path &virtual_path, const std::optional &node, std::filesystem::path::iterator iter) -> std::filesystem::path; + auto find_virtual_node(std::filesystem::path virtual_path, std::filesystem::path::iterator &out, bool only_visible = true) const -> std::optional; + auto find_virtual_node(std::filesystem::path virtual_path, bool only_visible = true) const -> std::optional; - bool is_forbidden(const std::filesystem::path &path) const; + auto is_forbidden(const std::filesystem::path &path) const -> bool; private: - static const FilepathSet &default_forbidden_paths(); + static auto default_forbidden_paths() -> const FilepathSet &; + + template + friend void serialize(Archive &archive, FileMapping &file_mapping, std::uint32_t version); - private: RootPathNode m_root_node; FilepathSet m_forbidden_paths = FileMapping::default_forbidden_paths(); }; diff --git a/include/FileShare/Config/KnownPeerStore.hpp b/include/FileShare/Config/KnownPeerStore.hpp new file mode 100644 index 0000000..406a2b1 --- /dev/null +++ b/include/FileShare/Config/KnownPeerStore.hpp @@ -0,0 +1,40 @@ +/* +** Project LibFileShareProtocol, 2025 +** +** Author Francois Michaut +** +** Started on Sat Aug 16 20:24:31 2025 Francois Michaut +** Last update Mon Aug 18 20:02:00 2025 Francois Michaut +** +** KnownPeerStore.hpp : Storage and retrival of known peers +*/ + +#include "FileShare/Peer/PreAuthPeer.hpp" +#include "FileShare/Utils/Strings.hpp" + +#include +#include + +// TODO: Make a custom cereal archive for known_peers : https://uscilab.github.io/cereal/serialization_archives.html#adding-more-archives +namespace FileShare { + class KnownPeerStore { + public: + // TODO: Figure out the constructor (std::string filepath = "default_path") ? + KnownPeerStore() = default; + + // Raises if uuid / public_key is not unique (skip silently if exact match) + void insert(std::string_view uuid, std::string_view public_key); + void remove(std::string_view uuid, std::string_view public_key); + + // raises if uuid / public_key dont match + auto contains(std::string_view uuid, std::string_view public_key) -> bool; + auto contains(const PreAuthPeer &peer) -> bool; + + private: + std::unordered_map> m_known_peers; + }; +} + +// TODO: Maybe store the peer certificates, so we can validate that they are not changing +// (eg: peer could bump its notAfter date all the time with the same pkey, and we would +// accept him forever) diff --git a/include/FileShare/Config/Serialization.hpp b/include/FileShare/Config/Serialization.hpp index 975d6ea..71dfd24 100644 --- a/include/FileShare/Config/Serialization.hpp +++ b/include/FileShare/Config/Serialization.hpp @@ -4,14 +4,15 @@ ** Author Francois Michaut ** ** Started on Sun Dec 10 10:56:44 2023 Francois Michaut -** Last update Sun Dec 10 17:27:40 2023 Francois Michaut +** Last update Sat Aug 23 20:42:50 2025 Francois Michaut ** ** Serialization.hpp : FileShare Config serialization functions */ #pragma once -#include "FileShare/Config.hpp" +#include "FileShare/Config/Config.hpp" +#include "FileShare/Config/ServerConfig.hpp" #include #include @@ -19,111 +20,75 @@ #include #include -static constexpr std::uint32_t file_share_config_version = 0; -static constexpr std::uint32_t file_share_file_mapping_version = 0; -static constexpr std::uint32_t file_share_path_node_version = 0; +static constexpr std::uint32_t FILE_SHARE_CONFIG_VERSION = 0; +static constexpr std::uint32_t FILE_SHARE_SERVER_CONFIG_VERSION = 0; +static constexpr std::uint32_t FILE_SHARE_FILE_MAPPING_VERSION = 0; +static constexpr std::uint32_t FILE_SHARE_PATH_NODE_VERSION = 0; -CEREAL_CLASS_VERSION(FileShare::Config, file_share_config_version); -CEREAL_CLASS_VERSION(FileShare::FileMapping, file_share_file_mapping_version); -CEREAL_CLASS_VERSION(FileShare::RootPathNode, file_share_path_node_version); -CEREAL_CLASS_VERSION(FileShare::PathNode, file_share_path_node_version); +CEREAL_CLASS_VERSION(FileShare::Config, FILE_SHARE_CONFIG_VERSION); // NOLINT +CEREAL_CLASS_VERSION(FileShare::ServerConfig, FILE_SHARE_SERVER_CONFIG_VERSION); // NOLINT +CEREAL_CLASS_VERSION(FileShare::FileMapping, FILE_SHARE_FILE_MAPPING_VERSION); // NOLINT +CEREAL_CLASS_VERSION(FileShare::RootPathNode, FILE_SHARE_PATH_NODE_VERSION); // NOLINT +CEREAL_CLASS_VERSION(FileShare::PathNode, FILE_SHARE_PATH_NODE_VERSION); // NOLINT + +// TODO: Make a custom cereal archive for .conf files : https://uscilab.github.io/cereal/serialization_archives.html#adding-more-archives +// Use .conf files for Config and ServerConfig +// Serialize UUID as a base64 encoding of the binary representation so its not easily editable namespace FileShare { template - void save(Archive &ar, const FileShare::Config &config, [[maybe_unused]] const std::uint32_t version) - { - ar( - config.get_transport_mode(), - config.get_file_mapping(), - config.get_downloads_folder(), - config.get_private_keys_dir(), - config.get_private_key_name(), - config.is_server_disabled() - ); + void serialize(Archive &archive, FileShare::ServerConfig &config, const std::uint32_t version) { + if (version > FILE_SHARE_SERVER_CONFIG_VERSION) { + throw std::runtime_error("ServerConfig file format is more recent than what this program supports"); + } + + if (version == FILE_SHARE_SERVER_CONFIG_VERSION) { + archive( + config.m_uuid, config.m_device_name, config.m_private_keys_dir, + config.m_private_key_name, config.m_disable_server + ); + } else { + throw std::runtime_error("ServerConfig file format is unsupported"); + } } template - void load(Archive &ar, FileShare::Config &config, const std::uint32_t version) - { - if (version > file_share_config_version) { + void serialize(Archive &archive, FileShare::Config &config, const std::uint32_t version) { + if (version > FILE_SHARE_CONFIG_VERSION) { throw std::runtime_error("Config file format is more recent than what this program supports"); } - FileShare::Config::TransportMode mode; - FileShare::FileMapping mapping; - std::filesystem::path downloads_folder; - std::filesystem::path private_keys_dir; - std::string private_key_name; - bool server_disabled; - - ar(mode, mapping, downloads_folder, private_keys_dir, private_key_name, server_disabled); - - config.set_transport_mode(mode) - .set_file_mapping(std::move(mapping)) - .set_downloads_folder(std::move(downloads_folder)) - .set_private_keys_dir(std::move(private_keys_dir)) - .set_private_key_name(std::move(private_key_name)) - .set_server_disabled(server_disabled); - } - - template - typename std::enable_if_t, void> - save(Archive &ar, const Node &root_node, [[maybe_unused]] const std::uint32_t version) - { - ar( - root_node.get_name_str(), - root_node.get_type(), - root_node.get_visibility(), - root_node.get_host_path(), - root_node.get_child_nodes() - ); + if (version == FILE_SHARE_CONFIG_VERSION) { + archive(config.m_transport_mode, config.m_filemap, config.m_downloads_folder); + } else { + throw std::runtime_error("Config file format is unsupported"); + } } template - typename std::enable_if_t, void> - load(Archive &ar, Node &root_node, const std::uint32_t version) - { - if (version > file_share_path_node_version) { + void serialize(Archive &archive, Node &node, const std::uint32_t version) + requires IsPathNode { + if (version > FILE_SHARE_PATH_NODE_VERSION) { throw std::runtime_error("PathNode file format is more recent than what this program supports"); } - std::string name; - PathNode::Type type; - PathNode::Visibility visibility; - std::filesystem::path host_path; - PathNode::NodeMap child_nodes; - - ar(name, type, visibility, host_path, child_nodes); - - root_node.set_name(std::move(name)); - root_node.set_type(type); - root_node.set_visibility(visibility); - root_node.set_host_path(std::move(host_path)); - root_node.set_child_nodes(std::move(child_nodes)); - } - - template - void save(Archive &ar, const FileShare::FileMapping &file_mapping, [[maybe_unused]] const std::uint32_t version) - { - ar( - file_mapping.get_root_node(), - file_mapping.get_forbidden_paths() - ); + if (version == FILE_SHARE_PATH_NODE_VERSION) { + archive(node.m_name, node.m_type, node.m_visibility, node.m_host_path, node.m_child_nodes); + } else { + throw std::runtime_error("PathNode file format is unsupported"); + } } template - void load(Archive &ar, FileShare::FileMapping &file_mapping, const std::uint32_t version) - { - if (version > file_share_file_mapping_version) { + void serialize(Archive &archive, FileShare::FileMapping &file_mapping, const std::uint32_t version) { + if (version > FILE_SHARE_FILE_MAPPING_VERSION) { throw std::runtime_error("FileMapping file format is more recent than what this program supports"); } - FileShare::RootPathNode root_node; - FileShare::FileMapping::FilepathSet forbidden_paths; - - ar(root_node, forbidden_paths); - - file_mapping.set_root_node(std::move(root_node)); - file_mapping.set_forbidden_paths(std::move(forbidden_paths)); + if (version == FILE_SHARE_FILE_MAPPING_VERSION) { + archive(file_mapping.m_root_node, file_mapping.m_forbidden_paths); + } else { + throw std::runtime_error("FileMapping file format is unsupported"); + } } } diff --git a/include/FileShare/Config/ServerConfig.hpp b/include/FileShare/Config/ServerConfig.hpp new file mode 100644 index 0000000..53b1d99 --- /dev/null +++ b/include/FileShare/Config/ServerConfig.hpp @@ -0,0 +1,73 @@ +/* +** Project LibFileShareProtocol, 2025 +** +** Author Francois Michaut +** +** Started on Wed Aug 6 15:09:50 2025 Francois Michaut +** Last update Fri Aug 22 20:41:53 2025 Francois Michaut +** +** ServerConfig.hpp : Server Configuration +*/ + +#pragma once + +#include + +namespace FileShare { + class ServerConfig { + public: + ServerConfig(); + + static constexpr std::filesystem::perms SECURE_FILE_PERMS = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write; + static constexpr std::filesystem::perms SECURE_FOLDER_PERMS = std::filesystem::perms::owner_all; + + static auto default_private_keys_dir() -> const std::filesystem::path &; + + // paths starting with '~/' will have this part replaced by the current user's home directory + static auto load(std::filesystem::path config_file = "") -> ServerConfig; + void save(std::filesystem::path config_file = "") const; + + [[nodiscard]] auto get_uuid() const -> const std::string & { return m_uuid; } + [[nodiscard]] auto get_device_name() const -> const std::string & { return m_device_name; } + auto set_device_name(std::string device_name) -> ServerConfig & { m_device_name = std::move(device_name); return *this; } + + [[nodiscard]] auto get_private_keys_dir() const -> const std::filesystem::path & { return m_private_keys_dir; } + [[nodiscard]] auto get_private_key_name() const -> const std::string & { return m_private_key_name; } + auto set_private_keys_dir(std::string_view path) -> ServerConfig &; + auto set_private_key_name(std::string name) -> ServerConfig &; + + [[nodiscard]] auto is_server_disabled() const -> bool { return m_disable_server; } + auto set_server_disabled(bool disabled) -> ServerConfig & { m_disable_server = disabled; return *this; } + + void validate_config() const; + private: + template + friend void serialize(Archive &archive, ServerConfig &config, std::uint32_t version); + + ServerConfig(std::string uuid); + + std::filesystem::path m_filepath; + + // Unique ID for this device. Needs to be globally unique. Will be generated internally. + std::string m_uuid; + // How this device will appear to others. Not required to be totally unique, + // but should be unique amongst the devices you own. + // Errors will be generated if that's not the case. + std::string m_device_name; + + // Folder where the private key/certificate will be stored/created. + // An error will be raised if it exists with unsecure permissions. + std::filesystem::path m_private_keys_dir = ServerConfig::default_private_keys_dir(); + // Name of the key/certificate file to use (without extension/prefix - will be added automatically) + std::string m_private_key_name = "file_share"; + + // If set to true, the server will not open a socket nor listen to + // incomming connections. You will still be able to connect to other + // server but they will not be able to initiate the connection. + // Set this to true if you want an extra layer of security by + // preventing external connections. + // Note that when you initiate a connection to another server it will + // be able to send commands as well. + bool m_disable_server = false; + }; +} diff --git a/include/FileShare/MessageQueue.hpp b/include/FileShare/MessageQueue.hpp index 12d5ec9..59d051a 100644 --- a/include/FileShare/MessageQueue.hpp +++ b/include/FileShare/MessageQueue.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Tue May 9 09:33:48 2023 Francois Michaut -** Last update Thu Aug 24 19:34:07 2023 Francois Michaut +** Last update Fri Aug 15 13:46:20 2025 Francois Michaut ** ** MessageQueue.hpp : A queue representing the messages sent/received and their status */ @@ -27,23 +27,26 @@ namespace FileShare { class MessageQueue { public: - using MessageMap = std::unordered_map; + using MessageMap = std::unordered_map; + + static constexpr Protocol::MessageID MAX_ID = 0xFF; MessageQueue() = default; - std::uint8_t available_send_slots() const; + auto available_send_slots() const -> std::uint8_t { return m_available_send_slots; } + + auto send_request(Protocol::Request request) -> Protocol::MessageID; // append to m_outgoing_requests + auto receive_request(Protocol::Request request) -> Protocol::MessageID; // append to m_incomming_requests + void send_reply(Protocol::MessageID request_id, Protocol::StatusCode status_code); + void receive_reply(Protocol::MessageID request_id, Protocol::StatusCode status_code); - std::uint8_t send_request(Protocol::Request request); // append to m_outgoing_requests - std::uint8_t receive_request(Protocol::Request request); // append to m_incomming_requests - void send_reply(std::uint8_t request_id, Protocol::StatusCode status_code); - void receive_reply(std::uint8_t request_id, Protocol::StatusCode status_code); + auto get_outgoing_requests() const -> const MessageMap & { return m_outgoing_requests; } + auto get_incomming_requests() const -> const MessageMap & { return m_incomming_requests; } - const MessageMap &get_outgoing_requests() const; - const MessageMap &get_incomming_requests() const; private: - std::uint8_t m_message_id = 0; + Protocol::MessageID m_message_id = 0; MessageMap m_outgoing_requests; MessageMap m_incomming_requests; - std::uint8_t m_available_send_slots = 0xFF; + std::uint8_t m_available_send_slots = MAX_ID; }; } diff --git a/include/FileShare/Peer/Peer.hpp b/include/FileShare/Peer/Peer.hpp new file mode 100644 index 0000000..c79e890 --- /dev/null +++ b/include/FileShare/Peer/Peer.hpp @@ -0,0 +1,106 @@ +/* +** Project LibFileShareProtocol, 2022 +** +** Author Francois Michaut +** +** Started on Sun Aug 28 09:23:07 2022 Francois Michaut +** Last update Sat Aug 23 00:08:01 2025 Francois Michaut +** +** Peer.hpp : Client to communicate with peers using the FileShareProtocol +*/ + +#pragma once + +#include "FileShare/Config/Config.hpp" +#include "FileShare/MessageQueue.hpp" +#include "FileShare/Peer/PeerBase.hpp" +#include "FileShare/Peer/PreAuthPeer.hpp" +#include "FileShare/TransferHandler.hpp" + +#include +#include +#include + +#include +#include + +// TODO handle UDP +namespace FileShare { + class Peer : public PeerBase { + public: + using ProgressCallback = std::function; + + Peer(PreAuthPeer &&peer, Config config = Peer::default_config()); + + // TODO: Allow copy ? What would that even mean ? + Peer(const Peer &) = delete; + Peer(Peer &&) = default; + auto operator=(const Peer &) -> Peer & = delete; + auto operator=(Peer &&) -> Peer & = default; + + ~Peer() override = default; + + // Call respond_to_request to answer to a Request Event + void respond_to_request(Protocol::Request request, Protocol::StatusCode status); + [[nodiscard]] auto pull_requests() -> std::vector; + + // Blocking functions + auto send_file(const std::string &filepath, const ProgressCallback &progress_callback = [](const std::string &, std::size_t, std::size_t) {}) -> Protocol::Response; + auto receive_file(std::string filepath, const ProgressCallback &progress_callback = [](const std::string &, std::size_t, std::size_t) {}) -> Protocol::Response; + auto list_files(std::string folderpath = "") -> Protocol::Response>; + + // TODO: Async functions + auto send_file_async(std::string filepath, const ProgressCallback &progress_callback = [](const std::string &, std::size_t, std::size_t) {}) -> Protocol::Response; + auto receive_file_async(std::string filepath, const ProgressCallback &progress_callback = [](const std::string &, std::size_t, std::size_t) {}) -> Protocol::Response; + auto list_files_async(std::string folderpath = "") -> Protocol::Response>; + + // TODO determine params + auto initiate_pairing() -> Protocol::Response; + auto accept_pairing() -> Protocol::Response; + + [[nodiscard]] auto get_config() const -> const Config & { return m_config; } + [[nodiscard]] auto get_config() -> Config & { return m_config; } + void set_config(Config config) { m_config = std::move(config); } + + private: + using UploadTransferMap = std::unordered_map; + using DownloadTransferMap = std::unordered_map; + using ListFilesTransferMap = std::unordered_map; + using FileListTransferMap = std::unordered_map; + + // TODO: Remove + [[deprecated]] auto wait_for_status(Protocol::MessageID message_id) -> Protocol::StatusCode; + + void send_reply(Protocol::MessageID message_id, Protocol::StatusCode status); + auto send_request(Protocol::CommandCode command, std::shared_ptr request_data) -> std::uint8_t; + auto send_request(Protocol::Request request) -> std::uint8_t; + + void authorize_request(Protocol::Request request) override; + auto parse_bytes(std::string_view raw_msg, Protocol::Request &out) -> std::size_t override; + + // TODO: Rename to handle_reply / process_reply / dispatch_reply ? + void receive_reply(Protocol::MessageID message_id, Protocol::StatusCode status); + + auto prepare_upload(std::filesystem::path host_filepath, std::string virtual_filepath, std::size_t packet_size, std::size_t packet_start) -> std::pair, Protocol::StatusCode>; + auto create_host_upload(std::filesystem::path host_filepath) -> UploadTransferMap::iterator; + auto create_upload(UploadTransferHandler handler) -> UploadTransferMap::iterator; + auto create_download(Protocol::MessageID request_id, const std::shared_ptr &data) -> DownloadTransferMap::iterator; + + protected: + static auto default_config() -> Config; + + private: + Config m_config; + + Protocol::Protocol m_protocol; + std::vector m_request_buffer; + MessageQueue m_message_queue; + + DownloadTransferMap m_download_transfers; + UploadTransferMap m_upload_transfers; + ListFilesTransferMap m_list_files_transfers; + FileListTransferMap m_file_list_transfers; + }; + + using Peer_ptr = std::shared_ptr; +} diff --git a/include/FileShare/Peer/PeerBase.hpp b/include/FileShare/Peer/PeerBase.hpp new file mode 100644 index 0000000..e3a6630 --- /dev/null +++ b/include/FileShare/Peer/PeerBase.hpp @@ -0,0 +1,70 @@ +/* +** Project LibFileShareProtocol, 2025 +** +** Author Francois Michaut +** +** Started on Mon Jul 28 19:12:40 2025 Francois Michaut +** Last update Fri Aug 22 13:08:11 2025 Francois Michaut +** +** PeerBase.hpp : Base of the Peer class +*/ + +#pragma once + +#include "FileShare/Protocol/Definitions.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace FileShare { + class PeerBase { + public: + virtual ~PeerBase() = default; + + PeerBase(const PeerBase &) = delete; + PeerBase(PeerBase &&) = default; + auto operator=(const PeerBase &) -> PeerBase & = delete; + auto operator=(PeerBase &&) -> PeerBase & = default; + + void disconnect(); + + [[nodiscard]] auto get_socket() const -> const CppSockets::TlsSocket & { return m_socket; } + [[nodiscard]] auto get_socket() -> CppSockets::TlsSocket & { return m_socket; } + + [[nodiscard]] auto get_device_uuid() const -> std::string_view { return m_device_uuid; } + [[nodiscard]] auto get_device_name() const -> std::string_view { return m_device_name; } + [[nodiscard]] auto get_public_key() const -> std::string_view { return m_public_key; } + + protected: + PeerBase(const CppSockets::IEndpoint &peer, CppSockets::TlsContext ctx = {}); + PeerBase(CppSockets::TlsSocket &&peer); + + virtual void authorize_request(Protocol::Request request) = 0; + virtual auto parse_bytes(std::string_view raw_msg, Protocol::Request &out) -> std::size_t = 0; + + void poll_requests(); + + auto get_buffer() -> std::string & { return m_buffer; } + + void set_device_uuid(std::string uuid) { m_device_uuid = std::move(uuid); } + void set_device_name(std::string name) { m_device_name = std::move(name); } + void set_public_key(std::string key) { m_public_key = std::move(key); } + + void read_peer_certificate(); + private: + CppSockets::TlsSocket m_socket; + + std::string m_device_uuid; + std::string m_device_name; + std::string m_public_key; + + std::string m_buffer; + }; + + using PeerBase_ptr = std::shared_ptr; +} diff --git a/include/FileShare/Peer/PreAuthPeer.hpp b/include/FileShare/Peer/PreAuthPeer.hpp new file mode 100644 index 0000000..aa7cf24 --- /dev/null +++ b/include/FileShare/Peer/PreAuthPeer.hpp @@ -0,0 +1,50 @@ +/* +** Project LibFileShareProtocol, 2025 +** +** Author Francois Michaut +** +** Started on Tue Jul 29 15:23:09 2025 Francois Michaut +** Last update Thu Aug 21 09:21:40 2025 Francois Michaut +** +** PreAuthPeer.hpp : Class to represent a Peer before it has been sucesfully Authenticated +*/ + +#pragma once + +#include "FileShare/Peer/PeerBase.hpp" +#include "FileShare/Protocol/Protocol.hpp" + +#include +#include + +namespace FileShare { + class PreAuthPeer : public PeerBase { + public: + enum Type { + CLIENT, + SERVER + }; + + PreAuthPeer(const CppSockets::IEndpoint &peer, Type type, CppSockets::TlsContext ctx = {}); + PreAuthPeer(CppSockets::TlsSocket &&peer, Type type); + + using PeerBase::poll_requests; + + void do_client_hello(); + + [[nodiscard]] auto get_type() const -> Type { return m_type; } + [[nodiscard]] auto get_protocol() const -> Protocol::Protocol; + [[nodiscard]] auto has_protocol() const -> bool { return m_protocol.has_value(); } + protected: + auto parse_bytes(std::string_view raw_msg, Protocol::Request &out) -> std::size_t override; + + private: + void authorize_request(Protocol::Request request) override; + + Type m_type; + std::optional m_protocol; + bool m_first_extra_request = true; + }; + + using PreAuthPeer_ptr = std::shared_ptr; +} diff --git a/include/FileShare/Protocol/Definitions.hpp b/include/FileShare/Protocol/Definitions.hpp index 02119db..8615405 100644 --- a/include/FileShare/Protocol/Definitions.hpp +++ b/include/FileShare/Protocol/Definitions.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sun Aug 28 09:28:47 2022 Francois Michaut -** Last update Thu Dec 7 11:06:44 2023 Francois Michaut +** Last update Fri Aug 22 22:53:48 2025 Francois Michaut ** ** Definitions.hpp : General definitions and classes */ @@ -22,9 +22,13 @@ namespace FileShare::Protocol { // TODO: rename MessageCode or similar (it is both Requests and Responses) // 1 byte - 0-0xFF - enum class CommandCode { + enum class CommandCode : std::uint8_t { RESPONSE = 0x00, + // TODO: Get theses out of the command_codes ? + SUPPORTED_VERSIONS = 0x01, + SELECTED_VERSION = 0x02, + SEND_FILE = 0x10, RECEIVE_FILE = 0x11, @@ -49,6 +53,7 @@ namespace FileShare::Protocol { // TODO: figure out next step on approval/reject ? BAD_REQUEST = 0x40, + UNAUTHORIZED = 0x41, INVALID_REQUEST_ID = 0x42, FORBIDDEN = 0x43, FILE_NOT_FOUND = 0x44, @@ -58,17 +63,19 @@ namespace FileShare::Protocol { INTERNAL_ERROR = 0x50, }; - CommandCode str_to_command(std::string str); - StatusCode str_to_status(std::string str); - - std::string command_to_str(CommandCode code); - std::string status_to_str(StatusCode code); - enum class FileType { FILE = 0x00, DIRECTORY = 0x01, }; + auto str_to_command(std::string_view str) -> CommandCode; + auto str_to_status(std::string_view str) -> StatusCode; + auto str_to_file_type(std::string_view str) -> FileType; + + auto command_to_str(CommandCode command) -> std::string_view; + auto status_to_str(StatusCode status) -> std::string_view; + auto file_type_to_str(FileType type) -> std::string_view; + using MessageID = std::uint8_t; class IRequestData; @@ -104,7 +111,9 @@ namespace FileShare::Protocol { }; } -std::ostream& operator<<(std::ostream& os, const FileShare::Protocol::StatusCode& status); -std::ostream& operator<<(std::ostream& os, const FileShare::Protocol::CommandCode& command); -std::istream& operator>>(std::istream& is, FileShare::Protocol::StatusCode& status); -std::istream& operator>>(std::istream& is, FileShare::Protocol::CommandCode& command); +auto operator<<(std::ostream& os, const FileShare::Protocol::StatusCode &status) -> std::ostream &; +auto operator<<(std::ostream& os, const FileShare::Protocol::CommandCode &command) -> std::ostream &; +auto operator<<(std::ostream& os, const FileShare::Protocol::FileType &type) -> std::ostream &; +auto operator>>(std::istream& is, FileShare::Protocol::StatusCode &status) -> std::istream &; +auto operator>>(std::istream& is, FileShare::Protocol::CommandCode &command) -> std::istream &; +auto operator>>(std::istream& is, FileShare::Protocol::FileType &type) -> std::istream &; diff --git a/include/FileShare/Protocol/Handler/v0.0.0/ProtocolHandler.hpp b/include/FileShare/Protocol/Handler/v0.0.0/ProtocolHandler.hpp index cfd3ee9..67d158b 100644 --- a/include/FileShare/Protocol/Handler/v0.0.0/ProtocolHandler.hpp +++ b/include/FileShare/Protocol/Handler/v0.0.0/ProtocolHandler.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Fri May 5 21:32:03 2023 Francois Michaut -** Last update Sat Dec 9 08:59:02 2023 Francois Michaut +** Last update Fri Aug 15 13:48:49 2025 Francois Michaut ** ** ProtocolHandler.hpp : ProtocolHandler for the v0.0.0 of the protocol */ @@ -14,35 +14,35 @@ #include "FileShare/Protocol/Protocol.hpp" #include "FileShare/Protocol/RequestData.hpp" -namespace FileShare::Protocol::Handler::v0_0_0 { +namespace FileShare::Protocol::Handler::v0_0_0 { // NOLINT(readability-identifier-naming) class ProtocolHandler : public IProtocolHandler { public: - virtual ~ProtocolHandler() = default; + ~ProtocolHandler() override = default; - std::string format_send_file(std::uint8_t message_id, const SendFileData &data) override; - std::string format_receive_file(std::uint8_t message_id, const ReceiveFileData &data) override; - std::string format_list_files(std::uint8_t message_id, const ListFilesData &data) override; + auto format_send_file(std::uint8_t message_id, const SendFileData &data) -> std::string override; + auto format_receive_file(std::uint8_t message_id, const ReceiveFileData &data) -> std::string override; + auto format_list_files(std::uint8_t message_id, const ListFilesData &data) -> std::string override; - std::string format_file_list(std::uint8_t message_id, const FileListData &data) override; - std::string format_data_packet(std::uint8_t message_id, const DataPacketData &data) override; - std::string format_ping(std::uint8_t message_id, const PingData &data) override; + auto format_file_list(std::uint8_t message_id, const FileListData &data) -> std::string override; + auto format_data_packet(std::uint8_t message_id, const DataPacketData &data) -> std::string override; + auto format_ping(std::uint8_t message_id, const PingData &data) -> std::string override; - std::string format_response(std::uint8_t message_id, const ResponseData &data) override; + auto format_response(std::uint8_t message_id, const ResponseData &data) -> std::string override; - std::string format_request(const Request &request) override; - std::size_t parse_request(std::string_view raw_msg, Request &out) override; + auto format_request(const Request &request) -> std::string override; + auto parse_request(std::string_view raw_msg, Request &out) -> std::size_t override; private: - const static std::size_t packet_size = 4096; // TODO experiment with this - const static std::size_t base_header_size = 6; - - std::shared_ptr get_request_data(CommandCode cmd, std::string_view payload); - - std::shared_ptr parse_response(std::string_view payload); - std::shared_ptr parse_send_file(std::string_view payload); - std::shared_ptr parse_receive_file(std::string_view payload); - std::shared_ptr parse_list_files(std::string_view payload); - std::shared_ptr parse_file_list(std::string_view payload); - std::shared_ptr parse_data_packet(std::string_view payload); - std::shared_ptr parse_ping(std::string_view payload); + constexpr static std::size_t PACKET_SIZE = 4096; // TODO experiment with this + constexpr static std::size_t BASE_HEADER_SIZE = 6; + + auto get_request_data(CommandCode cmd, std::string_view payload) -> std::shared_ptr; + + auto parse_response(std::string_view payload) -> std::shared_ptr; + auto parse_send_file(std::string_view payload) -> std::shared_ptr; + auto parse_receive_file(std::string_view payload) -> std::shared_ptr; + auto parse_list_files(std::string_view payload) -> std::shared_ptr; + auto parse_file_list(std::string_view payload) -> std::shared_ptr; + auto parse_data_packet(std::string_view payload) -> std::shared_ptr; + auto parse_ping(std::string_view payload) -> std::shared_ptr; }; } diff --git a/include/FileShare/Protocol/Protocol.hpp b/include/FileShare/Protocol/Protocol.hpp index 47ca724..bc3298a 100644 --- a/include/FileShare/Protocol/Protocol.hpp +++ b/include/FileShare/Protocol/Protocol.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Thu Aug 25 22:59:37 2022 Francois Michaut -** Last update Thu Aug 24 09:41:27 2023 Francois Michaut +** Last update Fri Aug 15 13:57:44 2025 Francois Michaut ** ** Protocol.hpp : Main class to interract with the protocol */ @@ -13,8 +13,8 @@ #include "FileShare/Protocol/RequestData.hpp" #include "FileShare/Protocol/Version.hpp" -#include "FileShare/Utils/FileHash.hpp" +#include #include #include #include @@ -22,20 +22,27 @@ namespace FileShare::Protocol { class IProtocolHandler { public: - static constexpr char const * const magic_bytes = "FSP_"; + static constexpr char const * const MAGIC_BYTES = "FSP_"; - virtual std::string format_send_file(std::uint8_t message_id, const SendFileData &data) = 0; - virtual std::string format_receive_file(std::uint8_t message_id, const ReceiveFileData &data) = 0; - virtual std::string format_list_files(std::uint8_t message_id, const ListFilesData &data) = 0; + virtual ~IProtocolHandler() = default; - virtual std::string format_file_list(std::uint8_t message_id, const FileListData &data) = 0; - virtual std::string format_data_packet(std::uint8_t message_id, const DataPacketData &data) = 0; - virtual std::string format_ping(std::uint8_t message_id, const PingData &data) = 0; + static auto format_client_version_list() -> std::string_view; + static auto format_server_selected_version(Version version) -> std::string; + static auto parse_client_version(std::string_view raw_msg, Request &out) -> std::size_t; + static auto parse_server_version(std::string_view raw_msg, Request &out) -> std::size_t; - virtual std::string format_response(std::uint8_t message_id, const ResponseData &data) = 0; + virtual auto format_send_file(std::uint8_t message_id, const SendFileData &data) -> std::string = 0; + virtual auto format_receive_file(std::uint8_t message_id, const ReceiveFileData &data) -> std::string = 0; + virtual auto format_list_files(std::uint8_t message_id, const ListFilesData &data) -> std::string = 0; - virtual std::string format_request(const Request &request) = 0; - virtual std::size_t parse_request(std::string_view raw_msg, Request &out) = 0; + virtual auto format_file_list(std::uint8_t message_id, const FileListData &data) -> std::string = 0; + virtual auto format_data_packet(std::uint8_t message_id, const DataPacketData &data) -> std::string = 0; + virtual auto format_ping(std::uint8_t message_id, const PingData &data) -> std::string = 0; + + virtual auto format_response(std::uint8_t message_id, const ResponseData &data) -> std::string = 0; + + virtual auto format_request(const Request &request) -> std::string = 0; + virtual auto parse_request(std::string_view raw_msg, Request &out) -> std::size_t = 0; }; class Protocol { @@ -43,15 +50,17 @@ namespace FileShare::Protocol { Protocol(Version version); Protocol(Version::VersionEnum version); + // TODO: Assignment Operator overloaded for Version. Calls set_version() + void set_version(Version version); - Version version() const { return m_version; } + [[nodiscard]] auto version() const -> Version { return m_version; } - bool operator==(const Protocol &) const = default; + auto operator==(const Protocol &) const -> bool = default; auto operator<=>(const Protocol&) const = default; - IProtocolHandler &handler() const { return *m_handler; } + [[nodiscard]] auto handler() const -> IProtocolHandler & { return *m_handler; } - static std::map> protocol_list; + const static std::map> PROTOCOL_LIST; private: Version m_version; std::shared_ptr m_handler; diff --git a/include/FileShare/Protocol/RequestData.hpp b/include/FileShare/Protocol/RequestData.hpp index eb0ea04..042b76f 100644 --- a/include/FileShare/Protocol/RequestData.hpp +++ b/include/FileShare/Protocol/RequestData.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sun Jul 16 11:25:51 2023 Francois Michaut -** Last update Sat Dec 9 18:54:32 2023 Francois Michaut +** Last update Thu Aug 14 19:36:01 2025 Francois Michaut ** ** RequestData.hpp : RequestData interface. Subclasses will represent every request payload */ @@ -12,31 +12,57 @@ #pragma once #include "FileShare/Protocol/Definitions.hpp" +#include "FileShare/Protocol/Version.hpp" #include "FileShare/Utils/FileHash.hpp" namespace FileShare::Protocol { class IRequestData { public: - virtual std::string debug_str() const = 0; + virtual ~IRequestData() = default; + + [[nodiscard]] virtual auto debug_str() const -> std::string = 0; }; class ResponseData : public IRequestData { public: - ResponseData() = default; ResponseData(StatusCode status); - virtual ~ResponseData() = default; + ~ResponseData() override = default; + + [[nodiscard]] auto debug_str() const -> std::string override; - std::string debug_str() const override; StatusCode status; }; + class SupportedVersionsData : public IRequestData { + public: + SupportedVersionsData(std::vector versions); + ~SupportedVersionsData() override = default; + + [[nodiscard]] auto debug_str() const -> std::string override; + + std::vector versions; + }; + + class SelectedVersionData : public IRequestData { + public: + SelectedVersionData(Version version); + ~SelectedVersionData() override = default; + + [[nodiscard]] auto debug_str() const -> std::string override; + + Version version; + }; + class SendFileData : public IRequestData { public: - SendFileData() = default; - SendFileData(std::string filepath, Utils::HashAlgorithm hash_algorithm, std::string filehash, std::filesystem::file_time_type last_updated, std::size_t packet_size, std::size_t total_packets); - virtual ~SendFileData() = default; + SendFileData( + std::string filepath, Utils::HashAlgorithm hash_algorithm, std::string filehash, + std::filesystem::file_time_type last_updated, std::size_t packet_size, + std::size_t total_packets + ); + ~SendFileData() override = default; - std::string debug_str() const override; + [[nodiscard]] auto debug_str() const -> std::string override; std::string filepath; Utils::HashAlgorithm hash_algorithm; @@ -48,11 +74,10 @@ namespace FileShare::Protocol { class ReceiveFileData : public IRequestData { public: - ReceiveFileData() = default; ReceiveFileData(std::string filepath, std::size_t packet_size, std::size_t packet_start); - virtual ~ReceiveFileData() = default; + ~ReceiveFileData() override = default; - std::string debug_str() const override; + [[nodiscard]] auto debug_str() const -> std::string override; std::string filepath; std::size_t packet_size; @@ -61,21 +86,20 @@ namespace FileShare::Protocol { class ListFilesData : public IRequestData { public: - ListFilesData(std::string folderpath = ""); - virtual ~ListFilesData() = default; + ListFilesData(std::string folderpath); + ~ListFilesData() override = default; - std::string debug_str() const override; + [[nodiscard]] auto debug_str() const -> std::string override; std::string folderpath; }; class FileListData : public IRequestData { public: - FileListData() = default; - FileListData(std::uint8_t request_id, std::size_t packet_id, std::vector files = {}); - virtual ~FileListData() = default; + FileListData(std::uint8_t request_id, std::size_t packet_id, std::vector files); + ~FileListData() override = default; - std::string debug_str() const override; + [[nodiscard]] auto debug_str() const -> std::string override; std::uint8_t request_id; std::size_t packet_id; @@ -84,11 +108,10 @@ namespace FileShare::Protocol { class DataPacketData : public IRequestData { public: - DataPacketData() = default; DataPacketData(std::uint8_t request_id, std::size_t packet_id, std::string data); - virtual ~DataPacketData() = default; + ~DataPacketData() override = default; - std::string debug_str() const override; + [[nodiscard]] auto debug_str() const -> std::string override; std::uint8_t request_id; std::size_t packet_id; @@ -98,18 +121,18 @@ namespace FileShare::Protocol { class PingData : public IRequestData { public: PingData() = default; - virtual ~PingData() = default; + ~PingData() override = default; - std::string debug_str() const override; + [[nodiscard]] auto debug_str() const -> std::string override; }; + // TODO: Currently unused class ApprovalStatusData : public IRequestData { public: - ApprovalStatusData() = default; ApprovalStatusData(std::uint8_t request_message_id, bool status); - virtual ~ApprovalStatusData() = default; + ~ApprovalStatusData() override = default; - std::string debug_str() const override; + [[nodiscard]] auto debug_str() const -> std::string override; std::uint8_t request_message_id; bool status; diff --git a/include/FileShare/Protocol/Version.hpp b/include/FileShare/Protocol/Version.hpp index 8ed69b0..d351e40 100644 --- a/include/FileShare/Protocol/Version.hpp +++ b/include/FileShare/Protocol/Version.hpp @@ -4,13 +4,16 @@ ** Author Francois Michaut ** ** Started on Fri May 5 19:42:09 2023 Francois Michaut -** Last update Wed Jul 19 19:54:56 2023 Francois Michaut +** Last update Sun Aug 24 11:20:56 2025 Francois Michaut ** ** Version.hpp : A class to represent a Protocol Version */ #pragma once +#include +#include + #include #include #include @@ -21,22 +24,31 @@ namespace FileShare::Protocol { // major version is in the highest 2hex digits // minor version is in the middle 2hex digits // patch version is in the last 2hex digits - enum VersionEnum { - v0_0_0 = 0x000000 + enum VersionEnum : std::uint32_t { + v0_0_0 = 0x000000, // v0_0_1 = 0x000001, - // v0_1_0 = 0x000100 + // v0_1_0 = 0x000100, + + MIN = v0_0_0, + MAX = v0_0_0 }; Version(VersionEnum version); constexpr operator VersionEnum() const { return m_version; } explicit operator bool() const = delete; - bool operator==(const Version &) const; - std::strong_ordering operator<=>(const Version&) const; + auto operator==(const Version &) const -> bool; + auto operator<=>(const Version&) const -> std::strong_ordering; + + [[nodiscard]] auto major() const -> std::uint8_t { return m_versions[0]; }; + [[nodiscard]] auto minor() const -> std::uint8_t { return m_versions[1]; }; + [[nodiscard]] auto patch() const -> std::uint8_t { return m_versions[2]; }; + + [[nodiscard]] auto to_string() const -> std::string_view; - std::uint8_t major() const { return m_versions[0]; }; - std::uint8_t minor() const { return m_versions[1]; }; - std::uint8_t patch() const { return m_versions[2]; }; + inline static constexpr auto NAMES = frozen::make_unordered_map({ + {v0_0_0, "v0.0.0"} + }); private: VersionEnum m_version; std::array m_versions; diff --git a/include/FileShare/Server.hpp b/include/FileShare/Server.hpp index e738dfe..e29f462 100644 --- a/include/FileShare/Server.hpp +++ b/include/FileShare/Server.hpp @@ -4,84 +4,152 @@ ** Author Francois Michaut ** ** Started on Mon Aug 29 19:01:51 2022 Francois Michaut -** Last update Sat Nov 11 23:34:39 2023 Francois Michaut +** Last update Fri Aug 22 00:39:26 2025 Francois Michaut ** -** Server.hpp : Server part used to receive qnd process requests of Clients +** Server.hpp : Server part used to receive qnd process requests of Peers */ #pragma once +#include "FileShare/Config/Config.hpp" +#include "FileShare/Config/KnownPeerStore.hpp" +#include "FileShare/Config/ServerConfig.hpp" +#include "FileShare/Peer/Peer.hpp" +#include "FileShare/Peer/PreAuthPeer.hpp" +#include "FileShare/Protocol/Definitions.hpp" + +#include +#include +#include +#include + +#include #include +#include #include -#include "FileShare/Protocol/Definitions.hpp" -#include "FileShare/Config.hpp" -#include "FileShare/Client.hpp" +struct pollfd; namespace FileShare { class Server { public: - using ClientAcceptCallback = std::function &client)>; - using ClientRequestCallback = std::function &peer, Protocol::Request &req)>; - class Event { public: + enum Type { + NONE, + CONNECT, + REQUEST + }; + + Event(Type, PeerBase_ptr, std::optional = {}); Event() = default; - Event(std::shared_ptr, std::optional); + ~Event() = default; + + Event(const Event &) = default; + Event(Event &&) = default; + auto operator=(const Event &) -> Event & = default; + auto operator=(Event &&) -> Event & = default; - std::shared_ptr &client(); - std::optional &request(); + auto type() -> Type & { return m_type; } + auto peer() -> PeerBase_ptr & { return m_peer; } + auto request() -> std::optional & { return m_request; } private: - std::shared_ptr m_client; + Type m_type = NONE; + PeerBase_ptr m_peer; std::optional m_request; }; - Server(std::shared_ptr server_endpoint = Server::default_endpoint(), Config config = Server::default_config()); - Server(Config config); + using PeerAcceptCallback = std::function; + using PeerRequestCallback = std::function; + using PeerRequestEventCallback = std::function; - // TODO: Server will handle the ProtocolVersion negotiation + Peer verification - std::shared_ptr &connect(CppSockets::TlsSocket peer); - std::shared_ptr &connect(const CppSockets::IEndpoint &peer); - std::shared_ptr &connect(CppSockets::TlsSocket peer, const Config &config); - std::shared_ptr &connect(const CppSockets::IEndpoint &peer, const Config &config); + using PeerMap = std::unordered_map; + using PreAuthPeerMap = std::unordered_map; - void accept_client(std::shared_ptr peer); + using FdVector = std::vector; - // Warning : changing the server configuration does NOT change the - // already connected Clients, only new ones. You need to manually update - // the configuration of each client. - Config &get_config(); - const Config &get_config() const; - void set_config(const Config &config); - static Config default_config(); + Server( + std::shared_ptr server_endpoint = Server::default_endpoint(), + ServerConfig config = Server::default_config(), + Config peer_config = Server::default_peer_config() + ); + Server(ServerConfig config, Config peer_config = Server::default_peer_config()); - const CppSockets::IEndpoint &get_server_endpoint() const; - const CppSockets::TlsSocket &get_socket() const; + ~Server(); + + // TODO: Allow copy ? What would that even mean ? + Server(const Server &) = delete; + Server(Server &&) = default; + auto operator=(const Server &) -> Server & = delete; + auto operator=(Server &&) -> Server & = default; // Call one of theses in a loop in your main program ! // Otherwise server won't accept incomming connections or process // incomming/outgoing messages. - void process_events(ClientAcceptCallback accept_cb, ClientRequestCallback request_cb); - bool pull_event(Event &result); // TODO: figure out how to accept commands here + void process_events(const PeerAcceptCallback &accept_cb, const PeerRequestCallback &request_cb); + void process_events(const PeerAcceptCallback &accept_cb, const PeerRequestEventCallback &request_cb); + auto pull_event(Event &result) -> bool; // TODO: figure out how to accept commands here + + // TODO: Server will handle the ProtocolVersion negotiation + Peer verification + auto connect(CppSockets::TlsSocket peer) -> Peer_ptr & { return connect(std::move(peer), this->m_peer_config); } + auto connect(const CppSockets::IEndpoint &peer) -> Peer_ptr & { return connect(peer, this->m_peer_config); } + auto connect(CppSockets::TlsSocket peer, const Config &config) -> Peer_ptr &; + auto connect(const CppSockets::IEndpoint &peer, const Config &config) -> Peer_ptr &; + + void accept_peer(PreAuthPeer_ptr peer, bool temporary_trust = false); + + auto get_config() -> ServerConfig & { return m_config; } + auto get_config() const -> const ServerConfig & { return m_config; } + void set_config(const ServerConfig &config) { m_config = config; } + + // Warning : changing the default peer configuration does NOT + // change the already connected Peers, only new ones. You need to + // manually update the configuration of each existing peer. + auto get_peer_config() -> Config & { return m_peer_config; } + auto get_peer_config() const -> const Config & { return m_peer_config; } + void set_peer_config(const Config &config) { m_peer_config = config; } + + static auto default_config() -> ServerConfig; + static auto default_peer_config() -> Config; - std::map> &get_clients(); - const std::map> &get_clients() const; + auto get_server_endpoint() const -> const CppSockets::IEndpoint & { return *m_server_endpoint; } + auto get_socket() const -> const CppSockets::TlsSocket & { return m_socket; } + + auto get_peers() const -> const PeerMap & { return m_peers; } + auto get_pending_peers() const -> const PreAuthPeerMap & { return m_pending_authorization_peers; } + + auto get_poll_fds() const -> const FdVector & { return m_fds; } void restart(); - bool disabled() const; + auto disabled() const -> bool { return m_config.is_server_disabled(); } + void set_disabled(bool disabled); private: - void initialize_download_directory(); void initialize_private_key(); void poll_events(); - bool handle_client_events(std::shared_ptr &client); - std::shared_ptr &insert_client(std::shared_ptr client); - static std::shared_ptr default_endpoint(); + auto handle_peer_events(FdVector::iterator iter) -> FdVector::iterator; + auto delete_peer(FdVector::iterator iter) -> FdVector::iterator; + auto delete_pre_auth_peer(FdVector::iterator iter, PreAuthPeerMap &map) -> FdVector::iterator; + auto insert_peer(PreAuthPeer_ptr &&peer) -> Peer_ptr &; + auto insert_peer(Peer_ptr peer) -> Peer_ptr &; + + static auto default_endpoint() -> std::shared_ptr; std::shared_ptr m_server_endpoint; + CppSockets::TlsContext m_ctx; CppSockets::TlsSocket m_socket; - Config m_config; - std::map> m_clients; + ServerConfig m_config; + Config m_peer_config; + + // TODO: Move the Peers management to a different class + KnownPeerStore m_known_peers; + PreAuthPeerMap m_handshake_peers; + PreAuthPeerMap m_pending_authorization_peers; + PeerMap m_peers; + + FdVector m_fds; std::vector m_events; }; } diff --git a/include/FileShare/TransferHandler.hpp b/include/FileShare/TransferHandler.hpp index 4608632..840ff88 100644 --- a/include/FileShare/TransferHandler.hpp +++ b/include/FileShare/TransferHandler.hpp @@ -4,12 +4,12 @@ ** Author Francois Michaut ** ** Started on Thu Aug 24 08:51:14 2023 Francois Michaut -** Last update Sat Dec 9 08:49:52 2023 Francois Michaut +** Last update Sat Aug 23 11:26:19 2025 Francois Michaut ** ** TransferHandler.hpp : Classes to handle the file transfers */ -#include "FileShare/Config.hpp" +#include "FileShare/Config/FileMapping.hpp" #include "FileShare/Protocol/RequestData.hpp" #include @@ -17,14 +17,23 @@ namespace FileShare { class ITransferHandler { public: - virtual bool finished() const = 0; + virtual ~ITransferHandler() = default; + + ITransferHandler() = default; + + ITransferHandler(const ITransferHandler &) = default; + ITransferHandler(ITransferHandler &&) = default; + auto operator=(const ITransferHandler &) -> ITransferHandler & = default; + auto operator=(ITransferHandler &&) -> ITransferHandler & = default; + + [[nodiscard]] virtual auto finished() const -> bool = 0; }; class IFileTransferHandler : public ITransferHandler { public: - std::size_t get_current_size() const; - std::size_t get_total_size() const; - std::shared_ptr get_original_request() const; + [[nodiscard]] auto get_current_size() const -> std::size_t; + [[nodiscard]] auto get_total_size() const -> std::size_t; + [[nodiscard]] auto get_original_request() const -> std::shared_ptr; protected: std::size_t m_transferred_size = 0; std::shared_ptr m_original_request; @@ -33,15 +42,16 @@ namespace FileShare { class DownloadTransferHandler : public IFileTransferHandler { public: DownloadTransferHandler(std::string destination_filename, std::shared_ptr original_request); - virtual ~DownloadTransferHandler() = default; + ~DownloadTransferHandler() override = default; void receive_packet(const Protocol::DataPacketData &data); - bool finished() const override; + bool m_keep = false; // TODO HACK: find a REAL solution + + auto finished() const -> bool override; private: void finish_transfer(); - private: std::string m_filename; std::string m_temp_filename; @@ -52,15 +62,15 @@ namespace FileShare { class UploadTransferHandler : public IFileTransferHandler { public: - UploadTransferHandler(std::string filepath, std::shared_ptr original_request, std::size_t packet_start); + UploadTransferHandler(const std::string &filepath, std::shared_ptr original_request, std::size_t packet_start); UploadTransferHandler(UploadTransferHandler &&other) noexcept = default; - virtual ~UploadTransferHandler() = default; + ~UploadTransferHandler() override = default; - UploadTransferHandler &operator=(UploadTransferHandler &&other) noexcept = default; + auto operator=(UploadTransferHandler &&other) noexcept -> UploadTransferHandler & = default; - std::shared_ptr get_next_packet(Protocol::MessageID original_request_id); + auto get_next_packet(Protocol::MessageID original_request_id) -> std::shared_ptr; - bool finished() const override; + auto finished() const -> bool override; private: std::size_t m_packet_id = 0; std::ifstream m_file; @@ -69,18 +79,18 @@ namespace FileShare { class ListFilesTransferHandler : public ITransferHandler { public: ListFilesTransferHandler(std::filesystem::path requested_path, FileMapping &file_mapping, std::size_t packet_size); - virtual ~ListFilesTransferHandler() = default; + ~ListFilesTransferHandler() override = default; - std::shared_ptr get_next_packet(Protocol::MessageID original_request_id); + auto get_next_packet(Protocol::MessageID original_request_id) -> std::shared_ptr; - bool finished() const override; + [[nodiscard]] auto finished() const -> bool override; private: std::filesystem::path m_requested_path; FileMapping &m_file_mapping; std::optional m_path_node; std::filesystem::directory_iterator m_directory_iterator; - PathNode::NodeMap::iterator m_node_iterator; + PathNode::NodeMap::const_iterator m_node_iterator; std::size_t m_packet_size; std::size_t m_current_id = 0; @@ -90,12 +100,12 @@ namespace FileShare { class FileListTransferHandler : public ITransferHandler { public: FileListTransferHandler() = default; - virtual ~FileListTransferHandler() = default; + ~FileListTransferHandler() override = default; void receive_packet(Protocol::FileListData data); - bool finished() const override; - [[nodiscard]] const std::vector &get_file_list() const; + [[nodiscard]] auto finished() const -> bool override; + [[nodiscard]] auto get_file_list() const -> const std::vector & { return m_file_list; } private: std::vector m_file_list; diff --git a/include/FileShare/Utils/Path.hpp b/include/FileShare/Utils/Path.hpp index fb269d9..c69f784 100644 --- a/include/FileShare/Utils/Path.hpp +++ b/include/FileShare/Utils/Path.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Thu Oct 13 18:59:40 2022 Francois Michaut -** Last update Thu Nov 23 08:56:46 2023 Francois Michaut +** Last update Wed Aug 6 16:14:08 2025 Francois Michaut ** ** Path.hpp : Utilities to manpulate paths in a cross plateform way */ @@ -15,11 +15,13 @@ #include namespace FileShare::Utils { - std::filesystem::path home_directoy(); - std::filesystem::path home_directoy(const std::string &user); - std::filesystem::path resolve_home_component(const std::filesystem::path &path); - std::vector resolve_home_components(const std::vector &paths); + auto home_directoy() -> std::filesystem::path; + auto home_directoy(const std::string &user) -> std::filesystem::path; + auto resolve_home_component(const std::filesystem::path &path) -> std::filesystem::path; + auto resolve_home_components(const std::vector &paths) -> std::vector; - bool path_contains_folder(const std::filesystem::path &path, const std::filesystem::path &folder); - std::filesystem::path::iterator find_folder_in_path(const std::filesystem::path &path, const std::filesystem::path &folder); + auto path_contains_folder(const std::filesystem::path &path, const std::filesystem::path &folder) -> bool; + auto find_folder_in_path(const std::filesystem::path &path, const std::filesystem::path &folder) -> std::filesystem::path::iterator; + + auto make_temporary_file(const std::string &basename) -> std::pair; } diff --git a/include/FileShare/Utils/Poll.hpp b/include/FileShare/Utils/Poll.hpp new file mode 100644 index 0000000..95c4508 --- /dev/null +++ b/include/FileShare/Utils/Poll.hpp @@ -0,0 +1,28 @@ +/* +** Project LibFileShareProtocol, 2025 +** +** Author Francois Michaut +** +** Started on Fri Jul 25 18:19:49 2025 Francois Michaut +** Last update Mon Aug 18 16:26:06 2025 Francois Michaut +** +** Poll.hpp : Cross-Plateform poll implementation +*/ + +#pragma once + +#include + +#include + +#ifdef OS_UNIX + #include +#else // Windows only + using nfds_t=std::size_t; +#endif + +namespace FileShare::Utils { + // TODO: Add support for Signals + auto poll(std::vector &fds, const struct timespec *timeout = nullptr) -> int; + auto poll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout = nullptr) -> int; +} diff --git a/include/FileShare/Utils/Serialize.hpp b/include/FileShare/Utils/Serialize.hpp index 3b0e634..810688f 100644 --- a/include/FileShare/Utils/Serialize.hpp +++ b/include/FileShare/Utils/Serialize.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sun May 14 21:46:04 2023 Francois Michaut -** Last update Tue Nov 28 13:41:33 2023 Francois Michaut +** Last update Fri Aug 15 20:46:49 2025 Francois Michaut ** ** Serialize.hpp : Utilities to serialize numbers */ @@ -16,8 +16,8 @@ #include namespace FileShare::Utils { - std::string serialize(std::int64_t); - std::string serialize(std::uint64_t); + auto serialize(std::int64_t) -> std::string; + auto serialize(std::uint64_t) -> std::string; void parse(std::string_view input, std::int64_t &output); void parse(std::string_view input, std::uint64_t &output); diff --git a/include/FileShare/Utils/StringHash.hpp b/include/FileShare/Utils/StringHash.hpp deleted file mode 100644 index 5ec5146..0000000 --- a/include/FileShare/Utils/StringHash.hpp +++ /dev/null @@ -1,25 +0,0 @@ -/* -** Project LibFileShareProtocol, 2023 -** -** Author Francois Michaut -** -** Started on Sun Nov 19 13:31:02 2023 Francois Michaut -** Last update Sun Nov 19 13:31:20 2023 Francois Michaut -** -** StringHash.hpp : Utility hash class for string+string view -*/ - -#include -#include - -namespace FileShare::Utils { - struct string_hash { - using hash_type = std::hash; - using is_transparent = void; - - std::size_t operator()(const char* str) const { return hash_type{}(str); } - std::size_t operator()(std::string_view str) const { return hash_type{}(str); } - std::size_t operator()(const std::string &str) const { return hash_type{}(str); } - std::size_t operator()(const std::filesystem::path &str) const { return hash_type{}(str.string().c_str()); } - }; -} diff --git a/include/FileShare/Utils/Strings.hpp b/include/FileShare/Utils/Strings.hpp new file mode 100644 index 0000000..b185e05 --- /dev/null +++ b/include/FileShare/Utils/Strings.hpp @@ -0,0 +1,125 @@ +/* +** Project LibFileShareProtocol, 2023 +** +** Author Francois Michaut +** +** Started on Sun Nov 19 13:31:02 2023 Francois Michaut +** Last update Mon Aug 11 10:24:57 2025 Francois Michaut +** +** Strings.hpp : Utility classes for string+string view +*/ + +#pragma once + +#include +#include +#include + +namespace FileShare::Utils { + struct string_hash { + using hash_type = std::hash; + using is_transparent = void; + + auto operator()(const char* str) const -> std::size_t { return hash_type{}(str); } + auto operator()(std::string_view str) const -> std::size_t { return hash_type{}(str); } + auto operator()(const std::string &str) const -> std::size_t { return hash_type{}(str); } + auto operator()(const std::filesystem::path &str) const -> std::size_t { return hash_type{}(str.string().c_str()); } + }; + + template + requires(sizeof(CharT) <= sizeof(wchar_t)) + struct ci_char_traits : public std::char_traits { + public: + static auto eq(CharT char1, CharT char2) -> bool { return to_lower(char1) == to_lower(char2); } + static auto ne(CharT char1, CharT char2) -> bool { return to_lower(char1) != to_lower(char2); } + static auto lt(CharT char1, CharT char2) -> bool { return to_lower(char1) < to_lower(char2); } + + static auto compare(const CharT* str1, const CharT* str2, size_t n) -> int { + while (n-- != 0) { + CharT char1 = to_lower(*str1); + CharT char2 = to_lower(*str2); + + if (char1 < char2) { + return -1; + } + if (char1 > char2) { + return 1; + } + + ++str1; + ++str2; + } + return 0; + } + + static auto find(const CharT* str, std::size_t n, CharT chr) -> const CharT* { + while (n-- > 0 && to_lower(*str) != to_lower(chr)) { + ++str; + } + return str; + } + + private: + constexpr static auto to_lower(CharT chr) -> CharT { + if constexpr (sizeof(CharT) <= sizeof(char)) { + return static_cast(std::tolower(static_cast(chr))); + } else { + return static_cast(std::towlower(static_cast(chr))); + } + } + }; + + template + using basic_ci_string = std::basic_string>; + + template + auto operator>>(std::istream &is, basic_ci_string &obj) -> std::istream & { + return is >> reinterpret_cast &>(obj); + } + + template + auto operator<<(std::ostream &os, const basic_ci_string &obj) -> std::ostream & { + return os << reinterpret_cast &>(obj); + } + + template + auto operator==(const basic_ci_string &lhs, const std::basic_string &rhs) noexcept -> bool { + return lhs == reinterpret_cast &>(rhs); + } + + // TODO: Add < > <= >= != if needed, and complete the following ones : + + // template + // basic_ci_string operator+(const std::basic_string &lhs, const std::basic_string &rhs) { + // } + + // template + // ??? operator+(const std::basic_string &lhs, const std::basic_string &rhs) { + // } + + // template + // void swap(basic_ci_string &lhs, basic_ci_string &rhs); + + // template + // void swap(basic_ci_string &lhs, std::basic_string &rhs); + + // template + // std::basic_istream &getline(std::basic_istream &&input, basic_ci_string &str, CharT delim = '\n'); + + template + using basic_ci_string_view = std::basic_string_view>; + + using ci_string = basic_ci_string; + // NOTE: Theses are completely untested. + using wci_string = basic_ci_string; + using u8ci_string = basic_ci_string; + using u16ci_string = basic_ci_string; + using u32ci_string = basic_ci_string; + + using ci_string_view = basic_ci_string_view; + // NOTE: Theses are completely untested. + using wci_string_view = basic_ci_string_view; + using u8ci_string_view = basic_ci_string_view; + using u16ci_string_view = basic_ci_string_view; + using u32ci_string_view = basic_ci_string_view; +} diff --git a/include/FileShare/Utils/Time.hpp b/include/FileShare/Utils/Time.hpp index 8f4f06a..6c89836 100644 --- a/include/FileShare/Utils/Time.hpp +++ b/include/FileShare/Utils/Time.hpp @@ -4,21 +4,21 @@ ** Author Francois Michaut ** ** Started on Tue Jul 18 08:17:11 2023 Francois Michaut -** Last update Tue Nov 28 13:30:18 2023 Francois Michaut +** Last update Thu Aug 14 19:27:50 2025 Francois Michaut ** ** Time.hpp : Time utilities */ #pragma once -#include - #include +#include + namespace FileShare::Utils { // TODO: see if this is portable / reliable template - std::uint64_t to_epoch(std::chrono::time_point time) { + auto to_epoch(std::chrono::time_point time) -> std::uint64_t { #ifdef OS_APPLE auto system_time = std::chrono::file_clock::to_sys(time); #else @@ -30,7 +30,7 @@ namespace FileShare::Utils { } template - std::chrono::time_point from_epoch(std::uint64_t epoch) { + auto from_epoch(std::uint64_t epoch) -> std::chrono::time_point { auto duration = std::chrono::seconds(epoch); std::chrono::time_point time_point(duration); diff --git a/include/FileShare/Utils/VarInt.hpp b/include/FileShare/Utils/VarInt.hpp index 10017d8..6d6b26e 100644 --- a/include/FileShare/Utils/VarInt.hpp +++ b/include/FileShare/Utils/VarInt.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sat May 6 12:40:55 2023 Francois Michaut -** Last update Sun May 14 14:48:15 2023 Francois Michaut +** Last update Thu Aug 14 14:20:52 2025 Francois Michaut ** ** VarInt.hpp : Variable size integer (similar to BigInt in the sense that ** it is infinite, but without arithmetic support) @@ -28,24 +28,24 @@ namespace FileShare::Utils { VarInt(const VarInt &); VarInt(VarInt &&) = default; - VarInt &operator=(const VarInt &); - VarInt &operator=(VarInt &&) = default; + auto operator=(const VarInt &) -> VarInt &; + auto operator=(VarInt &&) -> VarInt & = default; void reset(std::size_t); - bool parse(std::string_view input); - bool parse(std::string_view input, std::string_view &output); + auto parse(std::string_view input) -> bool; + auto parse(std::string_view input, std::string_view &output) -> bool; - std::string_view to_string() const; - std::size_t to_number() const; - std::size_t byte_size() const; + auto to_string() const -> std::string_view { return m_string; } + auto to_number() const -> std::size_t; + auto byte_size() const -> std::size_t { return m_values.size(); } #ifdef DEBUG void debug() const; static void debug(std::string_view str); #endif - std::strong_ordering operator<=>(const VarInt &other) const; - bool operator==(const VarInt &other) const = default; + auto operator<=>(const VarInt &other) const -> std::strong_ordering; + auto operator==(const VarInt &other) const -> bool = default; private: void reset_string_view() const; void reset_number_view() const; diff --git a/include/FileShare/Utils/Vector.hpp b/include/FileShare/Utils/Vector.hpp new file mode 100644 index 0000000..56c7fa6 --- /dev/null +++ b/include/FileShare/Utils/Vector.hpp @@ -0,0 +1,28 @@ +/* +** Project LibFileShareProtocol, 2025 +** +** Author Francois Michaut +** +** Started on Sun Aug 10 12:20:05 2025 Francois Michaut +** Last update Mon Aug 18 16:30:01 2025 Francois Michaut +** +** Vector.hpp : Utility functions for std::vector +*/ + +#include + +namespace FileShare { + template + auto delete_move(Vec &vector, Iter iter) -> Iter { + auto back = std::prev(vector.end()); + + if (iter == back) { + vector.pop_back(); + return vector.end(); + } + + *iter = std::move(*back); + vector.pop_back(); + return iter; + } +} diff --git a/source/Config.cpp b/source/Config.cpp deleted file mode 100644 index e58e47a..0000000 --- a/source/Config.cpp +++ /dev/null @@ -1,85 +0,0 @@ -/* -** Project LibFileShareProtocol, 2022 -** -** Author Francois Michaut -** -** Started on Tue Sep 13 11:29:35 2022 Francois Michaut -** Last update Sun Dec 10 17:46:39 2023 Francois Michaut -** -** FileShareConfig.cpp : FileShareConfig implementation -*/ - -#include "FileShare/Config.hpp" -#include "FileShare/Config/Serialization.hpp" -#include "FileShare/Utils/Path.hpp" - -#include - -#include - -#include - -namespace FileShare { - const std::filesystem::perms Config::secure_file_perms = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write; - const std::filesystem::perms Config::secure_folder_perms = std::filesystem::perms::owner_all; - - Config::Config() { - std::filesystem::directory_entry pkey_dir{m_private_keys_dir}; - - if (pkey_dir.exists()) { - if (!pkey_dir.is_directory()) - throw std::runtime_error("The private key/certificate path is not a directory"); -#ifdef OS_WINDOWS - // TODO: figure out security recomandations for windows -#else - if (pkey_dir.status().permissions() != secure_folder_perms) - throw std::runtime_error("The private key/certificate path has insecure permissions"); -#endif - } - if (m_downloads_folder.empty()) { - m_downloads_folder = FileShare::Utils::resolve_home_component("~/Downloads/FileShare"); // TODO: replace by cross-plateform way of getting the dowloads folder - } - } - - Config Config::from_file(std::filesystem::path config_file) { - config_file = FileShare::Utils::resolve_home_component(config_file); - Config new_config; - std::ifstream file(config_file, std::ios_base::binary | std::ios_base::in); - cereal::BinaryInputArchive ar(file); - - ar(new_config); - return new_config; - } - - void Config::to_file(std::filesystem::path config_file) { - config_file = FileShare::Utils::resolve_home_component(config_file); - std::ofstream file(config_file, std::ios_base::binary | std::ios_base::out | std::ios_base::trunc); - cereal::BinaryOutputArchive ar(file); - - ar(*this); - } - - const std::filesystem::path &Config::get_downloads_folder() const { return m_downloads_folder; } - Config &Config::set_downloads_folder(std::filesystem::path path) { m_downloads_folder = std::move(path); return *this; } - - const std::filesystem::path &Config::get_private_keys_dir() const { return m_private_keys_dir; } - const std::string &Config::get_private_key_name() const { return m_private_key_name; } - Config &Config::set_private_keys_dir(std::filesystem::path path) { m_private_keys_dir = std::move(path); return *this; } - Config &Config::set_private_key_name(std::string name) { m_private_key_name = std::move(name); return *this; } - - Config &Config::set_file_mapping(FileMapping mapping) { m_filemap = mapping; return *this; } - const FileMapping &Config::get_file_mapping() const { return m_filemap; } - FileMapping &Config::get_file_mapping() { return m_filemap; } - - Config::TransportMode Config::get_transport_mode() const { return m_transport_mode; } - Config &Config::set_transport_mode(TransportMode mode) { m_transport_mode = mode; return *this; } - - bool Config::is_server_disabled() const { return m_disable_server; } - Config &Config::set_server_disabled(bool disabled) { m_disable_server = disabled; return *this; } - - const std::filesystem::path &Config::default_private_keys_dir() { - static std::filesystem::path private_keys_dir = FileShare::Utils::resolve_home_component("~/.fsp/private").generic_string(); - - return private_keys_dir; - } -} diff --git a/source/Config/Config.cpp b/source/Config/Config.cpp new file mode 100644 index 0000000..90d747f --- /dev/null +++ b/source/Config/Config.cpp @@ -0,0 +1,70 @@ +/* +** Project LibFileShareProtocol, 2022 +** +** Author Francois Michaut +** +** Started on Tue Sep 13 11:29:35 2022 Francois Michaut +** Last update Fri Aug 22 20:39:00 2025 Francois Michaut +** +** FileShareConfig.cpp : FileShareConfig implementation +*/ + +#include "FileShare/Config/Config.hpp" +#include "FileShare/Config/Serialization.hpp" // NOLINT(misc-include-cleaner,unused-includes) +#include "FileShare/Utils/Path.hpp" + +#include + +#include +#include +#include + +const char * const DEFAULT_PATH = "~/.fsp/default_config"; + +namespace FileShare { + Config::Config() { + if (m_downloads_folder.empty()) { + // TODO: replace by cross-plateform way of getting the dowloads folder + m_downloads_folder = FileShare::Utils::resolve_home_component("~/Downloads/FileShare"); + } + } + + Config::Config(bool /*_*/) {} + + auto Config::set_downloads_folder(const std::filesystem::path &path) -> Config & { + m_downloads_folder = FileShare::Utils::resolve_home_component(path); + return *this; + } + + auto Config::load(std::filesystem::path config_file) -> Config { + if (config_file.empty()) { + config_file = DEFAULT_PATH; + } + + Config config(false); + config.m_filepath = FileShare::Utils::resolve_home_component(config_file); + std::ifstream file(config.m_filepath, std::ios_base::binary | std::ios_base::in); + cereal::BinaryInputArchive archive(file); + + archive(config); + // TODO: Need to validate config - if someone messes up the file, we could have problems + return config; + } + + void Config::save(std::filesystem::path config_file) const { + if (config_file.empty()) { + config_file = m_filepath; + } else { + config_file = FileShare::Utils::resolve_home_component(config_file); + } + + std::filesystem::path tmp_file = config_file.string() + ".tmp"; + std::ofstream file(tmp_file, std::ios_base::binary | std::ios_base::out | std::ios_base::trunc); + cereal::BinaryOutputArchive archive(file); + + archive(*this); + file.close(); + + std::filesystem::rename(tmp_file.c_str(), config_file.c_str()); + } +} diff --git a/source/Config/FileMapping.cpp b/source/Config/FileMapping.cpp index 7b9d978..1c1d936 100644 --- a/source/Config/FileMapping.cpp +++ b/source/Config/FileMapping.cpp @@ -4,153 +4,182 @@ ** Author Francois Michaut ** ** Started on Thu Nov 16 22:14:51 2023 Francois Michaut -** Last update Sat Aug 10 09:31:18 2024 Francois Michaut +** Last update Sun Aug 24 11:31:51 2025 Francois Michaut ** ** FileMapping.cpp : Config's PathNode implementation */ -#include "FileShare/Config.hpp" +#include "FileShare/Config/FileMapping.hpp" #include "FileShare/Utils/Path.hpp" -#include #include -#include +#include #include #include +#include +#include -static std::string stack_to_path(const std::deque &stack) { - std::stringstream ss; +namespace { + auto stack_to_path(const std::deque &stack) -> std::string { + std::stringstream ss; - if (stack.empty()) - return ""; + if (stack.empty()) + return ""; - for (auto iter = stack.begin(); iter != std::prev(stack.end()); iter++) { - const auto &node = *iter; - ss << node->get_name() << "/"; - } - if (stack.size() > 0) { + for (auto iter = stack.begin(); iter != std::prev(stack.end()); iter++) { + const auto &node = *iter; + ss << node->get_name() << "/"; + } ss << stack.back()->get_name(); + return ss.str(); } - return ss.str(); -} -static std::string trim_node_name(std::string_view name) { - const char sep = '/'; // TODO: do this properly for windows - const auto begin = name.find_first_not_of(sep); + // Removes trailing `/` at the begining and end of the path + auto trim_node_name(std::string_view name, bool trim_begining = true) -> std::string { + const char sep = '/'; // TODO: do this properly for windows + const auto begin = trim_begining ? name.find_first_not_of(sep) : 0; - if (begin == std::string::npos) - return ""; // no content + if (begin == std::string::npos) + return ""; // no content - const auto end = name.find_last_not_of(sep); - const auto range = end - begin + 1; // end - begin will give us number of caracters between first & last, but we want to include last + const auto end = name.find_last_not_of(sep); + const auto range = end - begin + 1; // end - begin will give us number of caracters between first & last, but we want to include last - return std::string(name.substr(begin, range)); -} + return std::string(name.substr(begin, range)); + } -static FileShare::PathNode::NodeMap vector_to_map(std::vector nodes) { - FileShare::PathNode::NodeMap result; + auto vector_to_map(std::vector nodes) -> FileShare::PathNode::NodeMap { + FileShare::PathNode::NodeMap result; - result.reserve(nodes.size()); - for (const auto &node : nodes) { - result.emplace(node.get_name(), std::move(node)); + result.reserve(nodes.size()); + for (auto &node : nodes) { + result.emplace(node.get_name(), std::move(node)); + } + return result; } - return result; } namespace FileShare { - PathNode::PathNode(std::string name, Visibility visibility, NodeMap child_nodes) : - m_name(std::move(name)), m_type(VIRTUAL), m_visibility(visibility), m_child_nodes(std::move(child_nodes)) + PathNode::PathNode( + std::string name, PathNode *parent, Visibility visibility, NodeMap child_nodes + ) : + m_name(std::move(name)), m_parent(parent), + m_child_nodes(std::move(child_nodes)), + m_type(Type::VIRTUAL), m_visibility(visibility) { assert_valid_name(m_name); + reassign_childs(); } - PathNode::PathNode(std::string name, Type type, std::filesystem::path host_path, Visibility visibility) : - m_name(std::move(name)), m_type(type), m_visibility(visibility), m_host_path(std::move(host_path)) + PathNode::PathNode( + std::string name, PathNode *parent, Type type, std::filesystem::path host_path, + Visibility visibility + ) : + m_name(std::move(name)), m_parent(parent), m_host_path(std::move(host_path)), + m_type(type), m_visibility(visibility) { assert_valid_name(m_name); } - bool PathNode::operator==(const PathNode &other) const noexcept { - return this->get_name() == other.get_name() && this->get_type() == other.get_type() && - this->get_visibility() == other.get_visibility() && this->get_child_nodes() == other.get_child_nodes() && - this->get_host_path() == other.get_host_path(); + PathNode::PathNode(const PathNode &other) : + m_name(other.m_name), m_parent(other.m_parent), + m_child_nodes(other.m_child_nodes), m_host_path(other.m_host_path), + m_type(other.m_type), m_visibility(other.m_visibility) + { + reassign_childs(); + } + + auto PathNode::operator=(const PathNode &other) -> PathNode & { + return *this = PathNode(other); + } + + auto PathNode::operator==(const PathNode &other) const noexcept -> bool { + const bool parent_present = m_parent != nullptr; + const bool other_parent_present = other.m_parent != nullptr; + const bool same_parent = (parent_present == other_parent_present) && ( + !parent_present || m_parent->get_ancestor_path() == other.m_parent->get_ancestor_path() + ); + + return same_parent && this->m_name == other.m_name && + this->m_host_path == other.m_host_path && + this->m_type == other.m_type && this->m_visibility == other.m_visibility; } - PathNode PathNode::make_virtual_node(std::string name, Visibility visibility) { - return PathNode(std::move(name), visibility, {}); + auto PathNode::make_virtual_node(std::string name, Visibility visibility) -> PathNode { + return {std::move(name), nullptr, visibility, {}}; } - PathNode PathNode::make_virtual_node(std::string name, Visibility visibility, NodeMap child_nodes) { - return PathNode(std::move(name), visibility, std::move(child_nodes)); + auto PathNode::make_virtual_node(std::string name, Visibility visibility, NodeMap child_nodes) -> PathNode { + return {std::move(name), nullptr, visibility, std::move(child_nodes)}; } - PathNode PathNode::make_virtual_node(std::string name, Visibility visibility, std::vector child_nodes) { - return PathNode(std::move(name), visibility, vector_to_map(std::move(child_nodes))); + auto PathNode::make_virtual_node(std::string name, Visibility visibility, std::vector child_nodes) -> PathNode { + return {std::move(name), nullptr, visibility, vector_to_map(std::move(child_nodes))}; } - PathNode PathNode::make_virtual_node(std::string name, NodeMap child_nodes, Visibility visibility) { - return PathNode(std::move(name), visibility, std::move(child_nodes)); + auto PathNode::make_virtual_node(std::string name, NodeMap child_nodes, Visibility visibility) -> PathNode { + return {std::move(name), nullptr, visibility, std::move(child_nodes)}; } - PathNode PathNode::make_virtual_node(std::string name, std::vector child_nodes, Visibility visibility) { + auto PathNode::make_virtual_node(std::string name, std::vector child_nodes, Visibility visibility) -> PathNode { return make_virtual_node(std::move(name), visibility, std::move(child_nodes)); } - PathNode PathNode::make_host_node(std::string name, Type type, std::filesystem::path host_path, Visibility visibility) { + auto PathNode::make_host_node(std::string name, Type type, std::filesystem::path host_path, Visibility visibility) -> PathNode { assert_not_virtual_type(type); - return PathNode(std::move(name), type, std::move(host_path), visibility); + return {std::move(name), nullptr, type, std::move(host_path), visibility}; + } + + auto PathNode::add_child_node(PathNode node) -> PathNode & { + (void)insert_child_node(std::move(node)); + return *this; } - PathNode &PathNode::add_child_node(PathNode node) { + auto PathNode::insert_child_node(PathNode node) -> PathNode & { assert_virtual_type(m_type); - auto result = m_child_nodes.emplace(node.get_name(), std::move(node)); + node.assign_parent(this); + + std::pair result = m_child_nodes.emplace( + node.get_name(), std::move(node) + ); if (!result.second) { throw std::runtime_error("A node already exists with that name"); } - return *this; + return result.first->second; } - PathNode &PathNode::remove_child_node(std::string_view name) { - std::erase_if(m_child_nodes, [&name](const auto &iter) { return iter.first == name;}); + auto PathNode::remove_child_node(const std::string &name) -> PathNode & { + m_child_nodes.erase(name); return *this; } - PathNode &PathNode::clear_child_nodes() { + auto PathNode::clear_child_nodes() -> PathNode & { m_child_nodes.clear(); return *this; } - PathNode::NodeMap &PathNode::get_child_nodes() { return m_child_nodes; } - const PathNode::NodeMap &PathNode::get_child_nodes() const { return m_child_nodes; } - PathNode &PathNode::set_child_nodes(NodeMap nodes) { + auto PathNode::set_child_nodes(NodeMap nodes) -> PathNode & { if (!nodes.empty()) { assert_virtual_type(m_type); } m_child_nodes = std::move(nodes); + + reassign_childs(); return *this; } - PathNode &PathNode::set_child_nodes(std::vector nodes) { - assert_virtual_type(m_type); - m_child_nodes = vector_to_map(std::move(nodes)); - return *this; + + auto PathNode::set_child_nodes(std::vector nodes) -> PathNode & { + return set_child_nodes(vector_to_map(std::move(nodes))); } - std::string_view PathNode::get_name() const { return m_name; } - const std::string &PathNode::get_name_str() const { return m_name; } - PathNode &PathNode::set_name(std::string name) { + auto PathNode::set_name(std::string name) -> PathNode & { assert_valid_name(name); m_name = std::move(name); return *this; } - bool PathNode::is_host() const { return !is_virtual(); } - bool PathNode::is_virtual() const { return m_type == VIRTUAL; } - bool PathNode::is_host_file() const { return m_type == HOST_FILE; } - bool PathNode::is_host_folder() const { return m_type == HOST_FOLDER; } - - PathNode::Type PathNode::get_type() const { return m_type; } - PathNode &PathNode::set_type(Type type) { + auto PathNode::set_type(Type type) -> PathNode & { if (!m_child_nodes.empty()) { assert_virtual_type(type); } else if (!m_host_path.empty()) { @@ -160,14 +189,7 @@ namespace FileShare { return *this; } - PathNode::Visibility PathNode::get_visibility() const { return m_visibility; } - PathNode &PathNode::set_visibility(Visibility visibility) { - m_visibility = visibility; - return *this; - } - - const std::filesystem::path &PathNode::get_host_path() const { return m_host_path; } - PathNode &PathNode::set_host_path(std::filesystem::path host_path) { + auto PathNode::set_host_path(std::filesystem::path host_path) -> PathNode & { if (!host_path.empty()) { assert_not_virtual_type(m_type); } @@ -175,14 +197,33 @@ namespace FileShare { return *this; } + auto PathNode::get_ancestor_path() const -> std::filesystem::path { + if (m_cached_ancestor_path) { + return *m_cached_ancestor_path; + } + + std::deque stack; + const PathNode *current_node = m_parent; + + while (current_node) { + stack.push_back(current_node); + current_node = current_node->m_parent; + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + auto *mutable_this = const_cast(this); + mutable_this->m_cached_ancestor_path = std::make_unique(stack_to_path(stack)); + + return *m_cached_ancestor_path; + } + void PathNode::assert_virtual_type(Type type) { - if (type != VIRTUAL) { + if (type != Type::VIRTUAL) { throw std::runtime_error("Only VIRTUAL PathNode can have child_nodes."); } } void PathNode::assert_not_virtual_type(Type type) { - if (type == VIRTUAL) { + if (type == Type::VIRTUAL) { throw std::runtime_error("VIRTUAL PathNode cannot have a host_path."); } } @@ -191,34 +232,70 @@ namespace FileShare { // TODO: do this properly for windows if (name.find('/') != std::string::npos) { throw std::runtime_error("PathNode name cannot contain '/'"); - } else if (name.empty()) { + } + if (name.empty()) { throw std::runtime_error("PathNode name cannot be empty"); } } - RootPathNode::RootPathNode(std::string root_name, PathNode::NodeMap root_nodes) : - PathNode(trim_node_name(root_name), Visibility::VISIBLE, std::move(root_nodes)) - { - set_name(root_name); + void PathNode::assign_parent(PathNode *other) { + m_parent = other; + m_cached_ancestor_path = nullptr; + } + + void PathNode::reassign_childs() { + for (auto &node : m_child_nodes) { + node.second.assign_parent(this); + } } - RootPathNode::RootPathNode(std::string root_name, std::vector root_nodes) : - PathNode(trim_node_name(root_name), Visibility::VISIBLE, vector_to_map(std::move(root_nodes))) + RootPathNode::RootPathNode(std::string_view root_name, PathNode::NodeMap root_nodes) : + PathNode(trim_node_name(root_name), nullptr, Visibility::VISIBLE, std::move(root_nodes)) { - set_name(root_name); + set_name(trim_node_name(root_name, false)); // We want to keep the leading `/` if any + assert_valid_name(get_name()); } + RootPathNode::RootPathNode(std::string_view root_name, std::vector root_nodes) : + RootPathNode(root_name, vector_to_map(std::move(root_nodes))) + {} + RootPathNode::RootPathNode(PathNode::NodeMap root_nodes) : - RootPathNode(default_root_name, std::move(root_nodes)) + RootPathNode(DEFAULT_ROOT_NAME, std::move(root_nodes)) {} RootPathNode::RootPathNode(std::vector root_nodes) : - RootPathNode(default_root_name, std::move(root_nodes)) + RootPathNode(DEFAULT_ROOT_NAME, std::move(root_nodes)) {} + auto RootPathNode::set_name(std::string name) -> PathNode & { + return PathNode::set_name(trim_node_name(name, false)); + } + + auto RootPathNode::set_type(Type type) -> PathNode & { + if (type != Type::VIRTUAL) { + throw std::runtime_error("RootPathNode type must always be VIRTUAL."); + } + return *this; + } + + auto RootPathNode::set_visibility(Visibility visibility) -> PathNode & { + if (visibility != Visibility::VISIBLE) { + throw std::runtime_error("RootPathNode visibility must always be VISIBLE."); + } + return *this; + } + + auto RootPathNode::set_host_path(std::filesystem::path host_path) -> PathNode & { + if (!host_path.empty()) { + throw std::runtime_error("RootPathNode cannot have a host_path."); + } + return *this; + } + void RootPathNode::assert_valid_name(std::string_view name) { if (name.empty()) { - throw std::runtime_error("PathNode name cannot be empty"); + throw std::runtime_error("RootPathNode name cannot be empty"); } // TODO: do this properly for windows auto last_slash = name.find_last_of('/'); @@ -226,35 +303,29 @@ namespace FileShare { if (first_char == std::string::npos) { throw std::runtime_error("RootPathNode name cannot be only made of directory separators"); - } else if (first_char < last_slash) { + } + if (first_char < last_slash) { throw std::runtime_error("RootPathNode name can only contain directory separators at the begining"); } + if (first_char > 2) { + throw std::runtime_error("RootPathNode name can only contain up to 2 directory separators at the begining"); + } } FileMapping::FileMapping(RootPathNode root_node, FilepathSet forbidden_paths) : m_root_node(std::move(root_node)), m_forbidden_paths(std::move(forbidden_paths)) {} - std::string_view FileMapping::get_root_name() const { return m_root_node.get_name(); } - void FileMapping::set_root_name(std::string root_name) { m_root_node.set_name(std::move(root_name)); } - - const RootPathNode &FileMapping::get_root_node() const { return m_root_node; } - RootPathNode &FileMapping::get_root_node() { return m_root_node; } - void FileMapping::set_root_node(RootPathNode root_node) { m_root_node = std::move(root_node); } - - const PathNode::NodeMap &FileMapping::get_root_nodes() const { return m_root_node.get_child_nodes(); } - PathNode::NodeMap &FileMapping::get_root_nodes() { return m_root_node.get_child_nodes(); } - void FileMapping::set_root_nodes(PathNode::NodeMap root_nodes) { m_root_node.set_child_nodes(std::move(root_nodes)); } - void FileMapping::set_root_nodes(std::vector root_nodes) { m_root_node.set_child_nodes(vector_to_map(std::move(root_nodes))); } - - const FileMapping::FilepathSet &FileMapping::get_forbidden_paths() const { return m_forbidden_paths; } - FileMapping::FilepathSet FileMapping::get_forbidden_paths() { return m_forbidden_paths; } - void FileMapping::set_forbidden_paths(FileMapping::FilepathSet paths) { m_forbidden_paths = std::move(paths); } + void FileMapping::set_root_nodes(std::vector root_nodes) { + m_root_node.set_child_nodes(vector_to_map(std::move(root_nodes))); + } - const FileMapping::FilepathSet &FileMapping::default_forbidden_paths() { + auto FileMapping::default_forbidden_paths() -> const FileMapping::FilepathSet & { // TODO: find all paths that should be forbidden static FilepathSet forbidden_paths = [](){ - auto vector = FileShare::Utils::resolve_home_components({"~/.ssh", "~/.fsp", "/etc/passwd", "/root"}); + auto vector = FileShare::Utils::resolve_home_components({ + "~/.ssh", "~/.fsp", "/etc/passwd", "/root" + }); return FilepathSet{vector.begin(), vector.end()}; }(); @@ -262,7 +333,7 @@ namespace FileShare { return forbidden_paths; } - std::filesystem::path FileMapping::host_to_virtual(const std::filesystem::path &path) const { + auto FileMapping::host_to_virtual(const std::filesystem::path &path) const -> std::filesystem::path { std::deque stack; std::unordered_set visited_set; const PathNode *current_node = &m_root_node; @@ -270,6 +341,7 @@ namespace FileShare { // TODO: make a better algo, way too many branches in this one stack.push_back(current_node); visited_set.insert(current_node); + while (current_node) { if (current_node->get_visibility() == PathNode::HIDDEN) { stack.pop_back(); @@ -314,14 +386,14 @@ namespace FileShare { // This way, peer knows that this file is "temporary" and it cannot request it normally } - std::filesystem::path FileMapping::virtual_to_host(const std::filesystem::path &virtual_path) const { + auto FileMapping::virtual_to_host(const std::filesystem::path &virtual_path) const -> std::filesystem::path { std::filesystem::path::iterator iter; auto node = find_virtual_node(virtual_path, iter); return virtual_to_host(virtual_path, node, iter); } - std::filesystem::path FileMapping::virtual_to_host(const std::filesystem::path &virtual_path, const std::optional &node, std::filesystem::path::iterator iter) const { + auto FileMapping::virtual_to_host(const std::filesystem::path &virtual_path, const std::optional &node, std::filesystem::path::iterator iter) -> std::filesystem::path { if (!node.has_value()) { return ""; } @@ -336,44 +408,46 @@ namespace FileShare { return node->get_host_path(); // Virtual nodes will return "" } - std::optional FileMapping::find_virtual_node(std::filesystem::path virtual_path, std::filesystem::path::iterator &iter, bool only_visible) const { + auto FileMapping::find_virtual_node(std::filesystem::path virtual_path, std::filesystem::path::iterator &out, bool only_visible) const -> std::optional { std::string str = trim_node_name(virtual_path.string()); const PathNode *result = &m_root_node; + // TODO: Instead of trimming, do smth else, cause right now `/fsp/aaa` will be considered a virtual path to /aaa -> What if /fsp is an actual host folder ?? const auto &root_name = trim_node_name(m_root_node.get_name()); // Root name is optional in the path if (str.starts_with(root_name)) { virtual_path = str.substr(root_name.size()); } - for (iter = virtual_path.begin(); iter != virtual_path.end(); iter++) { - if (*iter == "/") + for (out = virtual_path.begin(); out != virtual_path.end(); out++) { + if (*out == "/") continue; - auto &child_nodes = result->get_child_nodes(); - auto node = child_nodes.find(*iter); + const auto &child_nodes = result->get_child_nodes(); + auto node = child_nodes.find(*out); + // TODO: Why do we have only_visible param ? When would that not be true ? bool can_see_node = node != child_nodes.end() && (!only_visible || node->second.get_visibility() == PathNode::VISIBLE); if (can_see_node) { result = &node->second; if (result->get_type() == PathNode::HOST_FOLDER) { - iter++; // return past the node iterator + out++; // return past the node iterator return *result; } } else { return {}; } } - return *result; + return *result; // TODO: Doesn't this slice the root_node if it gets returned ? } - std::optional FileMapping::find_virtual_node(std::filesystem::path virtual_path, bool only_visible) const { + auto FileMapping::find_virtual_node(std::filesystem::path virtual_path, bool only_visible) const -> std::optional { std::filesystem::path::iterator iter; - return FileMapping::find_virtual_node(virtual_path, iter, only_visible); + return FileMapping::find_virtual_node(std::move(virtual_path), iter, only_visible); } - bool FileMapping::is_forbidden(const std::filesystem::path &path) const { + auto FileMapping::is_forbidden(const std::filesystem::path &path) const -> bool { if (m_forbidden_paths.contains(path)) return true; std::string path_str = path.generic_string(); diff --git a/source/Config/KnownPeerStore.cpp b/source/Config/KnownPeerStore.cpp new file mode 100644 index 0000000..e5a9413 --- /dev/null +++ b/source/Config/KnownPeerStore.cpp @@ -0,0 +1,55 @@ +/* +** Project LibFileShareProtocol, 2025 +** +** Author Francois Michaut +** +** Started on Mon Aug 18 17:16:41 2025 Francois Michaut +** Last update Thu Aug 21 14:06:50 2025 Francois Michaut +** +** KnownPeerStore.cpp : Implementation of the storage of known peers +*/ + +#include "FileShare/Config/KnownPeerStore.hpp" + +#include + +// TODO: Real implementation +namespace FileShare { + // Raises if uuid / public_key is not unique (skip silently if exact match) + void KnownPeerStore::insert(std::string_view uuid, std::string_view public_key) { + auto result = m_known_peers.emplace(uuid, public_key); + + // TODO: Support pubkey rotation + if (!result.second && result.first->second != public_key) { + throw std::runtime_error("A different PublicKey exists for this UUID"); + } + } + + void KnownPeerStore::remove(std::string_view uuid, std::string_view public_key) { + auto iter = m_known_peers.find(uuid); + + if (iter != m_known_peers.end()) { + if (iter->second != public_key) { + throw std::runtime_error("A different PublicKey exists for this UUID"); + } + m_known_peers.erase(iter); + } + } + + // raises if uuid / public_key not unique + auto KnownPeerStore::contains(std::string_view uuid, std::string_view public_key) -> bool { + auto iter = m_known_peers.find(uuid); + + if (iter == m_known_peers.end()) { + return false; + } + if (iter->second != public_key) { + throw std::runtime_error("A different PublicKey exists for this UUID"); + } + return true; + } + + auto KnownPeerStore::contains(const PreAuthPeer &peer) -> bool { + return contains(peer.get_device_uuid(), peer.get_public_key()); + } +} diff --git a/source/Config/ServerConfig.cpp b/source/Config/ServerConfig.cpp new file mode 100644 index 0000000..452ff8e --- /dev/null +++ b/source/Config/ServerConfig.cpp @@ -0,0 +1,99 @@ +/* +** Project LibFileShareProtocol, 2025 +** +** Author Francois Michaut +** +** Started on Wed Aug 6 15:19:24 2025 Francois Michaut +** Last update Fri Aug 22 20:42:47 2025 Francois Michaut +** +** ServerConfig.cpp : Server Configuration Implementation +*/ + +#include "FileShare/Config/Serialization.hpp" // NOLINT(misc-include-cleaner,unused-includes) +#include "FileShare/Config/ServerConfig.hpp" +#include "FileShare/Utils/Path.hpp" + +#include + +#include + +#include +#include + +const char * const DEFAULT_PATH = "~/.fsp/server_config"; + +namespace FileShare { + ServerConfig::ServerConfig() : + m_uuid("0000-0000-0000-0000") // TODO: Generate Random UUID + {} + + ServerConfig::ServerConfig(std::string uuid) : m_uuid(std::move(uuid)) {} + + auto ServerConfig::default_private_keys_dir() -> const std::filesystem::path & { + static const std::filesystem::path private_keys_dir = FileShare::Utils::resolve_home_component("~/.fsp/private").generic_string(); + + return private_keys_dir; + } + + auto ServerConfig::set_private_keys_dir(std::string_view path) -> ServerConfig & { + m_private_keys_dir = FileShare::Utils::resolve_home_component(path); + return *this; + } + + auto ServerConfig::set_private_key_name(std::string name) -> ServerConfig & { + if (name.find('/') != std::string::npos) { // TODO: Do this properly for Windows + throw std::runtime_error("Private Key Name cannot contain a directory separator"); + } + m_private_key_name = std::move(name); + return *this; + } + + auto ServerConfig::load(std::filesystem::path config_file) -> ServerConfig { + if (config_file.empty()) { + config_file = DEFAULT_PATH; + } + + // Avoid the default constructor which would wastefuly generate an UUID + ServerConfig config("0000-0000-0000-0000"); + config.m_filepath = FileShare::Utils::resolve_home_component(config_file); + std::ifstream file(config.m_filepath, std::ios_base::binary | std::ios_base::in); + cereal::BinaryInputArchive archive(file); + + archive(config); + // TODO: Need to validate config - if someone messes up the file, we could have problems + // (Or if private_keys_dir contains ~, it needs to be expanded) + return config; + } + + void ServerConfig::save(std::filesystem::path config_file) const { + if (config_file.empty()) { + config_file = m_filepath; + } else { + config_file = FileShare::Utils::resolve_home_component(config_file); + } + + std::filesystem::path tmp_file = config_file.string() + ".tmp"; + std::ofstream file(tmp_file, std::ios_base::binary | std::ios_base::out | std::ios_base::trunc); + cereal::BinaryOutputArchive archive(file); + + archive(*this); + file.close(); + + std::filesystem::rename(tmp_file.c_str(), config_file.c_str()); + } + + void ServerConfig::validate_config() const { + const std::filesystem::directory_entry pkey_dir{m_private_keys_dir}; + + if (pkey_dir.exists()) { + if (!pkey_dir.is_directory()) + throw std::runtime_error("The private key/certificate path is not a directory"); +#ifdef OS_WINDOWS + // TODO: figure out security recomandations for windows +#else + if (pkey_dir.status().permissions() != SECURE_FOLDER_PERMS) + throw std::runtime_error("The private key/certificate path has insecure permissions"); +#endif + } + } +} diff --git a/source/MessageQueue.cpp b/source/MessageQueue.cpp index 865f15e..959056c 100644 --- a/source/MessageQueue.cpp +++ b/source/MessageQueue.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Tue Aug 22 18:25:07 2023 Francois Michaut -** Last update Thu Nov 9 09:02:38 2023 Francois Michaut +** Last update Fri Aug 15 14:36:03 2025 Francois Michaut ** ** MessageQueue.cpp : Implementation of the queue representing the messages sent/received and their status */ @@ -14,7 +14,7 @@ #include namespace FileShare { - std::uint8_t MessageQueue::send_request(Protocol::Request request) { + auto MessageQueue::send_request(Protocol::Request request) -> Protocol::MessageID { if (m_available_send_slots == 0) { throw std::runtime_error("No more available slots"); } @@ -22,14 +22,15 @@ namespace FileShare { while (m_outgoing_requests.contains(m_message_id)) { auto status = m_outgoing_requests.at(m_message_id).status; + + // If there is a status already, this is an old request, we can replace it + // Except for APPROVAL_PENDING, since we are waiting for an anwser if (status.has_value() && status.value() != Protocol::StatusCode::APPROVAL_PENDING) { - // If there is a status already, this is an old request, we can replace it - // Except for APPROVAL_PENDING, since we are waiting for an anwser // NOTE: Not vulnerable to DOS attack, since peer would only be able to DOS - // itself if it fills message_queue with APPROVAL_PENDING + // its own connection if it fills our message_queue with APPROVAL_PENDING break; } - if (m_message_id == 255) { + if (m_message_id == MAX_ID) { m_message_id = 0; } else { m_message_id++; @@ -40,18 +41,20 @@ namespace FileShare { return m_message_id++; // Will return value before incrementation } - std::uint8_t MessageQueue::receive_request(Protocol::Request request) { - std::uint8_t message_id = request.message_id; + auto MessageQueue::receive_request(Protocol::Request request) -> Protocol::MessageID { + Protocol::MessageID message_id = request.message_id; + // TODO: Peer could override its own requests, make sure that doesn't break things m_incomming_requests[message_id] = Message{std::move(request), {}, ""}; return message_id; } - void MessageQueue::send_reply(std::uint8_t request_id, Protocol::StatusCode status_code) { + void MessageQueue::send_reply(Protocol::MessageID request_id, Protocol::StatusCode status_code) { m_incomming_requests.at(request_id).status = status_code; } - void MessageQueue::receive_reply(std::uint8_t request_id, Protocol::StatusCode status_code) { + void MessageQueue::receive_reply(Protocol::MessageID request_id, Protocol::StatusCode status_code) { + // TODO: Will throw if peer sends an invalid reply auto &request = m_outgoing_requests.at(request_id); bool is_approval_pending = false; bool has_value = request.status.has_value(); @@ -73,16 +76,4 @@ namespace FileShare { } request.status = status_code; } - - std::uint8_t MessageQueue::available_send_slots() const { - return m_available_send_slots; - } - - const MessageQueue::MessageMap &MessageQueue::get_outgoing_requests() const { - return m_outgoing_requests; - } - - const MessageQueue::MessageMap &MessageQueue::get_incomming_requests() const { - return m_incomming_requests; - } } diff --git a/source/Client.cpp b/source/Peer/Peer.cpp similarity index 65% rename from source/Client.cpp rename to source/Peer/Peer.cpp index 4cf3557..37187c9 100644 --- a/source/Client.cpp +++ b/source/Peer/Peer.cpp @@ -4,63 +4,31 @@ ** Author Francois Michaut ** ** Started on Mon Aug 29 20:50:53 2022 Francois Michaut -** Last update Sun Dec 10 18:46:14 2023 Francois Michaut +** Last update Sat Aug 23 12:59:50 2025 Francois Michaut ** -** Client.cpp : Implementation of the FileShareProtocol Client +** Peer.cpp : Implementation of the FileShareProtocol Client */ -#include "FileShare/Client.hpp" -#include "FileShare/Protocol/Protocol.hpp" +#include "FileShare/Peer/Peer.hpp" +#include "FileShare/Peer/PeerBase.hpp" +#include "FileShare/Protocol/Definitions.hpp" #include "FileShare/Protocol/RequestData.hpp" #include "FileShare/Server.hpp" +#include #include #include +#include // TODO: Use custom classes for exceptions namespace FileShare { - Client::Client(const CppSockets::IEndpoint &peer, std::string device_uuid, std::string public_key, Protocol::Protocol protocol, Config config) : - m_config(std::move(config)), m_protocol(std::move(protocol)), - m_device_uuid(device_uuid), m_public_key(public_key) - { - reconnect(peer); - } - - Client::Client(CppSockets::TlsSocket &&peer, std::string device_uuid, std::string public_key, Protocol::Protocol protocol, Config config) : - m_config(std::move(config)), m_protocol(std::move(protocol)), - m_device_uuid(device_uuid), m_public_key(public_key) - { - reconnect(std::move(peer)); - } - - const Config &Client::get_config() const { - return m_config; - } - - void Client::set_config(Config config) { - m_config = std::move(config); - } - - const CppSockets::TlsSocket &Client::get_socket() const { - return m_socket; - } - - void Client::reconnect(const CppSockets::IEndpoint &peer) { - m_socket = CppSockets::TlsSocket(AF_INET, SOCK_STREAM, 0); // TODO check if needed - m_socket.connect(peer); - // TODO authentification + protocol handshake - } + Peer::Peer(PreAuthPeer &&peer, FileShare::Config config) : + PeerBase(std::move(peer)), // TODO: Check its correct to move into base, and still use it afterwards + m_config(std::move(config)), m_protocol(peer.get_protocol()) + {} - void Client::reconnect(CppSockets::TlsSocket &&peer) { - if (!peer.connected()) { - throw std::runtime_error("Socket is not connected"); - } - m_socket = std::move(peer); - // TODO authentification + protocol handshake - } - - std::vector Client::pull_requests() { + auto Peer::pull_requests() -> std::vector { std::vector result; poll_requests(); @@ -70,7 +38,7 @@ namespace FileShare { return result; } - void Client::respond_to_request(Protocol::Request request, Protocol::StatusCode status) { + void Peer::respond_to_request(Protocol::Request request, Protocol::StatusCode status) { m_message_queue.receive_request(request); if (status != Protocol::StatusCode::STATUS_OK) { @@ -81,13 +49,19 @@ namespace FileShare { switch (request.code) { case Protocol::CommandCode::DATA_PACKET: { auto data = std::dynamic_pointer_cast(request.request); - auto &handler = m_download_transfers.at(data->request_id); + auto iter = m_download_transfers.find(data->request_id); - handler.receive_packet(*data); - if (handler.finished()) { - m_download_transfers.erase(data->request_id); + if (iter != m_download_transfers.end()) { + auto &handler = iter->second; + + handler.receive_packet(*data); + if (handler.finished() && !handler.m_keep) { + m_download_transfers.erase(data->request_id); + } + break; } - break; + send_reply(request.message_id, Protocol::StatusCode::INVALID_REQUEST_ID); + return; } case Protocol::CommandCode::SEND_FILE: { std::shared_ptr data = std::dynamic_pointer_cast(request.request); @@ -105,7 +79,8 @@ namespace FileShare { // Send reply to original RECEIVE_FILE before we send SEND_FILE send_reply(request.message_id, status); if (status == Protocol::StatusCode::STATUS_OK) { - create_upload(std::move(handler.value())); + // prepare_upload will always return a handler if status == OK + create_upload(std::move(handler.value())); // NOLINT(bugprone-unchecked-optional-access) } return; } @@ -117,15 +92,20 @@ namespace FileShare { auto result = m_list_files_transfers.emplace(std::piecewise_construct, std::forward_as_tuple(request.message_id), std::forward_as_tuple(std::move(handler))); send_reply(request.message_id, Protocol::StatusCode::STATUS_OK); + // TODO: Send more than one send_request(Protocol::CommandCode::FILE_LIST, result.first->second.get_next_packet(request.message_id)); return; } case Protocol::CommandCode::FILE_LIST: { auto data = std::dynamic_pointer_cast(request.request); - auto &handler = m_file_list_transfers.at(data->request_id); + auto handler = m_file_list_transfers.find(data->request_id); - handler.receive_packet(*data); - break; + if (handler != m_file_list_transfers.end()) { + handler->second.receive_packet(*data); + break; + } + send_reply(request.message_id, Protocol::StatusCode::INVALID_REQUEST_ID); + return; } default: break; @@ -133,11 +113,11 @@ namespace FileShare { send_reply(request.message_id, Protocol::StatusCode::STATUS_OK); } - Config Client::default_config() { - return Server::default_config(); + auto Peer::default_config() -> Config { + return Server::default_peer_config(); } - Protocol::Response Client::send_file(std::string filepath, ProgressCallback progress_callback) { + auto Peer::send_file(const std::string &filepath, const ProgressCallback &progress_callback) -> Protocol::Response { auto result = create_host_upload(filepath); Protocol::MessageID message_id = result->first; UploadTransferHandler &upload_handler = result->second; @@ -146,7 +126,7 @@ namespace FileShare { // TODO: handle APPROVAL_PENDING if (status != Protocol::StatusCode::STATUS_OK) { m_upload_transfers.erase(result); - return {status, {}}; + return {.code=status, .response={}}; } while (!upload_handler.finished()) { if (m_message_queue.available_send_slots() > 0) { @@ -161,10 +141,10 @@ namespace FileShare { } } - return {status, {}}; // TODO + return {.code=status, .response={}}; // TODO } - Protocol::Response Client::receive_file(std::string filepath, ProgressCallback progress_callback) { + auto Peer::receive_file(std::string filepath, const ProgressCallback &progress_callback) -> Protocol::Response { std::size_t packet_start = 0; // TODO std::size_t packet_size = 0; // TODO std::shared_ptr receive_file_data = std::make_shared(filepath, packet_size, packet_start); @@ -173,11 +153,11 @@ namespace FileShare { // TODO: handle APPROVAL_PENDING if (status != Protocol::StatusCode::STATUS_OK) { - return {status, {}}; + return {.code=status, .response={}}; } auto incomming_requests = m_message_queue.get_incomming_requests(); - auto request = std::find_if(incomming_requests.begin(), incomming_requests.end(), [&filepath](const auto &item) { + auto request = std::ranges::find_if(incomming_requests, [&filepath](const auto &item) { if (item.second.request.code != Protocol::CommandCode::SEND_FILE) { return false; } @@ -185,31 +165,39 @@ namespace FileShare { auto original_data = std::dynamic_pointer_cast(item.second.request.request); return filepath == original_data->filepath; }); - if (request == incomming_requests.end() || !m_download_transfers.contains(request->first)) { + auto transfer_iter = m_download_transfers.find(request->first); + if (request != incomming_requests.end() && request->second.status.has_value() + && request->second.status.value() == Protocol::StatusCode::UP_TO_DATE // NOLINT(bugprone-unchecked-optional-access) + ) { + return {.code = Protocol::StatusCode::UP_TO_DATE, .response = {}}; + } + if (request == incomming_requests.end() || transfer_iter == m_download_transfers.end()) { throw std::runtime_error("Failed to locate download transfer"); // TODO: we got STATUS_OK but no incomming request ? Something is wrong } - auto &transfer_handler = m_download_transfers.at(request->first); + auto &transfer_handler = transfer_iter->second; + transfer_handler.m_keep = true; while (!transfer_handler.finished()) { progress_callback(filepath, transfer_handler.get_current_size(), transfer_handler.get_total_size()); poll_requests(); // TODO: currently blocking, but if it changes, needs to add a poll() call to avoid spamming loop } - return {status, {}}; // TODO + m_download_transfers.erase(transfer_iter); + return {.code=status, .response={}}; // TODO } - Protocol::Response> Client::list_files(std::string folderpath) { + auto Peer::list_files(std::string folderpath) -> Protocol::Response> { std::shared_ptr list_files_data = std::make_shared(folderpath); Protocol::MessageID message_id = send_request(Protocol::CommandCode::LIST_FILES, list_files_data); Protocol::StatusCode status = wait_for_status(message_id); if (status != Protocol::StatusCode::STATUS_OK) { - return {status, {}}; + return {.code=status, .response={}}; } auto iter = m_file_list_transfers.find(message_id); if (iter == m_file_list_transfers.end()) { - std::runtime_error("Failed to locate list file transfer"); // TODO: we got STATUS_OK but no transfer? something is wrong. + throw std::runtime_error("Failed to locate list file transfer"); // TODO: we got STATUS_OK but no transfer? something is wrong. } auto &handler = iter->second; @@ -219,8 +207,9 @@ namespace FileShare { auto file_list = std::make_shared>(handler.get_file_list()); + // TODO: We need to erase from file_list_transfers for async somehow as well m_file_list_transfers.erase(message_id); - return {status, file_list}; + return {.code=status, .response=file_list}; } } diff --git a/source/Peer/PeerBase.cpp b/source/Peer/PeerBase.cpp new file mode 100644 index 0000000..0fcc7ef --- /dev/null +++ b/source/Peer/PeerBase.cpp @@ -0,0 +1,105 @@ +/* +** Project LibFileShareProtocol, 2025 +** +** Author Francois Michaut +** +** Started on Mon Jul 28 19:24:26 2025 Francois Michaut +** Last update Sat Aug 23 11:06:22 2025 Francois Michaut +** +** PeerBase.cpp : Implementation of the shared Base for the Peer class +*/ + +#include "FileShare/Peer/PeerBase.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace FileShare { + PeerBase::PeerBase(const CppSockets::IEndpoint &peer, CppSockets::TlsContext ctx) : + m_socket(AF_INET, SOCK_STREAM, 0, std::move(ctx)) + { + m_socket.connect(peer); + read_peer_certificate(); + } + + PeerBase::PeerBase(CppSockets::TlsSocket &&peer) { + if (!peer.connected()) { + throw std::runtime_error("Socket is not connected"); + } + m_socket = std::move(peer); + read_peer_certificate(); + } + + void PeerBase::disconnect() { + m_socket.close(); + } + + void PeerBase::read_peer_certificate() { + const auto &raw_cert = m_socket.get_peer_cert(); + + if (!raw_cert) { + throw std::runtime_error("Peer didn't provide a Certificate"); + } + CppSockets::Certificate cert {raw_cert.get(), false}; + // TODO: Validate certificate or throw + get uuid / name / public_key + + if (!cert.verify()) { + throw std::runtime_error("Invalid Certificate"); + } + const auto &raw_key = cert.get_pubkey(); + const auto &subject = cert.get_subject_name(); + const auto *device_uuid = subject.get_entry(NID_dnQualifier).get_data(); + const auto *device_name = subject.get_entry(NID_commonName).get_data(); + + const auto *device_uuid_str = ASN1_STRING_get0_data(device_uuid); + const auto *device_name_str = ASN1_STRING_get0_data(device_name); + CppSockets::BIO_ptr public_key {BIO_new(BIO_s_mem())}; + char *key_bytes; + std::size_t nb_bytes; + + // TODO: We dont want PEM. Use smth else + // Use X509_pubkey_digest ? + if (!PEM_write_bio_PUBKEY(public_key.get(), raw_key)) { + throw std::runtime_error("Failed to fetch public key"); + } + nb_bytes = BIO_get_mem_data(public_key.get(), &key_bytes); // NOLINT(cppcoreguidelines-pro-type-cstyle-cast) + + m_public_key = {key_bytes, nb_bytes}; + m_device_uuid = {reinterpret_cast(device_uuid_str), static_cast(ASN1_STRING_length(device_uuid))}; + m_device_name = {reinterpret_cast(device_name_str), static_cast(ASN1_STRING_length(device_name))}; + } + + void PeerBase::poll_requests() { + std::size_t total = 0; + std::string_view view; + Protocol::Request request; + std::size_t ret = 0; + + if (!m_socket.connected()) // TODO: Check if there is still buffered bytes + return; + m_buffer += m_socket.read(); // TODO: add a timeout + if (m_buffer.empty()) { + return; + } + view = m_buffer; + while (true) { + ret = parse_bytes(view, request); + if (ret == 0) { + break; + } + // TODO: if this raises an error, we risk to re-process the same requests + // (buffer not truncated) + authorize_request(request); + total += ret; + view = view.substr(ret); + } + m_buffer = m_buffer.substr(total); + } +} diff --git a/source/Client_private.cpp b/source/Peer/Peer_private.cpp similarity index 68% rename from source/Client_private.cpp rename to source/Peer/Peer_private.cpp index dcde5b3..ddfc3f4 100644 --- a/source/Client_private.cpp +++ b/source/Peer/Peer_private.cpp @@ -4,59 +4,57 @@ ** Author Francois Michaut ** ** Started on Mon Oct 23 21:33:10 2023 Francois Michaut -** Last update Sun Dec 10 18:47:02 2023 Francois Michaut +** Last update Sat Aug 23 11:29:59 2025 Francois Michaut ** -** Client_private.cpp : Private functions of Client implementation +** Peer_private.cpp : Private functions of Peer implementation */ -#include "FileShare/Client.hpp" +#include "FileShare/Peer/Peer.hpp" #include "FileShare/Errors/TransferErrors.hpp" +#include "FileShare/Utils/Poll.hpp" -#ifdef OS_UNIX - #include -#else // Windows only - using nfds_t=std::size_t; - - static auto &poll=WSAPoll; // alias function poll to WSAPoll -#endif +#include +#include namespace FileShare { - void Client::poll_requests() { - std::size_t total = 0; - std::string_view view; - Protocol::Request request; - std::size_t ret = 0; + auto Peer::parse_bytes(std::string_view raw_msg, Protocol::Request &out) -> std::size_t { + return m_protocol.handler().parse_request(raw_msg, out); + } - if (!m_socket.connected()) - return; - m_buffer += m_socket.read(); // TODO: add a timeout - if (m_buffer.empty()) { - return; - } - view = m_buffer; - while (true) { - ret = m_protocol.handler().parse_request(view, request); - if (ret == 0) { - break; - } - authorize_request(request); - total += ret; - view = view.substr(ret); - } - m_buffer = m_buffer.substr(total); + void Peer::send_reply(Protocol::MessageID message_id, Protocol::StatusCode status) { + std::string message = m_protocol.handler().format_response(message_id, status); + + m_message_queue.send_reply(message_id, status); + get_socket().write(message); + } + + auto Peer::send_request(Protocol::CommandCode command, std::shared_ptr request_data) -> Protocol::MessageID { + return send_request({command, std::move(request_data), 0}); + } + + auto Peer::send_request(Protocol::Request request) -> Protocol::MessageID { + Protocol::MessageID message_id = m_message_queue.send_request(request); + std::string message; + + request.message_id = message_id; + message = m_protocol.handler().format_request(request); + get_socket().write(message); + return message_id; } - void Client::authorize_request(Protocol::Request request) { + void Peer::authorize_request(Protocol::Request request) { // TODO: implement retries logic switch (request.code) { case Protocol::CommandCode::RESPONSE: { auto data = std::dynamic_pointer_cast(request.request); + // TODO: use find() and pass the iterator to receive_reply if (m_message_queue.get_outgoing_requests().contains(request.message_id)) { receive_reply(request.message_id, data->status); } return; } + case Protocol::CommandCode::DATA_PACKET: { auto data = std::dynamic_pointer_cast(request.request); @@ -67,11 +65,12 @@ namespace FileShare { } return; } + case Protocol::CommandCode::SEND_FILE: { // detect this is a send file in reply to a RECEIVE_FILE we sent, and auto-accept auto data = std::dynamic_pointer_cast(request.request); auto outgoing_requests = m_message_queue.get_outgoing_requests(); - auto original_request = std::find_if(outgoing_requests.begin(), outgoing_requests.end(), [&data](const auto &item) { + auto original_request = std::ranges::find_if(outgoing_requests, [&data](const auto &item) { if (item.second.request.code != Protocol::CommandCode::RECEIVE_FILE) { return false; } @@ -88,10 +87,12 @@ namespace FileShare { } break; // fallthrough default (manual approval) if no matching requests where found } + case Protocol::CommandCode::RECEIVE_FILE: { // respond_to_request will handle inexistant / forbidden paths return respond_to_request(request, Protocol::StatusCode::STATUS_OK); } + case Protocol::CommandCode::LIST_FILES: { auto data = std::dynamic_pointer_cast(request.request); auto &file_mapping = m_config.get_file_mapping(); @@ -102,6 +103,7 @@ namespace FileShare { } return respond_to_request(request, Protocol::StatusCode::STATUS_OK); } + case Protocol::CommandCode::FILE_LIST: { auto data = std::dynamic_pointer_cast(request.request); @@ -112,6 +114,8 @@ namespace FileShare { } return; } + + // TODO: Auto-Accept PING default: break; // Exit the switch but continue with the default APPROVAL_PENDING code. } @@ -121,14 +125,14 @@ namespace FileShare { m_request_buffer.emplace_back(std::move(request)); } - void Client::receive_reply(Protocol::MessageID message_id, Protocol::StatusCode status) { + void Peer::receive_reply(Protocol::MessageID message_id, Protocol::StatusCode status) { auto source_request = m_message_queue.get_outgoing_requests().at(message_id).request; if (status == Protocol::StatusCode::STATUS_OK && source_request.code == Protocol::CommandCode::RECEIVE_FILE) { // Peer accepted our request; but we will mark it as PENDING since we are waiting on the SEND_FILE packet - m_message_queue.receive_reply(message_id, Protocol::StatusCode::APPROVAL_PENDING); - return; + status = Protocol::StatusCode::APPROVAL_PENDING; } + m_message_queue.receive_reply(message_id, status); if (status != Protocol::StatusCode::STATUS_OK) { return; @@ -137,16 +141,20 @@ namespace FileShare { switch (source_request.code) { case Protocol::CommandCode::DATA_PACKET: { auto packet_data = std::dynamic_pointer_cast(source_request.request); - auto &handler = m_upload_transfers.at(packet_data->request_id); - std::shared_ptr new_packet = handler.get_next_packet(packet_data->request_id); + auto handler = m_upload_transfers.find(packet_data->request_id); - if (new_packet) { - send_request(Protocol::CommandCode::DATA_PACKET, new_packet); - } else { - m_upload_transfers.erase(packet_data->request_id); + if (handler != m_upload_transfers.end()) { + auto new_packet = handler->second.get_next_packet(packet_data->request_id); + + if (new_packet) { + send_request(Protocol::CommandCode::DATA_PACKET, new_packet); + } else { + m_upload_transfers.erase(handler); + } } break; } + case Protocol::CommandCode::SEND_FILE: { auto &handler = m_upload_transfers.at(message_id); @@ -156,67 +164,53 @@ namespace FileShare { send_request(Protocol::CommandCode::DATA_PACKET, new_packet); } - if (handler.finished()) { - m_upload_transfers.erase(message_id); - } break; } + case Protocol::CommandCode::LIST_FILES: { + // Peer accepted our request - preparing to receive data m_file_list_transfers.try_emplace(message_id); break; } + case Protocol::CommandCode::FILE_LIST: { auto data = std::dynamic_pointer_cast(source_request.request); - auto &handler = m_list_files_transfers.at(data->request_id); - auto new_packet = handler.get_next_packet(data->request_id); + auto handler = m_list_files_transfers.find(data->request_id); - if (new_packet) { - send_request(Protocol::CommandCode::FILE_LIST, new_packet); - } else { - m_upload_transfers.erase(message_id); + if (handler != m_list_files_transfers.end()) { + auto new_packet = handler->second.get_next_packet(data->request_id); + + if (new_packet) { + send_request(Protocol::CommandCode::FILE_LIST, new_packet); + } else { + m_upload_transfers.erase(message_id); + } } break; } + default: break; } } - void Client::send_reply(Protocol::MessageID message_id, Protocol::StatusCode status) { - auto response_data = std::make_shared(status); - std::string message = m_protocol.handler().format_response(message_id, status); - - m_message_queue.send_reply(message_id, status); - m_socket.write(message); - } - - Protocol::MessageID Client::send_request(Protocol::CommandCode command, std::shared_ptr request_data) { - return send_request({command, request_data, 0}); - } - - Protocol::MessageID Client::send_request(Protocol::Request request) { - Protocol::MessageID message_id = m_message_queue.send_request(request); - std::string message; - - request.message_id = message_id; - message = m_protocol.handler().format_request(request); - m_socket.write(message); - return message_id; - } - - std::pair, Protocol::StatusCode> Client::prepare_upload(std::filesystem::path host_filepath, std::string virtual_filepath, std::size_t packet_size, std::size_t packet_start) { + auto Peer::prepare_upload( + std::filesystem::path host_filepath, std::string virtual_filepath, std::size_t packet_size, + std::size_t packet_start + ) -> std::pair, Protocol::StatusCode> { + std::error_code ec; std::optional handler; + std::filesystem::directory_entry entry(host_filepath, ec); - if (host_filepath.empty() || m_config.get_file_mapping().is_forbidden(host_filepath)) { + // host_filepath will be empty if file is not visible, or doesn't have a mapping + if (ec || host_filepath.empty() || m_config.get_file_mapping().is_forbidden(host_filepath)) { return std::make_pair(std::move(handler), Protocol::StatusCode::FILE_NOT_FOUND); } - std::string file_hash = Utils::file_hash(Utils::HashAlgorithm::SHA512, host_filepath); - std::filesystem::directory_entry entry(host_filepath); std::filesystem::file_time_type file_updated_at = entry.last_write_time(); + std::string file_hash = Utils::file_hash(Utils::HashAlgorithm::SHA512, host_filepath); std::size_t file_size = entry.file_size(); - std::size_t total_packets = file_size / packet_size + (file_size % packet_size == 0 ? 0 : 1); - // TODO: add FILE_NOT_FOUND errors if file does not exists on filesystem + std::size_t total_packets = (file_size / packet_size) + (file_size % packet_size == 0 ? 0 : 1); if (packet_start > total_packets) { return std::make_pair(std::move(handler), Protocol::StatusCode::BAD_REQUEST); @@ -224,11 +218,13 @@ namespace FileShare { total_packets -= packet_start; std::shared_ptr send_file_data = std::make_shared(std::move(virtual_filepath), Utils::HashAlgorithm::SHA512, file_hash, file_updated_at, packet_size, total_packets); + handler = {host_filepath.string(), std::move(send_file_data), packet_start}; return std::make_pair(std::move(handler), Protocol::StatusCode::STATUS_OK); } - Client::UploadTransferMap::iterator Client::create_host_upload(std::filesystem::path host_filepath) { + auto Peer::create_host_upload(std::filesystem::path host_filepath) -> Peer::UploadTransferMap::iterator { + // TODO: QUICK uses 1200 bytes max for packets, for MTU optimization, maybe we should respect that as well ? constexpr std::size_t packet_size = 4096; // TODO: find a better place for this, duplicated from ProtocolHandler auto virtual_path = m_config.get_file_mapping().host_to_virtual(host_filepath); @@ -239,10 +235,10 @@ namespace FileShare { if (result.second == Protocol::StatusCode::STATUS_OK) { return create_upload(std::move(result.first.value())); } - throw std::runtime_error("Failed to send file '" + host_filepath.string() + "': " + Protocol::status_to_str(result.second)); + throw std::runtime_error("Failed to send file '" + host_filepath.string() + "': " + Protocol::status_to_str(result.second).data()); } - Client::UploadTransferMap::iterator Client::create_upload(UploadTransferHandler handler) { + auto Peer::create_upload(UploadTransferHandler handler) -> Peer::UploadTransferMap::iterator { Protocol::MessageID message_id = send_request(Protocol::CommandCode::SEND_FILE, handler.get_original_request()); auto result = m_upload_transfers.emplace( @@ -253,7 +249,7 @@ namespace FileShare { return result.first; } - Client::DownloadTransferMap::iterator Client::create_download(Protocol::MessageID request_id, const std::shared_ptr &data) { + auto Peer::create_download(Protocol::MessageID request_id, const std::shared_ptr &data) -> Peer::DownloadTransferMap::iterator { std::filesystem::path filepath(data->filepath); auto result = m_download_transfers.end(); @@ -261,7 +257,7 @@ namespace FileShare { result = m_download_transfers.emplace( std::piecewise_construct, std::forward_as_tuple(request_id), - std::forward_as_tuple((m_config.get_downloads_folder() / m_device_uuid / filepath.relative_path()).string(), data) + std::forward_as_tuple((m_config.get_downloads_folder() / get_device_uuid() / filepath.relative_path()).string(), data) ).first; send_reply(request_id, Protocol::StatusCode::STATUS_OK); } catch (Errors::Transfer::UpToDateError &) { @@ -271,27 +267,20 @@ namespace FileShare { } // TODO: deprecate - Protocol::StatusCode Client::wait_for_status(Protocol::MessageID message_id) { - auto &message = m_message_queue.get_outgoing_requests().at(message_id); + auto Peer::wait_for_status(Protocol::MessageID message_id) -> Protocol::StatusCode { + const auto &message = m_message_queue.get_outgoing_requests().at(message_id); // TODO HACK: this is not good as we could wait forever if peer is // trying to DDOS us by not sending a response ever. // Implement async model instead - while ((!message.status.has_value() || message.status.value() == Protocol::StatusCode::APPROVAL_PENDING) && m_socket.connected()) { - std::array fds; - nfds_t nfds = 1; - int nb_ready = 0; - - fds[0] = {m_socket.get_fd(), POLLIN, 0}; -#ifdef OS_LINUX - nb_ready = ppoll(fds.data(), nfds, nullptr, nullptr); // TODO: add timeout -#else - nb_ready = poll(fds.data(), nfds, -1); // TODO: add timeout -#endif + while ((!message.status.has_value() || message.status.value() == Protocol::StatusCode::APPROVAL_PENDING)) { + std::array fds = {pollfd{.fd = get_socket().get_fd(), .events = POLLIN, .revents = 0}}; + int nb_ready = Utils::poll(fds.data(), fds.size(), nullptr); // TODO: add timeout + if (nb_ready < 0) // TODO: handle signals throw std::runtime_error("Failed to poll for status"); poll_requests(); - if (!m_socket.connected()) + if (!get_socket().connected()) throw std::runtime_error("connection lost while waiting for status"); } return message.status.value(); diff --git a/source/Peer/PreAuthPeer.cpp b/source/Peer/PreAuthPeer.cpp new file mode 100644 index 0000000..6468da1 --- /dev/null +++ b/source/Peer/PreAuthPeer.cpp @@ -0,0 +1,113 @@ +/* +** Project LibFileShareProtocol, 2025 +** +** Author Francois Michaut +** +** Started on Thu Aug 14 12:00:55 2025 Francois Michaut +** Last update Thu Aug 21 09:21:57 2025 Francois Michaut +** +** PreAuthPeer.cpp : Implementation of the class to represent a Peer before it has been Authenticated +*/ + +#include "FileShare/Peer/PreAuthPeer.hpp" +#include "FileShare/Peer/PeerBase.hpp" +#include "FileShare/Protocol/Definitions.hpp" +#include "FileShare/Protocol/Protocol.hpp" +#include "FileShare/Protocol/RequestData.hpp" +#include "FileShare/Protocol/Version.hpp" + +#include +#include +#include + +namespace FileShare { + PreAuthPeer::PreAuthPeer(const CppSockets::IEndpoint &peer, Type type, CppSockets::TlsContext ctx) : + PeerBase(peer, std::move(ctx)), m_type(type) + {} + + PreAuthPeer::PreAuthPeer(CppSockets::TlsSocket &&peer, Type type) : + PeerBase(std::move(peer)), m_type(type) + {} + + void PreAuthPeer::do_client_hello() { + get_socket().write(Protocol::IProtocolHandler::format_client_version_list()); + } + + auto PreAuthPeer::get_protocol() const -> Protocol::Protocol { + if (m_protocol.has_value()) { + return m_protocol.value(); + } + throw std::runtime_error("PreAuthPeer hasn't negociated a Protocol yet"); + } + + auto PreAuthPeer::parse_bytes(std::string_view raw_msg, Protocol::Request &out) -> std::size_t { + std::size_t ret; + + if (m_protocol.has_value()) { + // In case the Peer sends the version and a command right after, we want to break the + // poll_requests() loop. This should allow known peers or CLIENT peers to be promoted + // before we reject the first requests as unauthorized. However, we give only 1 chance, + // after that if the peer is still in PreAuth, we will reject the requests. + if (m_first_extra_request) { + m_first_extra_request = false; + return 0; + } + + return m_protocol.value().handler().parse_request(raw_msg, out); + } + + try { + if (m_type == CLIENT) { + ret = Protocol::IProtocolHandler::parse_server_version(raw_msg, out); + } else { + ret = Protocol::IProtocolHandler::parse_client_version(raw_msg, out); + } + } catch (...) { + this->get_socket().close(); // Invalid request -> Abort + } + + return ret; + } + + void PreAuthPeer::authorize_request(Protocol::Request request) { + if (m_protocol.has_value()) { + // If Peer is sending requests while still in PreAuth, deny them + std::string message = m_protocol.value().handler().format_response(request.message_id, Protocol::StatusCode::UNAUTHORIZED); + + get_socket().write(message); + return; + } + + switch (request.code) { + case Protocol::CommandCode::SUPPORTED_VERSIONS: { + auto data = std::dynamic_pointer_cast(request.request); + + std::erase_if(data->versions, [](auto &version){ + return !Protocol::Version::NAMES.contains(version); + }); + if (data->versions.empty()) { + break; // No matching version -> Abort + } + + Protocol::Version version = std::ranges::max(data->versions); + + m_protocol = Protocol::Protocol(version); + get_socket().write(Protocol::IProtocolHandler::format_server_selected_version(version)); + return; + } + + case Protocol::CommandCode::SELECTED_VERSION: { + auto data = std::dynamic_pointer_cast(request.request); + + if (!Protocol::Version::NAMES.contains(data->version)) { + break; // Abort, server is asking for a version we dont support + } + m_protocol = Protocol::Protocol(data->version); + return; + } + default: + break; // Passthrough + } + this->get_socket().close(); // Abort connection + } +} diff --git a/source/Protocol/Handler/IProtocolHandler.cpp b/source/Protocol/Handler/IProtocolHandler.cpp new file mode 100644 index 0000000..fa6abdd --- /dev/null +++ b/source/Protocol/Handler/IProtocolHandler.cpp @@ -0,0 +1,122 @@ +/* +** Project LibFileShareProtocol, 2025 +** +** Author Francois Michaut +** +** Started on Mon Aug 11 10:31:09 2025 Francois Michaut +** Last update Fri Aug 15 14:38:09 2025 Francois Michaut +** +** IProtocolHandler.cpp : Functions common to all protocol versions +*/ + +#include "FileShare/Protocol/Definitions.hpp" +#include "FileShare/Protocol/Protocol.hpp" +#include "FileShare/Protocol/Version.hpp" +#include +#include +#include +#include +#include +#include +#include + +constexpr auto shift16 = 16U; +constexpr auto shift8 = 8U; + +constexpr auto mask8 = 0xFFU; +constexpr auto mask16 = 0xFF00U; +constexpr auto mask32 = 0xFF0000U; + +constexpr std::uint8_t version_count = FileShare::Protocol::Version::NAMES.size(); +constexpr auto &magic_bytes = FileShare::Protocol::IProtocolHandler::MAGIC_BYTES; +constexpr std::uint8_t static_bytes = 6; + +using ClientVersionPayoad = std::array; + +static consteval auto make_client_version_payload() -> ClientVersionPayoad { + const char cmd_code = static_cast(FileShare::Protocol::CommandCode::SUPPORTED_VERSIONS); + ClientVersionPayoad ret = {0, 0, 0, 0, cmd_code, version_count}; + std::uint8_t index = static_bytes; + + std::ranges::copy(std::string_view(magic_bytes), ret.begin()); + for (const auto &iter : FileShare::Protocol::Version::NAMES) { + ret[index] = static_cast((iter.first & mask32) >> shift16); + ret[index + 1] = static_cast((iter.first & mask16) >> shift8); + ret[index + 2] = static_cast(iter.first & mask8); + + index += 3; + } + + return ret; +} + +constexpr auto client_version_payload = make_client_version_payload(); + +namespace FileShare::Protocol { + auto IProtocolHandler::format_client_version_list() -> std::string_view { + return {client_version_payload.data(), client_version_payload.size()}; + } + + auto IProtocolHandler::format_server_selected_version(Version version) -> std::string { + std::string str = MAGIC_BYTES; + + str += static_cast(CommandCode::SELECTED_VERSION); + str += static_cast((version & mask32) >> shift16); + str += static_cast((version & mask16) >> shift8); + str += static_cast(version & mask8); + return str; + } + + auto IProtocolHandler::parse_client_version(std::string_view raw_msg, Request &out) -> std::size_t { + std::string_view client_begining = {client_version_payload.data(), 5}; + std::vector versions; + + if (raw_msg.size() < static_bytes) { + return 0; + } + if (!raw_msg.starts_with(client_begining)) { + throw std::runtime_error("Invalid Client Version message"); + } + std::uint8_t nb_versions = raw_msg[5]; + + if (raw_msg.size() < static_bytes + (nb_versions * 3)) { + return 0; + } + versions.reserve(nb_versions); + + std::string_view versions_str = raw_msg.substr(static_bytes); + for (std::uint8_t i = 0; i < nb_versions; i++) { + std::uint32_t version = (static_cast(versions_str[(i * 3) + 0]) << shift16) + + (static_cast(versions_str[(i * 3) + 1]) << shift8) + + (versions_str[(i * 3) + 2]); + + versions.emplace_back(static_cast(version)); + } + out.request = std::make_shared(std::move(versions)); + out.message_id = 0; + out.code = static_cast(raw_msg[4]); + + return static_bytes + (nb_versions * 3); + } + + auto IProtocolHandler::parse_server_version(std::string_view raw_msg, Request &out) -> std::size_t { + constexpr char cmd_byte = static_cast(CommandCode::SELECTED_VERSION); + + if (raw_msg.size() < 8) { + return 0; + } + if (!raw_msg.starts_with(MAGIC_BYTES) || raw_msg[4] != cmd_byte) { + throw std::runtime_error("Invalid Server Version message"); + } + + std::uint32_t version = (static_cast(raw_msg[5]) << shift16) + + (static_cast(raw_msg[6]) << shift8) + + (raw_msg[7]); + + out.request = std::make_shared(static_cast(version)); + out.message_id = 0; + out.code = static_cast(raw_msg[4]); + + return 8; + } +} diff --git a/source/Protocol/Handler/v0.0.0/ProtocolHandler.cpp b/source/Protocol/Handler/v0.0.0/ProtocolHandler.cpp index 6075244..e4bbb67 100644 --- a/source/Protocol/Handler/v0.0.0/ProtocolHandler.cpp +++ b/source/Protocol/Handler/v0.0.0/ProtocolHandler.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Fri May 5 21:35:06 2023 Francois Michaut -** Last update Sat Dec 9 08:58:26 2023 Francois Michaut +** Last update Fri Aug 15 13:48:30 2025 Francois Michaut ** ** ProtocolHandler.cpp : ProtocolHandler for the v0.0.0 of the protocol */ @@ -21,7 +21,9 @@ // TODO: enfore 8bytes limits on VARINTs namespace FileShare::Protocol::Handler::v0_0_0 { - std::string ProtocolHandler::format_request(const Request &request) { + const Utils::VarInt zero_varint = 0; + + auto ProtocolHandler::format_request(const Request &request) -> std::string { switch (request.code) { case CommandCode::RESPONSE: { auto data = std::dynamic_pointer_cast(request.request); @@ -66,20 +68,20 @@ namespace FileShare::Protocol::Handler::v0_0_0 { } } - std::size_t ProtocolHandler::parse_request(std::string_view raw_msg, Request &out) { - if (raw_msg.size() < base_header_size) + auto ProtocolHandler::parse_request(std::string_view raw_msg, Request &out) -> std::size_t { + if (raw_msg.size() < BASE_HEADER_SIZE) return 0; - if (raw_msg.rfind(magic_bytes, 0) != 0) + if (raw_msg.rfind(MAGIC_BYTES, 0) != 0) throw std::runtime_error("Missing magic bytes"); - CommandCode command_code = (CommandCode)raw_msg[4]; + auto command_code = static_cast(raw_msg[4]); std::uint8_t message_id = raw_msg[5]; std::size_t header_size; Utils::VarInt payload_size; if (!payload_size.parse(raw_msg.substr(6, 8))) throw std::runtime_error("MESSAGE_TOO_LONG"); - header_size = base_header_size + payload_size.byte_size(); + header_size = BASE_HEADER_SIZE + payload_size.byte_size(); if (raw_msg.size() < header_size + payload_size.to_number()) { return 0; // Payload is not complete, 0 bytes parsed } @@ -92,7 +94,7 @@ namespace FileShare::Protocol::Handler::v0_0_0 { return header_size + payload_size.to_number(); } - std::shared_ptr ProtocolHandler::get_request_data(CommandCode cmd, std::string_view payload) { + auto ProtocolHandler::get_request_data(CommandCode cmd, std::string_view payload) -> std::shared_ptr { switch (cmd) { case CommandCode::RESPONSE: return parse_response(payload); @@ -125,27 +127,27 @@ namespace FileShare::Protocol::Handler::v0_0_0 { // | 1 | // | ENUM | // --------------- - std::string ProtocolHandler::format_response(std::uint8_t message_id, const ResponseData &data) { + auto ProtocolHandler::format_response(std::uint8_t message_id, const ResponseData &data) -> std::string { std::string result; Utils::VarInt payload_size = 1; result.reserve(4 + 1 + 1 + payload_size.byte_size() + payload_size.to_number()); - result += magic_bytes; - result += (char)CommandCode::RESPONSE; - result += message_id; + result += MAGIC_BYTES; + result += static_cast(CommandCode::RESPONSE); + result += static_cast(message_id); result += payload_size.to_string(); - result += (char)data.status; + result += static_cast(data.status); return result; } - std::shared_ptr ProtocolHandler::parse_response(std::string_view payload) { - ResponseData data; + auto ProtocolHandler::parse_response(std::string_view payload) -> std::shared_ptr { Utils::VarInt varint; - if (payload.size() < 1) + if (payload.empty()) throw std::runtime_error("BAD_REQUEST"); - data.status = (StatusCode)payload[0]; - return std::make_shared(data); + auto status = static_cast(payload[0]); + + return std::make_shared(status); } // -------------------------------------------------------------------- @@ -161,9 +163,9 @@ namespace FileShare::Protocol::Handler::v0_0_0 { // | 8 | | - | | - | // | SIGNED INT | | VARINT | | VARINT | // ----------------------------------------------------- - std::string ProtocolHandler::format_send_file(std::uint8_t message_id, const SendFileData &data) { + auto ProtocolHandler::format_send_file(std::uint8_t message_id, const SendFileData &data) -> std::string { std::string result; - std::int64_t updated_at = Utils::to_epoch(data.last_updated); + std::uint64_t updated_at = Utils::to_epoch(data.last_updated); Utils::VarInt filepath_size = data.filepath.size(); Utils::VarInt packet_size = data.packet_size; Utils::VarInt total_packets = data.total_packets; @@ -175,13 +177,13 @@ namespace FileShare::Protocol::Handler::v0_0_0 { 1 + data.filehash.size() + 8 + packet_size.byte_size() + total_packets.byte_size(); result.reserve(4 + 1 + 1 + payload_size.byte_size() + payload_size.to_number()); - result += magic_bytes; - result += (char)CommandCode::SEND_FILE; - result += message_id; + result += MAGIC_BYTES; + result += static_cast(CommandCode::SEND_FILE); + result += static_cast(message_id); result += payload_size.to_string(); result += filepath_size.to_string(); result += data.filepath; - result += (std::uint8_t)data.hash_algorithm; + result += static_cast(data.hash_algorithm); result += data.filehash; result += Utils::serialize(updated_at); result += packet_size.to_string(); @@ -189,34 +191,43 @@ namespace FileShare::Protocol::Handler::v0_0_0 { return result; } - std::shared_ptr ProtocolHandler::parse_send_file(std::string_view payload) { - SendFileData data; + auto ProtocolHandler::parse_send_file(std::string_view payload) -> std::shared_ptr { Utils::VarInt varint; std::size_t algo_size; std::uint64_t updated_at; + std::string filepath; + Utils::HashAlgorithm hash_algorithm; + std::string filehash; + std::filesystem::file_time_type last_updated; + std::size_t packet_size; + std::size_t total_packets; + if (!varint.parse(payload, payload)) throw std::runtime_error("BAD_REQUEST"); if (payload.size() < varint.to_number()) throw std::runtime_error("BAD_REQUEST"); - data.filepath = payload.substr(0, varint.to_number()); + filepath = payload.substr(0, varint.to_number()); payload = payload.substr(varint.to_number()); - data.hash_algorithm = (Utils::HashAlgorithm)payload[0]; - algo_size = Utils::algo_hash_size(data.hash_algorithm); + hash_algorithm = static_cast(payload[0]); + + algo_size = Utils::algo_hash_size(hash_algorithm); if (payload.size() < 1 + algo_size + 8 + 1) throw std::runtime_error("BAD_REQUEST"); - data.filehash = payload.substr(1, algo_size); + filehash = payload.substr(1, algo_size); payload = payload.substr(1 + algo_size); Utils::parse(payload.substr(0, 8), updated_at); - data.last_updated = Utils::from_epoch(updated_at); + last_updated = Utils::from_epoch(updated_at); payload = payload.substr(8); if (!varint.parse(payload, payload)) throw std::runtime_error("BAD_REQUEST"); - data.packet_size = varint.to_number(); + packet_size = varint.to_number(); if (!varint.parse(payload, payload)) throw std::runtime_error("BAD_REQUEST"); - data.total_packets = varint.to_number(); - return std::make_shared(data); + total_packets = varint.to_number(); + return std::make_shared( + std::move(filepath), hash_algorithm, std::move(filehash), last_updated, packet_size, total_packets + ); } // ---------------------------------------------------------------------- @@ -228,18 +239,18 @@ namespace FileShare::Protocol::Handler::v0_0_0 { // | - | | FILEPATH_SIZE | | - | | - | // | VARINT | | - | | VARINT | | VARINT | // ---------------------------------------------------------------------- - std::string ProtocolHandler::format_receive_file(std::uint8_t message_id, const ReceiveFileData &data) { + auto ProtocolHandler::format_receive_file(std::uint8_t message_id, const ReceiveFileData &data) -> std::string { std::string result; Utils::VarInt filepath_size = data.filepath.size(); - Utils::VarInt v_packet_size = packet_size; + Utils::VarInt v_packet_size = PACKET_SIZE; Utils::VarInt v_packet_start = data.packet_start; Utils::VarInt payload_size = filepath_size.byte_size() + filepath_size.to_number() + v_packet_size.byte_size() + v_packet_start.byte_size(); result.reserve(4 + 1 + 1 + payload_size.byte_size() + payload_size.to_number()); - result += magic_bytes; - result += (char)CommandCode::RECEIVE_FILE; - result += message_id; + result += MAGIC_BYTES; + result += static_cast(CommandCode::RECEIVE_FILE); + result += static_cast(message_id); result += payload_size.to_string(); result += filepath_size.to_string(); result += data.filepath; @@ -248,23 +259,26 @@ namespace FileShare::Protocol::Handler::v0_0_0 { return result; } - std::shared_ptr ProtocolHandler::parse_receive_file(std::string_view payload) { - ReceiveFileData data; + auto ProtocolHandler::parse_receive_file(std::string_view payload) -> std::shared_ptr { Utils::VarInt varint; + std::string filepath; + std::size_t packet_size; + std::size_t packet_start; + if (!varint.parse(payload, payload)) throw std::runtime_error("BAD_REQUEST"); if (payload.size() < varint.to_number()) throw std::runtime_error("BAD_REQUEST"); - data.filepath = payload.substr(0, varint.to_number()); + filepath = payload.substr(0, varint.to_number()); payload = payload.substr(varint.to_number()); if (!varint.parse(payload, payload)) throw std::runtime_error("BAD_REQUEST"); - data.packet_size = varint.to_number(); + packet_size = varint.to_number(); if (!varint.parse(payload, payload)) throw std::runtime_error("BAD_REQUEST"); - data.packet_start = varint.to_number(); - return std::make_shared(data); + packet_start = varint.to_number(); + return std::make_shared(std::move(filepath), packet_size, packet_start); } // ------------------------------------------------------------------------- @@ -276,31 +290,32 @@ namespace FileShare::Protocol::Handler::v0_0_0 { // | - | | FOLDERPATH_SIZE | // | VARINT | | STRING | // ------------------------------------- - std::string ProtocolHandler::format_list_files(std::uint8_t message_id, const ListFilesData &data) { + auto ProtocolHandler::format_list_files(std::uint8_t message_id, const ListFilesData &data) -> std::string { std::string result; Utils::VarInt folderpath_size = data.folderpath.size(); Utils::VarInt payload_size = folderpath_size.byte_size() + folderpath_size.to_number(); result.reserve(4 + 1 + 1 + payload_size.byte_size() + payload_size.to_number()); - result += magic_bytes; - result += (char)CommandCode::LIST_FILES; - result += message_id; + result += MAGIC_BYTES; + result += static_cast(CommandCode::LIST_FILES); + result += static_cast(message_id); result += payload_size.to_string(); result += folderpath_size.to_string(); result += data.folderpath; return result; } - std::shared_ptr ProtocolHandler::parse_list_files(std::string_view payload) { - ListFilesData data; + auto ProtocolHandler::parse_list_files(std::string_view payload) -> std::shared_ptr { Utils::VarInt varint; + std::string folderpath; + if (!varint.parse(payload, payload)) throw std::runtime_error("BAD_REQUEST"); if (payload.size() < varint.to_number()) throw std::runtime_error("BAD_REQUEST"); - data.folderpath = payload.substr(0, varint.to_number()); - return std::make_shared(data); + folderpath = payload.substr(0, varint.to_number()); + return std::make_shared(std::move(folderpath)); } // ------------------------------------------------------------------ @@ -316,7 +331,7 @@ namespace FileShare::Protocol::Handler::v0_0_0 { // | ITEM_COUNT [ - , STRING , 1 ] | // | - [ VARINT , FILEPATH_SIZE, ENUM ] | // --------------------------------------------------------- - std::string ProtocolHandler::format_file_list(std::uint8_t message_id, const FileListData &data) { + auto ProtocolHandler::format_file_list(std::uint8_t message_id, const FileListData &data) -> std::string { std::string result; std::size_t array_total_size = 0; Utils::VarInt item_count = data.files.size(); @@ -324,45 +339,48 @@ namespace FileShare::Protocol::Handler::v0_0_0 { Utils::VarInt payload_size = 0; for (const auto &file : data.files) { - Utils::VarInt v = file.path.size(); + Utils::VarInt varint = file.path.size(); - array_total_size += v.byte_size() + v.to_number() + 1; + array_total_size += varint.byte_size() + varint.to_number() + 1; } payload_size = 1 + v_packet_id.byte_size() + item_count.byte_size() + array_total_size; result.reserve(4 + 1 + 1 + payload_size.byte_size() + payload_size.to_number()); - result += magic_bytes; - result += (char)CommandCode::FILE_LIST; - result += message_id; + result += MAGIC_BYTES; + result += static_cast(CommandCode::FILE_LIST); + result += static_cast(message_id); result += payload_size.to_string(); - result += data.request_id; + result += static_cast(data.request_id); result += v_packet_id.to_string(); result += item_count.to_string(); for (const auto &file : data.files) { - Utils::VarInt v = file.path.size(); + Utils::VarInt varint = file.path.size(); - result += v.to_string(); + result += varint.to_string(); result += file.path; - result += (std::uint8_t)file.file_type; + result += static_cast(file.file_type); } return result; } - std::shared_ptr ProtocolHandler::parse_file_list(std::string_view payload) { - FileListData data; + auto ProtocolHandler::parse_file_list(std::string_view payload) -> std::shared_ptr { Utils::VarInt varint; std::size_t nb_items; - data.request_id = payload[0]; + std::uint8_t request_id; + std::size_t packet_id; + std::vector files; + + request_id = payload[0]; payload = payload.substr(1); if (!varint.parse(payload, payload)) throw std::runtime_error("BAD_REQUEST"); - data.packet_id = varint.to_number(); + packet_id = varint.to_number(); if (!varint.parse(payload, payload)) throw std::runtime_error("BAD_REQUEST"); nb_items = varint.to_number(); - data.files.reserve(nb_items); + files.reserve(nb_items); for (std::size_t i = 0; i < nb_items; i++) { FileInfo file_info; @@ -372,11 +390,11 @@ namespace FileShare::Protocol::Handler::v0_0_0 { throw std::runtime_error("BAD_REQUEST"); file_info.path = payload.substr(0, varint.to_number()); payload = payload.substr(varint.to_number()); - file_info.file_type = (FileType)payload[0]; - data.files.emplace_back(file_info); + file_info.file_type = static_cast(payload[0]); + files.emplace_back(file_info); payload = payload.substr(1); } - return std::make_shared(data); + return std::make_shared(request_id, packet_id, std::move(files)); } // --------------------------------------------------------------------- @@ -388,7 +406,7 @@ namespace FileShare::Protocol::Handler::v0_0_0 { // | 1 | | - | | - | | PACKET_SIZE | // | - | | VARINT | | VARINT | | STRING | // --------------------------------------------------------------------- - std::string ProtocolHandler::format_data_packet(std::uint8_t message_id, const DataPacketData &data) { + auto ProtocolHandler::format_data_packet(std::uint8_t message_id, const DataPacketData &data) -> std::string { std::string result; Utils::VarInt packet_id = data.packet_id; Utils::VarInt packet_size = data.data.size(); @@ -396,32 +414,35 @@ namespace FileShare::Protocol::Handler::v0_0_0 { Utils::VarInt payload_size = 1 + packet_id.byte_size() + packet_size.byte_size() + packet_size.to_number(); result.reserve(4 + 1 + 1 + payload_size.byte_size()); - result += magic_bytes; - result += (char)CommandCode::DATA_PACKET; - result += message_id; + result += MAGIC_BYTES; + result += static_cast(CommandCode::DATA_PACKET); + result += static_cast(message_id); result += payload_size.to_string(); - result += data.request_id; + result += static_cast(data.request_id); result += packet_id.to_string(); result += packet_size.to_string(); result += data.data; return result; } - std::shared_ptr ProtocolHandler::parse_data_packet(std::string_view payload) { - DataPacketData data; + auto ProtocolHandler::parse_data_packet(std::string_view payload) -> std::shared_ptr { Utils::VarInt varint; - data.request_id = payload[0]; + std::uint8_t request_id; + std::size_t packet_id; + std::string data; + + request_id = payload[0]; payload = payload.substr(1); if (!varint.parse(payload, payload)) throw std::runtime_error("BAD_REQUEST"); - data.packet_id = varint.to_number(); + packet_id = varint.to_number(); if (!varint.parse(payload, payload)) throw std::runtime_error("BAD_REQUEST"); if (payload.size() < varint.to_number()) throw std::runtime_error("BAD_REQUEST"); - data.data = payload.substr(0, varint.to_number()); - return std::make_shared(data); + data = payload.substr(0, varint.to_number()); + return std::make_shared(request_id, packet_id, std::move(data)); } // ------------------------------------------------------------------ @@ -429,19 +450,18 @@ namespace FileShare::Protocol::Handler::v0_0_0 { // | 4 | | 1 | | 1 | | MAX(8) | // | STRING | | ENUM | | - | | VARINT | // ------------------------------------------------------------------ - std::string ProtocolHandler::format_ping(std::uint8_t message_id, [[maybe_unused]] const PingData &data) { + auto ProtocolHandler::format_ping(std::uint8_t message_id, [[maybe_unused]] const PingData &data) -> std::string { std::string result; - static const Utils::VarInt payload_size = 0; - result.reserve(4 + 1 + 1 + payload_size.byte_size()); - result += magic_bytes; - result += (char)CommandCode::PING; - result += message_id; - result += payload_size.to_string(); + result.reserve(4 + 1 + 1 + zero_varint.byte_size()); + result += MAGIC_BYTES; + result += static_cast(CommandCode::PING); + result += static_cast(message_id); + result += zero_varint.to_string(); return result; } - std::shared_ptr ProtocolHandler::parse_ping([[maybe_unused]] std::string_view payload) { + auto ProtocolHandler::parse_ping([[maybe_unused]] std::string_view payload) -> std::shared_ptr { return std::make_shared(); } } diff --git a/source/Protocol/Protocol.cpp b/source/Protocol/Protocol.cpp index d9d8b40..cc87d33 100644 --- a/source/Protocol/Protocol.cpp +++ b/source/Protocol/Protocol.cpp @@ -4,18 +4,21 @@ ** Author Francois Michaut ** ** Started on Thu Aug 25 23:16:42 2022 Francois Michaut -** Last update Sat Nov 11 17:33:45 2023 Francois Michaut +** Last update Fri Aug 22 23:17:35 2025 Francois Michaut ** ** Protocol.cpp : Implementation of the main Protocol class */ #include "FileShare/Protocol/Protocol.hpp" +#include "FileShare/Protocol/Definitions.hpp" #include "FileShare/Protocol/Handler/v0.0.0/ProtocolHandler.hpp" +#include "FileShare/Utils/Strings.hpp" +#include #include namespace FileShare::Protocol { - std::map> Protocol::protocol_list = { + const std::map> Protocol::PROTOCOL_LIST = { {Version::v0_0_0, std::make_shared()} }; @@ -30,16 +33,16 @@ namespace FileShare::Protocol { {} void Protocol::set_version(Version version) { - auto iter = protocol_list.find(version); + auto iter = PROTOCOL_LIST.find(version); - if (iter == protocol_list.end()) { + if (iter == PROTOCOL_LIST.end()) { throw std::runtime_error("Unsuported protocol version"); } m_handler = iter->second; } - CommandCode str_to_command(std::string str) { - static std::unordered_map str_to_command = { + auto str_to_command(std::string_view str) -> CommandCode { + const static std::unordered_map> str_to_command = { {"RESPONSE", CommandCode::RESPONSE}, {"SEND_FILE", CommandCode::SEND_FILE}, @@ -63,8 +66,8 @@ namespace FileShare::Protocol { throw std::runtime_error("Unknown command"); } - StatusCode str_to_status(std::string str) { - static std::unordered_map str_to_status = { + auto str_to_status(std::string_view str) -> StatusCode { + const static std::unordered_map> str_to_status = { {"STATUS_OK", StatusCode::STATUS_OK}, {"UP_TO_DATE", StatusCode::UP_TO_DATE}, {"MESSAGE_TOO_LONG", StatusCode::MESSAGE_TOO_LONG}, @@ -87,8 +90,22 @@ namespace FileShare::Protocol { throw std::runtime_error("Unknown status"); } - std::string command_to_str(CommandCode command) { - static std::unordered_map command_to_str = { + auto str_to_file_type(std::string_view str) -> FileType { + const static std::unordered_map> str_to_file_type = { + {"FILE", FileType::FILE}, + {"DIRECTORY", FileType::DIRECTORY}, + }; + + auto iter = str_to_file_type.find(str); + + if (iter != str_to_file_type.end()) { + return iter->second; + } + throw std::runtime_error("Unknown FileType"); + } + + auto command_to_str(CommandCode command) -> std::string_view { + const static std::unordered_map command_to_str = { {CommandCode::RESPONSE, "RESPONSE"}, {CommandCode::SEND_FILE, "SEND_FILE"}, @@ -113,14 +130,15 @@ namespace FileShare::Protocol { return "__UNKNOWN_COMMAND__"; } - std::string status_to_str(StatusCode status) { - static std::unordered_map status_to_str = { + auto status_to_str(StatusCode status) -> std::string_view { + const static std::unordered_map status_to_str = { {StatusCode::STATUS_OK, "STATUS_OK"}, {StatusCode::UP_TO_DATE, "UP_TO_DATE"}, {StatusCode::MESSAGE_TOO_LONG, "MESSAGE_TOO_LONG"}, {StatusCode::APPROVAL_PENDING, "APPROVAL_PENDING"}, {StatusCode::BAD_REQUEST, "BAD_REQUEST"}, + {StatusCode::UNAUTHORIZED, "UNAUTHORIZED"}, {StatusCode::INVALID_REQUEST_ID, "INVALID_REQUEST_ID"}, {StatusCode::FORBIDDEN, "FORBIDDEN"}, {StatusCode::FILE_NOT_FOUND, "FILE_NOT_FOUND"}, @@ -138,17 +156,36 @@ namespace FileShare::Protocol { return "__UNKNOWN_STATUS__"; } + + auto file_type_to_str(FileType type) -> std::string_view { + const static std::unordered_map file_type_to_str = { + {FileType::FILE, "FILE"}, + {FileType::DIRECTORY, "DIRECTORY"}, + }; + + auto iter = file_type_to_str.find(type); + + if (iter != file_type_to_str.end()) { + return iter->second; + } + + return "__UNKNOWN_STATUS__"; + } } -std::ostream& operator<<(std::ostream& os, const FileShare::Protocol::StatusCode& status) { +auto operator<<(std::ostream& os, const FileShare::Protocol::StatusCode& status) -> std::ostream & { return os << status_to_str(status); } -std::ostream& operator<<(std::ostream& os, const FileShare::Protocol::CommandCode& command) { +auto operator<<(std::ostream& os, const FileShare::Protocol::CommandCode& command) -> std::ostream & { return os << command_to_str(command); } -std::istream& operator>>(std::istream& is, FileShare::Protocol::StatusCode& status) { +auto operator<<(std::ostream& os, const FileShare::Protocol::FileType &type) -> std::ostream & { + return os << file_type_to_str(type); +} + +auto operator>>(std::istream& is, FileShare::Protocol::StatusCode& status) -> std::istream & { std::string str; is >> str; @@ -156,10 +193,18 @@ std::istream& operator>>(std::istream& is, FileShare::Protocol::StatusCode& stat return is; } -std::istream& operator>>(std::istream& is, FileShare::Protocol::CommandCode& command) { +auto operator>>(std::istream& is, FileShare::Protocol::CommandCode& command) -> std::istream & { std::string str; is >> str; command = FileShare::Protocol::str_to_command(str); return is; } + +auto operator>>(std::istream& is, FileShare::Protocol::FileType &type) -> std::istream & { + std::string str; + + is >> str; + type = FileShare::Protocol::str_to_file_type(str); + return is; +} diff --git a/source/Protocol/RequestData.cpp b/source/Protocol/RequestData.cpp index e0fdc84..3b28fec 100644 --- a/source/Protocol/RequestData.cpp +++ b/source/Protocol/RequestData.cpp @@ -4,47 +4,60 @@ ** Author Francois Michaut ** ** Started on Tue Jul 18 22:04:57 2023 Francois Michaut -** Last update Sat Dec 9 09:01:16 2023 Francois Michaut +** Last update Fri Aug 15 21:04:29 2025 Francois Michaut ** ** RequestData.cpp : RequestData implementation for the requests payloads */ #include "FileShare/Protocol/RequestData.hpp" -#include "FileShare/Utils/Serialize.hpp" +#include "FileShare/Protocol/Version.hpp" #include "FileShare/Utils/Time.hpp" #include +#include namespace FileShare::Protocol { ResponseData::ResponseData(StatusCode status) : status(status) {} - SendFileData::SendFileData(std::string filepath, Utils::HashAlgorithm hash_algorithm, std::string filehash, std::filesystem::file_time_type last_updated, std::size_t packet_size, std::size_t total_packets) : - filepath(filepath), hash_algorithm(hash_algorithm), filehash(filehash), last_updated(last_updated), packet_size(packet_size), total_packets(total_packets) + SupportedVersionsData::SupportedVersionsData(std::vector versions) : + versions(std::move(versions)) + {} + + SelectedVersionData::SelectedVersionData(Version version) : + version(version) + {} + + SendFileData::SendFileData( + std::string filepath, Utils::HashAlgorithm hash_algorithm, std::string filehash, + std::filesystem::file_time_type last_updated, std::size_t packet_size, std::size_t total_packets + ) : + filepath(std::move(filepath)), hash_algorithm(hash_algorithm), filehash(std::move(filehash)), + last_updated(last_updated), packet_size(packet_size), total_packets(total_packets) {} ReceiveFileData::ReceiveFileData(std::string filepath, std::size_t packet_size, std::size_t packet_start) : - filepath(filepath), packet_size(packet_size), packet_start(packet_start) + filepath(std::move(filepath)), packet_size(packet_size), packet_start(packet_start) {} ListFilesData::ListFilesData(std::string folderpath) : - folderpath(folderpath) + folderpath(std::move(folderpath)) {} FileListData::FileListData(std::uint8_t request_id, std::size_t packet_id, std::vector files) : - request_id(request_id), packet_id(packet_id), files(files) + request_id(request_id), packet_id(packet_id), files(std::move(files)) {} DataPacketData::DataPacketData(std::uint8_t request_id, std::size_t packet_id, std::string data) : - request_id(request_id), packet_id(packet_id), data(data) + request_id(request_id), packet_id(packet_id), data(std::move(data)) {} ApprovalStatusData::ApprovalStatusData(std::uint8_t request_message_id, bool status) : request_message_id(request_message_id), status(status) {} - std::string ResponseData::debug_str() const { + auto ResponseData::debug_str() const -> std::string { std::stringstream ss; ss << "ResponseData{" @@ -53,39 +66,53 @@ namespace FileShare::Protocol { return ss.str(); } - std::string PingData::debug_str() const { + auto SupportedVersionsData::debug_str() const -> std::string { std::stringstream ss; - ss << "PingData{}"; + ss << "SupportedVersionsData{" + << "versions (count = " << versions.size() << ") = ["; + for (const auto &version : versions) { + ss << version << ", "; + } + ss << "]}"; + return ss.str(); + } + + auto SelectedVersionData::debug_str() const -> std::string { + std::stringstream ss; + + ss << "SelectedVersionData{" + << "version = " << version + << "}"; return ss.str(); } - std::string DataPacketData::debug_str() const { + auto DataPacketData::debug_str() const -> std::string { std::stringstream ss; ss << "DataPacketData{" - << "request_id = " << (int)request_id + << "request_id = " << static_cast(request_id) << ", packed_id = " << packet_id << ", data_size = " << data.size() << "}"; return ss.str(); } - std::string FileListData::debug_str() const { + auto FileListData::debug_str() const -> std::string { std::stringstream ss; ss << "FileListData{" - << "request_id = " << (int)request_id + << "request_id = " << static_cast(request_id) << ", packet_id = " << packet_id << ", files (count = " << files.size() << ") = ["; for (const auto &file : files) { - ss << "{ path = " << file.path << ", file_type = " << (int)file.file_type << " }, "; + ss << "{ path = " << file.path << ", file_type = " << static_cast(file.file_type) << " }, "; } ss << "]}"; return ss.str(); } - std::string ListFilesData::debug_str() const { + auto ListFilesData::debug_str() const -> std::string { std::stringstream ss; ss << "ListFilesData{" @@ -94,7 +121,7 @@ namespace FileShare::Protocol { return ss.str(); } - std::string ReceiveFileData::debug_str() const { + auto ReceiveFileData::debug_str() const -> std::string { std::stringstream ss; ss << "ReceiveFileData{" @@ -105,7 +132,7 @@ namespace FileShare::Protocol { return ss.str(); } - std::string SendFileData::debug_str() const { + auto SendFileData::debug_str() const -> std::string { std::stringstream ss; ss << "SendFileData{" @@ -119,11 +146,18 @@ namespace FileShare::Protocol { return ss.str(); } - std::string ApprovalStatusData::debug_str() const { + auto PingData::debug_str() const -> std::string { + std::stringstream ss; + + ss << "PingData{}"; + return ss.str(); + } + + auto ApprovalStatusData::debug_str() const -> std::string { std::stringstream ss; ss << "ApprovalStatusData{" - << "request_message_id = " << (int)request_message_id + << "request_message_id = " << static_cast(request_message_id) << ", status = " << status << "}"; return ss.str(); diff --git a/source/Protocol/Version.cpp b/source/Protocol/Version.cpp index 9fb009b..b417273 100644 --- a/source/Protocol/Version.cpp +++ b/source/Protocol/Version.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sat May 6 10:37:57 2023 Francois Michaut -** Last update Tue Nov 28 13:40:51 2023 Francois Michaut +** Last update Sun Aug 24 11:21:02 2025 Francois Michaut ** ** ProtocolVersion.cpp : A class to represent a Protocol Version */ @@ -13,28 +13,40 @@ #include +#include +#include + namespace FileShare::Protocol { Version::Version(VersionEnum version) : m_version(version), m_versions({ - (std::uint8_t)((version & 0xFF0000) >> 16), - (std::uint8_t)((version & 0x00FF00) >> 8), - (std::uint8_t)(version & 0x0000FF) + static_cast((version & 0xFF0000) >> 16), + static_cast((version & 0x00FF00) >> 8), + static_cast(version & 0x0000FF) }) {} - bool Version::operator==(const Version &o) const { - return m_version == o.m_version; + auto Version::operator==(const Version &other) const -> bool { + return m_version == other.m_version; } - std::strong_ordering Version::operator<=>(const Version &o) const { + auto Version::operator<=>(const Version &other) const -> std::strong_ordering { #ifdef OS_APPLE - if (std::lexicographical_compare(m_versions.begin(), m_versions.end(), o.m_versions.begin(), o.m_versions.end())) + if (std::lexicographical_compare(m_versions.begin(), m_versions.end(), other.m_versions.begin(), other.m_versions.end())) return std::strong_ordering::less; - else if (std::lexicographical_compare(o.m_versions.begin(), o.m_versions.end(), m_versions.begin(), m_versions.end())) + else if (std::lexicographical_compare(other.m_versions.begin(), other.m_versions.end(), m_versions.begin(), m_versions.end())) return std::strong_ordering::greater; return std::strong_ordering::equal; #else - return m_versions <=> o.m_versions; + return m_versions <=> other.m_versions; #endif } + + auto Version::to_string() const -> std::string_view { + const auto *iter = Version::NAMES.find(this->m_version); + + if (iter == Version::NAMES.end()) { + throw std::runtime_error(std::format("Unkown Version: {:d}", static_cast(this->m_version))); + } + return iter->second; + } } diff --git a/source/Server.cpp b/source/Server.cpp index 2d6735a..3d30ab1 100644 --- a/source/Server.cpp +++ b/source/Server.cpp @@ -4,103 +4,209 @@ ** Author Francois Michaut ** ** Started on Sun Nov 6 21:06:10 2022 Francois Michaut -** Last update Sun Dec 10 18:07:06 2023 Francois Michaut +** Last update Fri Aug 22 23:57:25 2025 Francois Michaut ** ** Server.cpp : Server implementation */ #include "FileShare/Server.hpp" +#include "CppSockets/Tls/Socket.hpp" +#include "FileShare/Utils/Poll.hpp" +#include "FileShare/Utils/Vector.hpp" +#include +#include + +#include +#include +#include #include #include #include #include +#include #include -#include - -#ifdef OS_UNIX - #include -#else // Windows only - using nfds_t=std::size_t; +#include +#include +#include +#include + +constexpr const auto ORG_NAME = u8"FileShare"; +constexpr const auto ORG_NAME_SIZE = std::char_traits::length(ORG_NAME); +constexpr const auto ORG_UNIT_NAME = u8"FileShare Self-Signed Device Certificate"; +constexpr const auto ORG_UNIT_NAME_SIZE = std::char_traits::length(ORG_UNIT_NAME); + +namespace { + // TODO: Once we have a central server, make that server sign certificates so we dont have + // to rely on self-signed ones. Self-Signed will only be used on Offline networks. + auto verify_callback(int preverify_ok, X509_STORE_CTX *ctx) -> int { + int err = X509_STORE_CTX_get_error(ctx); // man X509_STORE_CTX_get_error + static std::basic_string_view org_name_cmp {ORG_NAME, ORG_NAME_SIZE}; + static std::basic_string_view org_unit_name_cmp {ORG_UNIT_NAME, ORG_UNIT_NAME_SIZE}; + + // If we want to get the Server instance of that SSL connection : + // SSL *ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); + // FileShare::Server *server = SSL_get_ex_data(ssl, mydata_index); -> use SSL_get_ex_new_index to get index + + if (err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) { + // This only returns a certificate if there was an error + X509 *err_cert = X509_STORE_CTX_get_current_cert(ctx); + + // TODO: Need to check before / after date ? + // TODO: Find a way to check revoked certificates -> https://en.wikipedia.org/wiki/Certificate_revocation_list + + X509_NAME *subject = X509_get_subject_name(err_cert); + int org_name_index = X509_NAME_get_index_by_NID(subject, NID_organizationName, -1); + int org_unit_name_index = X509_NAME_get_index_by_NID(subject, NID_organizationalUnitName, -1); + // int user_id_index = X509_NAME_get_index_by_NID(subject, NID_userId, -1); + int common_name_index = X509_NAME_get_index_by_NID(subject, NID_commonName, -1); + int dn_qualifier_index = X509_NAME_get_index_by_NID(subject, NID_dnQualifier, -1); + + X509_NAME_ENTRY *org_name = X509_NAME_get_entry(subject, org_name_index); + X509_NAME_ENTRY *org_unit_name = X509_NAME_get_entry(subject, org_unit_name_index); + // X509_NAME_ENTRY *user_id = X509_NAME_get_entry(subject, user_id_index); + X509_NAME_ENTRY *common_name = X509_NAME_get_entry(subject, common_name_index); + X509_NAME_ENTRY *dn_qualifier = X509_NAME_get_entry(subject, dn_qualifier_index); + + bool cert_valid = org_name && org_unit_name && /* user_id && */common_name && dn_qualifier; + + if (cert_valid) { + ASN1_STRING *org_name_str = X509_NAME_ENTRY_get_data(org_name); + ASN1_STRING *org_unit_name_str = X509_NAME_ENTRY_get_data(org_unit_name); + // ASN1_STRING *user_id_str = X509_NAME_ENTRY_get_data(user_id); + // ASN1_STRING *dn_qualifier_str = X509_NAME_ENTRY_get_data(dn_qualifier); + + const auto *org_name_data = reinterpret_cast( + ASN1_STRING_get0_data(org_name_str) + ); + const auto *org_unit_name_data = reinterpret_cast( + ASN1_STRING_get0_data(org_unit_name_str) + ); + + std::basic_string_view org_name_view { + org_name_data, static_cast(ASN1_STRING_length(org_name_str)) + }; + std::basic_string_view org_unit_name_view { + org_unit_name_data, static_cast(ASN1_STRING_length(org_unit_name_str)) + }; + + // TODO: Do this properly by converting encodings, so we can accept certs with different encodings + // File with available X509 fields NIDs : /usr/include/openssl/obj_mac.h + // File with available string types + max_lenght of X509 fields : /usr/include/openssl/asn1.h + if (org_name_view != org_name_cmp || org_unit_name_view != org_unit_name_cmp) { + cert_valid = false; + } + // TODO: Verify user_id_str && dn_qualifier_str are in UUID format (user_id_str is optional) + } - static auto &poll=WSAPoll; // alias function poll to WSAPoll -#endif + if (!cert_valid) { + X509_STORE_CTX_set_error(ctx, X509_V_ERR_APPLICATION_VERIFICATION); + return 0; + } + X509_STORE_CTX_set_error(ctx, X509_V_OK); + return 1; + } + // If there is an error other than SELF_SIGNED_CERT, we let it bubble up + return preverify_ok; + } +} namespace FileShare { - Server::Server(Config config) : Server(Server::default_endpoint(), std::move(config)) + // NOLINTNEXTLINE(hicpp-signed-bitwise) + static constexpr int VERIFY_MODE = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT | SSL_VERIFY_CLIENT_ONCE; + + Server::Server(ServerConfig config, Config peer_config) : + Server(Server::default_endpoint(), std::move(config), std::move(peer_config)) {} - Server::Server(std::shared_ptr server_endpoint, Config config) : - m_server_endpoint(server_endpoint), - m_config(std::move(config)) + Server::Server(std::shared_ptr server_endpoint, ServerConfig config, Config peer_config) : + m_server_endpoint(std::move(server_endpoint)), m_ctx(SSL_CTX_new(TLS_method())), + m_config(std::move(config)), m_peer_config(std::move(peer_config)) { + // Request for client certificate + verify it + m_ctx.set_verify(VERIFY_MODE, verify_callback); restart(); } + // "~Server() = default" needs to be defined in the source file, + // cause the header has an incomplete definition of struct pollfd + Server::~Server() = default; + void Server::restart() { + auto base_path = std::filesystem::path(m_config.get_private_keys_dir()); + auto key_path = base_path / (m_config.get_private_key_name() + "_key.pem"); + auto cert_path = base_path / (m_config.get_private_key_name() + "_cert.pem"); + initialize_private_key(); - initialize_download_directory(); + this->m_ctx.set_certificate(cert_path.generic_string(), key_path.generic_string()); + + if (m_fds.empty()) { + m_fds.emplace_back(pollfd{.fd = -1, .events = POLLIN, .revents = 0}); // Create server socket slot + } + if (!this->disabled()) { - // TODO: avoid this duplication - auto key_path = std::filesystem::path(m_config.get_private_keys_dir()) / (m_config.get_private_key_name() + "_key.pem"); - auto cert_path = std::filesystem::path(m_config.get_private_keys_dir()) / (m_config.get_private_key_name() + "_cert.pem"); - - this->m_socket = CppSockets::TlsSocket(AF_INET, SOCK_STREAM, 0); - this->m_socket.set_reuseaddr(true); - this->m_socket.set_certificate(cert_path.generic_string(), key_path.generic_string()); - this->m_socket.bind(*this->m_server_endpoint); - this->m_socket.listen(10); // TODO: configurable backlog + // Required in case the CTX params changes - SSL Sockets dont pick up on changes otherwise + m_socket = CppSockets::TlsSocket(AF_INET, SOCK_STREAM, 0, m_ctx); + m_socket.set_reuseaddr(true); + // TODO: Set non-blocking ? + m_socket.bind(*this->m_server_endpoint); + m_socket.listen(10); // TODO: configurable backlog + m_fds[0].fd = m_socket.get_fd(); + } else { + m_fds[0].fd = -1; + m_socket.close(); } } - std::shared_ptr &Server::connect(CppSockets::TlsSocket peer) { - return connect(std::move(peer), this->m_config); + void Server::set_disabled(bool disabled) { + m_config.set_server_disabled(disabled); + restart(); } - std::shared_ptr &Server::connect(const CppSockets::IEndpoint &peer) { - return connect(peer, this->m_config); - } + auto Server::connect(CppSockets::TlsSocket peer, const Config &config) -> std::shared_ptr & { + PreAuthPeer pre_auth(std::move(peer), PreAuthPeer::CLIENT); + + // TODO: Bad. Change it + pre_auth.do_client_hello(); + while (!pre_auth.has_protocol() && pre_auth.get_socket().connected()) { + pre_auth.poll_requests(); + } - std::shared_ptr &Server::connect(CppSockets::TlsSocket peer, const Config &config) { - // TODO: handle auth + version negotiation - std::shared_ptr client = std::make_shared(std::move(peer), "TODO", "TODO", Protocol::Protocol(Protocol::Version::v0_0_0), config); + std::shared_ptr client = std::make_shared(std::move(pre_auth), config); - return insert_client(std::move(client)); + m_fds.emplace_back(pollfd({.fd = client->get_socket().get_fd(), .events = POLLIN, .revents = 0})); + return insert_peer(std::move(client)); } - std::shared_ptr &Server::connect(const CppSockets::IEndpoint &peer, const Config &config) { - // TODO: handle auth + version negotiation - std::shared_ptr client = std::make_shared(peer, "TODO", "TODO", Protocol::Protocol(Protocol::Version::v0_0_0), config); + auto Server::connect(const CppSockets::IEndpoint &peer, const Config &config) -> std::shared_ptr & { + CppSockets::TlsSocket socket(AF_INET, SOCK_STREAM, 0, m_ctx); - return insert_client(std::move(client)); + socket.connect(peer); + return connect(std::move(socket), config); } - Config &Server::get_config() { return m_config; } - const Config &Server::get_config() const { return m_config; } - void Server::set_config(const Config &config) { m_config = config; } - const CppSockets::TlsSocket &Server::get_socket() const { return m_socket; } - const CppSockets::IEndpoint &Server::get_server_endpoint() const { return *m_server_endpoint; } - bool Server::disabled() const { return m_config.is_server_disabled(); } - std::map> &Server::get_clients() { return m_clients; } - - void Server::process_events(ClientAcceptCallback accept_cb, ClientRequestCallback request_cb) { + void Server::process_events(const PeerAcceptCallback &accept_cb, const PeerRequestCallback &request_cb) { poll_events(); - for (auto iter = m_events.begin(); iter != m_events.end(); iter++) { - auto request = iter->request(); + for (auto &iter : m_events) { + auto type = iter.type(); - if (request.has_value()) { - request_cb(*this, iter->client(), request.value()); - } else { - if (accept_cb(*this, iter->client())) { - insert_client(iter->client()); + if (type == Event::REQUEST) { + auto peer = std::dynamic_pointer_cast(iter.peer()); + + request_cb(*this, peer, iter.request().value()); + } else if (type == Event::CONNECT) { + auto peer = std::dynamic_pointer_cast(std::move(iter.peer())); + + if (accept_cb(*this, peer)) { + accept_peer(std::move(peer)); } } } m_events.clear(); } - bool Server::pull_event(Event &result) { + auto Server::pull_event(Event &result) -> bool { if (m_events.empty()) poll_events(); if (m_events.empty()) { @@ -112,134 +218,210 @@ namespace FileShare { return true; } - Config Server::default_config() - { + auto Server::default_config() -> ServerConfig { return {}; // TODO: explicitely set default params } - std::shared_ptr Server::default_endpoint() - { + auto Server::default_peer_config() -> Config { + return {}; // TODO: explicitely set default params + } + + auto Server::default_endpoint() -> std::shared_ptr { // TODO: choose a better port than 12345 return std::make_shared(CppSockets::IPv4("127.0.0.1"), 12345); } - void Server::accept_client(std::shared_ptr peer) { - insert_client(std::move(peer)); + void Server::accept_peer(PreAuthPeer_ptr peer, bool temporary_trust) { + if (!temporary_trust) { + // Add to known hosts + m_known_peers.insert(peer->get_device_uuid(), peer->get_public_key()); + } + m_pending_authorization_peers.erase(peer->get_socket().get_fd()); + insert_peer(std::move(peer)); } - // TODO: do not accept double connection from clients - std::shared_ptr &Server::insert_client(std::shared_ptr client) { - const auto &result = m_clients.emplace(client->get_socket().get_fd(), std::move(client)); + auto Server::insert_peer(PreAuthPeer_ptr &&peer) -> Peer_ptr & { + auto new_peer = std::make_shared(std::move(*peer), m_peer_config); - return result.first->second; // TODO: check this does return a reference + return insert_peer(new_peer); } - void Server::poll_events() { - // TODO: avoid re-allocating the vector every time - std::vector fds; - nfds_t nfds = m_clients.size() + 1; - struct timespec timeout = {1, 0}; // TODO: configurable wait (currently 1s) - int nb_ready = 0; - - fds.reserve(nfds); - fds.emplace_back(pollfd({m_socket.get_fd(), POLLIN, 0})); - for (auto &iter : m_clients) { - fds.emplace_back(pollfd({iter.first, POLLIN, 0})); + auto Server::insert_peer(Peer_ptr peer) -> Peer_ptr & { + RawSocketType client_fd = peer->get_socket().get_fd(); + const auto &result = m_peers.emplace(client_fd, std::move(peer)); + const auto &iter = result.first; + + if (!result.second) { + throw std::runtime_error("Peer already connected"); } -#ifdef OS_LINUX - nb_ready = ppoll(fds.data(), nfds, &timeout, nullptr); -#else - nb_ready = poll(fds.data(), nfds, timeout.tv_sec * 1000); -#endif + return iter->second; + } + + auto Server::delete_peer(FdVector::iterator iter) -> FdVector::iterator { + m_peers.erase(iter->fd); + return delete_move(m_fds, iter); + } + + auto Server::delete_pre_auth_peer(FdVector::iterator iter, PreAuthPeerMap &map) -> FdVector::iterator { + map.erase(iter->fd); + return delete_move(m_fds, iter); + } + + void Server::poll_events() { + // TODO: configurable wait (currently 1s) + struct timespec timeout = {.tv_sec = 1, .tv_nsec = 0}; + int nb_ready = Utils::poll(m_fds, &timeout); + // if (nb_ready < 0) // TODO: handle signals // throw std::runtime_error("Failed to poll"); - for (auto iter = fds.begin(); nb_ready > 0 && iter != fds.end(); iter++) { - if (iter->revents & (POLLIN | POLLHUP)) { - nb_ready++; - if (iter->fd == m_socket.get_fd()) { - auto tls_socket = m_socket.accept(); - std::shared_ptr client = std::make_shared(std::move(*tls_socket), "TODO", "TODO", Protocol::Version::v0_0_0, m_config); - std::optional req = {}; + for (auto iter = m_fds.begin(); nb_ready > 0 && iter != m_fds.end(); ) { + if (iter->revents & (POLLIN | POLLHUP)) { // NOLINT(hicpp-signed-bitwise) + nb_ready--; - m_events.emplace_back(std::move(client), req); + // TODO: Add try-catch in case peer fails smth + if (iter->fd == m_socket.get_fd()) { + std::unique_ptr tls_socket = m_socket.accept(); + auto fd = tls_socket->get_fd(); + auto peer = std::make_shared( + std::move(*tls_socket.release()), PreAuthPeer::SERVER + ); + + m_fds.emplace_back( + pollfd({.fd = fd, .events = POLLIN, .revents = 0}) + ); + m_handshake_peers.emplace(fd, std::move(peer)); + iter++; } else { - auto &client = m_clients.at(iter->fd); - - if (!handle_client_events(client)) { - m_clients.erase(iter->fd); - } + iter = handle_peer_events(iter); } + } else { + iter++; } } } - bool Server::handle_client_events(std::shared_ptr &client) { - std::vector requests = client->pull_requests(); + auto Server::handle_peer_events(FdVector::iterator iter) -> FdVector::iterator { + auto peer_iter = m_peers.find(iter->fd); + + if (peer_iter != m_peers.end()) { + std::shared_ptr peer = peer_iter->second; + std::vector requests = peer->pull_requests(); - for (auto iter = requests.begin(); iter != requests.end(); iter++) { - m_events.emplace_back(client, *iter); + for (auto &iter : requests) { + m_events.emplace_back(Event::REQUEST, peer, iter); + } + if (!peer->get_socket().connected()) { + return delete_peer(iter); + } + return ++iter; } - if (!client->get_socket().connected()) { - return false; + + auto pending_peer_iter = m_pending_authorization_peers.find(iter->fd); + + if (pending_peer_iter != m_pending_authorization_peers.end()) { + std::shared_ptr peer = pending_peer_iter->second; + + peer->poll_requests(); // Reject all Requests with Unauthorized status + if (!peer->get_socket().connected()) { + return delete_pre_auth_peer(iter, m_pending_authorization_peers); + } + return ++iter; } - return true; - } - void Server::initialize_download_directory() { - std::filesystem::directory_entry downloads{this->m_config.get_downloads_folder()}; + auto handshake_peer_iter = m_handshake_peers.find(iter->fd); + + if (handshake_peer_iter != m_handshake_peers.end()) { + std::shared_ptr &peer = handshake_peer_iter->second; + + peer->poll_requests(); + if (!peer->get_socket().connected()) { + return delete_pre_auth_peer(iter, m_handshake_peers); + } + if (peer->has_protocol()) { + if (m_known_peers.contains(*peer)) { + // Already trusted peer + insert_peer(std::move(peer)); + } else { + // Not yet trusted peer, going through authorization step + auto inserted = m_pending_authorization_peers.emplace(iter->fd, std::move(peer)); + std::shared_ptr new_peer = inserted.first->second; - if (!downloads.exists()) { - std::filesystem::create_directory(downloads.path()); - // TODO: change permissions ? Defaults are 777. - } else if (!downloads.is_directory()) { - throw std::runtime_error("The download destination is not a directory"); + m_events.emplace_back(Event::CONNECT, new_peer); + } + m_handshake_peers.erase(handshake_peer_iter); + } + return ++iter; } + + // Unknown FD -> Delete + return delete_move(m_fds, iter); } void Server::initialize_private_key() { + m_config.validate_config(); + + // TODO: Knows Hosts File + auto key_path = std::filesystem::path(m_config.get_private_keys_dir()) / (m_config.get_private_key_name() + "_key.pem"); auto cert_path = std::filesystem::path(m_config.get_private_keys_dir()) / (m_config.get_private_key_name() + "_cert.pem"); bool key_exists = std::filesystem::exists(key_path); bool cert_exists = std::filesystem::exists(cert_path); + if (key_exists ^ cert_exists) { + // TODO maybe add a param to force-generate ? + throw std::runtime_error("Found a private key or a certificate but not both"); // TODO: maybe missing certificate is ok if we implement key rotation ? + } + // Setup dir & permissions std::filesystem::create_directories(m_config.get_private_keys_dir()); - std::filesystem::permissions(m_config.get_private_keys_dir(), Config::secure_folder_perms, std::filesystem::perm_options::replace); + std::filesystem::permissions(m_config.get_private_keys_dir(), ServerConfig::SECURE_FOLDER_PERMS, std::filesystem::perm_options::replace); // Generate private key/certificate if not present - if (!key_exists && !cert_exists) { - CppSockets::EVP_PKEY_ptr pkey = {EVP_RSA_gen(4096), EVP_PKEY_free}; - CppSockets::X509_ptr x509 = {X509_new(), X509_free}; - int tls_ret = 1; - - if (pkey == nullptr || - (tls_ret = ASN1_INTEGER_set(X509_get_serialNumber(x509.get()), 1)) < 1 || - (X509_gmtime_adj(X509_getm_notBefore(x509.get()), 0)) == nullptr || - (X509_gmtime_adj(X509_getm_notAfter(x509.get()), 31536000L)) == nullptr || // TODO: implement key rotation / change this value of 1year? - (tls_ret = X509_set_pubkey(x509.get(), pkey.get())) < 1 - ) { - if (tls_ret < 1) { - throw std::runtime_error("Failed to generate key/certificate pair: " + std::string(ERR_error_string(ERR_get_error(), nullptr))); - } else if (pkey == nullptr) { - throw std::runtime_error("Failed to generate key/certificate pair: 'EVP_RSA_gen' returned NULL"); - } else { - throw std::runtime_error("Failed to generate key/certificate pair: 'X509_gmtime_adj' returned NULL"); - } - } - X509_NAME *name = X509_get_subject_name(x509.get()); - - if ((X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char *)"FileShare", -1, -1, 0) < 1) || // NOLINT(hicpp-signed-bitwise) - (X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *)"TODO_PUT_UUID_HERE", -1, -1, 0) < 1) || // NOLINT(hicpp-signed-bitwise) - (X509_set_issuer_name(x509.get(), name) < 1) || - (X509_sign(x509.get(), pkey.get(), EVP_sha256()) <= 0) // TODO: make it configurable ? (TODO: find what I wanted to make configurable about this) - ) { - throw std::runtime_error("Failed to sign certificate: " + std::string(ERR_error_string(ERR_get_error(), nullptr))); - } + if (!cert_exists) { + CppSockets::EVP_PKEY_ptr pkey {EVP_RSA_gen(4096)}; // NOLINT(cppcoreguidelines-pro-type-vararg, hicpp-vararg) + CppSockets::x509Certificate cert; + CppSockets::x509Name subject; + // TODO: Replace this with "FSP_" + UUID (16 bytes) -> 20 bytes + CppSockets::BIGNUM_ptr bignum {BN_bin2bn(reinterpret_cast("FSP_0000000000000000"), 20, nullptr)}; + + cert.set_public_key(pkey); + + // TODO: Actually put 20 random bytes here, this is used with issuer_name to revoke certs + // so it should be uniq + cert.set_serial_number(bignum.get()); + + cert.set_not_before(0, 0); + cert.set_not_after(365, 0); // TODO: implement key rotation / change this value of 1year? + // For key rotation, can we cross-sign the new certificate with the old one ? + // Or is that unsecure ? + // https://ravendb.net/articles/how-cross-signing-works-with-x509-certificates + + // NOTE: All this data is NOT trusted, and should NOT be relied upon blindfully. + // A malicious peer could craft a certificate containing any data to try to impersonate + // another one. Only the certificate pkey can truly prove identity. + + // NOLINTBEGIN(hicpp-signed-bitwise) + subject.add_entry(NID_organizationName, MBSTRING_ASC, ORG_NAME); + subject.add_entry(NID_organizationalUnitName, MBSTRING_ASC, ORG_UNIT_NAME); + subject.add_entry(NID_userId, MBSTRING_ASC, u8"TODO-PUT-USER-ID-HERE"); // TODO: When we have user accounts + // TODO: Since DEVICE_NAME could change, it probably shouldn't be part of the subject_name + // That would require the Server to re-generate certificate on name change, and if we + // store certificates to remember them, it will invalidate the certificate because of a + // name change... + std::u8string uuid {reinterpret_cast(m_config.get_uuid().c_str()), m_config.get_uuid().size()}; + std::u8string device_name {reinterpret_cast(m_config.get_device_name().c_str()), m_config.get_device_name().size()}; + + subject.add_entry(NID_commonName, MBSTRING_ASC, uuid); + subject.add_entry(NID_dnQualifier, MBSTRING_ASC, device_name); + // NOLINTEND(hicpp-signed-bitwise) + + cert.set_self_signed_name(subject); + cert.sign(pkey); std::error_code ec; { // scope for BIO uniq ptr so we can control when they are freed/closed - CppSockets::BIO_ptr pkey_file = {BIO_new_file(key_path.string().c_str(), "w"), BIO_free}; - CppSockets::BIO_ptr cert_file = {BIO_new_file(cert_path.string().c_str(), "w"), BIO_free}; + CppSockets::BIO_ptr pkey_file {BIO_new_file(key_path.string().c_str(), "w")}; + CppSockets::BIO_ptr cert_file {BIO_new_file(cert_path.string().c_str(), "w")}; if (!pkey_file || !cert_file) { throw std::runtime_error("Failed to open '" + (pkey_file == nullptr ? key_path.string() : cert_path.string()) + "' for writing"); @@ -247,7 +429,7 @@ namespace FileShare { // TODO: encrypt private key if (PEM_write_bio_PrivateKey(pkey_file.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr) < 1 || - PEM_write_bio_X509(cert_file.get(), x509.get()) < 1 + PEM_write_bio_X509(cert_file.get(), cert.get()) < 1 ) { std::filesystem::remove(key_path, ec); std::filesystem::remove(cert_path); @@ -258,7 +440,7 @@ namespace FileShare { } // Close both files // TODO: raise error if close fails - std::filesystem::permissions(key_path, Config::secure_file_perms, ec); + std::filesystem::permissions(key_path, ServerConfig::SECURE_FILE_PERMS, ec); if (ec) { std::error_code ec2; @@ -269,27 +451,17 @@ namespace FileShare { throw std::runtime_error("Failed to set secure permissions on the private key: " + ec.message()); } return; - } else if (key_exists ^ cert_exists) { - // TODO maybe add a param to force-generate ? - throw std::runtime_error("Found a private key or a certificate but not both"); // TODO: maybe missing certificate is ok if we implement key rotation ? } - if (std::filesystem::status(key_path).permissions() != Config::secure_file_perms) { - // TODO maybe add a param to force set ? + #ifndef OS_WINDOWS + if (std::filesystem::status(key_path).permissions() != ServerConfig::SECURE_FILE_PERMS) { + // TODO maybe add a param to force set ? throw std::runtime_error("Insecure permissions for the private key !"); -#endif } +#endif } - Server::Event::Event(std::shared_ptr client, std::optional request) : - m_client(std::move(client)), m_request(std::move(request)) + Server::Event::Event(Type type, PeerBase_ptr peer, std::optional request) : + m_type(type), m_peer(std::move(peer)), m_request(std::move(request)) {} - - std::shared_ptr &Server::Event::client() { - return m_client; - } - - std::optional &Server::Event::request() { - return m_request; - } } diff --git a/source/TransferHandler.cpp b/source/TransferHandler.cpp index 0326867..00230e7 100644 --- a/source/TransferHandler.cpp +++ b/source/TransferHandler.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Thu Aug 24 19:36:36 2023 Francois Michaut -** Last update Sat Dec 9 09:03:46 2023 Francois Michaut +** Last update Sat Aug 23 11:16:50 2025 Francois Michaut ** ** TransferHandler.cpp : Implementation of classes to handle the file transfers */ @@ -15,6 +15,8 @@ #include namespace FileShare { + // TODO: make download transfer handler return a STATUS instead. + // Can return status::up_to_date for instance, avoid handling this with exceptions DownloadTransferHandler::DownloadTransferHandler(std::string destination_filename, std::shared_ptr original_request) : m_filename(std::move(destination_filename)), m_temp_filename(m_filename + ".fsdownload") { @@ -22,17 +24,18 @@ namespace FileShare { if (std::filesystem::exists(m_temp_filename)) { // TODO: read from it and restart from where we left off throw Errors::Transfer::UpToDateError(m_temp_filename); - } else if(std::filesystem::exists(m_filename) && Utils::file_hash(m_original_request->hash_algorithm, m_filename) == m_original_request->filehash) { + } + if(std::filesystem::exists(m_filename) && Utils::file_hash(m_original_request->hash_algorithm, m_filename) == m_original_request->filehash) { // file is already up to date, don't need to download it again throw Errors::Transfer::UpToDateError(m_filename); - } else { - std::filesystem::create_directories(std::filesystem::path(m_temp_filename).parent_path()); - m_file.open(m_temp_filename, std::ios_base::out | std::ios_base::binary | std::ios_base::trunc); } + + std::filesystem::create_directories(std::filesystem::path(m_temp_filename).parent_path()); + m_file.open(m_temp_filename, std::ios_base::out | std::ios_base::binary | std::ios_base::trunc); } void DownloadTransferHandler::receive_packet(const Protocol::DataPacketData &data) { - auto missing = std::find(m_missing_ids.begin(), m_missing_ids.end(), data.packet_id); + auto missing = std::ranges::find(m_missing_ids, data.packet_id); if (missing != m_missing_ids.end()) m_missing_ids.erase(missing); @@ -63,7 +66,7 @@ namespace FileShare { } else { // data.packet_id == m_expected_id bool last_packet; - // TODO: this breaks with files of size 0 + // TODO FIXME: this breaks with files of size 0 m_expected_id++; // Increment before comparaison, cause if we need 2 total packets, we will receive ids 0 and 1. last_packet = m_expected_id == m_original_request->total_packets; @@ -71,7 +74,8 @@ namespace FileShare { if (data.data.size() != m_original_request->packet_size && !last_packet) { // TODO: something is wrong if this happens -> figure out what to do. throw std::runtime_error("Transfert size invalid"); - } else if (last_packet && m_missing_ids.empty()) { + } + if (last_packet && m_missing_ids.empty()) { finish_transfer(); } } @@ -87,26 +91,26 @@ namespace FileShare { } } - bool DownloadTransferHandler::finished() const { + auto DownloadTransferHandler::finished() const -> bool { return !m_file.is_open(); } - std::size_t IFileTransferHandler::get_current_size() const { + auto IFileTransferHandler::get_current_size() const -> std::size_t { // TODO: this is not exact : last packet might have a smaller size // return m_expected_id * m_original_request->packet_size; return m_transferred_size; } - std::size_t IFileTransferHandler::get_total_size() const { + auto IFileTransferHandler::get_total_size() const -> std::size_t { // TODO: this is not exact : last packet might have a smaller size return m_original_request->total_packets * m_original_request->packet_size; } - std::shared_ptr IFileTransferHandler::get_original_request() const { + auto IFileTransferHandler::get_original_request() const -> std::shared_ptr { return m_original_request; } - UploadTransferHandler::UploadTransferHandler(std::string filepath, std::shared_ptr original_request, std::size_t packet_start) { + UploadTransferHandler::UploadTransferHandler(const std::string &filepath, std::shared_ptr original_request, std::size_t packet_start) { m_original_request = std::move(original_request); m_file.open(filepath); // TODO: this won't raise on fail to open / fail to seek (need failibt, but failbit would raise if EOF while reading...) @@ -114,7 +118,8 @@ namespace FileShare { m_file.seekg(m_original_request->packet_size * packet_start); } - std::shared_ptr UploadTransferHandler::get_next_packet(Protocol::MessageID original_request_id) { + // TODO: Do we really need shared_ptrs for the transfer packets ? + auto UploadTransferHandler::get_next_packet(Protocol::MessageID original_request_id) -> std::shared_ptr { std::vector buffer; std::string data; std::shared_ptr data_packet_data; @@ -134,7 +139,7 @@ namespace FileShare { return data_packet_data; } - bool UploadTransferHandler::finished() const { + auto UploadTransferHandler::finished() const -> bool { return !m_file.is_open(); } @@ -150,7 +155,7 @@ namespace FileShare { return; } if (m_path_node->is_host_folder()) { - std::filesystem::path host_path = m_file_mapping.virtual_to_host(m_requested_path, m_path_node, out); + std::filesystem::path host_path = FileShare::FileMapping::virtual_to_host(m_requested_path, m_path_node, out); if (!host_path.empty()) { m_directory_iterator = std::filesystem::directory_iterator(host_path); @@ -160,7 +165,7 @@ namespace FileShare { } } - std::shared_ptr ListFilesTransferHandler::get_next_packet(Protocol::MessageID original_request_id) { + auto ListFilesTransferHandler::get_next_packet(Protocol::MessageID original_request_id) -> std::shared_ptr { if (m_extra_packet_sent || !m_path_node.has_value()) { return nullptr; } @@ -176,7 +181,7 @@ namespace FileShare { auto filepath = m_requested_path / m_directory_iterator->path().filename(); auto file_type = m_directory_iterator->is_directory() ? Protocol::FileType::DIRECTORY : Protocol::FileType::FILE; - vector.emplace_back(Protocol::FileInfo{filepath.string(), file_type}); + vector.emplace_back(Protocol::FileInfo{.path=filepath.string(), .file_type=file_type}); m_directory_iterator++; } break; @@ -190,7 +195,7 @@ namespace FileShare { if (entry.is_directory()) { break; // It's supposed to be a file, abort } - vector.emplace_back(Protocol::FileInfo{m_requested_path.string(), Protocol::FileType::FILE}); + vector.emplace_back(Protocol::FileInfo{.path=m_requested_path.string(), .file_type=Protocol::FileType::FILE}); break; } case PathNode::VIRTUAL: { @@ -200,7 +205,7 @@ namespace FileShare { const PathNode &node = m_node_iterator->second; auto file_type = node.is_host_file() ? Protocol::FileType::FILE : Protocol::FileType::DIRECTORY; - vector.emplace_back(Protocol::FileInfo{(m_requested_path / node.get_name()).string(), file_type}); + vector.emplace_back(Protocol::FileInfo{.path=(m_requested_path / node.get_name()).string(), .file_type=file_type}); m_node_iterator++; } break; @@ -208,13 +213,14 @@ namespace FileShare { } auto request = std::make_shared(original_request_id, m_current_id++, std::move(vector)); + // TODO: Can we get rid of the extra empty packet ? if (request->files.empty()) { m_extra_packet_sent = true; } return request; } - bool ListFilesTransferHandler::finished() const { + auto ListFilesTransferHandler::finished() const -> bool { if (!m_path_node.has_value()) return true; switch (m_path_node->get_type()) { @@ -233,7 +239,7 @@ namespace FileShare { m_missing_ids.push_back(m_current_id++); } } else if (data.packet_id < m_current_id) { - auto iter = std::find(m_missing_ids.begin(), m_missing_ids.end(), data.packet_id); + auto iter = std::ranges::find(m_missing_ids, data.packet_id); if (iter != m_missing_ids.end()) { m_missing_ids.erase(iter); @@ -250,9 +256,7 @@ namespace FileShare { } } - bool FileListTransferHandler::finished() const { + auto FileListTransferHandler::finished() const -> bool { return m_missing_ids.empty() && m_finished; } - - [[nodiscard]] const std::vector &FileListTransferHandler::get_file_list() const { return m_file_list; } } diff --git a/source/Utils/FileDescriptor.cpp b/source/Utils/FileDescriptor.cpp index bc204c0..952d9a2 100644 --- a/source/Utils/FileDescriptor.cpp +++ b/source/Utils/FileDescriptor.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Tue May 9 11:13:37 2023 Francois Michaut -** Last update Tue Jul 18 22:02:26 2023 Francois Michaut +** Last update Fri Aug 22 23:19:33 2025 Francois Michaut ** ** FileDescriptor.cpp : Helper wrapper class to auto close file descriptor */ @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -23,7 +24,7 @@ namespace FileShare::Utils { FileHandleBase::FileHandleBase(std::string filename) : - m_filename(filename) + m_filename(std::move(filename)) {} void FileHandleBase::report_error(const char *action, bool raise) const { @@ -33,10 +34,11 @@ namespace FileShare::Utils { if (!m_filename.empty()) oss << " '" << m_filename << '\''; oss << ": " << strerror(errno); - if (raise) + if (raise) { throw std::runtime_error(oss.str()); - else - std::cerr << oss.str() << std::endl; + } + + std::cerr << oss.str() << std::endl; } #ifndef OS_WINDOWS @@ -94,6 +96,7 @@ namespace FileShare::Utils { report_error("close", false); } } + #ifndef OS_WINDOWS int FileHandle::fd(bool raise) const { if (m_fd == -1) { diff --git a/source/Utils/FileHash.cpp b/source/Utils/FileHash.cpp index d917e46..a179126 100644 --- a/source/Utils/FileHash.cpp +++ b/source/Utils/FileHash.cpp @@ -4,55 +4,52 @@ ** Author Francois Michaut ** ** Started on Sat May 6 22:13:15 2023 Francois Michaut -** Last update Sun Dec 10 18:45:08 2023 Francois Michaut +** Last update Wed Aug 20 16:40:49 2025 Francois Michaut ** ** FileHash.cpp : Function to hash file contents */ +#include "FileShare/Utils/DebugPerf.hpp" #include "FileShare/Utils/FileDescriptor.hpp" #include "FileShare/Utils/FileHash.hpp" -#include "FileShare/Utils/DebugPerf.hpp" #include -#include +#include #include #include #include +#include #include -#include - #ifdef OS_UNIX #include #endif -#define READ_SIZE 0x8000 // = 32_768 bytes +constexpr auto READ_SIZE = 0x8000; // = 32_768 bytes // TODO: custom exceptions with errno support when needed namespace FileShare::Utils { - std::string file_hash(HashAlgorithm algo, const std::filesystem::path &path) { + auto file_hash(HashAlgorithm algo, const std::filesystem::path &path) -> std::string { DebugPerf debug("file_hash"); - CppSockets::EVP_MD_ptr md; - CppSockets::EVP_MD_CTX_ptr ctx; - unsigned char digest_buff[EVP_MAX_MD_SIZE]; + CppSockets::EVP_MD_ptr digest {EVP_MD_fetch(nullptr, algo_to_string(algo), nullptr)}; + CppSockets::EVP_MD_CTX_ptr ctx {EVP_MD_CTX_new()}; + std::array digest_buff = {0}; unsigned int output_size; FileHandle file(path, "r"); - std::int64_t ret = READ_SIZE; + std::size_t ret = READ_SIZE; std::vector buff(READ_SIZE); #if defined(OS_UNIX) && !defined(OS_APPLE) posix_fadvise(file.fd(false), 0, 0, POSIX_FADV_SEQUENTIAL); // Ignoring return - this is optional #endif - md = {EVP_MD_fetch(nullptr, algo_to_string(algo), nullptr), EVP_MD_free}; - ctx = {EVP_MD_CTX_new(), EVP_MD_CTX_free}; - if (!md || !ctx) + if (!digest || !ctx) throw std::runtime_error("Failed to intialize the hash context"); EVP_MD_CTX_set_flags(ctx.get(), EVP_MD_CTX_FLAG_FINALISE); - if (EVP_DigestInit(ctx.get(), md.get()) <= 0) + if (EVP_DigestInit(ctx.get(), digest.get()) <= 0) throw std::runtime_error("Failed to init the digest context"); while (ret == READ_SIZE) { @@ -64,19 +61,20 @@ namespace FileShare::Utils { if (ret == 0) break; - else if (ret < 0) + + if (ret < 0) throw std::runtime_error("File read failed"); if (EVP_DigestUpdate(ctx.get(), buff.data(), ret) <= 0) throw std::runtime_error("Failed to hash file data"); } - if (EVP_DigestFinal(ctx.get(), digest_buff, &output_size) <= 0) + if (EVP_DigestFinal(ctx.get(), digest_buff.data(), &output_size) <= 0) throw std::runtime_error("Failed to compute the file hash"); if (output_size != algo_hash_size(algo)) throw std::runtime_error("Hash size is not what was expected"); - return std::string((char *)digest_buff, output_size); + return {reinterpret_cast(digest_buff.data()), output_size}; } - std::size_t algo_hash_size(HashAlgorithm algo) { + auto algo_hash_size(HashAlgorithm algo) -> std::size_t{ switch (algo) { case HashAlgorithm::MD5: return MD5_DIGEST_LENGTH; @@ -89,7 +87,7 @@ namespace FileShare::Utils { } } - const char *algo_to_string(HashAlgorithm algo) { + auto algo_to_string(HashAlgorithm algo) -> const char * { switch (algo) { case HashAlgorithm::MD5: return "md5"; diff --git a/source/Utils/Path.cpp b/source/Utils/Path.cpp index 9a66408..fd79a3c 100644 --- a/source/Utils/Path.cpp +++ b/source/Utils/Path.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Thu Oct 13 19:09:01 2022 Francois Michaut -** Last update Thu Nov 23 09:05:00 2023 Francois Michaut +** Last update Wed Aug 6 22:32:34 2025 Francois Michaut ** ** Path.cpp : Implementation of utilities to manpulate paths in a cross plateform way */ @@ -25,7 +25,7 @@ #endif namespace FileShare::Utils { - std::filesystem::path home_directoy(const std::string &user) { + auto home_directoy(const std::string &user) -> std::filesystem::path { std::string res; char *home = nullptr; @@ -56,6 +56,7 @@ namespace FileShare::Utils { } } #else + // TODO: Make Threadsafe + Cache env + passwd entry if (user.empty()) { home = std::getenv("HOME"); } @@ -77,16 +78,16 @@ namespace FileShare::Utils { return res; } - std::filesystem::path home_directoy() { + auto home_directoy() -> std::filesystem::path { static std::filesystem::path home = home_directoy(""); return home; } - std::filesystem::path resolve_home_component(const std::filesystem::path &path) { + auto resolve_home_component(const std::filesystem::path &path) -> std::filesystem::path { auto str = path.generic_string(); - if (str.find('~') != 0) + if (!str.starts_with('~')) return path; auto pos = str.find('/'); auto home = pos == 1 ? home_directoy() : home_directoy(str.substr(1, (pos == std::string::npos ? pos : pos - 1))); @@ -98,21 +99,21 @@ namespace FileShare::Utils { return home / str.substr(pos + 1); } - std::vector resolve_home_components(const std::vector &paths) { + auto resolve_home_components(const std::vector &paths) -> std::vector { std::vector result; result.reserve(paths.size()); - for (auto &path : paths) { + for (const auto &path : paths) { result.emplace_back(resolve_home_component(path)); } return result; } - bool path_contains_folder(const std::filesystem::path &path, const std::filesystem::path &folder) { + auto path_contains_folder(const std::filesystem::path &path, const std::filesystem::path &folder) -> bool { return find_folder_in_path(path, folder) != path.end(); } - std::filesystem::path::iterator find_folder_in_path(const std::filesystem::path &path, const std::filesystem::path &folder) { + auto find_folder_in_path(const std::filesystem::path &path, const std::filesystem::path &folder) -> std::filesystem::path::iterator { auto path_iter = path.begin(); auto folder_iter = folder.begin(); diff --git a/source/Utils/Poll.cpp b/source/Utils/Poll.cpp new file mode 100644 index 0000000..586214c --- /dev/null +++ b/source/Utils/Poll.cpp @@ -0,0 +1,43 @@ +/* +** Project LibFileShareProtocol, 2025 +** +** Author Francois Michaut +** +** Started on Fri Jul 25 18:19:49 2025 Francois Michaut +** Last update Sat Aug 23 20:43:12 2025 Francois Michaut +** +** Poll.hpp : Cross-Plateform poll implementation +*/ + +#include "FileShare/Utils/Poll.hpp" + +#ifdef OS_WINDOWS + static auto &poll=WSAPoll; // alias function poll to WSAPoll +#endif + +namespace FileShare::Utils { + auto poll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout) -> int { + int nb_ready = 0; + +#ifdef OS_LINUX + nb_ready = ppoll(fds, nfds, timeout, nullptr); +#else + int timeout_ms = 0; + + if (timeout == nullptr) { + timeout_ms = -1; // Infinite Timeout + } else { + timeout_ms = (timeout->tv_sec * 1000) + (timeout->tv_nsec / 1000000); + } + nb_ready = poll(fds, nfds, timeout_ms); +#endif + + // TODO: Error management ? + // TODO: Signal Handling ? + return nb_ready; + } + + auto poll(std::vector &fds, const struct timespec *timeout) -> int { + return poll(fds.data(), fds.size(), timeout); + } +} diff --git a/source/Utils/VarInt.cpp b/source/Utils/VarInt.cpp index e0cd01d..4d0c593 100644 --- a/source/Utils/VarInt.cpp +++ b/source/Utils/VarInt.cpp @@ -4,11 +4,12 @@ ** Author Francois Michaut ** ** Started on Sat May 6 12:40:55 2023 Francois Michaut -** Last update Sun Jul 16 13:48:35 2023 Francois Michaut +** Last update Thu Aug 14 14:20:36 2025 Francois Michaut ** ** VarInt.hpp : Variable size integer implementation */ +#include #include #include "FileShare/Utils/VarInt.hpp" @@ -27,7 +28,7 @@ namespace FileShare::Utils { *this = other; } - VarInt &VarInt::operator=(const VarInt &other) { + auto VarInt::operator=(const VarInt &other) -> VarInt & { m_values = other.m_values; m_value = other.m_value; m_value_dirty = other.m_value_dirty; @@ -45,28 +46,28 @@ namespace FileShare::Utils { m_values.push_back(0); } while (value != 0) { - tmp = (value & 0x7F); - tmp |= (value - tmp == 0 ? 0 : 0x80); - m_values.push_back(tmp); - value >>= 7; + tmp = (value & 0x7FU); + tmp |= (value - tmp == 0 ? 0 : 0x80U); + m_values.push_back(static_cast(tmp)); + value >>= 7U; } reset_string_view(); } - bool VarInt::parse(std::string_view input) { + auto VarInt::parse(std::string_view input) -> bool { std::string_view dummy; return parse(input, dummy); } - bool VarInt::parse(std::string_view input, std::string_view &output) { + auto VarInt::parse(std::string_view input, std::string_view &output) -> bool { std::vector result; result.reserve(input.size()); for (auto iter = input.begin(); iter != input.end(); iter++) { result.push_back(*iter); // TODO: double check that the fact *iter is signed integer does not mess up the calculations - if ((*iter & 0x80) == 0) { + if ((static_cast(*iter) & 0x80U) == 0) { m_value_dirty = true; m_values = std::move(result); reset_string_view(); @@ -77,11 +78,7 @@ namespace FileShare::Utils { return false; } - std::string_view VarInt::to_string() const { - return m_string; - } - - std::size_t VarInt::to_number() const { + auto VarInt::to_number() const -> std::size_t { if (m_value_dirty) reset_number_view(); return m_value; @@ -91,11 +88,7 @@ namespace FileShare::Utils { m_string = std::string_view(m_values.begin(), m_values.end()); } - std::size_t VarInt::byte_size() const { - return m_values.size(); - } - - std::strong_ordering VarInt::operator<=>(const VarInt &other) const { + auto VarInt::operator<=>(const VarInt &other) const -> std::strong_ordering { // TODO: this won't work for infinite or negative numbers return m_value <=> other.m_value; } @@ -105,13 +98,13 @@ namespace FileShare::Utils { constexpr std::uint8_t leftover_value = (sizeof(std::size_t) * 8) % 7; constexpr std::size_t max_size = (sizeof(std::size_t) * 8) / 7; - if (m_values.size() > max_size && !(m_values.size() == (max_size + 1) && (std::uint8_t)m_values.back() == leftover_value)) + if (m_values.size() > max_size && (m_values.size() != (max_size + 1) || static_cast(m_values.back()) != leftover_value)) throw std::runtime_error("Value is too big to fit in a std::size_t"); // Iterate in reverse since we need the low significant bit first // and they are at the end of the string representation for (auto iter = m_values.rbegin(); iter != m_values.rend(); iter++) { - result = (result << 7) + (*iter & 0x7F); + result = (result << 7U) + (static_cast(*iter) & 0x7FU); } m_value = result; m_value_dirty = false; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a20826d..49701aa 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,7 +4,7 @@ ## Author Francois Michaut ## ## Started on Mon Feb 14 19:35:41 2022 Francois Michaut -## Last update Sat Dec 2 17:44:08 2023 Francois Michaut +## Last update Sun Aug 24 01:20:19 2025 Francois Michaut ## ## CMakeLists.txt : CMake building and running tests for FileShare ## @@ -28,8 +28,13 @@ add_executable(unit_tests target_compile_definitions(unit_tests PRIVATE DEBUG) target_link_libraries(unit_tests fsp) +target_compile_options(unit_tests PRIVATE + $<$,$,$>:-UNDEBUG> + $<$:/UNDEBUG> +) + foreach (test ${TestFiles}) - if (NOT ${test} STREQUAL test_driver.cpp) + if (NOT ${test} MATCHES "test_driver.cpp$") get_filename_component (DName ${test} DIRECTORY) get_filename_component (TName ${test} NAME_WE) if (DName STREQUAL "") diff --git a/tests/Config/TestFileMapping.cpp b/tests/Config/TestFileMapping.cpp index bfead7b..50535b6 100644 --- a/tests/Config/TestFileMapping.cpp +++ b/tests/Config/TestFileMapping.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Wed Nov 22 20:19:02 2023 Francois Michaut -** Last update Sun Dec 10 10:59:02 2023 Francois Michaut +** Last update Sun Aug 24 12:04:58 2025 Francois Michaut ** ** TestFileMapping.cpp : FileMapping classes implementation */ @@ -134,15 +134,21 @@ static void test_set_name() { assert(false); } catch (std::runtime_error &e) {} - RootPathNode node2("//test"); + RootPathNode root_node("//test/"); - assert(node2.get_name() == "//test"); - node2.set_name("//test2"); - assert(node2.get_name() == "//test2"); + assert(root_node.get_name() == "//test"); + root_node.set_name("//test2"); + assert(root_node.get_name() == "//test2"); try { // Crashes if / in middle of name - node2.set_name("test/test2"); + root_node.set_name("test/test2"); + assert(false); + } catch (std::runtime_error &e) {} + + try { + // Crashes if more than 2 / at begining of name + root_node.set_name("///test2"); assert(false); } catch (std::runtime_error &e) {} } @@ -181,7 +187,7 @@ static void test_host_to_virtual() { PathNode::make_host_node("opt", PathNode::HOST_FOLDER, "/opt", PathNode::HIDDEN), }; - FileMapping mapping({"//fsp", root_nodes}); + FileMapping mapping(RootPathNode{"//fsp", root_nodes}); assert(mapping.host_to_virtual("/non-existent/path").empty()); assert(mapping.host_to_virtual("/home").empty()); @@ -217,50 +223,41 @@ static void test_host_to_virtual() { assert(mapping.host_to_virtual("/opt/yolo/foo/bar").empty()); } +#include + static void test_find_virtual_node() { - PathNode test1_node = PathNode::make_host_node("test1", PathNode::HOST_FILE, "/home/user1/test", PathNode::VISIBLE); - PathNode test2_node = PathNode::make_host_node("test2", PathNode::HOST_FILE, "/home/user2/test", PathNode::HIDDEN); - PathNode downloads_node = PathNode::make_host_node("downloads", PathNode::HOST_FOLDER, "/home/user/downloads", PathNode::VISIBLE); - PathNode documents_node = PathNode::make_host_node("documents", PathNode::HOST_FOLDER, "/home/user/documents", PathNode::HIDDEN); - PathNode username_node = PathNode::make_virtual_node("username", PathNode::VISIBLE, { - // Visible file inside a visible folder -> should succeed - {test1_node}, - // Hidden file inside a visible folder -> should fail - {test2_node}, - // Visible folder inside a visible folder -> should succeed for the folder and anything inside it - {downloads_node}, - // Hidden folder inside a visible folder -> should fail for the folder and anything inside it - {documents_node} - }); - PathNode home_node = PathNode::make_virtual_node("home", PathNode::VISIBLE, {username_node}); - - PathNode passwd_node = PathNode::make_host_node("passwd", PathNode::HOST_FILE, "/etc/passwd", PathNode::HIDDEN); - PathNode fstab_node = PathNode::make_host_node("fstab", PathNode::HOST_FILE, "/etc/fstab", PathNode::VISIBLE); - PathNode etc_node = PathNode::make_virtual_node("etc", PathNode::VISIBLE, { - // Visible file inside a visible folder -> should succeed - {fstab_node}, - // Hidden file inside a visible folder -> should fail - {passwd_node} - }); - PathNode root_node = PathNode::make_virtual_node("root", PathNode::HIDDEN, { + FileMapping mapping; + RootPathNode &root_path_node = mapping.get_root_node(); + + + PathNode &home_node = root_path_node.insert_child_node(PathNode::make_virtual_node("home", PathNode::VISIBLE)); + PathNode &username_node = home_node.insert_child_node(PathNode::make_virtual_node("username", PathNode::VISIBLE)); + // Visible file inside a visible folder -> should succeed + PathNode &test1_node = username_node.insert_child_node(PathNode::make_host_node("test1", PathNode::HOST_FILE, "/home/user1/test", PathNode::VISIBLE)); + // Hidden file inside a visible folder -> should fail + PathNode &test2_node = username_node.insert_child_node(PathNode::make_host_node("test2", PathNode::HOST_FILE, "/home/user2/test", PathNode::HIDDEN)); + // Visible folder inside a visible folder -> should succeed for the folder and anything inside it + PathNode &downloads_node = username_node.insert_child_node(PathNode::make_host_node("downloads", PathNode::HOST_FOLDER, "/home/user/downloads", PathNode::VISIBLE)); + // Hidden folder inside a visible folder -> should fail for the folder and anything inside it + PathNode &documents_node = username_node.insert_child_node(PathNode::make_host_node("documents", PathNode::HOST_FOLDER, "/home/user/documents", PathNode::HIDDEN)); + + + PathNode &etc_node = root_path_node.insert_child_node(PathNode::make_virtual_node("etc", PathNode::VISIBLE)); + // Visible file inside a visible folder -> should succeed + PathNode &fstab_node = etc_node.insert_child_node(PathNode::make_host_node("fstab", PathNode::HOST_FILE, "/etc/fstab", PathNode::VISIBLE)); + // Hidden file inside a visible folder -> should fail + PathNode &passwd_node = etc_node.insert_child_node(PathNode::make_host_node("passwd", PathNode::HOST_FILE, "/etc/passwd", PathNode::HIDDEN)); + + + PathNode &root_node = root_path_node.insert_child_node(PathNode::make_virtual_node("root", PathNode::HIDDEN, { // Visible file inside a hidden folder -> should fail {PathNode::make_host_node("test1", PathNode::HOST_FILE, "/root/test1", PathNode::VISIBLE)}, // Hidden file inside a hidden folder -> should fail {PathNode::make_host_node("test2", PathNode::HOST_FILE, "/root/toto/test2", PathNode::HIDDEN)} - }); - PathNode tmp_node = PathNode::make_host_node("tmp", PathNode::HOST_FOLDER, "/tmp", PathNode::VISIBLE); - PathNode opt_node = PathNode::make_host_node("opt", PathNode::HOST_FOLDER, "/opt", PathNode::HIDDEN); - std::vector root_nodes = { - home_node, - etc_node, - root_node, - // Visible folder -> should succeed for the folder and anything inside it - tmp_node, - // Hidden folder -> should fail for the folder and anything inside it - opt_node, - }; + })); + PathNode &tmp_node = root_path_node.insert_child_node(PathNode::make_host_node("tmp", PathNode::HOST_FOLDER, "/tmp", PathNode::VISIBLE)); + PathNode &opt_node = root_path_node.insert_child_node(PathNode::make_host_node("opt", PathNode::HOST_FOLDER, "/opt", PathNode::HIDDEN)); - FileMapping mapping(root_nodes); assert(mapping.find_virtual_node("//fsp/non-existent/path").has_value() == false); assert(mapping.find_virtual_node("//fsp/home/") == home_node); diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..3c7da8a --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "dependencies": [ + "openssl" + ] +}