[
  {
    "path": ".clang-format",
    "content": "---\nBasedOnStyle: LLVM\nIndentWidth: 4\nTabWidth: 8\nAccessModifierOffset: -4\nAllowShortFunctionsOnASingleLine: Empty\nAllowShortIfStatementsOnASingleLine: false\nAllowShortLoopsOnASingleLine: false\nColumnLimit: 130\nIndentCaseLabels: false\nSortIncludes: true\n...\n"
  },
  {
    "path": ".dockerignore",
    "content": ".git\n.github\n.vscode\n\nbuild/*\n"
  },
  {
    "path": ".github/workflows/check.yaml",
    "content": "name: check\n\non:\n  pull_request:\n    branches: [master]\n\njobs:\n  format:\n    runs-on: ubuntu-latest\n    steps:\n      - name: checkout\n        uses: actions/checkout@v4\n      - name: check format\n        uses: jidicula/clang-format-action@v4.11.0\n        with:\n          check-path: 'src'\n          exclude-regex: 'argparse.h'\n\n  linux:\n    runs-on: ubuntu-latest\n    steps:\n    - name: checkout\n      uses: actions/checkout@v4\n    - name: build\n      run: docker build .\n\n  macos:\n    runs-on: macos-latest\n    steps:\n    - name: depends\n      run: brew update && brew install fmt poco spdlog\n    - name: checkout\n      uses: actions/checkout@v4\n    - name: build\n      run: |\n        if [ \"$RUNNER_ARCH\" == \"ARM64\" ]; then\n          export CPATH=/opt/homebrew/include\n          export LIBRARY_PATH=/opt/homebrew/lib\n        else\n          export CPATH=/usr/local/include\n          export LIBRARY_PATH=/usr/local/lib\n        fi\n        cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo\n        cmake --build build\n\n  windows:\n    runs-on: windows-latest\n    steps:\n    - name: depends\n      uses: msys2/setup-msys2@v2\n      with:\n        msystem: MINGW64\n        update: true\n        install: >-\n          mingw-w64-x86_64-cmake\n          mingw-w64-x86_64-ninja\n          mingw-w64-x86_64-gcc\n          mingw-w64-x86_64-spdlog\n          mingw-w64-x86_64-poco\n    - name: checkout\n      uses: actions/checkout@v4\n    - name: cache\n      uses: actions/cache@v4\n      with:\n        path: build\n        key: ${{ hashFiles('CMakeLists.txt') }}\n    - name: build\n      shell: msys2 {0}\n      run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build build\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: release\n\non:\n  push:\n    branches: [ master ]\n  release:\n    types: [ published ]\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n    steps:\n    - name: checkout\n      uses: actions/checkout@v4\n    - name: setup qemu\n      uses: docker/setup-qemu-action@v3\n    - name: setup docker buildx\n      uses: docker/setup-buildx-action@v3\n    - name: login docker hub\n      uses: docker/login-action@v3\n      with:\n        registry: docker.io\n        username: ${{ secrets.DOCKERHUB_USERNAME }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n    - name: login github container registry\n      uses: docker/login-action@v3\n      with:\n        registry: ghcr.io\n        username: ${{ github.actor }}\n        password: ${{ secrets.GITHUB_TOKEN }}\n    - name: setup version\n      if: github.event_name == 'release'\n      run: |\n        GIT_TAG=${{ github.event.release.tag_name }}\n        echo \"IMAGE_TAG=${GIT_TAG#v}\" >> $GITHUB_ENV\n    - name: build and push\n      uses: docker/build-push-action@v5\n      if: github.event_name == 'release'\n      with:\n        context: .\n        provenance: false\n        platforms: linux/amd64,linux/arm64,linux/arm/v7\n        push: true\n        tags: |\n          docker.io/${{ secrets.DOCKERHUB_USERNAME }}/candy:${{ env.IMAGE_TAG }}\n          docker.io/${{ secrets.DOCKERHUB_USERNAME }}/candy:latest\n          ghcr.io/${{ github.actor }}/candy:${{ env.IMAGE_TAG }}\n          ghcr.io/${{ github.actor }}/candy:latest\n\n  windows:\n    runs-on: windows-latest\n    steps:\n    - name: setup msys2\n      uses: msys2/setup-msys2@v2\n      with:\n        msystem: MINGW64\n        update: true\n        install: >-\n          mingw-w64-x86_64-cmake\n          mingw-w64-x86_64-ninja\n          mingw-w64-x86_64-gcc\n          mingw-w64-x86_64-spdlog\n          mingw-w64-x86_64-poco\n    - name: checkout\n      uses: actions/checkout@v4\n    - name: cache\n      uses: actions/cache@v4\n      with:\n        path: build\n        key: ${{ hashFiles('CMakeLists.txt') }}\n    - name: build\n      shell: msys2 {0}\n      run: |\n        cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build build\n        mkdir artifact\n        cp candy.cfg artifact\n        cp build/candy/wintun/bin/amd64/wintun.dll artifact\n        scripts/search-deps.sh build/candy-cli/candy.exe artifact\n        scripts/search-deps.sh build/candy-service/candy-service.exe artifact\n    - name: set release package name\n      shell: bash\n      if: github.event_name == 'release'\n      run: |\n        GIT_TAG=${{ github.event.release.tag_name }}\n        echo \"PKGNAME=candy_${GIT_TAG#v}+windows_amd64\" >> $GITHUB_ENV\n    - name: upload artifact\n      uses: actions/upload-artifact@v4\n      with:\n        name: windows-${{ github.event.release.tag_name || github.sha }}\n        path: artifact\n    - name: zip release\n      uses: thedoctor0/zip-release@0.7.5\n      if: github.event_name == 'release'\n      with:\n        type: 'zip'\n        filename: ${{ env.PKGNAME }}.zip\n        directory: artifact\n    - name: upload release\n      uses: softprops/action-gh-release@v2\n      if: github.event_name == 'release'\n      with:\n        files: artifact/${{ env.PKGNAME }}.zip\n"
  },
  {
    "path": ".github/workflows/standalone.yaml",
    "content": "name: standalone\n\non:\n  workflow_dispatch:\n  release:\n    types: [ published ]\n  pull_request:\n    branches: [master]\n    paths:\n      - 'scripts/build-standalone.sh'\n      - 'scripts/standalone.json'\n\njobs:\n  configure:\n    runs-on: ubuntu-latest\n    outputs:\n      matrix: ${{ steps.fetch.outputs.matrix }}\n    steps:\n     - name: Checkout to repository\n       uses: actions/checkout@v4\n     - name: fetch matrix data\n       id: fetch\n       run: echo \"matrix=$(jq -c . < scripts/standalone.json)\" >> $GITHUB_OUTPUT\n  build:\n    runs-on: ubuntu-latest\n    needs: configure\n    strategy:\n      fail-fast: false\n      matrix: ${{ fromJson(needs.configure.outputs.matrix) }}\n    env:\n      WORKSPACE: \"/opt\"\n    steps:\n      - name: checkout\n        uses: actions/checkout@v4\n      - name: Install UPX\n        uses: crazy-max/ghaction-upx@v3\n        with:\n          install-only: true\n      - name: cache\n        uses: actions/cache@v4\n        with:\n          path: ${{ env.WORKSPACE }}/toolchains\n          key: ${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('scripts/build-standalone.sh') }}\n      - name: Cross compile\n        run: |\n          ./scripts/build-standalone.sh\n        env:\n          CANDY_WORKSPACE: ${{ env.WORKSPACE }}\n          CANDY_OS: ${{ matrix.os }}\n          CANDY_ARCH: ${{ matrix.arch }}\n          CANDY_STRIP: \"0\"\n          CANDY_UPX: \"0\"\n          CANDY_TGZ: \"1\"\n      - name: upload\n        uses: actions/upload-artifact@v4\n        with:\n          name: candy-${{ matrix.os }}-${{ matrix.arch }}\n          path: ${{ env.WORKSPACE }}/output/${{ matrix.os }}-${{ matrix.arch }}\n      - name: prepare package\n        shell: bash\n        if: github.event_name == 'release'\n        run: |\n          GIT_TAG=${{ github.event.release.tag_name }}\n          PKG_PATH=${{ env.WORKSPACE }}/output/candy_${GIT_TAG#v}+${{ matrix.os }}_${{ matrix.arch }}.tar.gz\n          mv ${{ env.WORKSPACE }}/output/candy-${{ matrix.os }}-${{ matrix.arch }}.tar.gz $PKG_PATH\n          echo \"PKG_PATH=$PKG_PATH\" >> $GITHUB_ENV\n      - name: release\n        uses: softprops/action-gh-release@v2\n        if: github.event_name == 'release'\n        with:\n          files: ${{ env.PKG_PATH }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Prerequisites\n*.d\n\n# Compiled Object files\n*.slo\n*.lo\n*.o\n*.obj\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Compiled Dynamic libraries\n*.so\n*.dylib\n*.dll\n\n# Fortran module files\n*.mod\n*.smod\n\n# Compiled Static libraries\n*.lai\n*.la\n*.a\n*.lib\n\n# Executables\n*.exe\n*.out\n*.app\n\n# CMake Build files\n.cache\nbuild\n"
  },
  {
    "path": ".vscode/c_cpp_properties.json",
    "content": "{\n    \"configurations\": [\n        {\n            \"name\": \"Linux\",\n            \"includePath\": [\n                \"${workspaceFolder}/**\"\n            ],\n            \"defines\": [],\n            \"compilerPath\": \"/usr/bin/clang\",\n            \"cStandard\": \"c17\",\n            \"intelliSenseMode\": \"linux-clang-x64\"\n        }\n    ],\n    \"version\": 4\n}\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n    \"recommendations\": [\n        \"ms-vscode.cpptools-extension-pack\"\n    ]\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"(gdb) Launch\",\n            \"type\": \"cppdbg\",\n            \"request\": \"launch\",\n            \"program\": \"${workspaceFolder}/build/src/main/candy\",\n            \"args\": [\n                \"-c\",\n                \"candy.cfg\"\n            ],\n            \"stopAtEntry\": false,\n            \"cwd\": \"${workspaceFolder}\",\n            \"environment\": [],\n            \"externalConsole\": false,\n            \"MIMode\": \"gdb\",\n            \"setupCommands\": [\n                {\n                    \"description\": \"Enable pretty-printing for gdb\",\n                    \"text\": \"-enable-pretty-printing\",\n                    \"ignoreFailures\": true\n                },\n                {\n                    \"description\": \"Set Disassembly Flavor to Intel\",\n                    \"text\": \"-gdb-set disassembly-flavor intel\",\n                    \"ignoreFailures\": true\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"editor.detectIndentation\": false,\n    \"editor.tabSize\": 4,\n    \"editor.formatOnSave\": true,\n    \"editor.insertSpaces\": true,\n    \"editor.formatOnSaveMode\": \"modifications\",\n    \"files.insertFinalNewline\": true,\n    \"json.format.enable\": true,\n    \"C_Cpp.default.cppStandard\": \"c++23\",\n    \"C_Cpp.autoAddFileAssociations\": false,\n    \"C_Cpp.errorSquiggles\": \"disabled\"\n}\n"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n    \"tasks\": [\n        {\n            \"type\": \"cppbuild\",\n            \"label\": \"C/C++: g++ build active file\",\n            \"command\": \"/usr/bin/g++\",\n            \"args\": [\n                \"-fdiagnostics-color=always\",\n                \"-g\",\n                \"${file}\",\n                \"-o\",\n                \"${fileDirname}/${fileBasenameNoExtension}\"\n            ],\n            \"options\": {\n                \"cwd\": \"${fileDirname}\"\n            },\n            \"problemMatcher\": [\n                \"$gcc\"\n            ],\n            \"group\": {\n                \"kind\": \"build\",\n                \"isDefault\": true\n            },\n            \"detail\": \"Task generated by Debugger.\"\n        }\n    ],\n    \"version\": \"2.0.0\"\n}\n"
  },
  {
    "path": "CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.16)\nproject(Candy VERSION 6.1.7)\n\nset(CMAKE_CXX_STANDARD 17)\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\n\nadd_compile_definitions(CANDY_VERSION=\"${PROJECT_VERSION}\")\n\nset(BUILD_SHARED_LIBS OFF CACHE BOOL \"\" FORCE)\nset(CMAKE_SKIP_BUILD_RPATH TRUE)\n\nif (${CANDY_STATIC})\n    set(CANDY_STATIC_OPENSSL 1)\n    set(CANDY_STATIC_SPDLOG 1)\n    set(CANDY_STATIC_NLOHMANN_JSON 1)\n    set(CANDY_STATIC_POCO 1)\nendif()\n\nfind_package(PkgConfig REQUIRED)\ninclude(${CMAKE_CURRENT_SOURCE_DIR}/cmake/Fetch.cmake)\n\nif (${CANDY_STATIC_OPENSSL})\n    execute_process(\n        COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/openssl\n    )\n    execute_process(\n        COMMAND ${CMAKE_COMMAND} -DTARGET_OPENSSL=${TARGET_OPENSSL} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/openssl\n        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/openssl\n        RESULT_VARIABLE result\n    )\n    if(NOT result EQUAL \"0\")\n        message(FATAL_ERROR \"OpenSSL CMake failed\")\n    endif()\n\n    execute_process(\n        COMMAND ${CMAKE_COMMAND} --build ${CMAKE_CURRENT_BINARY_DIR}/openssl\n        RESULT_VARIABLE result\n    )\n    if(NOT result EQUAL \"0\")\n        message(FATAL_ERROR \"OpenSSL Download or Configure failed\")\n    endif()\n\n    include(ProcessorCount)\n    ProcessorCount(nproc)\n    if(nproc EQUAL 0)\n        set(nproc 1)\n    endif()\n    set(OPENSSL_ROOT_DIR ${CMAKE_CURRENT_BINARY_DIR}/openssl/openssl/src/openssl)\n    execute_process(\n        COMMAND make -j${nproc}\n        WORKING_DIRECTORY ${OPENSSL_ROOT_DIR}\n        RESULT_VARIABLE result\n    )\n    if(NOT result EQUAL \"0\")\n        message(FATAL_ERROR \"OpenSSL Build failed\")\n    endif()\n\n    set(OPENSSL_ROOT_DIR ${CMAKE_CURRENT_BINARY_DIR}/openssl/openssl/src/openssl)\n    set(OPENSSL_INCLUDE ${OPENSSL_ROOT_DIR}/include)\n    set(OPENSSL_LIB_CRYPTO ${OPENSSL_ROOT_DIR}/libcrypto.a)\n    set(OPENSSL_LIB_SSL ${OPENSSL_ROOT_DIR}/libssl.a)\n    include_directories(${OPENSSL_INCLUDE})\nelse()\n    find_package(OpenSSL REQUIRED)\nendif()\n\nif (${CANDY_STATIC_SPDLOG})\n    Fetch(spdlog \"https://github.com/gabime/spdlog.git\" \"v1.15.3\")\nelse()\n    find_package(spdlog REQUIRED)\nendif()\n\nif (${CANDY_STATIC_POCO})\n    set(ENABLE_DATA OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_DATA_MYSQL OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_DATA_POSTGRESQL OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_DATA_SQLITE OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_DATA_ODBC OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_MONGODB OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_REDIS OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_ENCODINGS OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_PROMETHEUS OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_PAGECOMPILER OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_PAGECOMPILER_FILE2PAGE OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_ACTIVERECORD OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_ACTIVERECORD_COMPILER OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_ZIP OFF CACHE BOOL \"\" FORCE)\n    set(ENABLE_JWT OFF CACHE BOOL \"\" FORCE)\n    Fetch(poco \"https://github.com/pocoproject/poco.git\" \"poco-1.13.3-release\")\nelse()\n    find_package(Poco REQUIRED COMPONENTS Foundation XML JSON Net NetSSL Util)\nendif()\n\nset(THREADS_PREFER_PTHREAD_FLAG ON)\nfind_package(Threads REQUIRED)\n\nadd_subdirectory(candy)\nadd_subdirectory(candy-cli)\nadd_subdirectory(candy-service)\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 lanthora\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Candy\n\n<p>\n<a href=\"https://github.com/lanthora/candy/releases/latest\"><img src=\"https://img.shields.io/github/release/lanthora/candy\" /></a>\n<a href=\"https://github.com/lanthora/candy/actions/workflows/release.yaml\"><img src=\"https://img.shields.io/github/actions/workflow/status/lanthora/candy/release.yaml\" /></a>\n<a href=\"https://github.com/lanthora/candy/graphs/contributors\"><img src=\"https://img.shields.io/github/contributors-anon/lanthora/candy\" /></a>\n<a href=\"https://github.com/lanthora/candy/issues\"><img src=\"https://img.shields.io/github/issues-raw/lanthora/candy\" /></a>\n<a href=\"https://github.com/lanthora/candy/pulls\"><img src=\"https://img.shields.io/github/issues-pr-raw/lanthora/candy\" /></a>\n</p>\n\n一个简单的组网工具.\n\n## 如何使用\n\n- [安装 Windows 客户端](https://docs.canets.org/install-client-for-windows)\n- [安装 macOS 客户端](https://docs.canets.org/install-client-for-macos)\n- [安装 Linux 客户端](https://docs.canets.org/install-client-for-linux)\n- [部署 Web 服务端](https://docs.canets.org/deploy-web-server)\n- [部署 CLI 服务端](https://docs.canets.org/deploy-cli-server)\n- [使用社区服务器](https://docs.canets.org/use-the-community-server)\n- [多局域网组网](https://docs.canets.org/software-defined-wide-area-network)\n\n## 相关项目\n\n- [Cacao](https://github.com/lanthora/cacao): WebUI 版的 Candy 服务器\n- [Cake](https://github.com/lanthora/cake): Qt 实现的 Candy GUI 桌面应用程序\n- [Candy Android](https://github.com/Jercrox/Candy_Android_Client): Android 客户端\n- [EasyTier](https://github.com/EasyTier/EasyTier): 一个简单、安全、去中心化的内网穿透 VPN 组网方案，使用 Rust 语言和 Tokio 框架实现\n- [candygo](https://github.com/SoraKasvgano/candygo): 一个简单的与candy原项目配置文件兼容的go版本\n\n## 交流群\n\n- QQ: 768305206\n- TG: [Click to Join](https://t.me/CandyUserGroup)\n"
  },
  {
    "path": "candy/.vscode/c_cpp_properties.json",
    "content": "{\n    \"configurations\": [\n        {\n            \"name\": \"Linux\",\n            \"includePath\": [\n                \"${workspaceFolder}/**\"\n            ],\n            \"defines\": [],\n            \"compilerPath\": \"/usr/bin/clang\",\n            \"cStandard\": \"c17\",\n            \"intelliSenseMode\": \"linux-clang-x64\"\n        }\n    ],\n    \"version\": 4\n}\n"
  },
  {
    "path": "candy/.vscode/settings.json",
    "content": "{\n    \"editor.detectIndentation\": false,\n    \"editor.tabSize\": 4,\n    \"editor.formatOnSave\": true,\n    \"editor.insertSpaces\": true,\n    \"editor.formatOnSaveMode\": \"modifications\",\n    \"files.insertFinalNewline\": true,\n    \"json.format.enable\": true,\n    \"C_Cpp.default.cppStandard\": \"c++17\",\n    \"C_Cpp.autoAddFileAssociations\": false,\n    \"C_Cpp.errorSquiggles\": \"disabled\"\n}\n"
  },
  {
    "path": "candy/CMakeLists.txt",
    "content": "add_library(candy-library)\n\nfile(GLOB_RECURSE SOURCES \"src/*.cc\")\ntarget_sources(candy-library PRIVATE ${SOURCES})\n\ntarget_include_directories(candy-library PUBLIC \n    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>\n    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>\n    $<INSTALL_INTERFACE:include>\n)\n\nif (${CANDY_STATIC_OPENSSL})\n    target_link_libraries(candy-library PRIVATE ${OPENSSL_LIB_CRYPTO} ${OPENSSL_LIB_SSL})\nelse()\n    target_link_libraries(candy-library PRIVATE OpenSSL::SSL OpenSSL::Crypto)\nendif()\n\ntarget_link_libraries(candy-library PRIVATE spdlog::spdlog)\ntarget_link_libraries(candy-library PRIVATE Poco::Foundation Poco::JSON Poco::Net Poco::NetSSL)\ntarget_link_libraries(candy-library PRIVATE Threads::Threads)\n\nif (${CMAKE_SYSTEM_NAME} STREQUAL \"Windows\")\n    target_link_libraries(candy-library PRIVATE ws2_32)\nendif()\n\nif (${CMAKE_SYSTEM_NAME} STREQUAL \"Windows\")\n    target_link_libraries(candy-library PRIVATE iphlpapi)\n    target_link_libraries(candy-library PRIVATE ws2_32)\n\n    set(WINTUN_VERSION 0.14.1)\n    set(WINTUN_ZIP wintun-${WINTUN_VERSION}.zip)\n    set(WINTUN_URL https://www.wintun.net/builds/${WINTUN_ZIP})\n\n    if (NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/${WINTUN_ZIP})\n        file(DOWNLOAD ${WINTUN_URL} ${CMAKE_CURRENT_BINARY_DIR}/${WINTUN_ZIP} STATUS DOWNLOAD_STATUS)\n        list(GET DOWNLOAD_STATUS 0 STATUS_CODE)\n        list(GET DOWNLOAD_STATUS 1 ERROR_MESSAGE)\n\n        if(${STATUS_CODE} EQUAL 0)\n            message(STATUS \"wintun download success\")\n        else()\n            message(FATAL_ERROR \"wintun download failed: ${ERROR_MESSAGE}\")\n        endif()\n    else()\n        message(STATUS \"use wintun cache\")\n    endif()\n\n    file(ARCHIVE_EXTRACT INPUT ${CMAKE_CURRENT_BINARY_DIR}/${WINTUN_ZIP})\n\n    include_directories(${CMAKE_CURRENT_BINARY_DIR}/wintun/include)\nendif()\n\nset_target_properties(candy-library PROPERTIES OUTPUT_NAME \"candy\")\n\nadd_library(Candy::Library ALIAS candy-library)\n"
  },
  {
    "path": "candy/include/candy/candy.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CANDY_H\n#define CANDY_CANDY_H\n\n#include \"client.h\"\n#include \"common.h\"\n#include \"server.h\"\n\n#endif\n"
  },
  {
    "path": "candy/include/candy/client.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CLIENT_H\n#define CANDY_CLIENT_H\n\n#include <Poco/JSON/Object.h>\n#include <optional>\n#include <string>\n\nnamespace candy {\nnamespace client {\n\nbool run(const std::string &id, const Poco::JSON::Object &config);\nbool shutdown(const std::string &id);\nstd::optional<Poco::JSON::Object> status(const std::string &id);\n\n} // namespace client\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/include/candy/common.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_COMMON_H\n#define CANDY_COMMON_H\n\n#include <string>\n\nnamespace candy {\nstatic const int VMAC_SIZE = 16;\n\nstd::string version();\nstd::string create_vmac();\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/include/candy/server.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_SERVER_H\n#define CANDY_SERVER_H\n\n#include <Poco/JSON/Object.h>\n#include <string>\n\nnamespace candy {\nnamespace server {\n\nbool run(const Poco::JSON::Object &config);\nbool shutdown();\n\n} // namespace server\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/candy/client.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"candy/client.h\"\n#include \"core/client.h\"\n#include \"utils/atomic.h\"\n#include <Poco/JSON/Object.h>\n#include <Poco/JSON/Stringifier.h>\n#include <map>\n#include <memory>\n#include <mutex>\n#include <optional>\n#include <shared_mutex>\n#include <spdlog/spdlog.h>\n\nnamespace candy {\nnamespace client {\n\nnamespace {\nusing Utils::Atomic;\n\nclass Instance {\npublic:\n    bool is_running() {\n        return this->running.load();\n    }\n\n    void exit() {\n        this->running.store(false);\n        if (auto client = this->client.lock()) {\n            client->shutdown();\n        }\n    }\n\n    Poco::JSON::Object status() {\n        Poco::JSON::Object data;\n        if (auto client = this->client.lock()) {\n            data.set(\"address\", client->getTunCidr());\n        }\n        return data;\n    }\n\n    std::shared_ptr<Client> create_client() {\n        auto client = std::make_shared<Client>();\n        this->client = client;\n        return client;\n    }\n\nprivate:\n    Atomic<bool> running = Atomic(true);\n    std::weak_ptr<Client> client;\n};\n\nstd::map<std::string, std::shared_ptr<Instance>> instance_map;\nstd::shared_mutex instance_mutex;\n\nstd::optional<std::shared_ptr<Instance>> try_create_instance(const std::string &id) {\n    std::unique_lock lock(instance_mutex);\n    auto it = instance_map.find(id);\n    if (it != instance_map.end()) {\n        spdlog::warn(\"instance already exists: id={}\", id);\n        return std::nullopt;\n    }\n    auto manager = std::make_shared<Instance>();\n    instance_map.emplace(id, manager);\n    return manager;\n}\n\nbool try_erase_instance(const std::string &id) {\n    std::unique_lock lock(instance_mutex);\n    return instance_map.erase(id) > 0;\n}\n\n} // namespace\n\nbool run(const std::string &id, const Poco::JSON::Object &config) {\n    auto instance = try_create_instance(id);\n    if (!instance) {\n        return false;\n    }\n\n    auto toString = [](const Poco::JSON::Object &obj) -> std::string {\n        std::ostringstream oss;\n        Poco::JSON::Stringifier::stringify(obj, oss);\n        return oss.str();\n    };\n\n    spdlog::info(\"run enter: id={} config={}\", id, toString(config));\n    while ((*instance)->is_running()) {\n        std::this_thread::sleep_for(std::chrono::seconds(1));\n        auto client = (*instance)->create_client();\n        client->setName(config.getValue<std::string>(\"name\"));\n        client->setPassword(config.getValue<std::string>(\"password\"));\n        client->setWebSocket(config.getValue<std::string>(\"websocket\"));\n        client->setTunAddress(config.getValue<std::string>(\"tun\"));\n        client->setVirtualMac(config.getValue<std::string>(\"vmac\"));\n        client->setExptTunAddress(config.getValue<std::string>(\"expt\"));\n        client->setStun(config.getValue<std::string>(\"stun\"));\n        client->setDiscoveryInterval(config.getValue<int>(\"discovery\"));\n        client->setRouteCost(config.getValue<int>(\"route\")), client->setMtu(config.getValue<int>(\"mtu\"));\n        client->setPort(config.getValue<int>(\"port\"));\n        client->setLocalhost(config.getValue<std::string>(\"localhost\"));\n        client->run();\n    }\n    spdlog::info(\"run exit: id={} \", id);\n\n    return try_erase_instance(id);\n}\n\nbool shutdown(const std::string &id) {\n    std::shared_lock lock(instance_mutex);\n    auto it = instance_map.find(id);\n    if (it == instance_map.end()) {\n        spdlog::warn(\"instance not found: id={}\", id);\n        return false;\n    }\n    if (auto instance = it->second) {\n        instance->exit();\n    }\n    return true;\n}\n\nstd::optional<Poco::JSON::Object> status(const std::string &id) {\n    std::shared_lock lock(instance_mutex);\n    auto it = instance_map.find(id);\n    if (it != instance_map.end()) {\n        if (auto instance = it->second) {\n            return instance->status();\n        }\n    }\n    return std::nullopt;\n}\n\n} // namespace client\n} // namespace candy\n"
  },
  {
    "path": "candy/src/candy/server.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"candy/server.h\"\n#include \"core/server.h\"\n#include \"utils/atomic.h\"\n\nnamespace candy {\nnamespace server {\n\nnamespace {\nUtils::Atomic<bool> running(true);\nstd::shared_ptr<Server> server;\n} // namespace\n\nbool run(const Poco::JSON::Object &config) {\n    while (running.load()) {\n        std::this_thread::sleep_for(std::chrono::seconds(1));\n        server = std::make_shared<Server>();\n        server->setWebSocket(config.getValue<std::string>(\"websocket\"));\n        server->setPassword(config.getValue<std::string>(\"password\"));\n        server->setDHCP(config.getValue<std::string>(\"dhcp\"));\n        server->setSdwan(config.getValue<std::string>(\"sdwan\"));\n        server->run();\n    }\n    return true;\n}\n\nbool shutdown() {\n    running.store(false);\n    server->shutdown();\n    return true;\n}\n\n} // namespace server\n} // namespace candy\n"
  },
  {
    "path": "candy/src/core/client.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"core/client.h\"\n#include \"core/message.h\"\n#include <Poco/String.h>\n#include <chrono>\n\nnamespace candy {\n\nMsg MsgQueue::read() {\n    std::unique_lock lock(msgMutex);\n    if (!msgCondition.wait_for(lock, std::chrono::seconds(1), [this] { return !msgQueue.empty(); })) {\n        return Msg(MsgKind::TIMEOUT);\n    }\n\n    Msg msg = std::move(msgQueue.front());\n    msgQueue.pop();\n    return msg;\n}\n\nvoid MsgQueue::write(Msg msg) {\n    {\n        std::unique_lock lock(this->msgMutex);\n        msgQueue.push(std::move(msg));\n    }\n    msgCondition.notify_one();\n}\n\nvoid MsgQueue::clear() {\n    std::unique_lock lock(this->msgMutex);\n    while (!msgQueue.empty()) {\n        msgQueue.pop();\n    }\n}\n\nvoid Client::setName(const std::string &name) {\n    this->tunName = name;\n    tun.setName(name);\n    ws.setName(name);\n}\n\nstd::string Client::getName() const {\n    return this->tunName;\n}\n\nstd::string Client::getTunCidr() const {\n    return ws.getTunCidr();\n}\n\nIP4 Client::address() {\n    return this->tun.getIP();\n}\n\nMsgQueue &Client::getTunMsgQueue() {\n    return this->tunMsgQueue;\n}\n\nMsgQueue &Client::getPeerMsgQueue() {\n    return this->peerMsgQueue;\n}\n\nMsgQueue &Client::getWsMsgQueue() {\n    return this->wsMsgQueue;\n}\n\nvoid Client::setPassword(const std::string &password) {\n    ws.setPassword(password);\n    peerManager.setPassword(password);\n}\n\nvoid Client::setWebSocket(const std::string &uri) {\n    ws.setWsServerUri(uri);\n}\n\nvoid Client::setTunAddress(const std::string &cidr) {\n    ws.setAddress(cidr);\n}\n\nvoid Client::setExptTunAddress(const std::string &cidr) {\n    ws.setExptTunAddress(cidr);\n}\n\nvoid Client::setVirtualMac(const std::string &vmac) {\n    ws.setVirtualMac(vmac);\n}\n\nvoid Client::setStun(const std::string &stun) {\n    peerManager.setStun(stun);\n}\n\nvoid Client::setDiscoveryInterval(int interval) {\n    peerManager.setDiscoveryInterval(interval);\n}\n\nvoid Client::setRouteCost(int cost) {\n    peerManager.setRouteCost(cost);\n}\n\nvoid Client::setPort(int port) {\n    peerManager.setPort(port);\n}\n\nvoid Client::setLocalhost(std::string ip) {\n    peerManager.setLocalhost(ip);\n}\n\nvoid Client::setMtu(int mtu) {\n    tun.setMTU(mtu);\n}\n\nvoid Client::run() {\n    this->running.store(true);\n\n    if (ws.run(this)) {\n        return;\n    }\n    if (tun.run(this)) {\n        return;\n    }\n    if (peerManager.run(this)) {\n        return;\n    }\n\n    ws.wait();\n    tun.wait();\n    peerManager.wait();\n\n    wsMsgQueue.clear();\n    tunMsgQueue.clear();\n    peerMsgQueue.clear();\n}\n\nbool Client::isRunning() {\n    return this->running.load();\n}\n\nvoid Client::shutdown() {\n    this->running.store(false);\n}\n\n} // namespace candy\n"
  },
  {
    "path": "candy/src/core/client.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CORE_CLIENT_H\n#define CANDY_CORE_CLIENT_H\n\n#include \"core/message.h\"\n#include \"peer/manager.h\"\n#include \"tun/tun.h\"\n#include \"utils/atomic.h\"\n#include \"websocket/client.h\"\n#include <condition_variable>\n#include <mutex>\n#include <queue>\n#include <string>\n\nnamespace candy {\n\nclass MsgQueue {\npublic:\n    Msg read();\n    void write(Msg msg);\n    void clear();\n\nprivate:\n    std::queue<Msg> msgQueue;\n    std::mutex msgMutex;\n    std::condition_variable msgCondition;\n};\n\nclass Client {\npublic:\n    void setName(const std::string &name);\n    void setPassword(const std::string &password);\n    void setWebSocket(const std::string &uri);\n    void setTunAddress(const std::string &cidr);\n    void setStun(const std::string &stun);\n    void setDiscoveryInterval(int interval);\n    void setRouteCost(int cost);\n    void setPort(int port);\n    void setLocalhost(std::string ip);\n    void setMtu(int mtu);\n\n    void setExptTunAddress(const std::string &cidr);\n    void setVirtualMac(const std::string &vmac);\n\n    void run();\n    bool isRunning();\n    void shutdown();\n\n    std::string getName() const;\n    std::string getTunCidr() const;\n    IP4 address();\n\nprivate:\n    Utils::Atomic<bool> running;\n\npublic:\n    MsgQueue &getTunMsgQueue();\n    MsgQueue &getPeerMsgQueue();\n    MsgQueue &getWsMsgQueue();\n\nprivate:\n    MsgQueue tunMsgQueue, peerMsgQueue, wsMsgQueue;\n\n    Tun tun;\n    PeerManager peerManager;\n    WebSocketClient ws;\n\nprivate:\n    std::string tunName;\n};\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/core/common.cc",
    "content": "#include \"candy/common.h\"\n#include \"core/version.h\"\n#include \"utils/random.h\"\n#include <string>\n\nnamespace candy {\n\nstd::string version() {\n    return CANDY_VERSION;\n}\n\nstd::string create_vmac() {\n    return randomHexString(VMAC_SIZE);\n}\n\n} // namespace candy"
  },
  {
    "path": "candy/src/core/message.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"core/message.h\"\n\nnamespace candy {\n\nMsg::Msg(MsgKind kind, std::string data) {\n    this->kind = kind;\n    this->data = std::move(data);\n}\n\nMsg::Msg(Msg &&packet) {\n    kind = packet.kind;\n    data = std::move(packet.data);\n}\n\nMsg &Msg::operator=(Msg &&packet) {\n    kind = packet.kind;\n    data = std::move(packet.data);\n    return *this;\n}\n\n} // namespace candy\n"
  },
  {
    "path": "candy/src/core/message.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CORE_MESSAGE_H\n#define CANDY_CORE_MESSAGE_H\n\n#include \"core/net.h\"\n#include <cstring>\n#include <string>\n\nnamespace candy {\n\nenum class MsgKind {\n    TIMEOUT,\n    PACKET,\n    TUNADDR,\n    SYSRT,\n    TRYP2P,\n    PUBINFO,\n    DISCOVERY,\n};\n\nstruct Msg {\n    MsgKind kind;\n    std::string data;\n\n    Msg(const Msg &) = delete;\n    Msg &operator=(const Msg &) = delete;\n\n    Msg(MsgKind kind = MsgKind::TIMEOUT, std::string = \"\");\n    Msg(Msg &&packet);\n    Msg &operator=(Msg &&packet);\n};\n\nnamespace CoreMsg {\n\nstruct PubInfo {\n    IP4 src;\n    IP4 dst;\n    IP4 ip;\n    uint16_t port;\n    bool local = false;\n};\n\n} // namespace CoreMsg\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/core/net.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"core/net.h\"\n#include <Poco/Net/IPAddress.h>\n#include <cstring>\n#include <exception>\n\nnamespace candy {\n\nIP4::IP4(const std::string &ip) {\n    fromString(ip);\n}\n\nIP4 IP4::operator=(const std::string &ip) {\n    fromString(ip);\n    return *this;\n}\n\nIP4::operator std::string() const {\n    return toString();\n}\n\nIP4::operator uint32_t() const {\n    uint32_t val = 0;\n    std::memcpy(&val, raw.data(), sizeof(val));\n    return val;\n}\n\nIP4 IP4::operator|(IP4 another) const {\n    for (int i = 0; i < raw.size(); ++i) {\n        another.raw[i] |= raw[i];\n    }\n    return another;\n}\n\nIP4 IP4::operator^(IP4 another) const {\n    for (int i = 0; i < raw.size(); ++i) {\n        another.raw[i] ^= raw[i];\n    }\n    return another;\n}\n\nIP4 IP4::operator~() const {\n    IP4 retval;\n    for (int i = 0; i < raw.size(); ++i) {\n        retval.raw[i] |= ~raw[i];\n    }\n    return retval;\n}\n\nbool IP4::operator==(IP4 another) const {\n    return raw == another.raw;\n}\n\nIP4 IP4::operator&(IP4 another) const {\n    for (int i = 0; i < raw.size(); ++i) {\n        another.raw[i] &= raw[i];\n    }\n    return another;\n}\n\nIP4 IP4::next() const {\n    IP4 ip;\n    uint32_t t = hton(ntoh(uint32_t(*this)) + 1);\n    std::memcpy(&ip, &t, sizeof(ip));\n    return ip;\n}\n\nint IP4::fromString(const std::string &ip) {\n    memcpy(raw.data(), Poco::Net::IPAddress(ip).addr(), 4);\n    return 0;\n}\n\nstd::string IP4::toString() const {\n    return Poco::Net::IPAddress(raw.data(), sizeof(raw)).toString();\n}\n\nint IP4::fromPrefix(int prefix) {\n    std::memset(raw.data(), 0, sizeof(raw));\n    for (int i = 0; i < prefix; ++i) {\n        raw[i / 8] |= (0x80 >> (i % 8));\n    }\n    return 0;\n}\n\nint IP4::toPrefix() {\n    int i;\n    for (i = 0; i < 32; ++i) {\n        if (!(raw[i / 8] & (0x80 >> (i % 8)))) {\n            break;\n        }\n    }\n    return i;\n}\n\nbool IP4::empty() const {\n    return raw[0] == 0 && raw[1] == 0 && raw[2] == 0 && raw[3] == 0;\n}\n\nvoid IP4::reset() {\n    this->raw.fill(0);\n}\n\nbool IP4Header::isIPv4() {\n    return (this->version_ihl >> 4) == 4;\n}\n\nbool IP4Header::isIPIP() {\n    return this->protocol == 0x04;\n}\n\nAddress::Address() {}\n\nAddress::Address(const std::string &cidr) {\n    if (!cidr.empty()) {\n        fromCidr(cidr);\n    }\n}\n\nIP4 &Address::Host() {\n    return this->host;\n}\n\nIP4 &Address::Mask() {\n    return this->mask;\n}\n\nIP4 Address::Net() {\n    return Host() & Mask();\n}\n\nAddress Address::Next() {\n    Address next;\n    next.mask = this->mask;\n    next.host = (Net() | (~Mask() & this->host.next()));\n    return next;\n}\n\nbool Address::isValid() {\n    if ((~mask & host) == 0) {\n        return false;\n    }\n    if (~(mask | host) == 0) {\n        return false;\n    }\n    return true;\n}\n\nint Address::fromCidr(const std::string &cidr) {\n    try {\n        std::size_t pos = cidr.find('/');\n        host.fromString(cidr.substr(0UL, pos));\n        mask.fromPrefix(std::stoi(cidr.substr(pos + 1)));\n    } catch (std::exception &e) {\n        spdlog::warn(\"address parse cidr failed: {}: {}\", e.what(), cidr);\n        return -1;\n    }\n    return 0;\n}\n\nstd::string Address::toCidr() {\n    return host.toString() + \"/\" + std::to_string(mask.toPrefix());\n}\n\n} // namespace candy\n"
  },
  {
    "path": "candy/src/core/net.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CORE_NET_H\n#define CANDY_CORE_NET_H\n\n#include <array>\n#include <cstdint>\n#include <spdlog/spdlog.h>\n#include <string>\n#include <type_traits>\n\nnamespace candy {\n\ntemplate <typename T> typename std::enable_if<std::is_integral<T>::value, T>::type byteswap(T value) {\n    static_assert(std::is_integral<T>::value, \"byteswap requires integral type\");\n\n    union {\n        T value;\n        uint8_t bytes[sizeof(T)];\n    } src, dst;\n\n    src.value = value;\n    for (size_t i = 0; i < sizeof(T); i++) {\n        dst.bytes[i] = src.bytes[sizeof(T) - i - 1];\n    }\n    return dst.value;\n}\n\ntemplate <typename T> T ntoh(T v) {\n    static_assert(std::is_integral<T>::value, \"ntoh requires integral type\");\n\n    uint8_t *bytes = reinterpret_cast<uint8_t *>(&v);\n    bool isLittleEndian = true;\n    {\n        uint16_t test = 0x0001;\n        isLittleEndian = (*reinterpret_cast<uint8_t *>(&test) == 0x01);\n    }\n\n    if (isLittleEndian) {\n        return byteswap(v);\n    }\n    return v;\n}\n\ntemplate <typename T> T hton(T v) {\n    return ntoh(v);\n}\n\nclass __attribute__((packed)) IP4 {\npublic:\n    IP4(const std::string &ip = \"0.0.0.0\");\n    IP4 operator=(const std::string &ip);\n    IP4 operator&(IP4 another) const;\n    IP4 operator|(IP4 another) const;\n    IP4 operator^(IP4 another) const;\n    IP4 operator~() const;\n    bool operator==(IP4 another) const;\n    operator std::string() const;\n    operator uint32_t() const;\n    IP4 next() const;\n    int fromString(const std::string &ip);\n    std::string toString() const;\n    int fromPrefix(int prefix);\n    int toPrefix();\n    bool empty() const;\n    void reset();\n\nprivate:\n    std::array<uint8_t, 4> raw;\n};\n\nstruct __attribute__((packed)) IP4Header {\n    uint8_t version_ihl;\n    uint8_t tos;\n    uint16_t tot_len;\n    uint16_t id;\n    uint16_t frag_off;\n    uint8_t ttl;\n    uint8_t protocol;\n    uint16_t check;\n    IP4 saddr;\n    IP4 daddr;\n\n    bool isIPv4();\n    bool isIPIP();\n};\n\nstruct __attribute__((packed)) SysRouteEntry {\n    IP4 dst;\n    IP4 mask;\n    IP4 nexthop;\n};\n\n/* 用于表示地址和掩码的组合,用于判断主机是否属于某个网络 */\nclass Address {\npublic:\n    Address();\n    Address(const std::string &cidr);\n\n    IP4 &Host();\n    IP4 &Mask();\n    IP4 Net();\n\n    // 当前网络内的下一个地址\n    Address Next();\n\n    // 判断是否是有效的主机地址\n    bool isValid();\n\n    int fromCidr(const std::string &cidr);\n    std::string toCidr();\n\n    bool empty() const {\n        return host.empty() && mask.empty();\n    }\n\nprivate:\n    IP4 host;\n    IP4 mask;\n};\n\n} // namespace candy\n\nnamespace std {\nusing candy::IP4;\ntemplate <> struct hash<IP4> {\n    size_t operator()(const IP4 &ip) const noexcept {\n        return hash<uint32_t>{}(ip);\n    }\n};\n} // namespace std\n\nnamespace {\n\nconstexpr std::size_t AES_256_GCM_IV_LEN = 12;\nconstexpr std::size_t AES_256_GCM_TAG_LEN = 16;\nconstexpr std::size_t AES_256_GCM_KEY_LEN = 32;\n\n} // namespace\n\n#endif\n"
  },
  {
    "path": "candy/src/core/server.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"core/server.h\"\n\nnamespace candy {\n\nvoid Server::setWebSocket(const std::string &uri) {\n    ws.setWebSocket(uri);\n}\n\nvoid Server::setPassword(const std::string &password) {\n    ws.setPassword(password);\n}\n\nvoid Server::setDHCP(const std::string &cidr) {\n    ws.setDHCP(cidr);\n}\n\nvoid Server::setSdwan(const std::string &sdwan) {\n    ws.setSdwan(sdwan);\n}\n\nvoid Server::run() {\n    running.store(true);\n    ws.run();\n    running.wait(true);\n    ws.shutdown();\n}\n\nvoid Server::shutdown() {\n    running.store(false);\n}\n\n} // namespace candy\n"
  },
  {
    "path": "candy/src/core/server.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CORE_SERVER_H\n#define CANDY_CORE_SERVER_H\n\n#include \"utils/atomic.h\"\n#include \"websocket/server.h\"\n#include <string>\n\nnamespace candy {\n\nclass Server {\npublic:\n    // 通过配置文件或命令行设置的参数\n    void setWebSocket(const std::string &uri);\n    void setPassword(const std::string &password);\n    void setDHCP(const std::string &cidr);\n    void setSdwan(const std::string &sdwan);\n\n    // 启动服务端,非阻塞\n    void run();\n    // 关闭客户端,阻塞,直到所有子模块退出\n    void shutdown();\n\nprivate:\n    // 目前只有一个 WebSocket 服务端的子模块\n    WebSocketServer ws;\n    Utils::Atomic<bool> running;\n};\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/core/version.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CORE_VERSION_H\n#define CANDY_CORE_VERSION_H\n\n#include <Poco/Platform.h>\n\n#if POCO_OS == POCO_OS_LINUX\n#define CANDY_SYSTEM \"linux\"\n#elif POCO_OS == POCO_OS_MAC_OS_X\n#define CANDY_SYSTEM \"macos\"\n#elif POCO_OS == POCO_OS_ANDROID\n#define CANDY_SYSTEM \"android\"\n#elif POCO_OS == POCO_OS_WINDOWS_NT\n#define CANDY_SYSTEM \"windows\"\n#else\n#define CANDY_SYSTEM \"unknown\"\n#endif\n\n#ifndef CANDY_VERSION\n#define CANDY_VERSION \"unknown\"\n#endif\n\n#endif\n"
  },
  {
    "path": "candy/src/peer/manager.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"peer/manager.h\"\n#include \"core/client.h\"\n#include \"core/message.h\"\n#include \"core/net.h\"\n#include \"peer/message.h\"\n#include \"utils/time.h\"\n#include <Poco/Net/NetException.h>\n#include <Poco/Net/NetworkInterface.h>\n#include <Poco/Timespan.h>\n#include <openssl/sha.h>\n#include <shared_mutex>\n#include <spdlog/fmt/bin_to_hex.h>\n#include <spdlog/spdlog.h>\n\nnamespace candy {\n\nint PeerManager::setPassword(const std::string &password) {\n    this->password = password;\n    return 0;\n}\n\nint PeerManager::setStun(const std::string &stun) {\n    this->stun.uri = stun;\n    return 0;\n}\n\nint PeerManager::setDiscoveryInterval(int interval) {\n    this->discoveryInterval = interval;\n    return 0;\n}\n\nint PeerManager::setRouteCost(int cost) {\n    if (cost < 0) {\n        this->routeCost = 0;\n    } else if (cost > 1000) {\n        this->routeCost = 1000;\n    } else {\n        this->routeCost = cost;\n    }\n    return 0;\n}\n\nint PeerManager::setPort(int port) {\n    if (port > 0 && port <= UINT16_MAX) {\n        this->listenPort = port;\n    }\n    return 0;\n}\n\nint PeerManager::setLocalhost(const std::string &ip) {\n    this->localhost.fromString(ip);\n    return 0;\n}\n\nint PeerManager::run(Client *client) {\n    this->client = client;\n    this->localP2PDisabled = false;\n\n    if (this->stun.update()) {\n        spdlog::critical(\"update stun failed\");\n        return -1;\n    }\n\n    this->msgThread = std::thread([&] {\n        spdlog::debug(\"start thread: peer manager msg\");\n        while (getClient().isRunning()) {\n            if (handlePeerQueue()) {\n                break;\n            }\n        }\n        getClient().shutdown();\n        spdlog::debug(\"stop thread: peer manager msg\");\n    });\n\n    return 0;\n}\n\nint PeerManager::wait() {\n    if (this->msgThread.joinable()) {\n        this->msgThread.join();\n    }\n    if (this->tickThread.joinable()) {\n        this->tickThread.join();\n    }\n    if (this->pollThread.joinable()) {\n        this->pollThread.join();\n    }\n\n    this->socket.close();\n\n    {\n        std::unique_lock lock(this->rtTableMutex);\n        this->rtTableMap.clear();\n    }\n\n    {\n        std::unique_lock lock(this->ipPeerMutex);\n        this->ipPeerMap.clear();\n    }\n\n    return 0;\n}\n\nstd::string PeerManager::getPassword() {\n    return this->password;\n}\n\nint PeerManager::handlePeerQueue() {\n    Msg msg = getClient().getPeerMsgQueue().read();\n    try {\n        switch (msg.kind) {\n        case MsgKind::TIMEOUT:\n            break;\n        case MsgKind::PACKET:\n            handlePacket(std::move(msg));\n            break;\n        case MsgKind::TUNADDR:\n            if (startTickThread()) {\n                return -1;\n            }\n            if (handleTunAddr(std::move(msg))) {\n                return -1;\n            }\n            break;\n        case MsgKind::SYSRT:\n            this->localP2PDisabled = true;\n            break;\n        case MsgKind::TRYP2P:\n            handleTryP2P(std::move(msg));\n            break;\n        case MsgKind::PUBINFO:\n            handlePubInfo(std::move(msg));\n            break;\n        default:\n            spdlog::warn(\"unexcepted peer message type: {}\", static_cast<int>(msg.kind));\n            break;\n        }\n    } catch (const Poco::Exception &e) {\n        spdlog::warn(\"peer manager handle queue failed: msg_kind={}, error={}\", static_cast<int>(msg.kind), e.message());\n        return 0;\n    } catch (const std::exception &e) {\n        spdlog::warn(\"peer manager handle queue failed: msg_kind={}, error={}\", static_cast<int>(msg.kind), e.what());\n        return 0;\n    }\n    return 0;\n}\n\nint PeerManager::sendPacket(IP4 dst, const Msg &msg) {\n    if (!sendPacketRelay(dst, msg)) {\n        return 0;\n    }\n    if (!sendPacketDirect(dst, msg)) {\n        return 0;\n    }\n    return -1;\n}\n\nint PeerManager::sendPacketDirect(IP4 dst, const Msg &msg) {\n    std::shared_lock ipPeerLock(this->ipPeerMutex);\n    auto it = this->ipPeerMap.find(dst);\n    if (it != this->ipPeerMap.end()) {\n        auto &peer = it->second;\n        if (peer.isConnected()) {\n            return peer.sendEncrypted(PeerMsg::Forward::create(msg.data));\n        }\n    }\n    return -1;\n}\n\nint PeerManager::sendPacketRelay(IP4 dst, const Msg &msg) {\n    {\n        std::shared_lock rtTableLock(this->rtTableMutex);\n        auto it = this->rtTableMap.find(dst);\n        if (it == this->rtTableMap.end()) {\n            return -1;\n        }\n        dst = it->second.next;\n    }\n    return sendPacketDirect(dst, msg);\n}\n\nint PeerManager::sendPubInfo(CoreMsg::PubInfo info) {\n    info.src = getClient().address();\n    if (info.local) {\n        info.ip = this->localhost;\n        info.port = this->socket.address().port();\n    } else {\n        info.ip = this->stun.ip;\n        info.port = this->stun.port;\n    }\n    getClient().getWsMsgQueue().write(Msg(MsgKind::PUBINFO, std::string((char *)(&info), sizeof(info))));\n    return 0;\n}\n\nIP4 PeerManager::getTunIp() {\n    return this->tunAddr.Host();\n}\n\nint PeerManager::handlePacket(Msg msg) {\n    auto header = (IP4Header *)msg.data.data();\n    if (!sendPacket(header->daddr, msg)) {\n        return 0;\n    }\n    getClient().getWsMsgQueue().write(std::move(msg));\n    return 0;\n}\n\nint PeerManager::handleTunAddr(Msg msg) {\n    if (this->tunAddr.fromCidr(msg.data)) {\n        spdlog::error(\"set tun addr failed: {}\", msg.data);\n        return -1;\n    }\n\n    std::string data;\n    data.append(this->password);\n    auto leaddr = hton(uint32_t(this->tunAddr.Host()));\n    data.append((char *)&leaddr, sizeof(leaddr));\n\n    this->key.resize(SHA256_DIGEST_LENGTH);\n    SHA256((unsigned char *)data.data(), data.size(), (unsigned char *)this->key.data());\n    return 0;\n}\n\nint PeerManager::handleTryP2P(Msg msg) {\n    IP4 src(msg.data);\n\n    {\n        std::shared_lock lock(this->ipPeerMutex);\n        auto it = this->ipPeerMap.find(src);\n        if (it != this->ipPeerMap.end()) {\n            it->second.tryConnecct();\n            return 0;\n        }\n    }\n\n    {\n        std::unique_lock lock(this->ipPeerMutex);\n        auto it = this->ipPeerMap.emplace(std::piecewise_construct, std::forward_as_tuple(src), std::forward_as_tuple(src, this));\n        if (it.second) {\n            it.first->second.tryConnecct();\n            return 0;\n        }\n    }\n\n    spdlog::warn(\"can not find peer: {}\", src.toString());\n    return 0;\n}\n\nint PeerManager::handlePubInfo(Msg msg) {\n    auto info = (CoreMsg::PubInfo *)(msg.data.data());\n\n    if (info->src == getClient().address() || info->dst != getClient().address()) {\n        spdlog::warn(\"invalid public info: src=[{}] dst=[{}]\", info->src.toString(), info->dst.toString());\n        return 0;\n    }\n\n    try {\n        {\n\n            std::shared_lock lock(this->ipPeerMutex);\n            auto it = this->ipPeerMap.find(info->src);\n            if (it != this->ipPeerMap.end()) {\n                it->second.handlePubInfo(info->ip, info->port, info->local);\n            }\n        }\n\n        {\n            std::unique_lock lock(this->ipPeerMutex);\n            auto it = this->ipPeerMap.emplace(std::piecewise_construct, std::forward_as_tuple(info->src),\n                                              std::forward_as_tuple(info->src, this));\n            if (it.second) {\n                it.first->second.handlePubInfo(info->ip, info->port, info->local);\n                return 0;\n            }\n        }\n    } catch (const Poco::Exception &e) {\n        spdlog::warn(\"peer manager handle pubinfo failed: src={}, ip={}, port={}, error={}\", info->src.toString(),\n                     info->ip.toString(), info->port, e.message());\n        return 0;\n    } catch (const std::exception &e) {\n        spdlog::warn(\"peer manager handle pubinfo failed: src={}, error={}\", info->src.toString(), e.what());\n        return 0;\n    }\n\n    return 0;\n}\n\nint PeerManager::startTickThread() {\n    if (this->localhost.empty()) {\n        try {\n            for (const auto &iface : Poco::Net::NetworkInterface::list()) {\n                if (iface.supportsIPv4() && !iface.isLoopback() && !iface.isPointToPoint() &&\n                    iface.type() != iface.NI_TYPE_OTHER) {\n                    auto firstAddress = iface.firstAddress(Poco::Net::IPAddress::IPv4);\n                    memcpy(&this->localhost, firstAddress.addr(), sizeof(this->localhost));\n                    spdlog::debug(\"localhost: {}\", this->localhost.toString());\n                    break;\n                }\n            }\n        } catch (std::exception &e) {\n            spdlog::warn(\"local ip failed: {}\", e.what());\n        }\n    }\n\n    if (this->initSocket()) {\n        return -1;\n    }\n    this->tickThread = std::thread([&] {\n        spdlog::debug(\"start thread: peer manager tick\");\n        while (getClient().isRunning()) {\n            auto wake_time = std::chrono::system_clock::now() + std::chrono::seconds(1);\n            if (tick()) {\n                break;\n            }\n            std::this_thread::sleep_until(wake_time);\n        }\n        getClient().shutdown();\n        spdlog::debug(\"stop thread: peer manager tick\");\n    });\n    return 0;\n}\n\nint PeerManager::tick() {\n    if (this->discoveryInterval && this->stun.enabled()) {\n        if ((++tickTick % discoveryInterval == 0)) {\n            getClient().getWsMsgQueue().write(Msg(MsgKind::DISCOVERY));\n        }\n    }\n    {\n        std::shared_lock ipPeerLock(this->ipPeerMutex);\n        for (auto &[ip, peer] : this->ipPeerMap) {\n            peer.tick();\n        }\n    }\n\n    if (this->stun.needed) {\n        sendStunRequest();\n        this->stun.needed = false;\n    }\n\n    return 0;\n}\n\nint PeerManager::initSocket() {\n    using Poco::Net::AddressFamily;\n    using Poco::Net::SocketAddress;\n\n    try {\n        this->socket.bind(SocketAddress(AddressFamily::IPv4, this->listenPort));\n        this->socket.setSendBufferSize(16 * 1024 * 1024);\n        this->socket.setReceiveBufferSize(16 * 1024 * 1024);\n        spdlog::debug(\"listen port: {}\", this->socket.address().port());\n    } catch (Poco::Net::NetException &e) {\n        spdlog::critical(\"peer socket init failed: {}: {}\", e.what(), e.message());\n        return -1;\n    }\n\n    this->decryptCtx = std::shared_ptr<EVP_CIPHER_CTX>(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free);\n\n    this->pollThread = std::thread([&]() {\n        spdlog::debug(\"start thread: peer manager poll\");\n        while (getClient().isRunning()) {\n            if (poll()) {\n                break;\n            }\n        }\n        getClient().shutdown();\n        spdlog::debug(\"stop thread: peer manager poll\");\n    });\n    return 0;\n}\n\nvoid PeerManager::sendStunRequest() {\n    try {\n        StunRequest request;\n        if (sendTo(&request, sizeof(request), this->stun.address) != sizeof(request)) {\n            spdlog::warn(\"the stun request was not completely sent\");\n        }\n    } catch (std::exception &e) {\n        spdlog::debug(\"send stun request failed: {}\", e.what());\n    }\n}\n\nvoid PeerManager::handleStunResponse(std::string buffer) {\n    if (buffer.length() < sizeof(StunResponse)) {\n        spdlog::debug(\"invalid stun response length: {}\", buffer.length());\n        return;\n    }\n    auto response = (StunResponse *)buffer.c_str();\n    if (ntoh(response->type) != 0x0101) {\n        spdlog::debug(\"invalid stun reponse type: {}\", ntoh(response->type));\n        return;\n    }\n\n    int pos = 0;\n    uint32_t ip = 0;\n    uint16_t port = 0;\n    uint8_t *attr = response->attr;\n    while (pos < ntoh(response->length)) {\n        // mapped address\n        if (ntoh(*(uint16_t *)(attr + pos)) == 0x0001) {\n            pos += 6; // 跳过 2 字节类型, 2 字节长度, 1 字节保留, 1 字节IP版本号,指向端口号\n            port = ntoh(*(uint16_t *)(attr + pos));\n            pos += 2; // 跳过2字节端口号,指向地址\n            ip = *(uint32_t *)(attr + pos);\n            break;\n        }\n        // xor mapped address\n        if (ntoh(*(uint16_t *)(attr + pos)) == 0x0020) {\n            pos += 6; // 跳过 2 字节类型, 2 字节长度, 1 字节保留, 1 字节IP版本号,指向端口号\n            port = ntoh(*(uint16_t *)(attr + pos)) ^ 0x2112;\n            pos += 2; // 跳过2字节端口号,指向地址\n            ip = (*(uint32_t *)(attr + pos)) ^ hton(0x2112a442);\n            break;\n        }\n        // 跳过 2 字节类型,指向属性长度\n        pos += 2;\n        // 跳过 2 字节长度和用该属性其他内容\n        pos += 2 + ntoh(*(uint16_t *)(attr + pos));\n    }\n    if (!ip || !port) {\n        spdlog::warn(\"stun response parse failed: {:n}\", spdlog::to_hex(buffer));\n        return;\n    }\n\n    memcpy(&this->stun.ip, &ip, sizeof(this->stun.ip));\n    this->stun.port = port;\n\n    std::shared_lock lock(this->ipPeerMutex);\n    for (auto &[tun, peer] : this->ipPeerMap) {\n        peer.handleStunResponse();\n    }\n\n    return;\n}\n\nvoid PeerManager::handleMessage(std::string buffer, const SocketAddress &address) {\n    switch (buffer.front()) {\n    case PeerMsgKind::HEARTBEAT:\n        handleHeartbeatMessage(std::move(buffer), address);\n        break;\n    case PeerMsgKind::FORWARD:\n        handleForwardMessage(std::move(buffer), address);\n        break;\n    case PeerMsgKind::DELAY:\n        if (clientRelayEnabled()) {\n            handleDelayMessage(std::move(buffer), address);\n        }\n        break;\n    case PeerMsgKind::ROUTE:\n        if (clientRelayEnabled()) {\n            handleRouteMessage(std::move(buffer), address);\n        }\n        break;\n    default:\n        spdlog::info(\"udp4 unknown message: {}\", address.toString());\n        break;\n    }\n}\n\nvoid PeerManager::handleHeartbeatMessage(std::string buffer, const SocketAddress &address) {\n    if (buffer.size() < sizeof(PeerMsg::Heartbeat)) {\n        spdlog::debug(\"udp4 heartbeat failed: len {} address {}\", buffer.length(), address.toString());\n        return;\n    }\n\n    auto heartbeat = (PeerMsg::Heartbeat *)buffer.c_str();\n    std::shared_lock lock(this->ipPeerMutex);\n    auto it = this->ipPeerMap.find(heartbeat->tunip);\n    if (it == this->ipPeerMap.end()) {\n        spdlog::debug(\"udp4 heartbeat find peer failed: tun ip {}\", heartbeat->tunip.toString());\n        return;\n    }\n    it->second.handleHeartbeatMessage(address, heartbeat->ack);\n}\n\nvoid PeerManager::handleForwardMessage(std::string buffer, const SocketAddress &address) {\n    if (buffer.size() < sizeof(PeerMsg::Forward)) {\n        spdlog::warn(\"invalid forward message: {:n}\", spdlog::to_hex(buffer));\n        return;\n    }\n    buffer.erase(0, 1);\n    auto header = (IP4Header *)buffer.data();\n    if (header->daddr == getTunIp()) {\n        getClient().getTunMsgQueue().write(Msg(MsgKind::PACKET, std::move(buffer)));\n    } else {\n        getClient().getPeerMsgQueue().write(Msg(MsgKind::PACKET, std::move(buffer)));\n    }\n}\n\nvoid PeerManager::handleDelayMessage(std::string buffer, const SocketAddress &address) {\n    if (buffer.size() < sizeof(PeerMsg::Delay)) {\n        spdlog::warn(\"invalid delay message: {:n}\", spdlog::to_hex(buffer));\n        return;\n    }\n\n    auto header = (PeerMsg::Delay *)buffer.data();\n\n    if (header->dst == getTunIp()) {\n        std::shared_lock ipPeerLock(this->ipPeerMutex);\n        auto it = this->ipPeerMap.find(header->src);\n        if (it != this->ipPeerMap.end()) {\n            auto &peer = it->second;\n            if (peer.isConnected()) {\n                peer.sendEncrypted(buffer);\n            }\n        }\n        return;\n    }\n\n    if (header->src == getTunIp()) {\n        std::shared_lock ipPeerLock(this->ipPeerMutex);\n        auto it = this->ipPeerMap.find(header->dst);\n        if (it != this->ipPeerMap.end()) {\n            auto &peer = it->second;\n            peer.rtt = bootTime() - ntoh(header->timestamp);\n            updateRtTable(PeerRouteEntry(header->dst, header->dst, peer.rtt));\n        }\n        return;\n    }\n}\n\nvoid PeerManager::handleRouteMessage(std::string buffer, const SocketAddress &address) {\n    if (!routeCost) {\n        return;\n    }\n\n    if (buffer.size() < sizeof(PeerMsg::Route)) {\n        spdlog::warn(\"invalid delay message: {:n}\", spdlog::to_hex(buffer));\n        return;\n    }\n    auto header = (PeerMsg::Route *)buffer.data();\n\n    if (header->dst != getTunIp()) {\n        updateRtTable(PeerRouteEntry(header->dst, header->next, ntoh(header->rtt)));\n    }\n}\n\nint PeerManager::poll() {\n    using Poco::Net::Socket;\n    using Poco::Net::SocketAddress;\n\n    try {\n        if (this->socket.poll(Poco::Timespan(1, 0), Poco::Net::Socket::SELECT_READ)) {\n            std::string buffer(1500, 0);\n            SocketAddress address;\n            auto size = this->socket.receiveFrom(buffer.data(), buffer.size(), address);\n            if (size > 0) {\n                buffer.resize(size);\n\n                if (this->stun.address == address) {\n                    handleStunResponse(buffer);\n                } else if (auto plaintext = decrypt(buffer)) {\n                    handleMessage(std::move(*plaintext), address);\n                }\n            }\n        }\n    } catch (Poco::Net::ConnectionResetException &e) {\n        // 忽略 UDP 的连接 Reset, Windows 特有的问题\n    } catch (std::exception &e) {\n        spdlog::warn(\"peer_manager poll failed: {}\", e.what());\n        return -1;\n    }\n    return 0;\n}\n\nstd::optional<std::string> PeerManager::decrypt(const std::string &ciphertext) {\n    int len = 0;\n    int plaintextLen = 0;\n    unsigned char *enc = NULL;\n    unsigned char plaintext[1500] = {0};\n    unsigned char iv[AES_256_GCM_IV_LEN] = {0};\n    unsigned char tag[AES_256_GCM_TAG_LEN] = {0};\n\n    if (this->key.size() != AES_256_GCM_KEY_LEN) {\n        spdlog::debug(\"invalid key length: {}\", this->key.size());\n        return std::nullopt;\n    }\n\n    if (ciphertext.size() < AES_256_GCM_IV_LEN + AES_256_GCM_TAG_LEN) {\n        spdlog::debug(\"invalid ciphertext length: {}\", ciphertext.size());\n        return std::nullopt;\n    }\n\n    std::lock_guard lock(this->decryptCtxMutex);\n    auto ctx = this->decryptCtx.get();\n\n    if (!EVP_CIPHER_CTX_reset(ctx)) {\n        spdlog::debug(\"decrypt reset cipher context failed\");\n        return std::nullopt;\n    }\n\n    enc = (unsigned char *)ciphertext.data();\n    memcpy(iv, enc, AES_256_GCM_IV_LEN);\n    memcpy(tag, enc + AES_256_GCM_IV_LEN, AES_256_GCM_TAG_LEN);\n    enc += AES_256_GCM_IV_LEN + AES_256_GCM_TAG_LEN;\n\n    if (!EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, (unsigned char *)key.data(), iv)) {\n        spdlog::debug(\"initialize cipher context failed\");\n        return std::nullopt;\n    }\n    if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, AES_256_GCM_IV_LEN, NULL)) {\n        spdlog::debug(\"set iv length failed\");\n        return std::nullopt;\n    }\n    if (!EVP_DecryptUpdate(ctx, plaintext, &len, enc, ciphertext.size() - AES_256_GCM_IV_LEN - AES_256_GCM_TAG_LEN)) {\n        spdlog::debug(\"decrypt update failed\");\n        return std::nullopt;\n    }\n    if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, AES_256_GCM_TAG_LEN, tag)) {\n        spdlog::debug(\"set tag failed\");\n        return std::nullopt;\n    }\n\n    plaintextLen = len;\n    if (!EVP_DecryptFinal_ex(ctx, plaintext + len, &len)) {\n        spdlog::debug(\"decrypt final failed\");\n        return std::nullopt;\n    }\n\n    plaintextLen += len;\n\n    std::string result;\n    result.append((char *)plaintext, plaintextLen);\n    return result;\n}\n\nint PeerManager::sendTo(const void *buffer, int length, const SocketAddress &address) {\n    std::lock_guard lock(this->socketMutex);\n    return this->socket.sendTo(buffer, length, address);\n}\n\nint PeerManager::getDiscoveryInterval() const {\n    return this->discoveryInterval;\n}\n\nbool PeerManager::clientRelayEnabled() const {\n    return this->routeCost > 0;\n}\n\nClient &PeerManager::getClient() {\n    return *this->client;\n}\n\nvoid PeerManager::showRtChange(const PeerRouteEntry &entry) {\n    std::string rtt = (entry.rtt == RTT_LIMIT) ? \"[deleted]\" : std::to_string(entry.rtt);\n    spdlog::debug(\"route: dst={} next={} delay={}\", entry.dst.toString(), entry.next.toString(), rtt);\n}\n\nint PeerManager::sendRtMessage(IP4 dst, int32_t rtt) {\n    PeerMsg::Route message;\n    message.type = PeerMsgKind::ROUTE;\n    message.dst = dst;\n    message.next = getTunIp();\n\n    if (rtt != RTT_LIMIT) {\n        rtt += routeCost;\n    }\n\n    message.rtt = ntoh(rtt);\n\n    for (auto &[_, peer] : this->ipPeerMap) {\n        if (peer.isConnected()) {\n            peer.sendEncrypted(std::string((char *)&message, sizeof(message)));\n        }\n    }\n\n    return 0;\n}\n\nint PeerManager::updateRtTable(PeerRouteEntry entry) {\n    bool isDirect = (entry.dst == entry.next);\n    bool isDelete = (entry.rtt < 0 || entry.rtt > 1000);\n\n    std::unique_lock lock(this->rtTableMutex);\n\n    auto oldEntry = this->rtTableMap.find(entry.dst);\n\n    if (isDirect && isDelete) {\n        for (auto it = this->rtTableMap.begin(); it != this->rtTableMap.end();) {\n            if (it->second.next == entry.next) {\n                it->second.rtt = RTT_LIMIT;\n                sendRtMessage(it->second.dst, it->second.rtt);\n                showRtChange(it->second);\n                it = this->rtTableMap.erase(it);\n                continue;\n            }\n            ++it;\n        }\n        return 0;\n    }\n\n    if (isDirect && !isDelete) {\n        if (oldEntry == this->rtTableMap.end() || oldEntry->second.next == entry.next || oldEntry->second.rtt > entry.rtt) {\n            this->rtTableMap[entry.dst] = entry;\n            sendRtMessage(entry.dst, entry.rtt);\n            showRtChange(entry);\n        }\n        return 0;\n    }\n\n    if (!isDirect && isDelete) {\n        if (oldEntry != this->rtTableMap.end() && oldEntry->second.next == entry.next) {\n            oldEntry->second.rtt = RTT_LIMIT;\n            sendRtMessage(oldEntry->second.dst, oldEntry->second.rtt);\n            showRtChange(oldEntry->second);\n            this->rtTableMap.erase(oldEntry);\n        }\n        return 0;\n    }\n\n    if (!isDirect && !isDelete) {\n        auto directEntry = this->rtTableMap.find(entry.next);\n        if (directEntry == this->rtTableMap.end()) {\n            return 0;\n        }\n        int32_t rttNow = directEntry->second.rtt + entry.rtt;\n        if (oldEntry == this->rtTableMap.end() || oldEntry->second.next == entry.next || oldEntry->second.rtt > rttNow) {\n            entry.rtt = rttNow;\n            this->rtTableMap[entry.dst] = entry;\n            sendRtMessage(entry.dst, entry.rtt);\n            showRtChange(entry);\n            return 0;\n        }\n        return 0;\n    }\n    return 0;\n}\n\n} // namespace candy\n"
  },
  {
    "path": "candy/src/peer/manager.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_PEER_MANAGER_H\n#define CANDY_PEER_MANAGER_H\n\n#include \"core/message.h\"\n#include \"core/net.h\"\n#include \"peer/message.h\"\n#include \"peer/peer.h\"\n#include <Poco/Net/DatagramSocket.h>\n#include <Poco/Net/ServerSocket.h>\n#include <Poco/Net/StreamSocket.h>\n#include <Poco/URI.h>\n#include <shared_mutex>\n#include <string>\n#include <thread>\n#include <unordered_map>\n#include <vector>\n\nnamespace candy {\n\nusing Poco::Net::SocketAddress;\n\nclass Client;\n\nstruct Stun {\n    std::string uri;\n    SocketAddress address;\n    bool needed = false;\n    IP4 ip;\n    uint16_t port;\n\n    bool enabled() {\n        return !this->address.host().isWildcard();\n    }\n\n    int update() {\n        try {\n            if (!this->uri.empty()) {\n                Poco::URI uri(this->uri);\n                if (!uri.getPort()) {\n                    uri.setPort(3478);\n                }\n                this->address = Poco::Net::SocketAddress(uri.getHost(), uri.getPort());\n            }\n            return 0;\n        } catch (std::exception &e) {\n            spdlog::warn(\"set stun server address failed: {}\", e.what());\n            return -1;\n        }\n    }\n};\n\nstruct PeerRouteEntry {\n    IP4 dst;\n    IP4 next;\n    int32_t rtt;\n\n    PeerRouteEntry(IP4 dst = IP4(), IP4 next = IP4(), int32_t rtt = RTT_LIMIT) : dst(dst), next(next), rtt(rtt) {}\n};\n\nclass PeerManager {\npublic:\n    int setPassword(const std::string &password);\n    int setStun(const std::string &stun);\n    int setDiscoveryInterval(int interval);\n    int setRouteCost(int cost);\n    int setPort(int port);\n    int setLocalhost(const std::string &ip);\n\n    int run(Client *client);\n    int wait();\n\n    std::string getPassword();\n\nprivate:\n    std::string password;\n    IP4 localhost;\n\npublic:\n    int sendPubInfo(CoreMsg::PubInfo info);\n    IP4 getTunIp();\n    int updateRtTable(PeerRouteEntry entry);\n\nprivate:\n    // 处理来自消息队列的数据\n    int handlePeerQueue();\n    int handlePacket(Msg msg);\n    int handleTunAddr(Msg msg);\n    int handleTryP2P(Msg msg);\n    int handlePubInfo(Msg msg);\n\n    std::thread msgThread;\n\n    int sendPacket(IP4 dst, const Msg &msg);\n    int sendPacketDirect(IP4 dst, const Msg &msg);\n    int sendPacketRelay(IP4 dst, const Msg &msg);\n\n    Address tunAddr;\n\n    int startTickThread();\n    int tick();\n    std::thread tickThread;\n    uint64_t tickTick = randomUint32();\n\n    std::shared_mutex ipPeerMutex;\n    std::unordered_map<IP4, Peer> ipPeerMap;\n\n    void showRtChange(const PeerRouteEntry &entry);\n    int sendRtMessage(IP4 dst, int32_t rtt);\n\n    std::shared_mutex rtTableMutex;\n    std::unordered_map<IP4, PeerRouteEntry> rtTableMap;\n\npublic:\n    Stun stun;\n    std::atomic<bool> localP2PDisabled;\n\nprivate:\n    int initSocket();\n    void sendStunRequest();\n    void handleStunResponse(std::string buffer);\n    void handleMessage(std::string buffer, const SocketAddress &address);\n    void handleHeartbeatMessage(std::string buffer, const SocketAddress &address);\n    void handleForwardMessage(std::string buffer, const SocketAddress &address);\n    void handleDelayMessage(std::string buffer, const SocketAddress &address);\n    void handleRouteMessage(std::string buffer, const SocketAddress &address);\n    int poll();\n\n    std::optional<std::string> decrypt(const std::string &ciphertext);\n    std::shared_ptr<EVP_CIPHER_CTX> decryptCtx;\n    std::mutex decryptCtxMutex;\n    std::string key;\n\n    // 默认监听端口,如果不配置,随机监听\n    uint16_t listenPort = 0;\n\npublic:\n    std::mutex socketMutex;\n    Poco::Net::DatagramSocket socket;\n    int sendTo(const void *buffer, int length, const SocketAddress &address);\n    int getDiscoveryInterval() const;\n    bool clientRelayEnabled() const;\n\nprivate:\n    std::thread pollThread;\n\n    int discoveryInterval = 0;\n    int routeCost = 0;\n\n    Client &getClient();\n    Client *client;\n};\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/peer/message.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"peer/message.h\"\n#include <string>\n\nnamespace candy {\n\nnamespace PeerMsg {\nstd::string Forward::create(const std::string &packet) {\n    std::string data;\n    data.push_back(PeerMsgKind::FORWARD);\n    data += packet;\n    return data;\n}\n} // namespace PeerMsg\n\n} // namespace candy\n"
  },
  {
    "path": "candy/src/peer/message.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_PEER_MESSAGE_H\n#define CANDY_PEER_MESSAGE_H\n\n#include \"core/net.h\"\n#include \"utils/random.h\"\n#include <cstdint>\n\nnamespace candy {\n\nnamespace PeerMsgKind {\n\nconstexpr uint8_t HEARTBEAT = 0;\nconstexpr uint8_t FORWARD = 1;\nconstexpr uint8_t DELAY = 2;\nconstexpr uint8_t ROUTE = 4;\n\n} // namespace PeerMsgKind\n\nstruct __attribute__((packed)) StunRequest {\n    uint8_t type[2] = {0x00, 0x01};\n    uint8_t length[2] = {0x00, 0x08};\n    uint8_t cookie[4] = {0x21, 0x12, 0xa4, 0x42};\n    uint32_t id[3] = {0x00};\n    struct __attribute__((packed)) {\n        uint8_t type[2] = {0x00, 0x03};\n        uint8_t length[2] = {0x00, 0x04};\n        uint8_t notset[4] = {0x00};\n    } attr;\n\n    StunRequest() {\n        id[0] = randomUint32();\n        id[1] = randomUint32();\n        id[2] = randomUint32();\n    }\n};\n\nstruct __attribute__((packed)) StunResponse {\n    uint16_t type;\n    uint16_t length;\n    uint32_t cookie;\n    uint8_t id[12];\n    uint8_t attr[0];\n};\n\nnamespace PeerMsg {\n\nstruct __attribute__((packed)) Heartbeat {\n    uint8_t kind;\n    IP4 tunip;\n    IP4 ip;\n    uint16_t port;\n    uint8_t ack;\n};\n\nstruct __attribute__((packed)) Forward {\n    uint8_t type;\n    IP4Header iph;\n\n    static std::string create(const std::string &packet);\n};\n\nstruct __attribute__((packed)) Delay {\n    uint8_t type;\n    IP4 src;\n    IP4 dst;\n    int64_t timestamp;\n};\n\nstruct __attribute__((packed)) Route {\n    uint8_t type;\n    IP4 dst;\n    IP4 next;\n    int32_t rtt;\n};\n\n} // namespace PeerMsg\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/peer/peer.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"peer/peer.h\"\n#include \"core/client.h\"\n#include \"core/message.h\"\n#include \"peer/manager.h\"\n#include \"peer/peer.h\"\n#include \"utils/time.h\"\n#include <Poco/Net/IPAddress.h>\n#include <Poco/Net/SocketAddress.h>\n#include <algorithm>\n#include <openssl/evp.h>\n#include <openssl/rand.h>\n#include <openssl/sha.h>\n#include <spdlog/spdlog.h>\n\nnamespace {\n\nusing namespace Poco::Net;\n\nbool isLocalNetwork(const SocketAddress &addr) {\n    IPAddress ip = addr.host();\n\n    if (ip.isV4()) {\n        return ip.isSiteLocal() || ip.isLinkLocal() || ip.isSiteLocalMC();\n    } else if (ip.isV6()) {\n        spdlog::error(\"unexpected ipv6 local address\");\n    }\n\n    return false;\n}\n\n} // namespace\n\nnamespace candy {\n\nPeer::Peer(const IP4 &addr, PeerManager *peerManager) : peerManager(peerManager), addr(addr) {\n    std::string data;\n    data.append(this->peerManager->getPassword());\n    auto leaddr = hton(uint32_t(this->addr));\n    data.append((char *)&leaddr, sizeof(leaddr));\n\n    this->key.resize(SHA256_DIGEST_LENGTH);\n    SHA256((unsigned char *)data.data(), data.size(), (unsigned char *)this->key.data());\n\n    this->encryptCtx = std::shared_ptr<EVP_CIPHER_CTX>(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free);\n}\n\nPeer::~Peer() {}\n\nvoid Peer::tryConnecct() {\n    if (this->state == PeerState::INIT) {\n        updateState(PeerState::PREPARING);\n    }\n}\n\nPeerManager &Peer::getManager() {\n    return *this->peerManager;\n}\n\nstd::optional<std::string> Peer::encrypt(const std::string &plaintext) {\n    int len = 0;\n    int ciphertextLen = 0;\n    unsigned char ciphertext[1500] = {0};\n    unsigned char iv[AES_256_GCM_IV_LEN] = {0};\n    unsigned char tag[AES_256_GCM_TAG_LEN] = {0};\n\n    if (!RAND_bytes(iv, AES_256_GCM_IV_LEN)) {\n        spdlog::debug(\"generate random iv failed\");\n        return std::nullopt;\n    }\n\n    std::lock_guard lock(this->encryptCtxMutex);\n    auto ctx = this->encryptCtx.get();\n\n    if (!EVP_CIPHER_CTX_reset(ctx)) {\n        spdlog::debug(\"encrypt reset cipher context failed\");\n        return std::nullopt;\n    }\n    if (!EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, (unsigned char *)key.data(), iv)) {\n        spdlog::debug(\"encrypt initialize cipher context failed\");\n        return std::nullopt;\n    }\n    if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, AES_256_GCM_IV_LEN, NULL)) {\n        spdlog::debug(\"set iv length failed\");\n        return std::nullopt;\n    }\n    if (!EVP_EncryptUpdate(ctx, ciphertext, &len, (unsigned char *)plaintext.data(), plaintext.size())) {\n        spdlog::debug(\"encrypt update failed\");\n        return std::nullopt;\n    }\n    ciphertextLen = len;\n    if (!EVP_EncryptFinal_ex(ctx, ciphertext + len, &len)) {\n        spdlog::debug(\"encrypt final failed\");\n        return std::nullopt;\n    }\n    ciphertextLen += len;\n    if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, AES_256_GCM_TAG_LEN, tag)) {\n        spdlog::debug(\"get tag failed\");\n        return std::nullopt;\n    }\n\n    std::string result;\n    result.append((char *)iv, AES_256_GCM_IV_LEN);\n    result.append((char *)tag, AES_256_GCM_TAG_LEN);\n    result.append((char *)ciphertext, ciphertextLen);\n    return result;\n}\n\nint Peer::sendEncrypted(const std::string &data) {\n    if (auto buffer = encrypt(data)) {\n        return send(*buffer);\n    }\n    return -1;\n}\n\nbool Peer::checkActivityWithin(std::chrono::system_clock::duration duration) {\n    return std::chrono::system_clock::now() - lastActiveTime < duration;\n}\n\nstd::optional<int32_t> Peer::isConnected() const {\n    if (this->state == PeerState::CONNECTED) {\n        return this->rtt;\n    }\n    return std::nullopt;\n}\n\nbool Peer::updateState(PeerState state) {\n    this->lastActiveTime = std::chrono::system_clock::now();\n\n    if (this->state == state) {\n        return false;\n    }\n\n    spdlog::debug(\"state: {} {} => {}\", this->addr.toString(), stateString(), stateString(state));\n\n    if (state == PeerState::INIT || state == PeerState::WAITING || state == PeerState::FAILED) {\n        resetState();\n    }\n\n    if (this->state == PeerState::WAITING && state == PeerState::INIT) {\n        this->retry = std::min(this->retry * 2, RETRY_MAX);\n    } else if (state == PeerState::INIT || state == PeerState::FAILED) {\n        this->retry = RETRY_MIN;\n    }\n\n    this->state = state;\n    return true;\n}\n\nstd::string Peer::stateString() const {\n    return this->stateString(this->state);\n}\n\nstd::string Peer::stateString(PeerState state) const {\n    switch (state) {\n    case PeerState::INIT:\n        return \"INIT\";\n    case PeerState::PREPARING:\n        return \"PREPARING\";\n    case PeerState::SYNCHRONIZING:\n        return \"SYNCHRONIZING\";\n    case PeerState::CONNECTING:\n        return \"CONNECTING\";\n    case PeerState::CONNECTED:\n        return \"CONNECTED\";\n    case PeerState::WAITING:\n        return \"WAITING\";\n    case PeerState::FAILED:\n        return \"FAILED\";\n    default:\n        return \"UNKNOWN\";\n    }\n}\n\nvoid Peer::handlePubInfo(IP4 ip, uint16_t port, bool local) {\n    try {\n        std::unique_lock lock(this->socketAddressMutex);\n        if (local) {\n            this->local = SocketAddress(ip.toString(), port);\n            return;\n        }\n\n        this->wide = SocketAddress(ip.toString(), port);\n    } catch (const Poco::Exception &e) {\n        spdlog::warn(\"peer handle pubinfo failed: ip={}, port={}, error={}\", ip.toString(), port, e.message());\n        return;\n    }\n\n    if (this->state == PeerState::CONNECTED) {\n        return;\n    }\n\n    if (this->state == PeerState::SYNCHRONIZING) {\n        updateState(PeerState::CONNECTING);\n        return;\n    }\n\n    if (this->state != PeerState::CONNECTING) {\n        updateState(PeerState::PREPARING);\n        CoreMsg::PubInfo info = {.dst = this->addr, .local = true};\n        getManager().sendPubInfo(info);\n        return;\n    }\n}\n\nvoid Peer::handleStunResponse() {\n    if (this->state != PeerState::PREPARING) {\n        return;\n    }\n    if (this->wide == std::nullopt) {\n        updateState(PeerState::SYNCHRONIZING);\n    } else {\n        updateState(PeerState::CONNECTING);\n    }\n    CoreMsg::PubInfo info = {.dst = this->addr};\n    getManager().sendPubInfo(info);\n}\n\nvoid Peer::tick() {\n    switch (this->state) {\n    case PeerState::INIT:\n        break;\n    case PeerState::PREPARING:\n        if (getManager().stun.enabled() && checkActivityWithin(std::chrono::seconds(10))) {\n            getManager().stun.needed = true;\n        } else {\n            updateState(PeerState::FAILED);\n        }\n        break;\n    case PeerState::SYNCHRONIZING:\n        if (checkActivityWithin(std::chrono::seconds(10))) {\n            sendHeartbeatMessage();\n        } else {\n            updateState(PeerState::FAILED);\n        }\n        break;\n    case PeerState::CONNECTING:\n        if (checkActivityWithin(std::chrono::seconds(10))) {\n            sendHeartbeatMessage();\n        } else {\n            updateState(PeerState::WAITING);\n        }\n        break;\n    case PeerState::CONNECTED:\n        if (checkActivityWithin(std::chrono::seconds(3))) {\n            sendHeartbeatMessage();\n            if (getManager().clientRelayEnabled() && tickCount % 60 == 0) {\n                sendDelayMessage();\n            }\n        } else {\n            updateState(PeerState::INIT);\n            if (getManager().clientRelayEnabled()) {\n                getManager().updateRtTable(PeerRouteEntry(addr, addr, RTT_LIMIT));\n            }\n        }\n        break;\n    case PeerState::WAITING:\n        if (!checkActivityWithin(std::chrono::seconds(this->retry))) {\n            updateState(PeerState::INIT);\n        }\n        break;\n    case PeerState::FAILED:\n        break;\n    default:\n        break;\n    }\n    ++tickCount;\n}\n\nvoid Peer::handleHeartbeatMessage(const SocketAddress &address, uint8_t heartbeatAck) {\n    if (this->state == PeerState::INIT || this->state == PeerState::WAITING || this->state == PeerState::FAILED) {\n        spdlog::debug(\"heartbeat peer state invalid: {} {}\", this->addr.toString(), stateString());\n        return;\n    }\n\n    if (!isLocalNetwork(address)) {\n        this->wide = address;\n    } else if (!getManager().localP2PDisabled) {\n        this->local = address;\n    } else {\n        return;\n    }\n\n    {\n        std::unique_lock lock(this->socketAddressMutex);\n        if (!this->real || isLocalNetwork(address) || !isLocalNetwork(*this->real)) {\n            this->real = address;\n        }\n    }\n\n    if (!this->ack) {\n        this->ack = 1;\n    }\n\n    if (heartbeatAck && updateState(PeerState::CONNECTED)) {\n        sendDelayMessage();\n    }\n}\n\nint Peer::send(const std::string &buffer) {\n    try {\n        std::shared_lock lock(this->socketAddressMutex);\n        if (this->real) {\n            if (buffer.size() == getManager().sendTo(buffer.data(), buffer.size(), *this->real)) {\n                return 0;\n            }\n        }\n    } catch (std::exception &e) {\n        spdlog::debug(\"peer send failed: {}\", e.what());\n    }\n    return -1;\n}\n\nvoid Peer::sendHeartbeatMessage() {\n    PeerMsg::Heartbeat heartbeat;\n    heartbeat.kind = PeerMsgKind::HEARTBEAT;\n    heartbeat.tunip = getManager().getTunIp();\n    heartbeat.ack = this->ack;\n\n    if (auto buffer = encrypt(std::string((char *)&heartbeat, sizeof(heartbeat)))) {\n        using Poco::Net::SocketAddress;\n        std::shared_lock lock(this->socketAddressMutex);\n        if (this->real && (this->state == PeerState::CONNECTED)) {\n            heartbeat.ip = this->real->host().toString();\n            heartbeat.port = this->real->port();\n            getManager().sendTo(buffer->data(), buffer->size(), *this->real);\n        }\n\n        if (this->wide && (this->state == PeerState::CONNECTING)) {\n            heartbeat.ip = this->wide->host().toString();\n            heartbeat.port = this->wide->port();\n            getManager().sendTo(buffer->data(), buffer->size(), *this->wide);\n        }\n\n        if (this->local && (this->state == PeerState::PREPARING || this->state == PeerState::SYNCHRONIZING ||\n                            this->state == PeerState::CONNECTING)) {\n            heartbeat.ip = this->local->host().toString();\n            heartbeat.port = this->local->port();\n            getManager().sendTo(buffer->data(), buffer->size(), *this->local);\n        }\n    }\n}\n\nvoid Peer::sendDelayMessage() {\n    PeerMsg::Delay delay;\n    delay.type = PeerMsgKind::DELAY;\n    delay.src = getManager().getTunIp();\n    delay.dst = this->addr;\n    delay.timestamp = hton(bootTime());\n    sendEncrypted(std::string((char *)&delay, sizeof(delay)));\n}\n\nvoid Peer::resetState() {\n    std::unique_lock lock(this->socketAddressMutex);\n    this->wide = std::nullopt;\n    this->local = std::nullopt;\n    this->real = std::nullopt;\n    this->ack = 0;\n    this->rtt = RTT_LIMIT;\n}\n\n} // namespace candy\n"
  },
  {
    "path": "candy/src/peer/peer.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_PEER_PEER_H\n#define CANDY_PEER_PEER_H\n\n#include \"core/net.h\"\n#include \"utils/random.h\"\n#include <Poco/Net/SocketAddress.h>\n#include <chrono>\n#include <cstdint>\n#include <map>\n#include <memory>\n#include <openssl/evp.h>\n#include <optional>\n#include <shared_mutex>\n#include <string>\n\nnamespace candy {\n\nclass PeerManager;\n\nconstexpr int32_t RTT_LIMIT = INT32_MAX;\nconstexpr int32_t RETRY_MIN = 30;\nconstexpr int32_t RETRY_MAX = 3600;\n\nusing Poco::Net::SocketAddress;\n\nenum class PeerState {\n    INIT,\n    PREPARING,\n    SYNCHRONIZING,\n    CONNECTING,\n    CONNECTED,\n    WAITING,\n    FAILED,\n};\n\nclass Peer {\npublic:\n    Peer(const IP4 &addr, PeerManager *peerManager);\n    ~Peer();\n\n    void tick();\n    void tryConnecct();\n    void handleStunResponse();\n    void handlePubInfo(IP4 ip, uint16_t port, bool local = false);\n\n    void handleHeartbeatMessage(const SocketAddress &address, uint8_t heartbeatAck);\n    int sendEncrypted(const std::string &buffer);\n    std::optional<int32_t> isConnected() const;\n\n    int32_t rtt = RTT_LIMIT;\n    uint32_t tickCount = randomUint32();\n\nprivate:\n    PeerManager &getManager();\n    PeerManager *peerManager;\n\n    std::optional<std::string> encrypt(const std::string &plaintext);\n    std::shared_ptr<EVP_CIPHER_CTX> encryptCtx;\n    std::mutex encryptCtxMutex;\n    std::string key;\n\n    std::string stateString() const;\n    std::string stateString(PeerState state) const;\n    bool updateState(PeerState state);\n    void resetState();\n    bool checkActivityWithin(std::chrono::system_clock::duration duration);\n    PeerState state = PeerState::INIT;\n    uint8_t ack = 0;\n    int32_t retry = RETRY_MIN;\n    std::chrono::system_clock::time_point lastActiveTime;\n\n    int send(const std::string &buffer);\n    void sendHeartbeatMessage();\n    void sendDelayMessage();\n\n    std::optional<SocketAddress> wide, local, real;\n    std::shared_mutex socketAddressMutex;\n\n    IP4 addr;\n};\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/tun/linux.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include <Poco/Platform.h>\n#if POCO_OS == POCO_OS_LINUX\n\n#include \"core/net.h\"\n#include \"tun/tun.h\"\n#include <arpa/inet.h>\n#include <fcntl.h>\n#include <linux/if_tun.h>\n#include <memory>\n#include <net/if.h>\n#include <net/route.h>\n#include <spdlog/spdlog.h>\n#include <string>\n#include <sys/ioctl.h>\n#include <unistd.h>\n\nnamespace candy {\n\nclass LinuxTun {\npublic:\n    int setName(const std::string &name) {\n        this->name = name.empty() ? \"candy\" : \"candy-\" + name;\n        return 0;\n    }\n\n    int setIP(IP4 ip) {\n        this->ip = ip;\n        return 0;\n    }\n\n    IP4 getIP() {\n        return this->ip;\n    }\n\n    int setMask(IP4 mask) {\n        this->mask = mask;\n        return 0;\n    }\n\n    int setMTU(int mtu) {\n        this->mtu = mtu;\n        return 0;\n    }\n\n    // 配置网卡,设置路由\n    int up() {\n        this->tunFd = open(\"/dev/net/tun\", O_RDWR);\n        if (this->tunFd < 0) {\n            spdlog::critical(\"open /dev/net/tun failed: {}\", strerror(errno));\n            close(this->tunFd);\n            return -1;\n        }\n        int flags = fcntl(this->tunFd, F_GETFL, 0);\n        if (flags < 0) {\n            spdlog::error(\"get tun flags failed: {}\", strerror(errno));\n            close(this->tunFd);\n            return -1;\n        }\n        flags |= O_NONBLOCK;\n        if (fcntl(this->tunFd, F_SETFL, flags) < 0) {\n            spdlog::error(\"set non-blocking tun failed: {}\", strerror(errno));\n            close(this->tunFd);\n            return -1;\n        }\n\n        // 设置设备名\n        struct ifreq ifr;\n        memset(&ifr, 0, sizeof(ifr));\n        strncpy(ifr.ifr_name, this->name.c_str(), IFNAMSIZ);\n        ifr.ifr_flags = IFF_TUN | IFF_NO_PI;\n        if (ioctl(this->tunFd, TUNSETIFF, &ifr) == -1) {\n            spdlog::critical(\"set tun interface failed: {}\", strerror(errno));\n            close(this->tunFd);\n            return -1;\n        }\n\n        // 创建 socket, 并通过这个 socket 更新网卡的其他配置\n        struct sockaddr_in *addr;\n        addr = (struct sockaddr_in *)&ifr.ifr_addr;\n        addr->sin_family = AF_INET;\n        int sockfd = socket(addr->sin_family, SOCK_DGRAM, 0);\n        if (sockfd == -1) {\n            spdlog::critical(\"create socket failed\");\n            close(this->tunFd);\n            return -1;\n        }\n\n        // 设置地址\n        addr->sin_addr.s_addr = this->ip;\n        if (ioctl(sockfd, SIOCSIFADDR, (caddr_t)&ifr) == -1) {\n            spdlog::critical(\"set ip address failed: ip {}\", this->ip.toString());\n            close(sockfd);\n            close(this->tunFd);\n            return -1;\n        }\n\n        // 设置掩码\n        addr->sin_addr.s_addr = this->mask;\n        if (ioctl(sockfd, SIOCSIFNETMASK, (caddr_t)&ifr) == -1) {\n            spdlog::critical(\"set mask failed: mask {}\", this->mask.toString());\n            close(sockfd);\n            close(this->tunFd);\n            return -1;\n        }\n\n        // 设置 MTU\n        ifr.ifr_mtu = this->mtu;\n        if (ioctl(sockfd, SIOCSIFMTU, (caddr_t)&ifr) == -1) {\n            spdlog::critical(\"set mtu failed: mtu {}\", this->mtu);\n            close(sockfd);\n            close(this->tunFd);\n            return -1;\n        }\n\n        // 设置 flags\n        if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) == -1) {\n            spdlog::critical(\"get interface flags failed\");\n            close(sockfd);\n            close(this->tunFd);\n            return -1;\n        }\n        ifr.ifr_flags |= IFF_UP | IFF_RUNNING;\n        if (ioctl(sockfd, SIOCSIFFLAGS, &ifr) == -1) {\n            spdlog::critical(\"set interface flags failed\");\n            close(sockfd);\n            close(this->tunFd);\n            return -1;\n        }\n\n        close(sockfd);\n\n        return 0;\n    }\n\n    int down() {\n        close(this->tunFd);\n        return 0;\n    }\n\n    int read(std::string &buffer) {\n        buffer.resize(this->mtu);\n        int n = ::read(this->tunFd, buffer.data(), buffer.size());\n        if (n >= 0) {\n            buffer.resize(n);\n            return n;\n        }\n\n        if (errno == EAGAIN || errno == EWOULDBLOCK) {\n            struct timeval timeout = {.tv_sec = 1};\n            fd_set set;\n\n            FD_ZERO(&set);\n            FD_SET(this->tunFd, &set);\n\n            select(this->tunFd + 1, &set, NULL, NULL, &timeout);\n            return 0;\n        }\n        spdlog::warn(\"tun read failed: {}\", strerror(errno));\n        return -1;\n    }\n\n    int write(const std::string &buffer) {\n        return ::write(this->tunFd, buffer.c_str(), buffer.size());\n    }\n\n    int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {\n        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);\n        if (sockfd == -1) {\n            spdlog::error(\"set route failed: create socket failed\");\n            return -1;\n        }\n\n        struct sockaddr_in *addr;\n        struct rtentry route;\n        memset(&route, 0, sizeof(route));\n\n        addr = (struct sockaddr_in *)&route.rt_dst;\n        addr->sin_family = AF_INET;\n        addr->sin_addr.s_addr = dst;\n\n        addr = (struct sockaddr_in *)&route.rt_genmask;\n        addr->sin_family = AF_INET;\n        addr->sin_addr.s_addr = mask;\n\n        addr = (struct sockaddr_in *)&route.rt_gateway;\n        addr->sin_family = AF_INET;\n        addr->sin_addr.s_addr = nexthop;\n\n        route.rt_flags = RTF_UP | RTF_GATEWAY;\n        if (ioctl(sockfd, SIOCADDRT, &route) == -1) {\n            spdlog::error(\"set route failed: ioctl failed\");\n            close(sockfd);\n            return -1;\n        }\n\n        close(sockfd);\n        return 0;\n    }\n\nprivate:\n    std::string name;\n    IP4 ip;\n    IP4 mask;\n    int mtu;\n    int timeout;\n    int tunFd;\n};\n\n} // namespace candy\n\nnamespace candy {\n\nTun::Tun() {\n    this->impl = std::make_shared<LinuxTun>();\n}\n\nTun::~Tun() {\n    this->impl.reset();\n}\n\nint Tun::setName(const std::string &name) {\n    std::shared_ptr<LinuxTun> tun;\n\n    tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);\n    tun->setName(name);\n    return 0;\n}\n\nint Tun::setAddress(const std::string &cidr) {\n    std::shared_ptr<LinuxTun> tun;\n    Address address;\n\n    if (address.fromCidr(cidr)) {\n        return -1;\n    }\n    spdlog::info(\"client address: {}\", address.toCidr());\n    tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);\n    if (tun->setIP(address.Host())) {\n        return -1;\n    }\n    if (tun->setMask(address.Mask())) {\n        return -1;\n    }\n    this->tunAddress = cidr;\n    return 0;\n}\n\nIP4 Tun::getIP() {\n    std::shared_ptr<LinuxTun> tun;\n    tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);\n    return tun->getIP();\n}\n\nint Tun::setMTU(int mtu) {\n    std::shared_ptr<LinuxTun> tun;\n    tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);\n    if (tun->setMTU(mtu)) {\n        return -1;\n    }\n    return 0;\n}\n\nint Tun::up() {\n    std::shared_ptr<LinuxTun> tun;\n    tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);\n    return tun->up();\n}\n\nint Tun::down() {\n    std::shared_ptr<LinuxTun> tun;\n    tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);\n    return tun->down();\n}\n\nint Tun::read(std::string &buffer) {\n    std::shared_ptr<LinuxTun> tun;\n    tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);\n    return tun->read(buffer);\n}\n\nint Tun::write(const std::string &buffer) {\n    std::shared_ptr<LinuxTun> tun;\n    tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);\n    return tun->write(buffer);\n}\n\nint Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {\n    std::shared_ptr<LinuxTun> tun;\n    tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);\n    return tun->setSysRtTable(dst, mask, nexthop);\n}\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/tun/macos.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include <Poco/Platform.h>\n#if POCO_OS == POCO_OS_MAC_OS_X\n\n#include \"core/net.h\"\n#include \"tun/tun.h\"\n#include <errno.h>\n#include <fcntl.h>\n#include <memory>\n// clang-format off\n#include <sys/socket.h>\n#include <net/if.h>\n// clang-format on\n#include <net/if_utun.h>\n#include <net/route.h>\n#include <netinet/in.h>\n#include <spdlog/fmt/bin_to_hex.h>\n#include <spdlog/spdlog.h>\n#include <string>\n#include <sys/ioctl.h>\n#include <sys/kern_control.h>\n#include <sys/select.h>\n#include <sys/sys_domain.h>\n#include <sys/uio.h>\n#include <unistd.h>\n\nnamespace candy {\n\nclass MacTun {\npublic:\n    int setName(const std::string &name) {\n        this->name = name.empty() ? \"candy\" : \"candy-\" + name;\n        return 0;\n    }\n\n    int setIP(IP4 ip) {\n        this->ip = ip;\n        return 0;\n    }\n\n    IP4 getIP() {\n        return this->ip;\n    }\n\n    int setMask(IP4 mask) {\n        this->mask = mask;\n        return 0;\n    }\n\n    int setMTU(int mtu) {\n        this->mtu = mtu;\n        return 0;\n    }\n\n    int up() {\n        // 创建设备,操作系统不允许自定义设备名,只能由内核分配\n        this->tunFd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL);\n        if (this->tunFd < 0) {\n            spdlog::critical(\"create socket failed: {}\", strerror(errno));\n            return -1;\n        }\n        int flags = fcntl(this->tunFd, F_GETFL, 0);\n        if (flags < 0) {\n            spdlog::error(\"get tun flags failed: {}\", strerror(errno));\n            close(this->tunFd);\n            return -1;\n        }\n        flags |= O_NONBLOCK;\n        if (fcntl(this->tunFd, F_SETFL, flags) < 0) {\n            spdlog::error(\"set non-blocking tun failed: {}\", strerror(errno));\n            close(this->tunFd);\n            return -1;\n        }\n\n        struct ctl_info info;\n        memset(&info, 0, sizeof(info));\n        strncpy(info.ctl_name, UTUN_CONTROL_NAME, MAX_KCTL_NAME);\n        if (ioctl(this->tunFd, CTLIOCGINFO, &info) == -1) {\n            spdlog::critical(\"get control id failed: {}\", strerror(errno));\n            close(this->tunFd);\n            return -1;\n        }\n\n        struct sockaddr_ctl ctl;\n        memset(&ctl, 0, sizeof(ctl));\n        ctl.sc_len = sizeof(ctl);\n        ctl.sc_family = AF_SYSTEM;\n        ctl.ss_sysaddr = AF_SYS_CONTROL;\n        ctl.sc_id = info.ctl_id;\n        ctl.sc_unit = 0;\n        if (connect(this->tunFd, (struct sockaddr *)&ctl, sizeof(ctl)) == -1) {\n            spdlog::critical(\"connect to control failed: {}\", strerror(errno));\n            close(this->tunFd);\n            return -1;\n        }\n\n        socklen_t ifname_len = sizeof(ifname);\n        if (getsockopt(this->tunFd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifname, &ifname_len) == -1) {\n            spdlog::critical(\"get interface name failed: {}\", strerror(errno));\n            close(this->tunFd);\n            return -1;\n        }\n\n        spdlog::debug(\"created utun interface: {}\", ifname);\n\n        struct ifreq ifr;\n        memset(&ifr, 0, sizeof(ifr));\n        strncpy(ifr.ifr_name, ifname, IFNAMSIZ);\n\n        // 创建 socket, 并通过这个 socket 更新网卡的其他配置\n        struct sockaddr_in *addr;\n        addr = (struct sockaddr_in *)&ifr.ifr_addr;\n        addr->sin_family = AF_INET;\n        int sockfd = socket(addr->sin_family, SOCK_DGRAM, 0);\n        if (sockfd == -1) {\n            spdlog::critical(\"create socket failed\");\n            close(this->tunFd);\n            return -1;\n        }\n\n        // 设置地址和掩码\n        struct ifaliasreq areq;\n        memset(&areq, 0, sizeof(areq));\n        strncpy(areq.ifra_name, ifname, IFNAMSIZ);\n        ((struct sockaddr_in *)&areq.ifra_addr)->sin_family = AF_INET;\n        ((struct sockaddr_in *)&areq.ifra_addr)->sin_len = sizeof(areq.ifra_addr);\n        ((struct sockaddr_in *)&areq.ifra_addr)->sin_addr.s_addr = this->ip;\n\n        ((struct sockaddr_in *)&areq.ifra_mask)->sin_family = AF_INET;\n        ((struct sockaddr_in *)&areq.ifra_mask)->sin_len = sizeof(areq.ifra_mask);\n        ((struct sockaddr_in *)&areq.ifra_mask)->sin_addr.s_addr = this->mask;\n\n        ((struct sockaddr_in *)&areq.ifra_broadaddr)->sin_family = AF_INET;\n        ((struct sockaddr_in *)&areq.ifra_broadaddr)->sin_len = sizeof(areq.ifra_broadaddr);\n        ((struct sockaddr_in *)&areq.ifra_broadaddr)->sin_addr.s_addr = (this->ip & this->mask);\n\n        if (ioctl(sockfd, SIOCAIFADDR, (void *)&areq) == -1) {\n            spdlog::critical(\"set ip mask failed: {}: ip {} mask {}\", strerror(errno), this->ip.toString(),\n                             this->mask.toString());\n            close(sockfd);\n            close(this->tunFd);\n            return -1;\n        }\n\n        // 设置 MTU\n        ifr.ifr_mtu = this->mtu;\n        if (ioctl(sockfd, SIOCSIFMTU, &ifr) == -1) {\n            spdlog::critical(\"set mtu failed: mtu {}\", this->mtu);\n            close(sockfd);\n            close(this->tunFd);\n            return -1;\n        }\n\n        // 设置 flags\n        if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) == -1) {\n            spdlog::critical(\"get interface flags failed\");\n            close(sockfd);\n            close(this->tunFd);\n            return -1;\n        }\n        ifr.ifr_flags |= IFF_UP | IFF_RUNNING;\n        if (ioctl(sockfd, SIOCSIFFLAGS, &ifr) == -1) {\n            spdlog::critical(\"set interface flags failed\");\n            close(sockfd);\n            close(this->tunFd);\n            return -1;\n        }\n        close(sockfd);\n\n        // 设置路由\n        if (setSysRtTable(this->ip & this->mask, this->mask, this->ip)) {\n            close(this->tunFd);\n            return -1;\n        }\n        return 0;\n    }\n\n    int down() {\n        close(this->tunFd);\n        return 0;\n    }\n\n    int read(std::string &buffer) {\n        buffer.resize(this->mtu);\n        struct iovec iov[2];\n        iov[0].iov_base = &this->packetinfo;\n        iov[0].iov_len = sizeof(this->packetinfo);\n        iov[1].iov_base = buffer.data();\n        iov[1].iov_len = buffer.size();\n\n        int n = ::readv(this->tunFd, iov, sizeof(iov) / sizeof(iov[0]));\n        if (n >= 0) {\n            buffer.resize(n - sizeof(this->packetinfo));\n            return n;\n        }\n\n        if (errno == EAGAIN || errno == EWOULDBLOCK) {\n            struct timeval timeout = {.tv_sec = 1};\n            fd_set set;\n\n            FD_ZERO(&set);\n            FD_SET(this->tunFd, &set);\n\n            select(this->tunFd + 1, &set, NULL, NULL, &timeout);\n            return 0;\n        }\n\n        spdlog::warn(\"tun read failed: error {}\", n);\n        return -1;\n    }\n\n    int write(const std::string &buffer) {\n        struct iovec iov[2];\n        iov[0].iov_base = &this->packetinfo;\n        iov[0].iov_len = sizeof(this->packetinfo);\n        iov[1].iov_base = (void *)buffer.data();\n        iov[1].iov_len = buffer.size();\n        return ::writev(this->tunFd, iov, sizeof(iov) / sizeof(iov[0])) - sizeof(sizeof(this->packetinfo));\n    }\n\n    int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {\n        struct {\n            struct rt_msghdr msghdr;\n            struct sockaddr_in addr[3];\n        } msg;\n\n        memset(&msg, 0, sizeof(msg));\n        msg.msghdr.rtm_msglen = sizeof(msg);\n        msg.msghdr.rtm_version = RTM_VERSION;\n        msg.msghdr.rtm_type = RTM_ADD;\n        msg.msghdr.rtm_addrs = RTA_DST | RTA_GATEWAY | RTA_NETMASK;\n        msg.msghdr.rtm_flags = RTF_UP | RTA_GATEWAY;\n        for (int idx = 0; idx < (int)(sizeof(msg.addr) / sizeof(msg.addr[0])); ++idx) {\n            msg.addr[idx].sin_len = sizeof(msg.addr[0]);\n            msg.addr[idx].sin_family = AF_INET;\n        }\n        msg.addr[0].sin_addr.s_addr = dst;\n        msg.addr[1].sin_addr.s_addr = nexthop;\n        msg.addr[2].sin_addr.s_addr = mask;\n\n        int routefd = socket(AF_ROUTE, SOCK_RAW, 0);\n        if (routefd < 0) {\n            spdlog::error(\"create route fd failed: {}\", strerror(routefd));\n            return -1;\n        }\n        if (::write(routefd, &msg, sizeof(msg)) == -1) {\n            spdlog::error(\"add route failed: {}\", strerror(errno));\n            close(routefd);\n            return -1;\n        }\n        close(routefd);\n        return 0;\n    }\n\nprivate:\n    std::string name;\n    char ifname[IFNAMSIZ] = {0};\n    IP4 ip;\n    IP4 mask;\n    int mtu;\n    int timeout;\n    int tunFd;\n\n    uint8_t packetinfo[4] = {0x00, 0x00, 0x00, 0x02};\n};\n\n} // namespace candy\n\nnamespace candy {\n\nTun::Tun() {\n    this->impl = std::make_shared<MacTun>();\n}\n\nTun::~Tun() {\n    this->impl.reset();\n}\n\nint Tun::setName(const std::string &name) {\n    std::shared_ptr<MacTun> tun;\n\n    tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);\n    tun->setName(name);\n    return 0;\n}\n\nint Tun::setAddress(const std::string &cidr) {\n    std::shared_ptr<MacTun> tun;\n    Address address;\n\n    if (address.fromCidr(cidr)) {\n        return -1;\n    }\n    spdlog::info(\"client address: {}\", address.toCidr());\n    tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);\n    if (tun->setIP(address.Host())) {\n        return -1;\n    }\n    if (tun->setMask(address.Mask())) {\n        return -1;\n    }\n    return 0;\n}\n\nIP4 Tun::getIP() {\n    std::shared_ptr<MacTun> tun;\n    tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);\n    return tun->getIP();\n}\n\nint Tun::setMTU(int mtu) {\n    std::shared_ptr<MacTun> tun;\n    tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);\n    if (tun->setMTU(mtu)) {\n        return -1;\n    }\n    return 0;\n}\n\nint Tun::up() {\n    std::shared_ptr<MacTun> tun;\n    tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);\n    return tun->up();\n}\n\nint Tun::down() {\n    std::shared_ptr<MacTun> tun;\n    tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);\n    return tun->down();\n}\n\nint Tun::read(std::string &buffer) {\n    std::shared_ptr<MacTun> tun;\n    tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);\n    return tun->read(buffer);\n}\n\nint Tun::write(const std::string &buffer) {\n    std::shared_ptr<MacTun> tun;\n    tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);\n    return tun->write(buffer);\n}\n\nint Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {\n    std::shared_ptr<MacTun> tun;\n    tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);\n    return tun->setSysRtTable(dst, mask, nexthop);\n}\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/tun/tun.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"tun/tun.h\"\n#include \"core/client.h\"\n#include \"core/message.h\"\n#include \"core/net.h\"\n#include <mutex>\n#include <shared_mutex>\n#include <spdlog/fmt/bin_to_hex.h>\n\nnamespace candy {\n\nint Tun::run(Client *client) {\n    this->client = client;\n    this->msgThread = std::thread([&] {\n        spdlog::debug(\"start thread: tun msg\");\n        while (getClient().isRunning()) {\n            if (handleTunQueue()) {\n                break;\n            }\n        }\n        getClient().shutdown();\n        spdlog::debug(\"stop thread: tun msg\");\n    });\n    return 0;\n}\n\nint Tun::wait() {\n    if (this->tunThread.joinable()) {\n        this->tunThread.join();\n    }\n    if (this->msgThread.joinable()) {\n        this->msgThread.join();\n    }\n    {\n        std::unique_lock lock(this->sysRtMutex);\n        this->sysRtTable.clear();\n    }\n    return 0;\n}\n\nint Tun::handleTunDevice() {\n    std::string buffer;\n    int error = read(buffer);\n    if (error <= 0) {\n        return 0;\n    }\n    if (buffer.length() < sizeof(IP4Header)) {\n        return 0;\n    }\n    IP4Header *header = (IP4Header *)buffer.data();\n    if (!header->isIPv4()) {\n        return 0;\n    }\n\n    IP4 nextHop = [&]() {\n        std::shared_lock lock(this->sysRtMutex);\n        for (auto const &rt : sysRtTable) {\n            if ((header->daddr & rt.mask) == rt.dst) {\n                return rt.nexthop;\n            }\n        }\n        return IP4();\n    }();\n    if (!nextHop.empty()) {\n        buffer.insert(0, sizeof(IP4Header), 0);\n        header = (IP4Header *)buffer.data();\n        header->protocol = 0x04;\n        header->saddr = getIP();\n        header->daddr = nextHop;\n    }\n\n    if (header->daddr == getIP()) {\n        write(buffer);\n        return 0;\n    }\n\n    this->client->getPeerMsgQueue().write(Msg(MsgKind::PACKET, std::move(buffer)));\n    return 0;\n}\n\nint Tun::handleTunQueue() {\n    Msg msg = this->client->getTunMsgQueue().read();\n    switch (msg.kind) {\n    case MsgKind::TIMEOUT:\n        break;\n    case MsgKind::PACKET:\n        handlePacket(std::move(msg));\n        break;\n    case MsgKind::TUNADDR:\n        if (handleTunAddr(std::move(msg))) {\n            return -1;\n        }\n        break;\n    case MsgKind::SYSRT:\n        handleSysRt(std::move(msg));\n        break;\n    default:\n        spdlog::warn(\"unexcepted tun message type: {}\", static_cast<int>(msg.kind));\n        break;\n    }\n    return 0;\n}\n\nint Tun::handlePacket(Msg msg) {\n    if (msg.data.size() < sizeof(IP4Header)) {\n        spdlog::warn(\"invalid IPv4 packet: {:n}\", spdlog::to_hex(msg.data));\n        return 0;\n    }\n    IP4Header *header = (IP4Header *)msg.data.data();\n    if (header->isIPIP()) {\n        msg.data.erase(0, sizeof(IP4Header));\n        header = (IP4Header *)msg.data.data();\n    }\n    write(msg.data);\n    return 0;\n}\n\nint Tun::handleTunAddr(Msg msg) {\n    if (setAddress(msg.data)) {\n        return -1;\n    }\n\n    if (up()) {\n        spdlog::critical(\"tun up failed\");\n        return -1;\n    }\n\n    this->tunThread = std::thread([&] {\n        spdlog::debug(\"start thread: tun\");\n        while (getClient().isRunning()) {\n            if (handleTunDevice()) {\n                break;\n            }\n        }\n        getClient().shutdown();\n        spdlog::debug(\"stop thread: tun\");\n\n        if (down()) {\n            spdlog::critical(\"tun down failed\");\n            return;\n        }\n    });\n\n    return 0;\n}\n\nint Tun::handleSysRt(Msg msg) {\n    SysRouteEntry *rt = (SysRouteEntry *)msg.data.data();\n    if (rt->nexthop != getIP()) {\n        spdlog::info(\"route: {}/{} via {}\", rt->dst.toString(), rt->mask.toPrefix(), rt->nexthop.toString());\n        if (setSysRtTable(*rt)) {\n            return -1;\n        }\n    }\n    return 0;\n}\n\nint Tun::setSysRtTable(const SysRouteEntry &entry) {\n    std::unique_lock lock(this->sysRtMutex);\n    this->sysRtTable.push_back(entry);\n    return setSysRtTable(entry.dst, entry.mask, entry.nexthop);\n}\n\nClient &Tun::getClient() {\n    return *this->client;\n}\n\n} // namespace candy\n"
  },
  {
    "path": "candy/src/tun/tun.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_TUN_TUN_H\n#define CANDY_TUN_TUN_H\n\n#include \"core/message.h\"\n#include \"core/net.h\"\n#include <any>\n#include <list>\n#include <shared_mutex>\n#include <string>\n#include <thread>\n\nnamespace candy {\n\nclass Client;\n\nclass Tun {\npublic:\n    Tun();\n    ~Tun();\n\n    int setName(const std::string &name);\n    int setMTU(int mtu);\n\n    int run(Client *client);\n    int wait();\n\n    IP4 getIP();\n\nprivate:\n    int setAddress(const std::string &cidr);\n\n    // 处理来自 TUN 设备的数据\n    int handleTunDevice();\n\n    // 处理来自消息队列的数据\n    int handleTunQueue();\n    int handlePacket(Msg msg);\n    int handleTunAddr(Msg msg);\n    int handleSysRt(Msg msg);\n\n    std::string tunAddress;\n    std::thread tunThread;\n    std::thread msgThread;\n\nprivate:\n    int up();\n    int down();\n\n    int read(std::string &buffer);\n    int write(const std::string &buffer);\n\n    int setSysRtTable(const SysRouteEntry &entry);\n    int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop);\n\n    std::shared_mutex sysRtMutex;\n    std::list<SysRouteEntry> sysRtTable;\n\nprivate:\n    std::any impl;\n\nprivate:\n    Client &getClient();\n    Client *client;\n};\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/tun/unknown.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include <Poco/Platform.h>\n\n#if POCO_OS != POCO_OS_LINUX && POCO_OS != POCO_OS_MAC_OS_X && POCO_OS != POCO_OS_WINDOWS_NT\n\n#include \"tun/tun.h\"\n\nnamespace candy {\n\nTun::Tun() {}\n\nTun::~Tun() {}\n\nint Tun::setName(const std::string &name) {\n    return -1;\n}\n\nint Tun::setAddress(const std::string &cidr) {\n    return -1;\n}\n\nint Tun::setMTU(int mtu) {\n    return -1;\n}\n\nint Tun::up() {\n    return -1;\n}\n\nint Tun::down() {\n    return -1;\n}\n\nint Tun::read(std::string &buffer) {\n    return -1;\n}\n\nint Tun::write(const std::string &buffer) {\n    return -1;\n}\n\nint Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {\n    return -1;\n}\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/tun/windows.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include <Poco/Platform.h>\n#if POCO_OS == POCO_OS_WINDOWS_NT\n\n#include \"core/net.h\"\n#include \"tun/tun.h\"\n#include \"utils/codecvt.h\"\n#include <memory>\n#include <openssl/sha.h>\n#include <spdlog/fmt/bin_to_hex.h>\n#include <spdlog/spdlog.h>\n#include <stack>\n#include <string>\n// clang-format off\n#include <winsock2.h>\n#include <windows.h>\n#include <ws2ipdef.h>\n#include <iphlpapi.h>\n#include <guiddef.h>\n#include <mstcpip.h>\n#include <winternl.h>\n#include <netioapi.h>\n#include <stdarg.h>\n#include <stdio.h>\n#include <stdlib.h>\n// clang-format on\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wunknown-pragmas\"\n#include <wintun.h>\n#pragma GCC diagnostic pop\n\nnamespace candy {\n\nWINTUN_CREATE_ADAPTER_FUNC *WintunCreateAdapter;\nWINTUN_CLOSE_ADAPTER_FUNC *WintunCloseAdapter;\nWINTUN_OPEN_ADAPTER_FUNC *WintunOpenAdapter;\nWINTUN_GET_ADAPTER_LUID_FUNC *WintunGetAdapterLUID;\nWINTUN_GET_RUNNING_DRIVER_VERSION_FUNC *WintunGetRunningDriverVersion;\nWINTUN_DELETE_DRIVER_FUNC *WintunDeleteDriver;\nWINTUN_SET_LOGGER_FUNC *WintunSetLogger;\nWINTUN_START_SESSION_FUNC *WintunStartSession;\nWINTUN_END_SESSION_FUNC *WintunEndSession;\nWINTUN_GET_READ_WAIT_EVENT_FUNC *WintunGetReadWaitEvent;\nWINTUN_RECEIVE_PACKET_FUNC *WintunReceivePacket;\nWINTUN_RELEASE_RECEIVE_PACKET_FUNC *WintunReleaseReceivePacket;\nWINTUN_ALLOCATE_SEND_PACKET_FUNC *WintunAllocateSendPacket;\nWINTUN_SEND_PACKET_FUNC *WintunSendPacket;\n\nclass Holder {\npublic:\n    static bool Ok() {\n        static Holder instance;\n        return instance.wintun;\n    }\n\nprivate:\n    Holder() {\n        this->wintun = LoadLibraryExW(L\"wintun.dll\", NULL, LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32);\n        if (!this->wintun) {\n            spdlog::critical(\"load wintun.dll failed\");\n            return;\n        }\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wstrict-aliasing\"\n#define X(Name) ((*(FARPROC *)&Name = GetProcAddress(this->wintun, #Name)) == NULL)\n        if (X(WintunCreateAdapter) || X(WintunCloseAdapter) || X(WintunOpenAdapter) || X(WintunGetAdapterLUID) ||\n            X(WintunGetRunningDriverVersion) || X(WintunDeleteDriver) || X(WintunSetLogger) || X(WintunStartSession) ||\n            X(WintunEndSession) || X(WintunGetReadWaitEvent) || X(WintunReceivePacket) || X(WintunReleaseReceivePacket) ||\n            X(WintunAllocateSendPacket) || X(WintunSendPacket))\n#undef X\n#pragma GCC diagnostic pop\n        {\n            spdlog::critical(\"get function from wintun.dll failed\");\n            FreeLibrary(this->wintun);\n            this->wintun = NULL;\n            return;\n        }\n    }\n\n    ~Holder() {\n        if (this->wintun) {\n            WintunDeleteDriver();\n            FreeLibrary(this->wintun);\n            this->wintun = NULL;\n        }\n    }\n\n    HMODULE wintun = NULL;\n};\n\nclass WindowsTun {\npublic:\n    int setName(const std::string &name) {\n        this->name = name.empty() ? \"candy\" : name;\n        return 0;\n    }\n\n    int setIP(IP4 ip) {\n        this->ip = ip;\n        return 0;\n    }\n\n    IP4 getIP() {\n        return this->ip;\n    }\n\n    int setPrefix(uint32_t prefix) {\n        this->prefix = prefix;\n        return 0;\n    }\n\n    int setMTU(int mtu) {\n        this->mtu = mtu;\n        return 0;\n    }\n\n    int up() {\n        if (!Holder::Ok()) {\n            spdlog::critical(\"init wintun failed\");\n            return -1;\n        }\n\n        GUID Guid;\n        std::string data = \"CandyGuid\" + this->name;\n        unsigned char hash[SHA256_DIGEST_LENGTH];\n        SHA256((unsigned char *)data.c_str(), data.size(), hash);\n        memcpy(&Guid, hash, sizeof(Guid));\n        this->adapter = WintunCreateAdapter(UTF8ToUTF16(this->name).c_str(), L\"Candy\", &Guid);\n        if (!this->adapter) {\n            spdlog::critical(\"create wintun adapter failed: {}\", GetLastError());\n            return -1;\n        }\n        int Error;\n        MIB_UNICASTIPADDRESS_ROW AddressRow;\n        InitializeUnicastIpAddressEntry(&AddressRow);\n        WintunGetAdapterLUID(this->adapter, &AddressRow.InterfaceLuid);\n        AddressRow.Address.Ipv4.sin_family = AF_INET;\n        AddressRow.Address.Ipv4.sin_addr.S_un.S_addr = this->ip;\n        AddressRow.OnLinkPrefixLength = this->prefix;\n        AddressRow.DadState = IpDadStatePreferred;\n        Error = CreateUnicastIpAddressEntry(&AddressRow);\n        if (Error != ERROR_SUCCESS) {\n            spdlog::critical(\"create unicast ip address entry failed: {}\", Error);\n            return -1;\n        }\n\n        MIB_IPINTERFACE_ROW Interface = {0};\n        Interface.Family = AF_INET;\n        Interface.InterfaceLuid = AddressRow.InterfaceLuid;\n        Error = GetIpInterfaceEntry(&Interface);\n        if (Error != NO_ERROR) {\n            spdlog::critical(\"get ip interface entry failed: {}\", Error);\n            return -1;\n        }\n        this->ifindex = Interface.InterfaceIndex;\n        Interface.SitePrefixLength = 0;\n        Interface.NlMtu = this->mtu;\n        Error = SetIpInterfaceEntry(&Interface);\n        if (Error != NO_ERROR) {\n            spdlog::critical(\"set ip interface entry failed: {}\", Error);\n            return -1;\n        }\n\n        this->session = WintunStartSession(this->adapter, WINTUN_MIN_RING_CAPACITY);\n        if (!this->session) {\n            spdlog::critical(\"start wintun session failed: {}\", GetLastError());\n            return -1;\n        }\n        return 0;\n    }\n\n    int down() {\n        while (!routes.empty()) {\n            DeleteIpForwardEntry(&routes.top());\n            routes.pop();\n        }\n\n        if (this->session) {\n            WintunEndSession(this->session);\n            this->session = NULL;\n        }\n        if (this->adapter) {\n            WintunCloseAdapter(this->adapter);\n            this->adapter = NULL;\n        }\n        return 0;\n    }\n\n    int read(std::string &buffer) {\n        if (this->session) {\n            DWORD size;\n            BYTE *packet = WintunReceivePacket(this->session, &size);\n            if (packet) {\n                buffer.assign((char *)packet, size);\n                WintunReleaseReceivePacket(this->session, packet);\n                return size;\n            }\n            if (GetLastError() == ERROR_NO_MORE_ITEMS) {\n                WaitForSingleObject(WintunGetReadWaitEvent(this->session), 1000);\n                return 0;\n            }\n            spdlog::error(\"wintun read failed: {}\", GetLastError());\n        }\n        return -1;\n    }\n\n    int write(const std::string &buffer) {\n        if (this->session) {\n            BYTE *packet = WintunAllocateSendPacket(this->session, buffer.size());\n            if (packet) {\n                memcpy(packet, buffer.c_str(), buffer.size());\n                WintunSendPacket(this->session, packet);\n                return buffer.size();\n            }\n            if (GetLastError() == ERROR_BUFFER_OVERFLOW) {\n                return 0;\n            }\n            spdlog::error(\"wintun write failed: {}\", GetLastError());\n        }\n        return -1;\n    }\n\n    int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {\n        MIB_IPFORWARDROW route;\n\n        route.dwForwardDest = dst;\n        route.dwForwardMask = mask;\n        route.dwForwardNextHop = nexthop;\n        route.dwForwardIfIndex = this->ifindex;\n\n        route.dwForwardProto = MIB_IPPROTO_NETMGMT;\n        route.dwForwardNextHopAS = 0;\n        route.dwForwardAge = INFINITE;\n        route.dwForwardType = MIB_IPROUTE_TYPE_INDIRECT;\n        route.dwForwardMetric1 = route.dwForwardType + 1;\n        route.dwForwardMetric2 = MIB_IPROUTE_METRIC_UNUSED;\n        route.dwForwardMetric3 = MIB_IPROUTE_METRIC_UNUSED;\n        route.dwForwardMetric4 = MIB_IPROUTE_METRIC_UNUSED;\n        route.dwForwardMetric5 = MIB_IPROUTE_METRIC_UNUSED;\n\n        DWORD result = CreateIpForwardEntry(&route);\n        if (result == NO_ERROR) {\n            routes.push(route);\n        } else {\n            spdlog::error(\"add route failed: {}\", result);\n        }\n\n        return 0;\n    }\n\nprivate:\n    std::string name;\n    IP4 ip;\n    uint32_t prefix;\n    int mtu;\n    int timeout;\n    NET_IFINDEX ifindex;\n    std::stack<MIB_IPFORWARDROW> routes;\n\n    WINTUN_ADAPTER_HANDLE adapter = NULL;\n    WINTUN_SESSION_HANDLE session = NULL;\n};\n\n} // namespace candy\n\nnamespace candy {\n\nTun::Tun() {\n    this->impl = std::make_shared<WindowsTun>();\n}\n\nTun::~Tun() {\n    this->impl.reset();\n}\n\nint Tun::setName(const std::string &name) {\n    std::shared_ptr<WindowsTun> tun;\n\n    tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);\n    tun->setName(name);\n    return 0;\n}\n\nint Tun::setAddress(const std::string &cidr) {\n    std::shared_ptr<WindowsTun> tun;\n    Address address;\n\n    if (address.fromCidr(cidr)) {\n        return -1;\n    }\n    spdlog::info(\"client address: {}\", address.toCidr());\n    tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);\n    if (tun->setIP(address.Host())) {\n        return -1;\n    }\n    if (tun->setPrefix(address.Mask().toPrefix())) {\n        return -1;\n    }\n    return 0;\n}\n\nIP4 Tun::getIP() {\n    std::shared_ptr<WindowsTun> tun;\n    tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);\n    return tun->getIP();\n}\n\nint Tun::setMTU(int mtu) {\n    std::shared_ptr<WindowsTun> tun;\n    tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);\n    if (tun->setMTU(mtu)) {\n        return -1;\n    }\n    return 0;\n}\n\nint Tun::up() {\n    std::shared_ptr<WindowsTun> tun;\n    tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);\n    return tun->up();\n}\n\nint Tun::down() {\n    std::shared_ptr<WindowsTun> tun;\n    tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);\n    return tun->down();\n}\n\nint Tun::read(std::string &buffer) {\n    std::shared_ptr<WindowsTun> tun;\n    tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);\n    return tun->read(buffer);\n}\n\nint Tun::write(const std::string &buffer) {\n    std::shared_ptr<WindowsTun> tun;\n    tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);\n    return tun->write(buffer);\n}\n\nint Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {\n    std::shared_ptr<WindowsTun> tun;\n    tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);\n    return tun->setSysRtTable(dst, mask, nexthop);\n}\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/utils/atomic.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_UTILS_ATOMIC_H\n#define CANDY_UTILS_ATOMIC_H\n\n#include <condition_variable>\n\nnamespace candy {\nnamespace Utils {\n\ntemplate <typename T> class Atomic {\npublic:\n    explicit Atomic(T initial = T()) : value(initial) {}\n\n    T load() const {\n        std::lock_guard<std::mutex> lock(mutex);\n        return value;\n    }\n\n    void store(T new_value) {\n        std::lock_guard<std::mutex> lock(mutex);\n        value = new_value;\n        cv.notify_all();\n    }\n\n    void wait(const T &expected) {\n        std::unique_lock<std::mutex> lock(mutex);\n        cv.wait(lock, [this, &expected] { return value != expected; });\n    }\n\n    template <typename Predicate> void wait_until(Predicate pred) {\n        std::unique_lock<std::mutex> lock(mutex);\n        cv.wait(lock, pred);\n    }\n\n    void notify_one() {\n        std::lock_guard<std::mutex> lock(mutex);\n        cv.notify_one();\n    }\n\n    void notify_all() {\n        std::lock_guard<std::mutex> lock(mutex);\n        cv.notify_all();\n    }\n\nprivate:\n    T value;\n    mutable std::mutex mutex;\n    std::condition_variable cv;\n};\n\n} // namespace Utils\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/utils/codecvt.cc",
    "content": "#include <Poco/Platform.h>\n#if POCO_OS == POCO_OS_WINDOWS_NT\n#include \"utils/codecvt.h\"\n#include <windows.h>\n\nnamespace candy {\n\nstd::string UTF16ToUTF8(const std::wstring &utf16Str) {\n    if (utf16Str.empty())\n        return \"\";\n\n    int utf8Size = WideCharToMultiByte(CP_UTF8, 0, utf16Str.c_str(), -1, nullptr, 0, nullptr, nullptr);\n\n    if (utf8Size == 0) {\n        return \"\";\n    }\n\n    std::string utf8Str(utf8Size, '\\0');\n    WideCharToMultiByte(CP_UTF8, 0, utf16Str.c_str(), -1, &utf8Str[0], utf8Size, nullptr, nullptr);\n\n    utf8Str.resize(utf8Size - 1);\n    return utf8Str;\n}\n\nstd::wstring UTF8ToUTF16(const std::string &utf8Str) {\n    if (utf8Str.empty())\n        return L\"\";\n\n    int utf16Size = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, nullptr, 0);\n\n    if (utf16Size == 0) {\n        return L\"\";\n    }\n\n    std::wstring utf16Str(utf16Size, L'\\0');\n    MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, &utf16Str[0], utf16Size);\n\n    utf16Str.resize(utf16Size - 1);\n    return utf16Str;\n}\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/utils/codecvt.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_UTILS_CODECVT_H\n#define CANDY_UTILS_CODECVT_H\n\n#include <string>\n\nnamespace candy {\n\nstd::string UTF16ToUTF8(const std::wstring &utf16Str);\nstd::wstring UTF8ToUTF16(const std::string &utf8Str);\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/utils/random.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"utils/random.h\"\n#include <iostream>\n#include <random>\n#include <sstream>\n\nnamespace {\n\nint randomHex() {\n    std::random_device device;\n    std::mt19937 engine(device());\n    std::uniform_int_distribution<int> distrib(0, 15);\n    return distrib(engine);\n}\n} // namespace\n\nnamespace candy {\n\nuint32_t randomUint32() {\n    std::random_device device;\n    std::mt19937 engine(device());\n    std::uniform_int_distribution<uint32_t> distrib;\n    return distrib(engine);\n}\n\nstd::string randomHexString(int length) {\n    std::stringstream ss;\n    for (int i = 0; i < length; i++) {\n        ss << std::hex << randomHex();\n    }\n    return ss.str();\n}\n\n} // namespace candy\n"
  },
  {
    "path": "candy/src/utils/random.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_UTILS_RANDOM_H\n#define CANDY_UTILS_RANDOM_H\n\n#include <cstdint>\n#include <string>\n\nnamespace candy {\n\nuint32_t randomUint32();\nstd::string randomHexString(int length);\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/utils/time.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"utils/time.h\"\n#include \"core/net.h\"\n#include <Poco/Net/DatagramSocket.h>\n#include <chrono>\n#include <ctime>\n#include <iomanip>\n#include <iostream>\n#include <spdlog/fmt/bin_to_hex.h>\n#include <spdlog/spdlog.h>\n#include <sstream>\n#include <string>\n#include <unistd.h>\n\nnamespace candy {\n\nint64_t unixTime() {\n    using namespace std::chrono;\n    return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();\n}\n\nint64_t bootTime() {\n    using namespace std::chrono;\n    auto now = steady_clock::now();\n    return duration_cast<milliseconds>(now.time_since_epoch()).count();\n}\n\nstd::string getCurrentTimeWithMillis() {\n    auto now = std::chrono::system_clock::now();\n\n    auto ms_tp = std::chrono::time_point_cast<std::chrono::milliseconds>(now);\n    auto epoch = ms_tp.time_since_epoch();\n    auto value = std::chrono::duration_cast<std::chrono::milliseconds>(epoch).count();\n\n    std::time_t now_time_t = std::chrono::system_clock::to_time_t(now);\n    std::tm *ptm = std::localtime(&now_time_t);\n\n    std::ostringstream oss;\n    oss << std::put_time(ptm, \"%Y-%m-%d %H:%M:%S\");\n    oss << '.' << std::setfill('0') << std::setw(3) << (value % 1000);\n\n    return oss.str();\n}\n\n} // namespace candy\n"
  },
  {
    "path": "candy/src/utils/time.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_UTILS_TIME_H\n#define CANDY_UTILS_TIME_H\n\n#include <cstdint>\n#include <string>\n\nnamespace candy {\n\nint64_t unixTime();\nint64_t bootTime();\n\nstd::string getCurrentTimeWithMillis();\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/websocket/client.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"websocket/client.h\"\n#include \"core/client.h\"\n#include \"core/message.h\"\n#include \"core/net.h\"\n#include \"core/version.h\"\n#include \"utils/time.h\"\n#include \"websocket/message.h\"\n#include <Poco/Net/HTTPMessage.h>\n#include <Poco/Net/HTTPRequest.h>\n#include <Poco/Net/HTTPResponse.h>\n#include <Poco/Net/HTTPSClientSession.h>\n#include <Poco/Timespan.h>\n#include <Poco/URI.h>\n#include <memory>\n#include <spdlog/fmt/bin_to_hex.h>\n#include <spdlog/spdlog.h>\n\nnamespace candy {\n\nint WebSocketClient::setName(const std::string &name) {\n    this->name = name;\n    return 0;\n}\n\nint WebSocketClient::setPassword(const std::string &password) {\n    this->password = password;\n    return 0;\n}\n\nint WebSocketClient::setWsServerUri(const std::string &uri) {\n    this->wsServerUri = uri;\n    return 0;\n}\n\nint WebSocketClient::setExptTunAddress(const std::string &cidr) {\n    this->exptTunCidr = cidr;\n    return 0;\n}\n\nint WebSocketClient::setAddress(const std::string &cidr) {\n    this->tunCidr = cidr;\n    return 0;\n}\n\nint WebSocketClient::setVirtualMac(const std::string &vmac) {\n    this->vmac = vmac;\n    return 0;\n}\n\nstd::string WebSocketClient::getTunCidr() const {\n    return this->tunCidr;\n}\n\nint WebSocketClient::run(Client *client) {\n    this->client = client;\n\n    if (connect()) {\n        spdlog::critical(\"websocket client connect failed\");\n        return -1;\n    }\n\n    sendVirtualMacMsg();\n    if (this->tunCidr.empty()) {\n        sendExptTunMsg();\n    } else {\n        sendAuthMsg();\n    }\n\n    this->msgThread = std::thread([&] {\n        spdlog::debug(\"start thread: websocket client msg\");\n        while (getClient().isRunning()) {\n            handleWsQueue();\n        }\n        getClient().shutdown();\n        spdlog::debug(\"stop thread: websocket client msg\");\n    });\n\n    this->wsThread = std::thread([&] {\n        spdlog::debug(\"start thread: websocket client ws\");\n        while (getClient().isRunning()) {\n            if (handleWsConn()) {\n                break;\n            }\n        }\n        getClient().shutdown();\n        spdlog::debug(\"stop thread: websocket client ws\");\n    });\n\n    return 0;\n}\n\nint WebSocketClient::wait() {\n    if (this->msgThread.joinable()) {\n        this->msgThread.join();\n    }\n    if (this->wsThread.joinable()) {\n        this->wsThread.join();\n    }\n    return 0;\n}\n\nvoid WebSocketClient::handleWsQueue() {\n    Msg msg = this->client->getWsMsgQueue().read();\n    switch (msg.kind) {\n    case MsgKind::TIMEOUT:\n        break;\n    case MsgKind::PACKET:\n        handlePacket(std::move(msg));\n        break;\n    case MsgKind::PUBINFO:\n        handlePubInfo(std::move(msg));\n        break;\n    case MsgKind::DISCOVERY:\n        handleDiscovery(std::move(msg));\n        break;\n    default:\n        spdlog::warn(\"unexcepted websocket message type: {}\", static_cast<int>(msg.kind));\n        break;\n    }\n}\n\nvoid WebSocketClient::handlePacket(Msg msg) {\n    IP4Header *header = (IP4Header *)msg.data.data();\n\n    msg.data.insert(0, 1, WsMsgKind::FORWARD);\n    sendFrame(msg.data);\n}\n\nvoid WebSocketClient::handlePubInfo(Msg msg) {\n    CoreMsg::PubInfo *info = (CoreMsg::PubInfo *)(msg.data.data());\n    if (info->local) {\n        WsMsg::ConnLocal buffer;\n        buffer.ge.src = info->src;\n        buffer.ge.dst = info->dst;\n        buffer.ip = info->ip;\n        buffer.port = hton(info->port);\n        sendFrame(&buffer, sizeof(buffer));\n    } else {\n        WsMsg::Conn buffer;\n        buffer.src = info->src;\n        buffer.dst = info->dst;\n        buffer.ip = info->ip;\n        buffer.port = hton(info->port);\n        sendFrame(&buffer, sizeof(buffer));\n    }\n}\n\nvoid WebSocketClient::handleDiscovery(Msg msg) {\n    sendDiscoveryMsg(IP4(\"255.255.255.255\"));\n}\n\nint WebSocketClient::handleWsConn() {\n    try {\n        std::string buffer;\n        int flags = 0;\n\n        if (!this->ws->poll(Poco::Timespan(1, 0), Poco::Net::Socket::SELECT_READ | Poco::Net::Socket::SELECT_ERROR)) {\n            if (bootTime() - this->timestamp > 30000) {\n                spdlog::warn(\"websocket pong timeout\");\n                return -1;\n            }\n            if (bootTime() - this->timestamp > 15000) {\n                sendPingMessage();\n            }\n            return 0;\n        }\n\n        if (this->ws->getError()) {\n            spdlog::warn(\"websocket connection error: {}\", this->ws->getError());\n            return -1;\n        }\n\n        buffer.resize(1500);\n        int length = this->ws->receiveFrame(buffer.data(), buffer.size(), flags);\n        if (length == 0 && flags == 0) {\n            spdlog::info(\"abnormal disconnect\");\n            return -1;\n        }\n        if ((flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_PING) {\n            this->timestamp = bootTime();\n            flags = (int)Poco::Net::WebSocket::FRAME_FLAG_FIN | (int)Poco::Net::WebSocket::FRAME_OP_PONG;\n            sendFrame(buffer.data(), length, flags);\n            return 0;\n        }\n        if ((flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_PONG) {\n            this->timestamp = bootTime();\n            return 0;\n        }\n        if ((flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_CLOSE) {\n            spdlog::info(\"websocket close: {}\", buffer);\n            return -1;\n        }\n        if (length > 0) {\n            this->timestamp = bootTime();\n            buffer.resize(length);\n            handleWsMsg(std::move(buffer));\n            return 0;\n        }\n        return 0;\n    } catch (std::exception &e) {\n        spdlog::warn(\"handle ws conn failed: {}\", e.what());\n        return -1;\n    }\n}\n\nvoid WebSocketClient::handleWsMsg(std::string buffer) {\n    uint8_t msgKind = buffer.front();\n    switch (msgKind) {\n    case WsMsgKind::FORWARD:\n        handleForwardMsg(std::move(buffer));\n        break;\n    case WsMsgKind::EXPTTUN:\n        handleExptTunMsg(std::move(buffer));\n        break;\n    case WsMsgKind::UDP4CONN:\n        handleUdp4ConnMsg(std::move(buffer));\n        break;\n    case WsMsgKind::DISCOVERY:\n        handleDiscoveryMsg(std::move(buffer));\n        break;\n    case WsMsgKind::ROUTE:\n        handleRouteMsg(std::move(buffer));\n        break;\n    case WsMsgKind::GENERAL:\n        handleGeneralMsg(std::move(buffer));\n        break;\n    default:\n        spdlog::debug(\"unknown websocket message kind: {}\", msgKind);\n        break;\n    }\n}\n\nvoid WebSocketClient::handleForwardMsg(std::string buffer) {\n    if (buffer.size() < sizeof(WsMsg::Forward)) {\n        spdlog::warn(\"invalid forward message: {:n}\", spdlog::to_hex(buffer));\n        return;\n    }\n    // 移除一个字节的类型\n    buffer.erase(0, 1);\n    // 尝试与源地址建立对等连接\n    IP4Header *header = (IP4Header *)buffer.data();\n    // 每次通过服务端转发收到报文都触发一次尝试 P2P 连接, 用于暗示通过服务端转发是个非常耗时的操作\n    this->client->getPeerMsgQueue().write(Msg(MsgKind::TRYP2P, header->saddr.toString()));\n    // 最后把报文移动到 TUN 模块, 因为有移动操作所以必须在最后执行\n    this->client->getTunMsgQueue().write(Msg(MsgKind::PACKET, std::move(buffer)));\n}\n\nvoid WebSocketClient::handleExptTunMsg(std::string buffer) {\n    if (buffer.size() < sizeof(WsMsg::ExptTun)) {\n        spdlog::warn(\"invalid expt tun message: {:n}\", spdlog::to_hex(buffer));\n        return;\n    }\n    WsMsg::ExptTun *header = (WsMsg::ExptTun *)buffer.data();\n    Address exptTun(header->cidr);\n    this->tunCidr = exptTun.toCidr();\n    sendAuthMsg();\n}\n\nvoid WebSocketClient::handleUdp4ConnMsg(std::string buffer) {\n    if (buffer.size() < sizeof(WsMsg::Conn)) {\n        spdlog::warn(\"invalid udp4conn message: {:n}\", spdlog::to_hex(buffer));\n        return;\n    }\n    WsMsg::Conn *header = (WsMsg::Conn *)buffer.data();\n    CoreMsg::PubInfo info = {.src = header->src, .dst = header->dst, .ip = header->ip, .port = ntoh(header->port)};\n    this->client->getPeerMsgQueue().write(Msg(MsgKind::PUBINFO, std::string((char *)(&info), sizeof(info))));\n}\n\nvoid WebSocketClient::handleDiscoveryMsg(std::string buffer) {\n    if (buffer.size() < sizeof(WsMsg::Discovery)) {\n        spdlog::warn(\"invalid discovery message: {:n}\", spdlog::to_hex(buffer));\n        return;\n    }\n    WsMsg::Discovery *header = (WsMsg::Discovery *)buffer.data();\n    if (header->dst == IP4(\"255.255.255.255\")) {\n        sendDiscoveryMsg(header->src);\n    }\n    this->client->getPeerMsgQueue().write(Msg(MsgKind::TRYP2P, header->src.toString()));\n}\n\nvoid WebSocketClient::handleRouteMsg(std::string buffer) {\n    if (buffer.size() < sizeof(WsMsg::SysRoute)) {\n        spdlog::warn(\"invalid route message: {:n}\", spdlog::to_hex(buffer));\n        return;\n    }\n    WsMsg::SysRoute *header = (WsMsg::SysRoute *)buffer.data();\n    SysRouteEntry *rt = header->rtTable;\n    for (uint8_t idx = 0; idx < header->size; ++idx) {\n        this->client->getTunMsgQueue().write(Msg(MsgKind::SYSRT, std::string((char *)(rt + idx), sizeof(SysRouteEntry))));\n        this->client->getPeerMsgQueue().write(Msg(MsgKind::SYSRT));\n    }\n}\n\nvoid WebSocketClient::handleGeneralMsg(std::string buffer) {\n    if (buffer.size() < sizeof(WsMsg::ConnLocal)) {\n        spdlog::warn(\"invalid udp4conn local message: {:n}\", spdlog::to_hex(buffer));\n        return;\n    }\n    WsMsg::ConnLocal *header = (WsMsg::ConnLocal *)buffer.data();\n    CoreMsg::PubInfo info = {\n        .src = header->ge.src,\n        .dst = header->ge.dst,\n        .ip = header->ip,\n        .port = ntoh(header->port),\n        .local = true,\n    };\n    this->client->getPeerMsgQueue().write(Msg(MsgKind::PUBINFO, std::string((char *)(&info), sizeof(info))));\n}\n\nvoid WebSocketClient::sendFrame(const std::string &buffer, int flags) {\n    sendFrame(buffer.c_str(), buffer.size(), flags);\n}\n\nvoid WebSocketClient::sendFrame(const void *buffer, int length, int flags) {\n    if (this->ws) {\n        try {\n            this->ws->sendFrame(buffer, length, flags);\n        } catch (std::exception &e) {\n            spdlog::critical(\"websocket send frame failed: {}\", e.what());\n        }\n    }\n}\n\nvoid WebSocketClient::sendVirtualMacMsg() {\n    WsMsg::VMac buffer(this->vmac);\n    buffer.updateHash(this->password);\n    sendFrame(&buffer, sizeof(buffer));\n}\n\nvoid WebSocketClient::sendExptTunMsg() {\n    Address exptTun(this->exptTunCidr);\n    WsMsg::ExptTun buffer(exptTun.toCidr());\n    buffer.updateHash(this->password);\n    sendFrame(&buffer, sizeof(buffer));\n}\n\nvoid WebSocketClient::sendAuthMsg() {\n    Address address(this->tunCidr);\n    WsMsg::Auth buffer(address.Host());\n    buffer.updateHash(this->password);\n    sendFrame(&buffer, sizeof(buffer));\n    this->client->getTunMsgQueue().write(Msg(MsgKind::TUNADDR, address.toCidr()));\n    this->client->getPeerMsgQueue().write(Msg(MsgKind::TUNADDR, address.toCidr()));\n    sendPingMessage();\n}\n\nvoid WebSocketClient::sendDiscoveryMsg(IP4 dst) {\n    Address address(this->tunCidr);\n\n    WsMsg::Discovery buffer;\n    buffer.dst = dst;\n    buffer.src = address.Host();\n\n    sendFrame(&buffer, sizeof(buffer));\n}\n\nstd::string WebSocketClient::hostName() {\n    char hostname[64] = {0};\n    if (!gethostname(hostname, sizeof(hostname))) {\n        return std::string(hostname, strnlen(hostname, sizeof(hostname)));\n    }\n    return \"\";\n}\n\nvoid WebSocketClient::sendPingMessage() {\n    int flags = (int)Poco::Net::WebSocket::FRAME_FLAG_FIN | (int)Poco::Net::WebSocket::FRAME_OP_PING;\n    sendFrame(pingMessage, flags);\n}\n\nint WebSocketClient::connect() {\n    std::shared_ptr<Poco::URI> uri;\n    try {\n        uri = std::make_shared<Poco::URI>(wsServerUri);\n    } catch (std::exception &e) {\n        spdlog::critical(\"invalid websocket server: {}: {}\", wsServerUri, e.what());\n        return -1;\n    }\n\n    try {\n        const std::string path = uri->getPath().empty() ? \"/\" : uri->getPath();\n        Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_GET, path, Poco::Net::HTTPMessage::HTTP_1_1);\n        Poco::Net::HTTPResponse response;\n        if (uri->getScheme() == \"wss\") {\n            using Poco::Net::Context;\n            Context::Ptr context = new Context(Context::TLS_CLIENT_USE, \"\", \"\", \"\", Context::VERIFY_NONE);\n            Poco::Net::HTTPSClientSession cs(uri->getHost(), uri->getPort(), context);\n            this->ws = std::make_shared<Poco::Net::WebSocket>(cs, request, response);\n        } else if (uri->getScheme() == \"ws\") {\n            Poco::Net::HTTPClientSession cs(uri->getHost(), uri->getPort());\n            this->ws = std::make_shared<Poco::Net::WebSocket>(cs, request, response);\n        } else {\n            spdlog::critical(\"invalid websocket scheme: {}\", wsServerUri);\n            return -1;\n        }\n        // Blocking mode may cause receiveFrame to hang and use 100% CPU\n        this->ws->setBlocking(false);\n        this->timestamp = bootTime();\n        this->pingMessage = fmt::format(\"candy::{}::{}::{}\", CANDY_SYSTEM, CANDY_VERSION, hostName());\n        spdlog::debug(\"client info: {}\", this->pingMessage);\n        return 0;\n    } catch (std::exception &e) {\n        spdlog::critical(\"websocket connect failed: {}\", e.what());\n        return -1;\n    }\n}\n\nint WebSocketClient::disconnect() {\n    try {\n        if (this->ws) {\n            this->ws->shutdown();\n            this->ws->close();\n            this->ws.reset();\n        }\n    } catch (std::exception &e) {\n        spdlog::debug(\"websocket disconnect failed: {}\", e.what());\n    }\n    return 0;\n}\n\nClient &WebSocketClient::getClient() {\n    return *this->client;\n}\n\n} // namespace candy\n"
  },
  {
    "path": "candy/src/websocket/client.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_WEBSOCKET_CLIENT_H\n#define CANDY_WEBSOCKET_CLIENT_H\n\n#include \"core/message.h\"\n#include \"core/net.h\"\n#include <Poco/Net/WebSocket.h>\n#include <functional>\n#include <memory>\n#include <string>\n#include <thread>\n\nnamespace candy {\n\nclass Client;\n\nclass WebSocketClient {\npublic:\n    int setName(const std::string &name);\n    int setPassword(const std::string &password);\n    int setWsServerUri(const std::string &uri);\n    int setExptTunAddress(const std::string &cidr);\n    int setAddress(const std::string &cidr);\n    int setVirtualMac(const std::string &vmac);\n    int setTunUpdateCallback(std::function<int(const std::string &)> callback);\n\n    std::string getTunCidr() const;\n\n    int run(Client *client);\n    int wait();\n\nprivate:\n    void handleWsQueue();\n    void handlePacket(Msg msg);\n    void handlePubInfo(Msg msg);\n    void handleDiscovery(Msg msg);\n\n    std::thread msgThread;\n\n    int handleWsConn();\n    void handleWsMsg(std::string buffer);\n    void handleForwardMsg(std::string buffer);\n    void handleExptTunMsg(std::string buffer);\n    void handleUdp4ConnMsg(std::string buffer);\n    void handleDiscoveryMsg(std::string buffer);\n    void handleRouteMsg(std::string buffer);\n    void handleGeneralMsg(std::string buffer);\n    std::thread wsThread;\n\n    void sendFrame(const std::string &buffer, int flags = Poco::Net::WebSocket::FRAME_BINARY);\n    void sendFrame(const void *buffer, int length, int flags = Poco::Net::WebSocket::FRAME_BINARY);\n\n    void sendVirtualMacMsg();\n    void sendExptTunMsg();\n    void sendAuthMsg();\n    void sendDiscoveryMsg(IP4 dst);\n\n    std::function<int(const std::string &)> addressUpdateCallback;\n\nprivate:\n    std::string hostName();\n    void sendPingMessage();\n\nprivate:\n    int connect();\n    int disconnect();\n\n    std::shared_ptr<Poco::Net::WebSocket> ws;\n    std::string pingMessage;\n    int64_t timestamp;\n\nprivate:\n    std::string wsServerUri;\n    std::string exptTunCidr;\n    std::string tunCidr;\n    std::string vmac;\n    std::string name;\n    std::string password;\n\n    Client &getClient();\n    Client *client;\n};\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/websocket/message.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"websocket/message.h\"\n#include \"utils/time.h\"\n\nnamespace candy {\nnamespace WsMsg {\n\nAuth::Auth(IP4 ip) {\n    this->type = WsMsgKind::AUTH;\n    this->ip = ip;\n    this->timestamp = hton(unixTime());\n}\n\nvoid Auth::updateHash(const std::string &password) {\n    std::string data;\n    data.append(password);\n    data.append((char *)&ip, sizeof(ip));\n    data.append((char *)&timestamp, sizeof(timestamp));\n    SHA256((unsigned char *)data.data(), data.size(), this->hash);\n}\n\nbool Auth::check(const std::string &password) {\n    int64_t localTime = unixTime();\n    int64_t remoteTime = ntoh(this->timestamp);\n    if (std::abs(localTime - remoteTime) > 300) {\n        spdlog::warn(\"auth header timestamp check failed: server {} client {}\", localTime, remoteTime);\n    }\n\n    uint8_t reported[SHA256_DIGEST_LENGTH];\n    std::memcpy(reported, this->hash, SHA256_DIGEST_LENGTH);\n\n    updateHash(password);\n\n    if (std::memcmp(reported, this->hash, SHA256_DIGEST_LENGTH)) {\n        spdlog::warn(\"auth header hash check failed\");\n        return false;\n    }\n    return true;\n}\n\nForward::Forward() {\n    this->type = WsMsgKind::FORWARD;\n}\n\nExptTun::ExptTun(const std::string &cidr) {\n    this->type = WsMsgKind::EXPTTUN;\n    this->timestamp = hton(unixTime());\n    std::strcpy(this->cidr, cidr.c_str());\n}\n\nvoid ExptTun::updateHash(const std::string &password) {\n    std::string data;\n    data.append(password);\n    data.append((char *)&this->timestamp, sizeof(this->timestamp));\n    SHA256((unsigned char *)data.data(), data.size(), this->hash);\n}\n\nbool ExptTun::check(const std::string &password) {\n    int64_t localTime = unixTime();\n    int64_t remoteTime = ntoh(this->timestamp);\n    if (std::abs(localTime - remoteTime) > 300) {\n        spdlog::warn(\"expected address header timestamp check failed: server {} client {}\", localTime, remoteTime);\n    }\n\n    uint8_t reported[SHA256_DIGEST_LENGTH];\n    std::memcpy(reported, this->hash, SHA256_DIGEST_LENGTH);\n\n    updateHash(password);\n\n    if (std::memcmp(reported, this->hash, SHA256_DIGEST_LENGTH)) {\n        spdlog::warn(\"expected address header hash check failed\");\n        return false;\n    }\n    return true;\n}\n\nConn::Conn() {\n    this->type = WsMsgKind::UDP4CONN;\n}\n\nVMac::VMac(const std::string &vmac) {\n    this->type = WsMsgKind::VMAC;\n    this->timestamp = hton(unixTime());\n    if (vmac.length() >= sizeof(this->vmac)) {\n        memcpy(this->vmac, vmac.c_str(), sizeof(this->vmac));\n    } else {\n        memset(this->vmac, 0, sizeof(this->vmac));\n    }\n}\n\nvoid VMac::updateHash(const std::string &password) {\n    std::string data;\n    data.append(password);\n    data.append((char *)&this->vmac, sizeof(this->vmac));\n    data.append((char *)&this->timestamp, sizeof(this->timestamp));\n    SHA256((unsigned char *)data.data(), data.size(), this->hash);\n}\n\nbool VMac::check(const std::string &password) {\n    int64_t localTime = unixTime();\n    int64_t remoteTime = ntoh(this->timestamp);\n    if (std::abs(localTime - remoteTime) > 300) {\n        spdlog::warn(\"vmac message timestamp check failed: server {} client {}\", localTime, remoteTime);\n    }\n\n    uint8_t reported[SHA256_DIGEST_LENGTH];\n    std::memcpy(reported, this->hash, SHA256_DIGEST_LENGTH);\n\n    updateHash(password);\n\n    if (std::memcmp(reported, this->hash, SHA256_DIGEST_LENGTH)) {\n        spdlog::warn(\"vmac message hash check failed\");\n        return false;\n    }\n    return true;\n}\n\nDiscovery::Discovery() {\n    this->type = WsMsgKind::DISCOVERY;\n}\n\nGeneral::General() {\n    this->type = WsMsgKind::GENERAL;\n}\n\nConnLocal::ConnLocal() {\n    this->ge.subtype = GeSubType::LOCALUDP4CONN;\n    this->ge.extra = 0;\n}\n} // namespace WsMsg\n} // namespace candy\n"
  },
  {
    "path": "candy/src/websocket/message.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_WEBSOCKET_MESSAGE_H\n#define CANDY_WEBSOCKET_MESSAGE_H\n\n#include \"core/net.h\"\n#include <openssl/sha.h>\n\nnamespace candy {\n\nnamespace WsMsgKind {\nconstexpr uint8_t AUTH = 0;\nconstexpr uint8_t FORWARD = 1;\nconstexpr uint8_t EXPTTUN = 2;\nconstexpr uint8_t UDP4CONN = 3;\nconstexpr uint8_t VMAC = 4;\nconstexpr uint8_t DISCOVERY = 5;\nconstexpr uint8_t ROUTE = 6;\nconstexpr uint8_t GENERAL = 255;\n} // namespace WsMsgKind\n\nnamespace GeSubType {\nconstexpr uint8_t LOCALUDP4CONN = 0;\n}\n\nnamespace WsMsg {\n\nstruct __attribute__((packed)) Auth {\n    uint8_t type;\n    IP4 ip;\n    int64_t timestamp;\n    uint8_t hash[SHA256_DIGEST_LENGTH];\n\n    Auth(IP4 ip);\n    void updateHash(const std::string &password);\n    bool check(const std::string &password);\n};\n\nstruct __attribute__((packed)) Forward {\n    uint8_t type;\n    IP4Header iph;\n\n    Forward();\n};\n\nstruct __attribute__((packed)) ExptTun {\n    uint8_t type;\n    int64_t timestamp;\n    char cidr[32] = {0};\n    uint8_t hash[SHA256_DIGEST_LENGTH];\n\n    ExptTun(const std::string &cidr);\n    void updateHash(const std::string &password);\n    bool check(const std::string &password);\n};\n\nstruct __attribute__((packed)) Conn {\n    uint8_t type;\n    IP4 src;\n    IP4 dst;\n    IP4 ip;\n    uint16_t port;\n\n    Conn();\n};\n\nstruct __attribute__((packed)) VMac {\n    uint8_t type;\n    uint8_t vmac[16];\n    int64_t timestamp;\n    uint8_t hash[SHA256_DIGEST_LENGTH];\n\n    VMac(const std::string &vmac);\n    void updateHash(const std::string &password);\n    bool check(const std::string &password);\n};\n\nstruct __attribute__((packed)) Discovery {\n    uint8_t type;\n    IP4 src;\n    IP4 dst;\n\n    Discovery();\n};\n\nstruct __attribute__((packed)) SysRoute {\n    uint8_t type;\n    uint8_t size;\n    uint16_t reserved;\n    SysRouteEntry rtTable[0];\n};\n\nstruct __attribute__((packed)) General {\n    uint8_t type;\n    uint8_t subtype;\n    uint16_t extra;\n    IP4 src;\n    IP4 dst;\n\n    General();\n};\n\nstruct __attribute__((packed)) ConnLocal {\n    General ge;\n    IP4 ip;\n    uint16_t port;\n\n    ConnLocal();\n};\n\n} // namespace WsMsg\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy/src/websocket/server.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"websocket/server.h\"\n#include \"core/net.h\"\n#include \"utils/time.h\"\n#include \"websocket/message.h\"\n#include <Poco/Net/HTTPRequestHandler.h>\n#include <Poco/Net/HTTPRequestHandlerFactory.h>\n#include <Poco/Net/HTTPServerRequest.h>\n#include <Poco/Net/HTTPServerResponse.h>\n#include <Poco/Net/ServerSocket.h>\n#include <Poco/Net/WebSocket.h>\n#include <Poco/Timespan.h>\n#include <Poco/URI.h>\n#include <exception>\n#include <functional>\n#include <memory>\n#include <mutex>\n#include <shared_mutex>\n#include <spdlog/fmt/bin_to_hex.h>\n#include <spdlog/spdlog.h>\n#include <sstream>\n\n/**\n * Poco 的 WebSocket 服务端接口有点难用,简单封装一下,并对外提供一个回调函数,回调函数的参数表示独立的\n * WebSocket客户端,函数返回会释放连接\n */\nnamespace {\n\nusing WebSocketHandler = std::function<void(Poco::Net::WebSocket &ws)>;\n\nclass HTTPRequestHandler : public Poco::Net::HTTPRequestHandler {\npublic:\n    HTTPRequestHandler(WebSocketHandler wsHandler) : wsHandler(wsHandler) {}\n    void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::HTTPServerResponse &response) {\n        try {\n            Poco::Net::WebSocket ws(request, response);\n            wsHandler(ws);\n            ws.close();\n        } catch (const std::exception &e) {\n            response.setStatus(Poco::Net::HTTPResponse::HTTP_FORBIDDEN);\n            response.setReason(\"Forbidden\");\n            response.setContentLength(0);\n            response.send();\n        }\n    }\n\nprivate:\n    WebSocketHandler wsHandler;\n};\n\nclass HTTPRequestHandlerFactory : public Poco::Net::HTTPRequestHandlerFactory {\npublic:\n    HTTPRequestHandlerFactory(WebSocketHandler wsHandler) : wsHandler(wsHandler) {}\n\n    Poco::Net::HTTPRequestHandler *createRequestHandler(const Poco::Net::HTTPServerRequest &request) {\n        return new HTTPRequestHandler(wsHandler);\n    }\n\nprivate:\n    WebSocketHandler wsHandler;\n};\n\n}; // namespace\n\nnamespace candy {\n\nvoid WsCtx::sendFrame(const std::string &frame, int flags) {\n    this->ws->sendFrame(frame.data(), frame.size(), flags);\n}\n\nint WebSocketServer::setWebSocket(const std::string &uri) {\n    try {\n        Poco::URI parser(uri);\n        if (parser.getScheme() != \"ws\") {\n            spdlog::critical(\"websocket server only support ws\");\n            return -1;\n        }\n        this->host = parser.getHost();\n        this->port = parser.getPort();\n        return 0;\n    } catch (std::exception &e) {\n        spdlog::critical(\"invalid websocket uri: {}: {}\", uri, e.what());\n        return -1;\n    }\n}\n\nint WebSocketServer::setPassword(const std::string &password) {\n    this->password = password;\n    return 0;\n}\n\nint WebSocketServer::setDHCP(const std::string &cidr) {\n    if (cidr.empty()) {\n        return 0;\n    }\n    return this->dhcp.fromCidr(cidr);\n}\n\nint WebSocketServer::setSdwan(const std::string &sdwan) {\n    if (sdwan.empty()) {\n        return 0;\n    }\n    std::string route;\n    std::stringstream stream(sdwan);\n    while (std::getline(stream, route, ';')) {\n        std::string addr;\n        SysRoute rt;\n        std::stringstream ss(route);\n        // dev\n        if (!std::getline(ss, addr, ',') || rt.dev.fromCidr(addr) || rt.dev.Host() != rt.dev.Net()) {\n            spdlog::critical(\"invalid route device: {}\", route);\n            return -1;\n        }\n        // dst\n        if (!std::getline(ss, addr, ',') || rt.dst.fromCidr(addr) || rt.dst.Host() != rt.dst.Net()) {\n            spdlog::critical(\"invalid route dest: {}\", route);\n            return -1;\n        }\n        // next\n        if (!std::getline(ss, addr, ',') || rt.next.fromString(addr)) {\n            spdlog::critical(\"invalid route nexthop: {}\", route);\n            return -1;\n        }\n        spdlog::info(\"route: dev={} dst={} next={}\", rt.dev.toCidr(), rt.dst.toCidr(), rt.next.toString());\n        this->routes.push_back(rt);\n    }\n    return 0;\n}\n\nint WebSocketServer::run() {\n    listen();\n    return 0;\n}\n\nint WebSocketServer::shutdown() {\n    this->running = false;\n    if (this->httpServer) {\n        this->httpServer->stopAll();\n    }\n    this->routes.clear();\n    return 0;\n}\n\nvoid WebSocketServer::handleMsg(WsCtx &ctx) {\n    uint8_t msgKind = ctx.buffer.front();\n    switch (msgKind) {\n    case WsMsgKind::AUTH:\n        handleAuthMsg(ctx);\n        break;\n    case WsMsgKind::FORWARD:\n        handleForwardMsg(ctx);\n        break;\n    case WsMsgKind::EXPTTUN:\n        handleExptTunMsg(ctx);\n        break;\n    case WsMsgKind::UDP4CONN:\n        handleUdp4ConnMsg(ctx);\n        break;\n    case WsMsgKind::VMAC:\n        handleVMacMsg(ctx);\n        break;\n    case WsMsgKind::DISCOVERY:\n        handleDiscoveryMsg(ctx);\n        break;\n    case WsMsgKind::GENERAL:\n        HandleGeneralMsg(ctx);\n        break;\n    }\n}\n\nvoid WebSocketServer::handleAuthMsg(WsCtx &ctx) {\n    if (ctx.buffer.length() < sizeof(WsMsg::Auth)) {\n        spdlog::warn(\"invalid auth message: len {}\", ctx.buffer.length());\n        ctx.status = -1;\n        return;\n    }\n\n    WsMsg::Auth *header = (WsMsg::Auth *)ctx.buffer.data();\n    if (!header->check(this->password)) {\n        spdlog::warn(\"auth header check failed: buffer {:n}\", spdlog::to_hex(ctx.buffer));\n        ctx.status = -1;\n        return;\n    }\n\n    ctx.ip = header->ip;\n\n    {\n        std::unique_lock lock(ipCtxMutex);\n        auto it = ipCtxMap.find(header->ip);\n        if (it != ipCtxMap.end()) {\n            it->second->status = -1;\n            spdlog::info(\"reconnect: {}\", it->second->ip.toString());\n        } else {\n            spdlog::info(\"connect: {}\", ctx.ip.toString());\n        }\n        ipCtxMap[header->ip] = &ctx;\n    }\n\n    updateSysRoute(ctx);\n}\n\nvoid WebSocketServer::handleForwardMsg(WsCtx &ctx) {\n    if (ctx.ip.empty()) {\n        spdlog::debug(\"unauthorized forward websocket client\");\n        ctx.status = -1;\n        return;\n    }\n\n    if (ctx.buffer.length() < sizeof(WsMsg::Forward)) {\n        spdlog::debug(\"invalid forawrd message: len {}\", ctx.buffer.length());\n        ctx.status = -1;\n        return;\n    }\n\n    WsMsg::Forward *header = (WsMsg::Forward *)ctx.buffer.data();\n\n    {\n        std::shared_lock lock(this->ipCtxMutex);\n        auto it = this->ipCtxMap.find(header->iph.daddr);\n        if (it != this->ipCtxMap.end()) {\n            it->second->sendFrame(ctx.buffer);\n            return;\n        }\n    }\n\n    bool broadcast = [&] {\n        // 多播地址\n        if ((header->iph.daddr & IP4(\"240.0.0.0\")) == IP4(\"224.0.0.0\")) {\n            return true;\n        }\n        // 广播\n        if (header->iph.daddr == IP4(\"255.255.255.255\")) {\n            return true;\n        }\n        // 服务端没有配置动态分配地址的范围,没法检查是否为定向广播\n        if (this->dhcp.empty()) {\n            return false;\n        }\n        // 网络号不同,不是定向广播\n        if ((this->dhcp.Mask() & header->iph.daddr) != this->dhcp.Net()) {\n            return false;\n        }\n        // 主机号部分不全为 1,不是定向广播\n        if (~((header->iph.daddr & ~this->dhcp.Mask()) ^ this->dhcp.Mask())) {\n            return false;\n        }\n        return true;\n    }();\n\n    if (broadcast) {\n        std::shared_lock lock(this->ipCtxMutex);\n        for (auto c : this->ipCtxMap) {\n            if (c.second->ip != ctx.ip) {\n                c.second->sendFrame(ctx.buffer);\n            }\n        }\n        return;\n    }\n\n    spdlog::debug(\"forward failed: source {} dest {}\", header->iph.saddr.toString(), header->iph.daddr.toString());\n    return;\n}\n\nvoid WebSocketServer::handleExptTunMsg(WsCtx &ctx) {\n    if (ctx.buffer.length() < sizeof(WsMsg::ExptTun)) {\n        spdlog::warn(\"invalid dynamic address message: len {}\", ctx.buffer.length());\n        ctx.status = -1;\n        return;\n    }\n    WsMsg::ExptTun *header = (WsMsg::ExptTun *)ctx.buffer.data();\n    if (!header->check(this->password)) {\n        spdlog::warn(\"dynamic address header check failed: buffer {:n}\", spdlog::to_hex(ctx.buffer));\n        ctx.status = -1;\n        return;\n    }\n    if (this->dhcp.empty()) {\n        spdlog::warn(\"unable to allocate dynamic address\");\n        ctx.status = -1;\n        return;\n    }\n    Address exptTun;\n    if (exptTun.fromCidr(header->cidr)) {\n        spdlog::warn(\"dynamic address header cidr invalid: buffer {:n}\", spdlog::to_hex(ctx.buffer));\n        ctx.status = -1;\n        return;\n    }\n    // 判断能否直接使用申请的地址\n    bool direct = [&]() {\n        if (dhcp.Net() != exptTun.Net()) {\n            return false;\n        }\n        std::shared_lock lock(this->ipCtxMutex);\n        auto oldCtx = this->ipCtxMap.find(exptTun.Host());\n        if (oldCtx == this->ipCtxMap.end()) {\n            return true;\n        }\n        return ctx.vmac == oldCtx->second->vmac;\n    }();\n    if (!direct) {\n        exptTun = this->dhcp;\n        std::shared_lock lock(this->ipCtxMutex);\n        do {\n            exptTun = exptTun.Next();\n            if (exptTun.Host() == this->dhcp.Host()) {\n                spdlog::warn(\"all addresses in the network are assigned\");\n                ctx.status = -1;\n                return;\n            }\n        } while (!exptTun.isValid() && this->ipCtxMap.find(exptTun.Host()) != this->ipCtxMap.end());\n        this->dhcp = exptTun;\n    }\n    header->timestamp = hton(unixTime());\n    std::strcpy(header->cidr, exptTun.toCidr().c_str());\n    header->updateHash(this->password);\n    ctx.sendFrame(ctx.buffer);\n}\n\nvoid WebSocketServer::handleUdp4ConnMsg(WsCtx &ctx) {\n    if (ctx.ip.empty()) {\n        spdlog::debug(\"unauthorized peer websocket client\");\n        ctx.status = -1;\n        return;\n    }\n\n    if (ctx.buffer.length() < sizeof(WsMsg::Conn)) {\n        spdlog::warn(\"invalid peer conn message: len {}\", ctx.buffer.length());\n        ctx.status = -1;\n        return;\n    }\n\n    WsMsg::Conn *header = (WsMsg::Conn *)ctx.buffer.data();\n    if (ctx.ip != header->src) {\n        spdlog::debug(\"peer source address does not match: auth {} source {}\", ctx.ip.toString(), header->src.toString());\n        ctx.status = -1;\n        return;\n    }\n    std::shared_lock lock(this->ipCtxMutex);\n    auto it = this->ipCtxMap.find(header->dst);\n    if (it == this->ipCtxMap.end()) {\n        spdlog::debug(\"peer dest address not logged in: source {} dst {}\", header->src.toString(), header->dst.toString());\n        return;\n    }\n    it->second->sendFrame(ctx.buffer);\n    return;\n}\n\nvoid WebSocketServer::handleVMacMsg(WsCtx &ctx) {\n    if (ctx.buffer.length() < sizeof(WsMsg::VMac)) {\n        spdlog::warn(\"invalid vmac message: len {}\", ctx.buffer.length());\n        ctx.status = -1;\n        return;\n    }\n\n    WsMsg::VMac *header = (WsMsg::VMac *)ctx.buffer.data();\n    if (!header->check(this->password)) {\n        spdlog::warn(\"vmac message check failed: buffer {:n}\", spdlog::to_hex(ctx.buffer));\n        ctx.status = -1;\n        return;\n    }\n\n    ctx.vmac.assign((char *)header->vmac, sizeof(header->vmac));\n    return;\n}\n\nvoid WebSocketServer::handleDiscoveryMsg(WsCtx &ctx) {\n    if (ctx.ip.empty()) {\n        spdlog::debug(\"unauthorized discovery websocket client\");\n        ctx.status = -1;\n        return;\n    }\n\n    if (ctx.buffer.length() < sizeof(WsMsg::Discovery)) {\n        spdlog::debug(\"invalid discovery message: len {}\", ctx.buffer.length());\n        ctx.status = -1;\n        return;\n    }\n\n    WsMsg::Discovery *header = (WsMsg::Discovery *)ctx.buffer.data();\n    if (ctx.ip != header->src) {\n        spdlog::debug(\"discovery source address does not match: auth {} source {}\", ctx.ip.toString(), header->src.toString());\n        ctx.status = -1;\n        return;\n    }\n\n    std::shared_lock lock(this->ipCtxMutex);\n    if (header->dst == IP4(\"255.255.255.255\")) {\n        for (auto c : this->ipCtxMap) {\n            if (c.first != header->src) {\n                c.second->sendFrame(ctx.buffer);\n            }\n        }\n        return;\n    }\n    auto it = this->ipCtxMap.find(header->dst);\n    if (it != this->ipCtxMap.end()) {\n        it->second->sendFrame(ctx.buffer);\n        return;\n    }\n}\n\nvoid WebSocketServer::HandleGeneralMsg(WsCtx &ctx) {\n    if (ctx.ip.empty()) {\n        spdlog::debug(\"unauthorized general websocket client\");\n        ctx.status = -1;\n        return;\n    }\n\n    if (ctx.buffer.length() < sizeof(WsMsg::General)) {\n        spdlog::debug(\"invalid general message: len {}\", ctx.buffer.length());\n        ctx.status = -1;\n        return;\n    }\n\n    WsMsg::General *header = (WsMsg::General *)ctx.buffer.data();\n\n    if (ctx.ip != header->src) {\n        spdlog::debug(\"general source address does not match: auth {} source {}\", ctx.ip.toString(), header->src.toString());\n        ctx.status = -1;\n        return;\n    }\n\n    std::shared_lock lock(this->ipCtxMutex);\n    if (header->dst == IP4(\"255.255.255.255\")) {\n        for (auto c : this->ipCtxMap) {\n            if (c.first != header->src) {\n                c.second->sendFrame(ctx.buffer);\n            }\n        }\n        return;\n    }\n    auto it = this->ipCtxMap.find(header->dst);\n    if (it != this->ipCtxMap.end()) {\n        it->second->sendFrame(ctx.buffer);\n        return;\n    }\n}\n\nvoid WebSocketServer::updateSysRoute(WsCtx &ctx) {\n    ctx.buffer.resize(sizeof(WsMsg::SysRoute));\n    WsMsg::SysRoute *header = (WsMsg::SysRoute *)ctx.buffer.data();\n    memset(header, 0, sizeof(WsMsg::SysRoute));\n    header->type = WsMsgKind::ROUTE;\n\n    for (auto rt : this->routes) {\n        if ((rt.dev.Mask() & ctx.ip) == rt.dev.Host()) {\n            SysRouteEntry item;\n            item.dst = rt.dst.Net();\n            item.mask = rt.dst.Mask();\n            item.nexthop = rt.next;\n            ctx.buffer.append((char *)(&item), sizeof(item));\n            header->size += 1;\n        }\n        // 100 条路由报文大小是 1204 字节,超过 100 条后分批发送\n        if (header->size > 100) {\n            ctx.sendFrame(ctx.buffer);\n            ctx.buffer.resize(sizeof(WsMsg::SysRoute));\n            header->size = 0;\n        }\n    }\n\n    if (header->size > 0) {\n        ctx.sendFrame(ctx.buffer);\n    }\n}\n\nint WebSocketServer::listen() {\n    try {\n        Poco::Net::ServerSocket socket(Poco::Net::SocketAddress(host, port));\n\n        Poco::Net::HTTPServerParams *params = new Poco::Net::HTTPServerParams();\n        params->setMaxThreads(0x00FFFFFF);\n\n        this->running = true;\n        WebSocketHandler wsHandler = [this](Poco::Net::WebSocket &ws) { handleWebsocket(ws); };\n        this->httpServer = std::make_shared<Poco::Net::HTTPServer>(new HTTPRequestHandlerFactory(wsHandler), socket, params);\n        this->httpServer->start();\n        spdlog::info(\"listen on: {}:{}\", host, port);\n        return 0;\n    } catch (std::exception &e) {\n        spdlog::critical(\"listen failed: {}\", e.what());\n        return -1;\n    }\n}\n\nvoid WebSocketServer::handleWebsocket(Poco::Net::WebSocket &ws) {\n    ws.setReceiveTimeout(Poco::Timespan(1, 0));\n    WsCtx ctx = {.ws = &ws};\n\n    int flags = 0;\n    int length = 0;\n    std::string buffer;\n    while (this->running && ctx.status == 0) {\n        try {\n            buffer.resize(1500);\n            length = ws.receiveFrame(buffer.data(), buffer.size(), flags);\n            int frameOp = flags & Poco::Net::WebSocket::FRAME_OP_BITMASK;\n\n            // 响应 Ping 报文\n            if (frameOp == Poco::Net::WebSocket::FRAME_OP_PING) {\n                flags = (int)Poco::Net::WebSocket::FRAME_FLAG_FIN | (int)Poco::Net::WebSocket::FRAME_OP_PONG;\n                ws.sendFrame(buffer.data(), buffer.size(), flags);\n                continue;\n            }\n\n            // 客户端主动关闭连接\n            if ((length == 0 && flags == 0) || frameOp == Poco::Net::WebSocket::FRAME_OP_CLOSE) {\n                break;\n            }\n\n            if (frameOp == Poco::Net::WebSocket::FRAME_OP_BINARY && length > 0) {\n                // 调整 buffer 为真实大小并移动到 ctx\n                buffer.resize(length);\n                ctx.buffer = std::move(buffer);\n\n                // 处理客户端请求\n                handleMsg(ctx);\n\n                // 重新初始化 buffer\n                buffer = std::string();\n            }\n        } catch (Poco::TimeoutException const &e) {\n            // 超时异常,不做处理\n            continue;\n        } catch (std::exception &e) {\n            // 未知异常,退出这个客户端\n            spdlog::debug(\"handle websocket failed: {}\", e.what());\n            break;\n        }\n    }\n\n    {\n        std::unique_lock lock(ipCtxMutex);\n        auto it = ipCtxMap.find(ctx.ip);\n        if (it != ipCtxMap.end() && it->second == &ctx) {\n            ipCtxMap.erase(it);\n            spdlog::info(\"disconnect: {}\", ctx.ip.toString());\n        }\n    }\n}\n\n} // namespace candy\n"
  },
  {
    "path": "candy/src/websocket/server.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_WEBSOCKET_SERVER_H\n#define CANDY_WEBSOCKET_SERVER_H\n\n#include \"core/net.h\"\n#include <Poco/Net/HTTPServer.h>\n#include <Poco/Net/WebSocket.h>\n#include <list>\n#include <memory>\n#include <shared_mutex>\n#include <string>\n\nnamespace candy {\n\nstruct WsCtx {\n    Poco::Net::WebSocket *ws;\n\n    std::string buffer;\n    int status;\n\n    IP4 ip;\n    std::string vmac;\n\n    void sendFrame(const std::string &frame, int flags = Poco::Net::WebSocket::FRAME_BINARY);\n};\n\nstruct SysRoute {\n    // 通过地址和掩码确定策略下发给哪些客户端\n    Address dev;\n    // 系统路由策略中的地址掩码和下一跳\n    Address dst;\n    IP4 next;\n};\n\nclass WebSocketServer {\npublic:\n    int setWebSocket(const std::string &uri);\n    int setPassword(const std::string &password);\n    int setDHCP(const std::string &cidr);\n    int setSdwan(const std::string &sdwan);\n    int run();\n    int shutdown();\n\nprivate:\n    std::string host;\n    uint16_t port;\n    std::string password;\n    Address dhcp;\n    std::list<SysRoute> routes;\n\nprivate:\n    void handleMsg(WsCtx &ctx);\n    void handleAuthMsg(WsCtx &ctx);\n    void handleForwardMsg(WsCtx &ctx);\n    void handleExptTunMsg(WsCtx &ctx);\n    void handleUdp4ConnMsg(WsCtx &ctx);\n    void handleVMacMsg(WsCtx &ctx);\n    void handleDiscoveryMsg(WsCtx &ctx);\n    void HandleGeneralMsg(WsCtx &ctx);\n\n    // 更新客户端系统路由\n    void updateSysRoute(WsCtx &ctx);\n\n    // 保存 IP 到对应连接指针的映射\n    std::unordered_map<IP4, WsCtx *> ipCtxMap;\n    // 操作 map 时需要加锁,以确保操作时指针有效\n    std::shared_mutex ipCtxMutex;\n\n    bool running;\n\nprivate:\n    // 开始监听,新的请求将调用 handleWebsocket\n    int listen();\n    // 同步的处理每个客户独的请求,函数返回后连接将断开\n    void handleWebsocket(Poco::Net::WebSocket &ws);\n\n    std::shared_ptr<Poco::Net::HTTPServer> httpServer;\n};\n\n} // namespace candy\n\n#endif\n"
  },
  {
    "path": "candy-cli/CMakeLists.txt",
    "content": "file(GLOB_RECURSE SOURCES \"src/*.cc\")\nadd_executable(candy-cli ${SOURCES})\n\ntarget_include_directories(candy-cli PUBLIC \n    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>\n    $<INSTALL_INTERFACE:include>\n)\n\nset_target_properties(candy-cli PROPERTIES OUTPUT_NAME \"candy\")\n\ntarget_link_libraries(candy-cli PRIVATE spdlog::spdlog)\ntarget_link_libraries(candy-cli PRIVATE Poco::Foundation Poco::JSON)\ntarget_link_libraries(candy-cli PRIVATE Candy::Library)\n\ninstall(TARGETS candy-cli)\n\nadd_executable(Candy::CLI ALIAS candy-cli)\n"
  },
  {
    "path": "candy-cli/src/argparse.h",
    "content": "/*\n  __ _ _ __ __ _ _ __   __ _ _ __ ___  ___\n / _` | '__/ _` | '_ \\ / _` | '__/ __|/ _ \\ Argument Parser for Modern C++\n| (_| | | | (_| | |_) | (_| | |  \\__ \\  __/ http://github.com/p-ranav/argparse\n \\__,_|_|  \\__, | .__/ \\__,_|_|  |___/\\___|\n           |___/|_|\n\nLicensed under the MIT License <http://opensource.org/licenses/MIT>.\nSPDX-License-Identifier: MIT\nCopyright (c) 2019-2022 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>\nand other contributors.\n\nPermission is hereby  granted, free of charge, to any  person obtaining a copy\nof this software and associated  documentation files (the \"Software\"), to deal\nin the Software  without restriction, including without  limitation the rights\nto  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell\ncopies  of  the Software,  and  to  permit persons  to  whom  the Software  is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE  IS PROVIDED \"AS  IS\", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR\nIMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,\nFITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE\nAUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER\nLIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n*/\n#pragma once\n\n#include <cerrno>\n\n#ifndef ARGPARSE_MODULE_USE_STD_MODULE\n#include <algorithm>\n#include <any>\n#include <array>\n#include <charconv>\n#include <cstdlib>\n#include <functional>\n#include <iomanip>\n#include <iostream>\n#include <iterator>\n#include <limits>\n#include <list>\n#include <map>\n#include <numeric>\n#include <optional>\n#include <set>\n#include <sstream>\n#include <stdexcept>\n#include <string>\n#include <string_view>\n#include <tuple>\n#include <type_traits>\n#include <utility>\n#include <variant>\n#include <vector>\n#endif\n\n#ifndef ARGPARSE_CUSTOM_STRTOF\n#define ARGPARSE_CUSTOM_STRTOF strtof\n#endif\n\n#ifndef ARGPARSE_CUSTOM_STRTOD\n#define ARGPARSE_CUSTOM_STRTOD strtod\n#endif\n\n#ifndef ARGPARSE_CUSTOM_STRTOLD\n#define ARGPARSE_CUSTOM_STRTOLD strtold\n#endif\n\nnamespace argparse {\n\nnamespace details { // namespace for helper methods\n\ntemplate <typename T, typename = void> struct HasContainerTraits : std::false_type {};\n\ntemplate <> struct HasContainerTraits<std::string> : std::false_type {};\n\ntemplate <> struct HasContainerTraits<std::string_view> : std::false_type {};\n\ntemplate <typename T>\nstruct HasContainerTraits<T, std::void_t<typename T::value_type, decltype(std::declval<T>().begin()),\n                                         decltype(std::declval<T>().end()), decltype(std::declval<T>().size())>>\n    : std::true_type {};\n\ntemplate <typename T> inline constexpr bool IsContainer = HasContainerTraits<T>::value;\n\ntemplate <typename T, typename = void> struct HasStreamableTraits : std::false_type {};\n\ntemplate <typename T>\nstruct HasStreamableTraits<T, std::void_t<decltype(std::declval<std::ostream &>() << std::declval<T>())>> : std::true_type {};\n\ntemplate <typename T> inline constexpr bool IsStreamable = HasStreamableTraits<T>::value;\n\nconstexpr std::size_t repr_max_container_size = 5;\n\ntemplate <typename T> std::string repr(T const &val) {\n    if constexpr (std::is_same_v<T, bool>) {\n        return val ? \"true\" : \"false\";\n    } else if constexpr (std::is_convertible_v<T, std::string_view>) {\n        return '\"' + std::string{std::string_view{val}} + '\"';\n    } else if constexpr (IsContainer<T>) {\n        std::stringstream out;\n        out << \"{\";\n        const auto size = val.size();\n        if (size > 1) {\n            out << repr(*val.begin());\n            std::for_each(std::next(val.begin()),\n                          std::next(val.begin(), static_cast<typename T::iterator::difference_type>(\n                                                     std::min<std::size_t>(size, repr_max_container_size) - 1)),\n                          [&out](const auto &v) { out << \" \" << repr(v); });\n            if (size <= repr_max_container_size) {\n                out << \" \";\n            } else {\n                out << \"...\";\n            }\n        }\n        if (size > 0) {\n            out << repr(*std::prev(val.end()));\n        }\n        out << \"}\";\n        return out.str();\n    } else if constexpr (IsStreamable<T>) {\n        std::stringstream out;\n        out << val;\n        return out.str();\n    } else {\n        return \"<not representable>\";\n    }\n}\n\nnamespace {\n\ntemplate <typename T> constexpr bool standard_signed_integer = false;\ntemplate <> constexpr bool standard_signed_integer<signed char> = true;\ntemplate <> constexpr bool standard_signed_integer<short int> = true;\ntemplate <> constexpr bool standard_signed_integer<int> = true;\ntemplate <> constexpr bool standard_signed_integer<long int> = true;\ntemplate <> constexpr bool standard_signed_integer<long long int> = true;\n\ntemplate <typename T> constexpr bool standard_unsigned_integer = false;\ntemplate <> constexpr bool standard_unsigned_integer<unsigned char> = true;\ntemplate <> constexpr bool standard_unsigned_integer<unsigned short int> = true;\ntemplate <> constexpr bool standard_unsigned_integer<unsigned int> = true;\ntemplate <> constexpr bool standard_unsigned_integer<unsigned long int> = true;\ntemplate <> constexpr bool standard_unsigned_integer<unsigned long long int> = true;\n\n} // namespace\n\nconstexpr int radix_2 = 2;\nconstexpr int radix_8 = 8;\nconstexpr int radix_10 = 10;\nconstexpr int radix_16 = 16;\n\ntemplate <typename T> constexpr bool standard_integer = standard_signed_integer<T> || standard_unsigned_integer<T>;\n\ntemplate <class F, class Tuple, class Extra, std::size_t... I>\nconstexpr decltype(auto) apply_plus_one_impl(F &&f, Tuple &&t, Extra &&x, std::index_sequence<I...> /*unused*/) {\n    return std::invoke(std::forward<F>(f), std::get<I>(std::forward<Tuple>(t))..., std::forward<Extra>(x));\n}\n\ntemplate <class F, class Tuple, class Extra> constexpr decltype(auto) apply_plus_one(F &&f, Tuple &&t, Extra &&x) {\n    return details::apply_plus_one_impl(std::forward<F>(f), std::forward<Tuple>(t), std::forward<Extra>(x),\n                                        std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<Tuple>>>{});\n}\n\nconstexpr auto pointer_range(std::string_view s) noexcept {\n    return std::tuple(s.data(), s.data() + s.size());\n}\n\ntemplate <class CharT, class Traits>\nconstexpr bool starts_with(std::basic_string_view<CharT, Traits> prefix, std::basic_string_view<CharT, Traits> s) noexcept {\n    return s.substr(0, prefix.size()) == prefix;\n}\n\nenum class chars_format { scientific = 0xf1, fixed = 0xf2, hex = 0xf4, binary = 0xf8, general = fixed | scientific };\n\nstruct ConsumeBinaryPrefixResult {\n    bool is_binary;\n    std::string_view rest;\n};\n\nconstexpr auto consume_binary_prefix(std::string_view s) -> ConsumeBinaryPrefixResult {\n    if (starts_with(std::string_view{\"0b\"}, s) || starts_with(std::string_view{\"0B\"}, s)) {\n        s.remove_prefix(2);\n        return {true, s};\n    }\n    return {false, s};\n}\n\nstruct ConsumeHexPrefixResult {\n    bool is_hexadecimal;\n    std::string_view rest;\n};\n\nusing namespace std::literals;\n\nconstexpr auto consume_hex_prefix(std::string_view s) -> ConsumeHexPrefixResult {\n    if (starts_with(\"0x\"sv, s) || starts_with(\"0X\"sv, s)) {\n        s.remove_prefix(2);\n        return {true, s};\n    }\n    return {false, s};\n}\n\ntemplate <class T, auto Param> inline auto do_from_chars(std::string_view s) -> T {\n    T x{0};\n    auto [first, last] = pointer_range(s);\n    auto [ptr, ec] = std::from_chars(first, last, x, Param);\n    if (ec == std::errc()) {\n        if (ptr == last) {\n            return x;\n        }\n        throw std::invalid_argument{\"pattern '\" + std::string(s) + \"' does not match to the end\"};\n    }\n    if (ec == std::errc::invalid_argument) {\n        throw std::invalid_argument{\"pattern '\" + std::string(s) + \"' not found\"};\n    }\n    if (ec == std::errc::result_out_of_range) {\n        throw std::range_error{\"'\" + std::string(s) + \"' not representable\"};\n    }\n    return x; // unreachable\n}\n\ntemplate <class T, auto Param = 0> struct parse_number {\n    auto operator()(std::string_view s) -> T {\n        return do_from_chars<T, Param>(s);\n    }\n};\n\ntemplate <class T> struct parse_number<T, radix_2> {\n    auto operator()(std::string_view s) -> T {\n        if (auto [ok, rest] = consume_binary_prefix(s); ok) {\n            return do_from_chars<T, radix_2>(rest);\n        }\n        throw std::invalid_argument{\"pattern not found\"};\n    }\n};\n\ntemplate <class T> struct parse_number<T, radix_16> {\n    auto operator()(std::string_view s) -> T {\n        if (starts_with(\"0x\"sv, s) || starts_with(\"0X\"sv, s)) {\n            if (auto [ok, rest] = consume_hex_prefix(s); ok) {\n                try {\n                    return do_from_chars<T, radix_16>(rest);\n                } catch (const std::invalid_argument &err) {\n                    throw std::invalid_argument(\"Failed to parse '\" + std::string(s) + \"' as hexadecimal: \" + err.what());\n                } catch (const std::range_error &err) {\n                    throw std::range_error(\"Failed to parse '\" + std::string(s) + \"' as hexadecimal: \" + err.what());\n                }\n            }\n        } else {\n            // Allow passing hex numbers without prefix\n            // Shape 'x' already has to be specified\n            try {\n                return do_from_chars<T, radix_16>(s);\n            } catch (const std::invalid_argument &err) {\n                throw std::invalid_argument(\"Failed to parse '\" + std::string(s) + \"' as hexadecimal: \" + err.what());\n            } catch (const std::range_error &err) {\n                throw std::range_error(\"Failed to parse '\" + std::string(s) + \"' as hexadecimal: \" + err.what());\n            }\n        }\n\n        throw std::invalid_argument{\"pattern '\" + std::string(s) + \"' not identified as hexadecimal\"};\n    }\n};\n\ntemplate <class T> struct parse_number<T> {\n    auto operator()(std::string_view s) -> T {\n        auto [ok, rest] = consume_hex_prefix(s);\n        if (ok) {\n            try {\n                return do_from_chars<T, radix_16>(rest);\n            } catch (const std::invalid_argument &err) {\n                throw std::invalid_argument(\"Failed to parse '\" + std::string(s) + \"' as hexadecimal: \" + err.what());\n            } catch (const std::range_error &err) {\n                throw std::range_error(\"Failed to parse '\" + std::string(s) + \"' as hexadecimal: \" + err.what());\n            }\n        }\n\n        auto [ok_binary, rest_binary] = consume_binary_prefix(s);\n        if (ok_binary) {\n            try {\n                return do_from_chars<T, radix_2>(rest_binary);\n            } catch (const std::invalid_argument &err) {\n                throw std::invalid_argument(\"Failed to parse '\" + std::string(s) + \"' as binary: \" + err.what());\n            } catch (const std::range_error &err) {\n                throw std::range_error(\"Failed to parse '\" + std::string(s) + \"' as binary: \" + err.what());\n            }\n        }\n\n        if (starts_with(\"0\"sv, s)) {\n            try {\n                return do_from_chars<T, radix_8>(rest);\n            } catch (const std::invalid_argument &err) {\n                throw std::invalid_argument(\"Failed to parse '\" + std::string(s) + \"' as octal: \" + err.what());\n            } catch (const std::range_error &err) {\n                throw std::range_error(\"Failed to parse '\" + std::string(s) + \"' as octal: \" + err.what());\n            }\n        }\n\n        try {\n            return do_from_chars<T, radix_10>(rest);\n        } catch (const std::invalid_argument &err) {\n            throw std::invalid_argument(\"Failed to parse '\" + std::string(s) + \"' as decimal integer: \" + err.what());\n        } catch (const std::range_error &err) {\n            throw std::range_error(\"Failed to parse '\" + std::string(s) + \"' as decimal integer: \" + err.what());\n        }\n    }\n};\n\nnamespace {\n\ntemplate <class T> inline const auto generic_strtod = nullptr;\ntemplate <> inline const auto generic_strtod<float> = ARGPARSE_CUSTOM_STRTOF;\ntemplate <> inline const auto generic_strtod<double> = ARGPARSE_CUSTOM_STRTOD;\ntemplate <> inline const auto generic_strtod<long double> = ARGPARSE_CUSTOM_STRTOLD;\n\n} // namespace\n\ntemplate <class T> inline auto do_strtod(std::string const &s) -> T {\n    if (isspace(static_cast<unsigned char>(s[0])) || s[0] == '+') {\n        throw std::invalid_argument{\"pattern '\" + s + \"' not found\"};\n    }\n\n    auto [first, last] = pointer_range(s);\n    char *ptr;\n\n    errno = 0;\n    auto x = generic_strtod<T>(first, &ptr);\n    if (errno == 0) {\n        if (ptr == last) {\n            return x;\n        }\n        throw std::invalid_argument{\"pattern '\" + s + \"' does not match to the end\"};\n    }\n    if (errno == ERANGE) {\n        throw std::range_error{\"'\" + s + \"' not representable\"};\n    }\n    return x; // unreachable\n}\n\ntemplate <class T> struct parse_number<T, chars_format::general> {\n    auto operator()(std::string const &s) -> T {\n        if (auto r = consume_hex_prefix(s); r.is_hexadecimal) {\n            throw std::invalid_argument{\"chars_format::general does not parse hexfloat\"};\n        }\n        if (auto r = consume_binary_prefix(s); r.is_binary) {\n            throw std::invalid_argument{\"chars_format::general does not parse binfloat\"};\n        }\n\n        try {\n            return do_strtod<T>(s);\n        } catch (const std::invalid_argument &err) {\n            throw std::invalid_argument(\"Failed to parse '\" + s + \"' as number: \" + err.what());\n        } catch (const std::range_error &err) {\n            throw std::range_error(\"Failed to parse '\" + s + \"' as number: \" + err.what());\n        }\n    }\n};\n\ntemplate <class T> struct parse_number<T, chars_format::hex> {\n    auto operator()(std::string const &s) -> T {\n        if (auto r = consume_hex_prefix(s); !r.is_hexadecimal) {\n            throw std::invalid_argument{\"chars_format::hex parses hexfloat\"};\n        }\n        if (auto r = consume_binary_prefix(s); r.is_binary) {\n            throw std::invalid_argument{\"chars_format::hex does not parse binfloat\"};\n        }\n\n        try {\n            return do_strtod<T>(s);\n        } catch (const std::invalid_argument &err) {\n            throw std::invalid_argument(\"Failed to parse '\" + s + \"' as hexadecimal: \" + err.what());\n        } catch (const std::range_error &err) {\n            throw std::range_error(\"Failed to parse '\" + s + \"' as hexadecimal: \" + err.what());\n        }\n    }\n};\n\ntemplate <class T> struct parse_number<T, chars_format::binary> {\n    auto operator()(std::string const &s) -> T {\n        if (auto r = consume_hex_prefix(s); r.is_hexadecimal) {\n            throw std::invalid_argument{\"chars_format::binary does not parse hexfloat\"};\n        }\n        if (auto r = consume_binary_prefix(s); !r.is_binary) {\n            throw std::invalid_argument{\"chars_format::binary parses binfloat\"};\n        }\n\n        return do_strtod<T>(s);\n    }\n};\n\ntemplate <class T> struct parse_number<T, chars_format::scientific> {\n    auto operator()(std::string const &s) -> T {\n        if (auto r = consume_hex_prefix(s); r.is_hexadecimal) {\n            throw std::invalid_argument{\"chars_format::scientific does not parse hexfloat\"};\n        }\n        if (auto r = consume_binary_prefix(s); r.is_binary) {\n            throw std::invalid_argument{\"chars_format::scientific does not parse binfloat\"};\n        }\n        if (s.find_first_of(\"eE\") == std::string::npos) {\n            throw std::invalid_argument{\"chars_format::scientific requires exponent part\"};\n        }\n\n        try {\n            return do_strtod<T>(s);\n        } catch (const std::invalid_argument &err) {\n            throw std::invalid_argument(\"Failed to parse '\" + s + \"' as scientific notation: \" + err.what());\n        } catch (const std::range_error &err) {\n            throw std::range_error(\"Failed to parse '\" + s + \"' as scientific notation: \" + err.what());\n        }\n    }\n};\n\ntemplate <class T> struct parse_number<T, chars_format::fixed> {\n    auto operator()(std::string const &s) -> T {\n        if (auto r = consume_hex_prefix(s); r.is_hexadecimal) {\n            throw std::invalid_argument{\"chars_format::fixed does not parse hexfloat\"};\n        }\n        if (auto r = consume_binary_prefix(s); r.is_binary) {\n            throw std::invalid_argument{\"chars_format::fixed does not parse binfloat\"};\n        }\n        if (s.find_first_of(\"eE\") != std::string::npos) {\n            throw std::invalid_argument{\"chars_format::fixed does not parse exponent part\"};\n        }\n\n        try {\n            return do_strtod<T>(s);\n        } catch (const std::invalid_argument &err) {\n            throw std::invalid_argument(\"Failed to parse '\" + s + \"' as fixed notation: \" + err.what());\n        } catch (const std::range_error &err) {\n            throw std::range_error(\"Failed to parse '\" + s + \"' as fixed notation: \" + err.what());\n        }\n    }\n};\n\ntemplate <typename StrIt> std::string join(StrIt first, StrIt last, const std::string &separator) {\n    if (first == last) {\n        return \"\";\n    }\n    std::stringstream value;\n    value << *first;\n    ++first;\n    while (first != last) {\n        value << separator << *first;\n        ++first;\n    }\n    return value.str();\n}\n\ntemplate <typename T> struct can_invoke_to_string {\n    template <typename U> static auto test(int) -> decltype(std::to_string(std::declval<U>()), std::true_type{});\n\n    template <typename U> static auto test(...) -> std::false_type;\n\n    static constexpr bool value = decltype(test<T>(0))::value;\n};\n\ntemplate <typename T> struct IsChoiceTypeSupported {\n    using CleanType = typename std::decay<T>::type;\n    static const bool value = std::is_integral<CleanType>::value || std::is_same<CleanType, std::string>::value ||\n                              std::is_same<CleanType, std::string_view>::value || std::is_same<CleanType, const char *>::value;\n};\n\ntemplate <typename StringType> std::size_t get_levenshtein_distance(const StringType &s1, const StringType &s2) {\n    std::vector<std::vector<std::size_t>> dp(s1.size() + 1, std::vector<std::size_t>(s2.size() + 1, 0));\n\n    for (std::size_t i = 0; i <= s1.size(); ++i) {\n        for (std::size_t j = 0; j <= s2.size(); ++j) {\n            if (i == 0) {\n                dp[i][j] = j;\n            } else if (j == 0) {\n                dp[i][j] = i;\n            } else if (s1[i - 1] == s2[j - 1]) {\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                dp[i][j] = 1 + std::min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]});\n            }\n        }\n    }\n\n    return dp[s1.size()][s2.size()];\n}\n\ntemplate <typename ValueType>\nstd::string get_most_similar_string(const std::map<std::string, ValueType> &map, const std::string &input) {\n    std::string most_similar{};\n    std::size_t min_distance = std::numeric_limits<std::size_t>::max();\n\n    for (const auto &entry : map) {\n        std::size_t distance = get_levenshtein_distance(entry.first, input);\n        if (distance < min_distance) {\n            min_distance = distance;\n            most_similar = entry.first;\n        }\n    }\n\n    return most_similar;\n}\n\n} // namespace details\n\nenum class nargs_pattern { optional, any, at_least_one };\n\nenum class default_arguments : unsigned int {\n    none = 0,\n    help = 1,\n    version = 2,\n    all = help | version,\n};\n\ninline default_arguments operator&(const default_arguments &a, const default_arguments &b) {\n    return static_cast<default_arguments>(static_cast<std::underlying_type<default_arguments>::type>(a) &\n                                          static_cast<std::underlying_type<default_arguments>::type>(b));\n}\n\nclass ArgumentParser;\n\nclass Argument {\n    friend class ArgumentParser;\n    friend auto operator<<(std::ostream &stream, const ArgumentParser &parser) -> std::ostream &;\n\n    template <std::size_t N, std::size_t... I>\n    explicit Argument(std::string_view prefix_chars, std::array<std::string_view, N> &&a, std::index_sequence<I...> /*unused*/)\n        : m_accepts_optional_like_value(false), m_is_optional((is_optional(a[I], prefix_chars) || ...)), m_is_required(false),\n          m_is_repeatable(false), m_is_used(false), m_is_hidden(false), m_prefix_chars(prefix_chars) {\n        ((void)m_names.emplace_back(a[I]), ...);\n        std::sort(m_names.begin(), m_names.end(), [](const auto &lhs, const auto &rhs) {\n            return lhs.size() == rhs.size() ? lhs < rhs : lhs.size() < rhs.size();\n        });\n    }\n\npublic:\n    template <std::size_t N>\n    explicit Argument(std::string_view prefix_chars, std::array<std::string_view, N> &&a)\n        : Argument(prefix_chars, std::move(a), std::make_index_sequence<N>{}) {}\n\n    Argument &help(std::string help_text) {\n        m_help = std::move(help_text);\n        return *this;\n    }\n\n    Argument &metavar(std::string metavar) {\n        m_metavar = std::move(metavar);\n        return *this;\n    }\n\n    template <typename T> Argument &default_value(T &&value) {\n        m_num_args_range = NArgsRange{0, m_num_args_range.get_max()};\n        m_default_value_repr = details::repr(value);\n\n        if constexpr (std::is_convertible_v<T, std::string_view>) {\n            m_default_value_str = std::string{std::string_view{value}};\n        } else if constexpr (details::can_invoke_to_string<T>::value) {\n            m_default_value_str = std::to_string(value);\n        }\n\n        m_default_value = std::forward<T>(value);\n        return *this;\n    }\n\n    Argument &default_value(const char *value) {\n        return default_value(std::string(value));\n    }\n\n    Argument &required() {\n        m_is_required = true;\n        return *this;\n    }\n\n    Argument &implicit_value(std::any value) {\n        m_implicit_value = std::move(value);\n        m_num_args_range = NArgsRange{0, 0};\n        return *this;\n    }\n\n    // This is shorthand for:\n    //   program.add_argument(\"foo\")\n    //     .default_value(false)\n    //     .implicit_value(true)\n    Argument &flag() {\n        default_value(false);\n        implicit_value(true);\n        return *this;\n    }\n\n    template <class F, class... Args>\n    auto action(F &&callable,\n                Args &&...bound_args) -> std::enable_if_t<std::is_invocable_v<F, Args..., std::string const>, Argument &> {\n        using action_type =\n            std::conditional_t<std::is_void_v<std::invoke_result_t<F, Args..., std::string const>>, void_action, valued_action>;\n        if constexpr (sizeof...(Args) == 0) {\n            m_action.emplace<action_type>(std::forward<F>(callable));\n        } else {\n            m_action.emplace<action_type>(\n                [f = std::forward<F>(callable), tup = std::make_tuple(std::forward<Args>(bound_args)...)](\n                    std::string const &opt) mutable { return details::apply_plus_one(f, tup, opt); });\n        }\n        return *this;\n    }\n\n    auto &store_into(bool &var) {\n        flag();\n        if (m_default_value.has_value()) {\n            var = std::any_cast<bool>(m_default_value);\n        }\n        action([&var](const auto & /*unused*/) { var = true; });\n        return *this;\n    }\n\n    template <typename T, typename std::enable_if<std::is_integral<T>::value>::type * = nullptr> auto &store_into(T &var) {\n        if (m_default_value.has_value()) {\n            var = std::any_cast<T>(m_default_value);\n        }\n        action([&var](const auto &s) { var = details::parse_number<T, details::radix_10>()(s); });\n        return *this;\n    }\n\n    auto &store_into(double &var) {\n        if (m_default_value.has_value()) {\n            var = std::any_cast<double>(m_default_value);\n        }\n        action([&var](const auto &s) { var = details::parse_number<double, details::chars_format::general>()(s); });\n        return *this;\n    }\n\n    auto &store_into(std::string &var) {\n        if (m_default_value.has_value()) {\n            var = std::any_cast<std::string>(m_default_value);\n        }\n        action([&var](const std::string &s) { var = s; });\n        return *this;\n    }\n\n    auto &store_into(std::vector<std::string> &var) {\n        if (m_default_value.has_value()) {\n            var = std::any_cast<std::vector<std::string>>(m_default_value);\n        }\n        action([this, &var](const std::string &s) {\n            if (!m_is_used) {\n                var.clear();\n            }\n            m_is_used = true;\n            var.push_back(s);\n        });\n        return *this;\n    }\n\n    auto &store_into(std::vector<int> &var) {\n        if (m_default_value.has_value()) {\n            var = std::any_cast<std::vector<int>>(m_default_value);\n        }\n        action([this, &var](const std::string &s) {\n            if (!m_is_used) {\n                var.clear();\n            }\n            m_is_used = true;\n            var.push_back(details::parse_number<int, details::radix_10>()(s));\n        });\n        return *this;\n    }\n\n    auto &store_into(std::set<std::string> &var) {\n        if (m_default_value.has_value()) {\n            var = std::any_cast<std::set<std::string>>(m_default_value);\n        }\n        action([this, &var](const std::string &s) {\n            if (!m_is_used) {\n                var.clear();\n            }\n            m_is_used = true;\n            var.insert(s);\n        });\n        return *this;\n    }\n\n    auto &store_into(std::set<int> &var) {\n        if (m_default_value.has_value()) {\n            var = std::any_cast<std::set<int>>(m_default_value);\n        }\n        action([this, &var](const std::string &s) {\n            if (!m_is_used) {\n                var.clear();\n            }\n            m_is_used = true;\n            var.insert(details::parse_number<int, details::radix_10>()(s));\n        });\n        return *this;\n    }\n\n    auto &append() {\n        m_is_repeatable = true;\n        return *this;\n    }\n\n    // Cause the argument to be invisible in usage and help\n    auto &hidden() {\n        m_is_hidden = true;\n        return *this;\n    }\n\n    template <char Shape, typename T> auto scan() -> std::enable_if_t<std::is_arithmetic_v<T>, Argument &> {\n        static_assert(!(std::is_const_v<T> || std::is_volatile_v<T>), \"T should not be cv-qualified\");\n        auto is_one_of = [](char c, auto... x) constexpr { return ((c == x) || ...); };\n\n        if constexpr (is_one_of(Shape, 'd') && details::standard_integer<T>) {\n            action(details::parse_number<T, details::radix_10>());\n        } else if constexpr (is_one_of(Shape, 'i') && details::standard_integer<T>) {\n            action(details::parse_number<T>());\n        } else if constexpr (is_one_of(Shape, 'u') && details::standard_unsigned_integer<T>) {\n            action(details::parse_number<T, details::radix_10>());\n        } else if constexpr (is_one_of(Shape, 'b') && details::standard_unsigned_integer<T>) {\n            action(details::parse_number<T, details::radix_2>());\n        } else if constexpr (is_one_of(Shape, 'o') && details::standard_unsigned_integer<T>) {\n            action(details::parse_number<T, details::radix_8>());\n        } else if constexpr (is_one_of(Shape, 'x', 'X') && details::standard_unsigned_integer<T>) {\n            action(details::parse_number<T, details::radix_16>());\n        } else if constexpr (is_one_of(Shape, 'a', 'A') && std::is_floating_point_v<T>) {\n            action(details::parse_number<T, details::chars_format::hex>());\n        } else if constexpr (is_one_of(Shape, 'e', 'E') && std::is_floating_point_v<T>) {\n            action(details::parse_number<T, details::chars_format::scientific>());\n        } else if constexpr (is_one_of(Shape, 'f', 'F') && std::is_floating_point_v<T>) {\n            action(details::parse_number<T, details::chars_format::fixed>());\n        } else if constexpr (is_one_of(Shape, 'g', 'G') && std::is_floating_point_v<T>) {\n            action(details::parse_number<T, details::chars_format::general>());\n        } else {\n            static_assert(alignof(T) == 0, \"No scan specification for T\");\n        }\n\n        return *this;\n    }\n\n    Argument &nargs(std::size_t num_args) {\n        m_num_args_range = NArgsRange{num_args, num_args};\n        return *this;\n    }\n\n    Argument &nargs(std::size_t num_args_min, std::size_t num_args_max) {\n        m_num_args_range = NArgsRange{num_args_min, num_args_max};\n        return *this;\n    }\n\n    Argument &nargs(nargs_pattern pattern) {\n        switch (pattern) {\n        case nargs_pattern::optional:\n            m_num_args_range = NArgsRange{0, 1};\n            break;\n        case nargs_pattern::any:\n            m_num_args_range = NArgsRange{0, (std::numeric_limits<std::size_t>::max)()};\n            break;\n        case nargs_pattern::at_least_one:\n            m_num_args_range = NArgsRange{1, (std::numeric_limits<std::size_t>::max)()};\n            break;\n        }\n        return *this;\n    }\n\n    Argument &remaining() {\n        m_accepts_optional_like_value = true;\n        return nargs(nargs_pattern::any);\n    }\n\n    template <typename T> void add_choice(T &&choice) {\n        static_assert(details::IsChoiceTypeSupported<T>::value, \"Only string or integer type supported for choice\");\n        static_assert(std::is_convertible_v<T, std::string_view> || details::can_invoke_to_string<T>::value,\n                      \"Choice is not convertible to string_type\");\n        if (!m_choices.has_value()) {\n            m_choices = std::vector<std::string>{};\n        }\n\n        if constexpr (std::is_convertible_v<T, std::string_view>) {\n            m_choices.value().push_back(std::string{std::string_view{std::forward<T>(choice)}});\n        } else if constexpr (details::can_invoke_to_string<T>::value) {\n            m_choices.value().push_back(std::to_string(std::forward<T>(choice)));\n        }\n    }\n\n    Argument &choices() {\n        if (!m_choices.has_value()) {\n            throw std::runtime_error(\"Zero choices provided\");\n        }\n        return *this;\n    }\n\n    template <typename T, typename... U> Argument &choices(T &&first, U &&...rest) {\n        add_choice(std::forward<T>(first));\n        choices(std::forward<U>(rest)...);\n        return *this;\n    }\n\n    void find_default_value_in_choices_or_throw() const {\n\n        const auto &choices = m_choices.value();\n\n        if (m_default_value.has_value()) {\n            if (std::find(choices.begin(), choices.end(), m_default_value_str) == choices.end()) {\n                // provided arg not in list of allowed choices\n                // report error\n\n                std::string choices_as_csv =\n                    std::accumulate(choices.begin(), choices.end(), std::string(),\n                                    [](const std::string &a, const std::string &b) { return a + (a.empty() ? \"\" : \", \") + b; });\n\n                throw std::runtime_error(std::string{\"Invalid default value \"} + m_default_value_repr + \" - allowed options: {\" +\n                                         choices_as_csv + \"}\");\n            }\n        }\n    }\n\n    template <typename Iterator> void find_value_in_choices_or_throw(Iterator it) const {\n\n        const auto &choices = m_choices.value();\n\n        if (std::find(choices.begin(), choices.end(), *it) == choices.end()) {\n            // provided arg not in list of allowed choices\n            // report error\n\n            std::string choices_as_csv =\n                std::accumulate(choices.begin(), choices.end(), std::string(),\n                                [](const std::string &a, const std::string &b) { return a + (a.empty() ? \"\" : \", \") + b; });\n\n            throw std::runtime_error(std::string{\"Invalid argument \"} + details::repr(*it) + \" - allowed options: {\" +\n                                     choices_as_csv + \"}\");\n        }\n    }\n\n    /* The dry_run parameter can be set to true to avoid running the actions,\n     * and setting m_is_used. This may be used by a pre-processing step to do\n     * a first iteration over arguments.\n     */\n    template <typename Iterator>\n    Iterator consume(Iterator start, Iterator end, std::string_view used_name = {}, bool dry_run = false) {\n        if (!m_is_repeatable && m_is_used) {\n            throw std::runtime_error(std::string(\"Duplicate argument \").append(used_name));\n        }\n        m_used_name = used_name;\n\n        if (m_choices.has_value()) {\n            // Check each value in (start, end) and make sure\n            // it is in the list of allowed choices/options\n            std::size_t i = 0;\n            auto max_number_of_args = m_num_args_range.get_max();\n            for (auto it = start; it != end; ++it) {\n                if (i == max_number_of_args) {\n                    break;\n                }\n                find_value_in_choices_or_throw(it);\n                i += 1;\n            }\n        }\n\n        const auto num_args_max = m_num_args_range.get_max();\n        const auto num_args_min = m_num_args_range.get_min();\n        std::size_t dist = 0;\n        if (num_args_max == 0) {\n            if (!dry_run) {\n                m_values.emplace_back(m_implicit_value);\n                std::visit([](const auto &f) { f({}); }, m_action);\n                m_is_used = true;\n            }\n            return start;\n        }\n        if ((dist = static_cast<std::size_t>(std::distance(start, end))) >= num_args_min) {\n            if (num_args_max < dist) {\n                end = std::next(start, static_cast<typename Iterator::difference_type>(num_args_max));\n            }\n            if (!m_accepts_optional_like_value) {\n                end = std::find_if(start, end, std::bind(is_optional, std::placeholders::_1, m_prefix_chars));\n                dist = static_cast<std::size_t>(std::distance(start, end));\n                if (dist < num_args_min) {\n                    throw std::runtime_error(\"Too few arguments for '\" + std::string(m_used_name) + \"'.\");\n                }\n            }\n\n            struct ActionApply {\n                void operator()(valued_action &f) {\n                    std::transform(first, last, std::back_inserter(self.m_values), f);\n                }\n\n                void operator()(void_action &f) {\n                    std::for_each(first, last, f);\n                    if (!self.m_default_value.has_value()) {\n                        if (!self.m_accepts_optional_like_value) {\n                            self.m_values.resize(static_cast<std::size_t>(std::distance(first, last)));\n                        }\n                    }\n                }\n\n                Iterator first, last;\n                Argument &self;\n            };\n            if (!dry_run) {\n                std::visit(ActionApply{start, end, *this}, m_action);\n                m_is_used = true;\n            }\n            return end;\n        }\n        if (m_default_value.has_value()) {\n            if (!dry_run) {\n                m_is_used = true;\n            }\n            return start;\n        }\n        throw std::runtime_error(\"Too few arguments for '\" + std::string(m_used_name) + \"'.\");\n    }\n\n    /*\n     * @throws std::runtime_error if argument values are not valid\n     */\n    void validate() const {\n        if (m_is_optional) {\n            if (!m_is_used && !m_default_value.has_value() && m_is_required) {\n                throw_required_arg_not_used_error();\n            }\n            if (m_is_used && m_is_required && m_values.empty()) {\n                throw_required_arg_no_value_provided_error();\n            }\n        } else {\n            if (!m_num_args_range.contains(m_values.size()) && !m_default_value.has_value()) {\n                throw_nargs_range_validation_error();\n            }\n        }\n\n        if (m_choices.has_value()) {\n            // Make sure the default value (if provided)\n            // is in the list of choices\n            find_default_value_in_choices_or_throw();\n        }\n    }\n\n    std::string get_names_csv(char separator = ',') const {\n        return std::accumulate(m_names.begin(), m_names.end(), std::string{\"\"},\n                               [&](const std::string &result, const std::string &name) {\n                                   return result.empty() ? name : result + separator + name;\n                               });\n    }\n\n    std::string get_usage_full() const {\n        std::stringstream usage;\n\n        usage << get_names_csv('/');\n        const std::string metavar = !m_metavar.empty() ? m_metavar : \"VAR\";\n        if (m_num_args_range.get_max() > 0) {\n            usage << \" \" << metavar;\n            if (m_num_args_range.get_max() > 1) {\n                usage << \"...\";\n            }\n        }\n        return usage.str();\n    }\n\n    std::string get_inline_usage() const {\n        std::stringstream usage;\n        // Find the longest variant to show in the usage string\n        std::string longest_name = m_names.front();\n        for (const auto &s : m_names) {\n            if (s.size() > longest_name.size()) {\n                longest_name = s;\n            }\n        }\n        if (!m_is_required) {\n            usage << \"[\";\n        }\n        usage << longest_name;\n        const std::string metavar = !m_metavar.empty() ? m_metavar : \"VAR\";\n        if (m_num_args_range.get_max() > 0) {\n            usage << \" \" << metavar;\n            if (m_num_args_range.get_max() > 1 && m_metavar.find(\"> <\") == std::string::npos) {\n                usage << \"...\";\n            }\n        }\n        if (!m_is_required) {\n            usage << \"]\";\n        }\n        if (m_is_repeatable) {\n            usage << \"...\";\n        }\n        return usage.str();\n    }\n\n    std::size_t get_arguments_length() const {\n\n        std::size_t names_size = std::accumulate(std::begin(m_names), std::end(m_names), std::size_t(0),\n                                                 [](const auto &sum, const auto &s) { return sum + s.size(); });\n\n        if (is_positional(m_names.front(), m_prefix_chars)) {\n            // A set metavar means this replaces the names\n            if (!m_metavar.empty()) {\n                // Indent and metavar\n                return 2 + m_metavar.size();\n            }\n\n            // Indent and space-separated\n            return 2 + names_size + (m_names.size() - 1);\n        }\n        // Is an option - include both names _and_ metavar\n        // size = text + (\", \" between names)\n        std::size_t size = names_size + 2 * (m_names.size() - 1);\n        if (!m_metavar.empty() && m_num_args_range == NArgsRange{1, 1}) {\n            size += m_metavar.size() + 1;\n        }\n        return size + 2; // indent\n    }\n\n    friend std::ostream &operator<<(std::ostream &stream, const Argument &argument) {\n        std::stringstream name_stream;\n        name_stream << \"  \"; // indent\n        if (argument.is_positional(argument.m_names.front(), argument.m_prefix_chars)) {\n            if (!argument.m_metavar.empty()) {\n                name_stream << argument.m_metavar;\n            } else {\n                name_stream << details::join(argument.m_names.begin(), argument.m_names.end(), \" \");\n            }\n        } else {\n            name_stream << details::join(argument.m_names.begin(), argument.m_names.end(), \", \");\n            // If we have a metavar, and one narg - print the metavar\n            if (!argument.m_metavar.empty() && argument.m_num_args_range == NArgsRange{1, 1}) {\n                name_stream << \" \" << argument.m_metavar;\n            } else if (!argument.m_metavar.empty() &&\n                       argument.m_num_args_range.get_min() == argument.m_num_args_range.get_max() &&\n                       argument.m_metavar.find(\"> <\") != std::string::npos) {\n                name_stream << \" \" << argument.m_metavar;\n            }\n        }\n\n        // align multiline help message\n        auto stream_width = stream.width();\n        auto name_padding = std::string(name_stream.str().size(), ' ');\n        auto pos = std::string::size_type{};\n        auto prev = std::string::size_type{};\n        auto first_line = true;\n        auto hspace = \"  \"; // minimal space between name and help message\n        stream << name_stream.str();\n        std::string_view help_view(argument.m_help);\n        while ((pos = argument.m_help.find('\\n', prev)) != std::string::npos) {\n            auto line = help_view.substr(prev, pos - prev + 1);\n            if (first_line) {\n                stream << hspace << line;\n                first_line = false;\n            } else {\n                stream.width(stream_width);\n                stream << name_padding << hspace << line;\n            }\n            prev += pos - prev + 1;\n        }\n        if (first_line) {\n            stream << hspace << argument.m_help;\n        } else {\n            auto leftover = help_view.substr(prev, argument.m_help.size() - prev);\n            if (!leftover.empty()) {\n                stream.width(stream_width);\n                stream << name_padding << hspace << leftover;\n            }\n        }\n\n        // print nargs spec\n        if (!argument.m_help.empty()) {\n            stream << \" \";\n        }\n        stream << argument.m_num_args_range;\n\n        bool add_space = false;\n        if (argument.m_default_value.has_value() && argument.m_num_args_range != NArgsRange{0, 0}) {\n            stream << \"[default: \" << argument.m_default_value_repr << \"]\";\n            add_space = true;\n        } else if (argument.m_is_required) {\n            stream << \"[required]\";\n            add_space = true;\n        }\n        if (argument.m_is_repeatable) {\n            if (add_space) {\n                stream << \" \";\n            }\n            stream << \"[may be repeated]\";\n        }\n        stream << \"\\n\";\n        return stream;\n    }\n\n    template <typename T> bool operator!=(const T &rhs) const {\n        return !(*this == rhs);\n    }\n\n    /*\n     * Compare to an argument value of known type\n     * @throws std::logic_error in case of incompatible types\n     */\n    template <typename T> bool operator==(const T &rhs) const {\n        if constexpr (!details::IsContainer<T>) {\n            return get<T>() == rhs;\n        } else {\n            using ValueType = typename T::value_type;\n            auto lhs = get<T>();\n            return std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs), std::end(rhs),\n                              [](const auto &a, const auto &b) { return std::any_cast<const ValueType &>(a) == b; });\n        }\n    }\n\n    /*\n     * positional:\n     *    _empty_\n     *    '-'\n     *    '-' decimal-literal\n     *    !'-' anything\n     */\n    static bool is_positional(std::string_view name, std::string_view prefix_chars) {\n        auto first = lookahead(name);\n\n        if (first == eof) {\n            return true;\n        }\n        if (prefix_chars.find(static_cast<char>(first)) != std::string_view::npos) {\n            name.remove_prefix(1);\n            if (name.empty()) {\n                return true;\n            }\n            return is_decimal_literal(name);\n        }\n        return true;\n    }\n\nprivate:\n    class NArgsRange {\n        std::size_t m_min;\n        std::size_t m_max;\n\n    public:\n        NArgsRange(std::size_t minimum, std::size_t maximum) : m_min(minimum), m_max(maximum) {\n            if (minimum > maximum) {\n                throw std::logic_error(\"Range of number of arguments is invalid\");\n            }\n        }\n\n        bool contains(std::size_t value) const {\n            return value >= m_min && value <= m_max;\n        }\n\n        bool is_exact() const {\n            return m_min == m_max;\n        }\n\n        bool is_right_bounded() const {\n            return m_max < (std::numeric_limits<std::size_t>::max)();\n        }\n\n        std::size_t get_min() const {\n            return m_min;\n        }\n\n        std::size_t get_max() const {\n            return m_max;\n        }\n\n        // Print help message\n        friend auto operator<<(std::ostream &stream, const NArgsRange &range) -> std::ostream & {\n            if (range.m_min == range.m_max) {\n                if (range.m_min != 0 && range.m_min != 1) {\n                    stream << \"[nargs: \" << range.m_min << \"] \";\n                }\n            } else {\n                if (range.m_max == (std::numeric_limits<std::size_t>::max)()) {\n                    stream << \"[nargs: \" << range.m_min << \" or more] \";\n                } else {\n                    stream << \"[nargs=\" << range.m_min << \"..\" << range.m_max << \"] \";\n                }\n            }\n            return stream;\n        }\n\n        bool operator==(const NArgsRange &rhs) const {\n            return rhs.m_min == m_min && rhs.m_max == m_max;\n        }\n\n        bool operator!=(const NArgsRange &rhs) const {\n            return !(*this == rhs);\n        }\n    };\n\n    void throw_nargs_range_validation_error() const {\n        std::stringstream stream;\n        if (!m_used_name.empty()) {\n            stream << m_used_name << \": \";\n        } else {\n            stream << m_names.front() << \": \";\n        }\n        if (m_num_args_range.is_exact()) {\n            stream << m_num_args_range.get_min();\n        } else if (m_num_args_range.is_right_bounded()) {\n            stream << m_num_args_range.get_min() << \" to \" << m_num_args_range.get_max();\n        } else {\n            stream << m_num_args_range.get_min() << \" or more\";\n        }\n        stream << \" argument(s) expected. \" << m_values.size() << \" provided.\";\n        throw std::runtime_error(stream.str());\n    }\n\n    void throw_required_arg_not_used_error() const {\n        std::stringstream stream;\n        stream << m_names.front() << \": required.\";\n        throw std::runtime_error(stream.str());\n    }\n\n    void throw_required_arg_no_value_provided_error() const {\n        std::stringstream stream;\n        stream << m_used_name << \": no value provided.\";\n        throw std::runtime_error(stream.str());\n    }\n\n    static constexpr int eof = std::char_traits<char>::eof();\n\n    static auto lookahead(std::string_view s) -> int {\n        if (s.empty()) {\n            return eof;\n        }\n        return static_cast<int>(static_cast<unsigned char>(s[0]));\n    }\n\n    /*\n     * decimal-literal:\n     *    '0'\n     *    nonzero-digit digit-sequence_opt\n     *    integer-part fractional-part\n     *    fractional-part\n     *    integer-part '.' exponent-part_opt\n     *    integer-part exponent-part\n     *\n     * integer-part:\n     *    digit-sequence\n     *\n     * fractional-part:\n     *    '.' post-decimal-point\n     *\n     * post-decimal-point:\n     *    digit-sequence exponent-part_opt\n     *\n     * exponent-part:\n     *    'e' post-e\n     *    'E' post-e\n     *\n     * post-e:\n     *    sign_opt digit-sequence\n     *\n     * sign: one of\n     *    '+' '-'\n     */\n    static bool is_decimal_literal(std::string_view s) {\n        auto is_digit = [](auto c) constexpr {\n            switch (c) {\n            case '0':\n            case '1':\n            case '2':\n            case '3':\n            case '4':\n            case '5':\n            case '6':\n            case '7':\n            case '8':\n            case '9':\n                return true;\n            default:\n                return false;\n            }\n        };\n\n        // precondition: we have consumed or will consume at least one digit\n        auto consume_digits = [=](std::string_view sd) {\n            // NOLINTNEXTLINE(readability-qualified-auto)\n            auto it = std::find_if_not(std::begin(sd), std::end(sd), is_digit);\n            return sd.substr(static_cast<std::size_t>(it - std::begin(sd)));\n        };\n\n        switch (lookahead(s)) {\n        case '0': {\n            s.remove_prefix(1);\n            if (s.empty()) {\n                return true;\n            }\n            goto integer_part;\n        }\n        case '1':\n        case '2':\n        case '3':\n        case '4':\n        case '5':\n        case '6':\n        case '7':\n        case '8':\n        case '9': {\n            s = consume_digits(s);\n            if (s.empty()) {\n                return true;\n            }\n            goto integer_part_consumed;\n        }\n        case '.': {\n            s.remove_prefix(1);\n            goto post_decimal_point;\n        }\n        default:\n            return false;\n        }\n\n    integer_part:\n        s = consume_digits(s);\n    integer_part_consumed:\n        switch (lookahead(s)) {\n        case '.': {\n            s.remove_prefix(1);\n            if (is_digit(lookahead(s))) {\n                goto post_decimal_point;\n            } else {\n                goto exponent_part_opt;\n            }\n        }\n        case 'e':\n        case 'E': {\n            s.remove_prefix(1);\n            goto post_e;\n        }\n        default:\n            return false;\n        }\n\n    post_decimal_point:\n        if (is_digit(lookahead(s))) {\n            s = consume_digits(s);\n            goto exponent_part_opt;\n        }\n        return false;\n\n    exponent_part_opt:\n        switch (lookahead(s)) {\n        case eof:\n            return true;\n        case 'e':\n        case 'E': {\n            s.remove_prefix(1);\n            goto post_e;\n        }\n        default:\n            return false;\n        }\n\n    post_e:\n        switch (lookahead(s)) {\n        case '-':\n        case '+':\n            s.remove_prefix(1);\n        }\n        if (is_digit(lookahead(s))) {\n            s = consume_digits(s);\n            return s.empty();\n        }\n        return false;\n    }\n\n    static bool is_optional(std::string_view name, std::string_view prefix_chars) {\n        return !is_positional(name, prefix_chars);\n    }\n\n    /*\n     * Get argument value given a type\n     * @throws std::logic_error in case of incompatible types\n     */\n    template <typename T> T get() const {\n        if (!m_values.empty()) {\n            if constexpr (details::IsContainer<T>) {\n                return any_cast_container<T>(m_values);\n            } else {\n                return std::any_cast<T>(m_values.front());\n            }\n        }\n        if (m_default_value.has_value()) {\n            return std::any_cast<T>(m_default_value);\n        }\n        if constexpr (details::IsContainer<T>) {\n            if (!m_accepts_optional_like_value) {\n                return any_cast_container<T>(m_values);\n            }\n        }\n\n        throw std::logic_error(\"No value provided for '\" + m_names.back() + \"'.\");\n    }\n\n    /*\n     * Get argument value given a type.\n     * @pre The object has no default value.\n     * @returns The stored value if any, std::nullopt otherwise.\n     */\n    template <typename T> auto present() const -> std::optional<T> {\n        if (m_default_value.has_value()) {\n            throw std::logic_error(\"Argument with default value always presents\");\n        }\n        if (m_values.empty()) {\n            return std::nullopt;\n        }\n        if constexpr (details::IsContainer<T>) {\n            return any_cast_container<T>(m_values);\n        }\n        return std::any_cast<T>(m_values.front());\n    }\n\n    template <typename T> static auto any_cast_container(const std::vector<std::any> &operand) -> T {\n        using ValueType = typename T::value_type;\n\n        T result;\n        std::transform(std::begin(operand), std::end(operand), std::back_inserter(result),\n                       [](const auto &value) { return std::any_cast<ValueType>(value); });\n        return result;\n    }\n\n    void set_usage_newline_counter(int i) {\n        m_usage_newline_counter = i;\n    }\n\n    void set_group_idx(std::size_t i) {\n        m_group_idx = i;\n    }\n\n    std::vector<std::string> m_names;\n    std::string_view m_used_name;\n    std::string m_help;\n    std::string m_metavar;\n    std::any m_default_value;\n    std::string m_default_value_repr;\n    std::optional<std::string> m_default_value_str; // used for checking default_value against choices\n    std::any m_implicit_value;\n    std::optional<std::vector<std::string>> m_choices{std::nullopt};\n    using valued_action = std::function<std::any(const std::string &)>;\n    using void_action = std::function<void(const std::string &)>;\n    std::variant<valued_action, void_action> m_action{std::in_place_type<valued_action>,\n                                                      [](const std::string &value) { return value; }};\n    std::vector<std::any> m_values;\n    NArgsRange m_num_args_range{1, 1};\n    // Bit field of bool values. Set default value in ctor.\n    bool m_accepts_optional_like_value : 1;\n    bool m_is_optional : 1;\n    bool m_is_required : 1;\n    bool m_is_repeatable : 1;\n    bool m_is_used : 1;\n    bool m_is_hidden : 1;            // if set, does not appear in usage or help\n    std::string_view m_prefix_chars; // ArgumentParser has the prefix_chars\n    int m_usage_newline_counter = 0;\n    std::size_t m_group_idx = 0;\n};\n\nclass ArgumentParser {\npublic:\n    explicit ArgumentParser(std::string program_name = {}, std::string version = \"1.0\",\n                            default_arguments add_args = default_arguments::all, bool exit_on_default_arguments = true,\n                            std::ostream &os = std::cout)\n        : m_program_name(std::move(program_name)), m_version(std::move(version)),\n          m_exit_on_default_arguments(exit_on_default_arguments), m_parser_path(m_program_name) {\n        if ((add_args & default_arguments::help) == default_arguments::help) {\n            add_argument(\"-h\", \"--help\")\n                .action([&](const auto & /*unused*/) {\n                    os << help().str();\n                    if (m_exit_on_default_arguments) {\n                        std::exit(0);\n                    }\n                })\n                .default_value(false)\n                .help(\"shows help message and exits\")\n                .implicit_value(true)\n                .nargs(0);\n        }\n        if ((add_args & default_arguments::version) == default_arguments::version) {\n            add_argument(\"-v\", \"--version\")\n                .action([&](const auto & /*unused*/) {\n                    os << m_version << std::endl;\n                    if (m_exit_on_default_arguments) {\n                        std::exit(0);\n                    }\n                })\n                .default_value(false)\n                .help(\"prints version information and exits\")\n                .implicit_value(true)\n                .nargs(0);\n        }\n    }\n\n    ~ArgumentParser() = default;\n\n    // ArgumentParser is meant to be used in a single function.\n    // Setup everything and parse arguments in one place.\n    //\n    // ArgumentParser internally uses std::string_views,\n    // references, iterators, etc.\n    // Many of these elements become invalidated after a copy or move.\n    ArgumentParser(const ArgumentParser &other) = delete;\n    ArgumentParser &operator=(const ArgumentParser &other) = delete;\n    ArgumentParser(ArgumentParser &&) noexcept = delete;\n    ArgumentParser &operator=(ArgumentParser &&) = delete;\n\n    explicit operator bool() const {\n        auto arg_used =\n            std::any_of(m_argument_map.cbegin(), m_argument_map.cend(), [](auto &it) { return it.second->m_is_used; });\n        auto subparser_used = std::any_of(m_subparser_used.cbegin(), m_subparser_used.cend(), [](auto &it) { return it.second; });\n\n        return m_is_parsed && (arg_used || subparser_used);\n    }\n\n    // Parameter packing\n    // Call add_argument with variadic number of string arguments\n    template <typename... Targs> Argument &add_argument(Targs... f_args) {\n        using array_of_sv = std::array<std::string_view, sizeof...(Targs)>;\n        auto argument = m_optional_arguments.emplace(std::cend(m_optional_arguments), m_prefix_chars, array_of_sv{f_args...});\n\n        if (!argument->m_is_optional) {\n            m_positional_arguments.splice(std::cend(m_positional_arguments), m_optional_arguments, argument);\n        }\n        argument->set_usage_newline_counter(m_usage_newline_counter);\n        argument->set_group_idx(m_group_names.size());\n\n        index_argument(argument);\n        return *argument;\n    }\n\n    class MutuallyExclusiveGroup {\n        friend class ArgumentParser;\n\n    public:\n        MutuallyExclusiveGroup() = delete;\n\n        explicit MutuallyExclusiveGroup(ArgumentParser &parent, bool required = false)\n            : m_parent(parent), m_required(required), m_elements({}) {}\n\n        MutuallyExclusiveGroup(const MutuallyExclusiveGroup &other) = delete;\n        MutuallyExclusiveGroup &operator=(const MutuallyExclusiveGroup &other) = delete;\n\n        MutuallyExclusiveGroup(MutuallyExclusiveGroup &&other) noexcept\n            : m_parent(other.m_parent), m_required(other.m_required), m_elements(std::move(other.m_elements)) {\n            other.m_elements.clear();\n        }\n\n        template <typename... Targs> Argument &add_argument(Targs... f_args) {\n            auto &argument = m_parent.add_argument(std::forward<Targs>(f_args)...);\n            m_elements.push_back(&argument);\n            argument.set_usage_newline_counter(m_parent.m_usage_newline_counter);\n            argument.set_group_idx(m_parent.m_group_names.size());\n            return argument;\n        }\n\n    private:\n        ArgumentParser &m_parent;\n        bool m_required{false};\n        std::vector<Argument *> m_elements{};\n    };\n\n    MutuallyExclusiveGroup &add_mutually_exclusive_group(bool required = false) {\n        m_mutually_exclusive_groups.emplace_back(*this, required);\n        return m_mutually_exclusive_groups.back();\n    }\n\n    // Parameter packed add_parents method\n    // Accepts a variadic number of ArgumentParser objects\n    template <typename... Targs> ArgumentParser &add_parents(const Targs &...f_args) {\n        for (const ArgumentParser &parent_parser : {std::ref(f_args)...}) {\n            for (const auto &argument : parent_parser.m_positional_arguments) {\n                auto it = m_positional_arguments.insert(std::cend(m_positional_arguments), argument);\n                index_argument(it);\n            }\n            for (const auto &argument : parent_parser.m_optional_arguments) {\n                auto it = m_optional_arguments.insert(std::cend(m_optional_arguments), argument);\n                index_argument(it);\n            }\n        }\n        return *this;\n    }\n\n    // Ask for the next optional arguments to be displayed on a separate\n    // line in usage() output. Only effective if set_usage_max_line_width() is\n    // also used.\n    ArgumentParser &add_usage_newline() {\n        ++m_usage_newline_counter;\n        return *this;\n    }\n\n    // Ask for the next optional arguments to be displayed in a separate section\n    // in usage() and help (<< *this) output.\n    // For usage(), this is only effective if set_usage_max_line_width() is\n    // also used.\n    ArgumentParser &add_group(std::string group_name) {\n        m_group_names.emplace_back(std::move(group_name));\n        return *this;\n    }\n\n    ArgumentParser &add_description(std::string description) {\n        m_description = std::move(description);\n        return *this;\n    }\n\n    ArgumentParser &add_epilog(std::string epilog) {\n        m_epilog = std::move(epilog);\n        return *this;\n    }\n\n    // Add a un-documented/hidden alias for an argument.\n    // Ideally we'd want this to be a method of Argument, but Argument\n    // does not own its owing ArgumentParser.\n    ArgumentParser &add_hidden_alias_for(Argument &arg, std::string_view alias) {\n        for (auto it = m_optional_arguments.begin(); it != m_optional_arguments.end(); ++it) {\n            if (&(*it) == &arg) {\n                m_argument_map.insert_or_assign(std::string(alias), it);\n                return *this;\n            }\n        }\n        throw std::logic_error(\"Argument is not an optional argument of this parser\");\n    }\n\n    /* Getter for arguments and subparsers.\n     * @throws std::logic_error in case of an invalid argument or subparser name\n     */\n    template <typename T = Argument> T &at(std::string_view name) {\n        if constexpr (std::is_same_v<T, Argument>) {\n            return (*this)[name];\n        } else {\n            std::string str_name(name);\n            auto subparser_it = m_subparser_map.find(str_name);\n            if (subparser_it != m_subparser_map.end()) {\n                return subparser_it->second->get();\n            }\n            throw std::logic_error(\"No such subparser: \" + str_name);\n        }\n    }\n\n    ArgumentParser &set_prefix_chars(std::string prefix_chars) {\n        m_prefix_chars = std::move(prefix_chars);\n        return *this;\n    }\n\n    ArgumentParser &set_assign_chars(std::string assign_chars) {\n        m_assign_chars = std::move(assign_chars);\n        return *this;\n    }\n\n    /* Call parse_args_internal - which does all the work\n     * Then, validate the parsed arguments\n     * This variant is used mainly for testing\n     * @throws std::runtime_error in case of any invalid argument\n     */\n    void parse_args(const std::vector<std::string> &arguments) {\n        parse_args_internal(arguments);\n        // Check if all arguments are parsed\n        for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) {\n            argument->validate();\n        }\n\n        // Check each mutually exclusive group and make sure\n        // there are no constraint violations\n        for (const auto &group : m_mutually_exclusive_groups) {\n            auto mutex_argument_used{false};\n            Argument *mutex_argument_it{nullptr};\n            for (Argument *arg : group.m_elements) {\n                if (!mutex_argument_used && arg->m_is_used) {\n                    mutex_argument_used = true;\n                    mutex_argument_it = arg;\n                } else if (mutex_argument_used && arg->m_is_used) {\n                    // Violation\n                    throw std::runtime_error(\"Argument '\" + arg->get_usage_full() + \"' not allowed with '\" +\n                                             mutex_argument_it->get_usage_full() + \"'\");\n                }\n            }\n\n            if (!mutex_argument_used && group.m_required) {\n                // at least one argument from the group is\n                // required\n                std::string argument_names{};\n                std::size_t i = 0;\n                std::size_t size = group.m_elements.size();\n                for (Argument *arg : group.m_elements) {\n                    if (i + 1 == size) {\n                        // last\n                        argument_names += std::string(\"'\") + arg->get_usage_full() + std::string(\"' \");\n                    } else {\n                        argument_names += std::string(\"'\") + arg->get_usage_full() + std::string(\"' or \");\n                    }\n                    i += 1;\n                }\n                throw std::runtime_error(\"One of the arguments \" + argument_names + \"is required\");\n            }\n        }\n    }\n\n    /* Call parse_known_args_internal - which does all the work\n     * Then, validate the parsed arguments\n     * This variant is used mainly for testing\n     * @throws std::runtime_error in case of any invalid argument\n     */\n    std::vector<std::string> parse_known_args(const std::vector<std::string> &arguments) {\n        auto unknown_arguments = parse_known_args_internal(arguments);\n        // Check if all arguments are parsed\n        for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) {\n            argument->validate();\n        }\n        return unknown_arguments;\n    }\n\n    /* Main entry point for parsing command-line arguments using this\n     * ArgumentParser\n     * @throws std::runtime_error in case of any invalid argument\n     */\n    // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays)\n    void parse_args(int argc, const char *const argv[]) {\n        parse_args({argv, argv + argc});\n    }\n\n    /* Main entry point for parsing command-line arguments using this\n     * ArgumentParser\n     * @throws std::runtime_error in case of any invalid argument\n     */\n    // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays)\n    auto parse_known_args(int argc, const char *const argv[]) {\n        return parse_known_args({argv, argv + argc});\n    }\n\n    /* Getter for options with default values.\n     * @throws std::logic_error if parse_args() has not been previously called\n     * @throws std::logic_error if there is no such option\n     * @throws std::logic_error if the option has no value\n     * @throws std::bad_any_cast if the option is not of type T\n     */\n    template <typename T = std::string> T get(std::string_view arg_name) const {\n        if (!m_is_parsed) {\n            throw std::logic_error(\"Nothing parsed, no arguments are available.\");\n        }\n        return (*this)[arg_name].get<T>();\n    }\n\n    /* Getter for options without default values.\n     * @pre The option has no default value.\n     * @throws std::logic_error if there is no such option\n     * @throws std::bad_any_cast if the option is not of type T\n     */\n    template <typename T = std::string> auto present(std::string_view arg_name) const -> std::optional<T> {\n        return (*this)[arg_name].present<T>();\n    }\n\n    /* Getter that returns true for user-supplied options. Returns false if not\n     * user-supplied, even with a default value.\n     */\n    auto is_used(std::string_view arg_name) const {\n        return (*this)[arg_name].m_is_used;\n    }\n\n    /* Getter that returns true if a subcommand is used.\n     */\n    auto is_subcommand_used(std::string_view subcommand_name) const {\n        return m_subparser_used.at(std::string(subcommand_name));\n    }\n\n    /* Getter that returns true if a subcommand is used.\n     */\n    auto is_subcommand_used(const ArgumentParser &subparser) const {\n        return is_subcommand_used(subparser.m_program_name);\n    }\n\n    /* Indexing operator. Return a reference to an Argument object\n     * Used in conjunction with Argument.operator== e.g., parser[\"foo\"] == true\n     * @throws std::logic_error in case of an invalid argument name\n     */\n    Argument &operator[](std::string_view arg_name) const {\n        std::string name(arg_name);\n        auto it = m_argument_map.find(name);\n        if (it != m_argument_map.end()) {\n            return *(it->second);\n        }\n        if (!is_valid_prefix_char(arg_name.front())) {\n            const auto legal_prefix_char = get_any_valid_prefix_char();\n            const auto prefix = std::string(1, legal_prefix_char);\n\n            // \"-\" + arg_name\n            name = prefix + name;\n            it = m_argument_map.find(name);\n            if (it != m_argument_map.end()) {\n                return *(it->second);\n            }\n            // \"--\" + arg_name\n            name = prefix + name;\n            it = m_argument_map.find(name);\n            if (it != m_argument_map.end()) {\n                return *(it->second);\n            }\n        }\n        throw std::logic_error(\"No such argument: \" + std::string(arg_name));\n    }\n\n    // Print help message\n    friend auto operator<<(std::ostream &stream, const ArgumentParser &parser) -> std::ostream & {\n        stream.setf(std::ios_base::left);\n\n        auto longest_arg_length = parser.get_length_of_longest_argument();\n\n        stream << parser.usage() << \"\\n\\n\";\n\n        if (!parser.m_description.empty()) {\n            stream << parser.m_description << \"\\n\\n\";\n        }\n\n        const bool has_visible_positional_args =\n            std::find_if(parser.m_positional_arguments.begin(), parser.m_positional_arguments.end(),\n                         [](const auto &argument) { return !argument.m_is_hidden; }) != parser.m_positional_arguments.end();\n        if (has_visible_positional_args) {\n            stream << \"Positional arguments:\\n\";\n        }\n\n        for (const auto &argument : parser.m_positional_arguments) {\n            if (!argument.m_is_hidden) {\n                stream.width(static_cast<std::streamsize>(longest_arg_length));\n                stream << argument;\n            }\n        }\n\n        if (!parser.m_optional_arguments.empty()) {\n            stream << (!has_visible_positional_args ? \"\" : \"\\n\") << \"Optional arguments:\\n\";\n        }\n\n        for (const auto &argument : parser.m_optional_arguments) {\n            if (argument.m_group_idx == 0 && !argument.m_is_hidden) {\n                stream.width(static_cast<std::streamsize>(longest_arg_length));\n                stream << argument;\n            }\n        }\n\n        for (size_t i_group = 0; i_group < parser.m_group_names.size(); ++i_group) {\n            stream << \"\\n\" << parser.m_group_names[i_group] << \" (detailed usage):\\n\";\n            for (const auto &argument : parser.m_optional_arguments) {\n                if (argument.m_group_idx == i_group + 1 && !argument.m_is_hidden) {\n                    stream.width(static_cast<std::streamsize>(longest_arg_length));\n                    stream << argument;\n                }\n            }\n        }\n\n        bool has_visible_subcommands = std::any_of(parser.m_subparser_map.begin(), parser.m_subparser_map.end(),\n                                                   [](auto &p) { return !p.second->get().m_suppress; });\n\n        if (has_visible_subcommands) {\n            stream << (parser.m_positional_arguments.empty() ? (parser.m_optional_arguments.empty() ? \"\" : \"\\n\") : \"\\n\")\n                   << \"Subcommands:\\n\";\n            for (const auto &[command, subparser] : parser.m_subparser_map) {\n                if (subparser->get().m_suppress) {\n                    continue;\n                }\n\n                stream << std::setw(2) << \" \";\n                stream << std::setw(static_cast<int>(longest_arg_length - 2)) << command;\n                stream << \" \" << subparser->get().m_description << \"\\n\";\n            }\n        }\n\n        if (!parser.m_epilog.empty()) {\n            stream << '\\n';\n            stream << parser.m_epilog << \"\\n\\n\";\n        }\n\n        return stream;\n    }\n\n    // Format help message\n    auto help() const -> std::stringstream {\n        std::stringstream out;\n        out << *this;\n        return out;\n    }\n\n    // Sets the maximum width for a line of the Usage message\n    ArgumentParser &set_usage_max_line_width(size_t w) {\n        this->m_usage_max_line_width = w;\n        return *this;\n    }\n\n    // Asks to display arguments of mutually exclusive group on separate lines in\n    // the Usage message\n    ArgumentParser &set_usage_break_on_mutex() {\n        this->m_usage_break_on_mutex = true;\n        return *this;\n    }\n\n    // Format usage part of help only\n    auto usage() const -> std::string {\n        std::stringstream stream;\n\n        std::string curline(\"Usage: \");\n        curline += this->m_program_name;\n        const bool multiline_usage = this->m_usage_max_line_width < std::numeric_limits<std::size_t>::max();\n        const size_t indent_size = curline.size();\n\n        const auto deal_with_options_of_group = [&](std::size_t group_idx) {\n            bool found_options = false;\n            // Add any options inline here\n            const MutuallyExclusiveGroup *cur_mutex = nullptr;\n            int usage_newline_counter = -1;\n            for (const auto &argument : this->m_optional_arguments) {\n                if (argument.m_is_hidden) {\n                    continue;\n                }\n                if (multiline_usage) {\n                    if (argument.m_group_idx != group_idx) {\n                        continue;\n                    }\n                    if (usage_newline_counter != argument.m_usage_newline_counter) {\n                        if (usage_newline_counter >= 0) {\n                            if (curline.size() > indent_size) {\n                                stream << curline << std::endl;\n                                curline = std::string(indent_size, ' ');\n                            }\n                        }\n                        usage_newline_counter = argument.m_usage_newline_counter;\n                    }\n                }\n                found_options = true;\n                const std::string arg_inline_usage = argument.get_inline_usage();\n                const MutuallyExclusiveGroup *arg_mutex = get_belonging_mutex(&argument);\n                if ((cur_mutex != nullptr) && (arg_mutex == nullptr)) {\n                    curline += ']';\n                    if (this->m_usage_break_on_mutex) {\n                        stream << curline << std::endl;\n                        curline = std::string(indent_size, ' ');\n                    }\n                } else if ((cur_mutex == nullptr) && (arg_mutex != nullptr)) {\n                    if ((this->m_usage_break_on_mutex && curline.size() > indent_size) ||\n                        curline.size() + 3 + arg_inline_usage.size() > this->m_usage_max_line_width) {\n                        stream << curline << std::endl;\n                        curline = std::string(indent_size, ' ');\n                    }\n                    curline += \" [\";\n                } else if ((cur_mutex != nullptr) && (arg_mutex != nullptr)) {\n                    if (cur_mutex != arg_mutex) {\n                        curline += ']';\n                        if (this->m_usage_break_on_mutex ||\n                            curline.size() + 3 + arg_inline_usage.size() > this->m_usage_max_line_width) {\n                            stream << curline << std::endl;\n                            curline = std::string(indent_size, ' ');\n                        }\n                        curline += \" [\";\n                    } else {\n                        curline += '|';\n                    }\n                }\n                cur_mutex = arg_mutex;\n                if (curline.size() + 1 + arg_inline_usage.size() > this->m_usage_max_line_width) {\n                    stream << curline << std::endl;\n                    curline = std::string(indent_size, ' ');\n                    curline += \" \";\n                } else if (cur_mutex == nullptr) {\n                    curline += \" \";\n                }\n                curline += arg_inline_usage;\n            }\n            if (cur_mutex != nullptr) {\n                curline += ']';\n            }\n            return found_options;\n        };\n\n        const bool found_options = deal_with_options_of_group(0);\n\n        if (found_options && multiline_usage && !this->m_positional_arguments.empty()) {\n            stream << curline << std::endl;\n            curline = std::string(indent_size, ' ');\n        }\n        // Put positional arguments after the optionals\n        for (const auto &argument : this->m_positional_arguments) {\n            if (argument.m_is_hidden) {\n                continue;\n            }\n            const std::string pos_arg = !argument.m_metavar.empty() ? argument.m_metavar : argument.m_names.front();\n            if (curline.size() + 1 + pos_arg.size() > this->m_usage_max_line_width) {\n                stream << curline << std::endl;\n                curline = std::string(indent_size, ' ');\n            }\n            curline += \" \";\n            if (argument.m_num_args_range.get_min() == 0 && !argument.m_num_args_range.is_right_bounded()) {\n                curline += \"[\";\n                curline += pos_arg;\n                curline += \"]...\";\n            } else if (argument.m_num_args_range.get_min() == 1 && !argument.m_num_args_range.is_right_bounded()) {\n                curline += pos_arg;\n                curline += \"...\";\n            } else {\n                curline += pos_arg;\n            }\n        }\n\n        if (multiline_usage) {\n            // Display options of other groups\n            for (std::size_t i = 0; i < m_group_names.size(); ++i) {\n                stream << curline << std::endl << std::endl;\n                stream << m_group_names[i] << \":\" << std::endl;\n                curline = std::string(indent_size, ' ');\n                deal_with_options_of_group(i + 1);\n            }\n        }\n\n        stream << curline;\n\n        // Put subcommands after positional arguments\n        if (!m_subparser_map.empty()) {\n            stream << \" {\";\n            std::size_t i{0};\n            for (const auto &[command, subparser] : m_subparser_map) {\n                if (subparser->get().m_suppress) {\n                    continue;\n                }\n\n                if (i == 0) {\n                    stream << command;\n                } else {\n                    stream << \",\" << command;\n                }\n                ++i;\n            }\n            stream << \"}\";\n        }\n\n        return stream.str();\n    }\n\n    // Printing the one and only help message\n    // I've stuck with a simple message format, nothing fancy.\n    [[deprecated(\"Use cout << program; instead.  See also help().\")]] std::string print_help() const {\n        auto out = help();\n        std::cout << out.rdbuf();\n        return out.str();\n    }\n\n    void add_subparser(ArgumentParser &parser) {\n        parser.m_parser_path = m_program_name + \" \" + parser.m_program_name;\n        auto it = m_subparsers.emplace(std::cend(m_subparsers), parser);\n        m_subparser_map.insert_or_assign(parser.m_program_name, it);\n        m_subparser_used.insert_or_assign(parser.m_program_name, false);\n    }\n\n    void set_suppress(bool suppress) {\n        m_suppress = suppress;\n    }\n\n    template <typename T> void set_if_used(const std::string key, T &val) {\n        if (is_used(key)) {\n            val = get<T>(key);\n        }\n    }\n\nprotected:\n    const MutuallyExclusiveGroup *get_belonging_mutex(const Argument *arg) const {\n        for (const auto &mutex : m_mutually_exclusive_groups) {\n            if (std::find(mutex.m_elements.begin(), mutex.m_elements.end(), arg) != mutex.m_elements.end()) {\n                return &mutex;\n            }\n        }\n        return nullptr;\n    }\n\n    bool is_valid_prefix_char(char c) const {\n        return m_prefix_chars.find(c) != std::string::npos;\n    }\n\n    char get_any_valid_prefix_char() const {\n        return m_prefix_chars[0];\n    }\n\n    /*\n     * Pre-process this argument list. Anything starting with \"--\", that\n     * contains an =, where the prefix before the = has an entry in the\n     * options table, should be split.\n     */\n    std::vector<std::string> preprocess_arguments(const std::vector<std::string> &raw_arguments) const {\n        std::vector<std::string> arguments{};\n        for (const auto &arg : raw_arguments) {\n\n            const auto argument_starts_with_prefix_chars = [this](const std::string &a) -> bool {\n                if (!a.empty()) {\n\n                    const auto legal_prefix = [this](char c) -> bool { return m_prefix_chars.find(c) != std::string::npos; };\n\n                    // Windows-style\n                    // if '/' is a legal prefix char\n                    // then allow single '/' followed by argument name, followed by an\n                    // assign char, e.g., ':' e.g., 'test.exe /A:Foo'\n                    const auto windows_style = legal_prefix('/');\n\n                    if (windows_style) {\n                        if (legal_prefix(a[0])) {\n                            return true;\n                        }\n                    } else {\n                        // Slash '/' is not a legal prefix char\n                        // For all other characters, only support long arguments\n                        // i.e., the argument must start with 2 prefix chars, e.g,\n                        // '--foo' e,g, './test --foo=Bar -DARG=yes'\n                        if (a.size() > 1) {\n                            return (legal_prefix(a[0]) && legal_prefix(a[1]));\n                        }\n                    }\n                }\n                return false;\n            };\n\n            // Check that:\n            // - We don't have an argument named exactly this\n            // - The argument starts with a prefix char, e.g., \"--\"\n            // - The argument contains an assign char, e.g., \"=\"\n            auto assign_char_pos = arg.find_first_of(m_assign_chars);\n\n            if (m_argument_map.find(arg) == m_argument_map.end() && argument_starts_with_prefix_chars(arg) &&\n                assign_char_pos != std::string::npos) {\n                // Get the name of the potential option, and check it exists\n                std::string opt_name = arg.substr(0, assign_char_pos);\n                if (m_argument_map.find(opt_name) != m_argument_map.end()) {\n                    // This is the name of an option! Split it into two parts\n                    arguments.push_back(std::move(opt_name));\n                    arguments.push_back(arg.substr(assign_char_pos + 1));\n                    continue;\n                }\n            }\n            // If we've fallen through to here, then it's a standard argument\n            arguments.push_back(arg);\n        }\n        return arguments;\n    }\n\n    /*\n     * @throws std::runtime_error in case of any invalid argument\n     */\n    void parse_args_internal(const std::vector<std::string> &raw_arguments) {\n        auto arguments = preprocess_arguments(raw_arguments);\n        if (m_program_name.empty() && !arguments.empty()) {\n            m_program_name = arguments.front();\n        }\n        auto end = std::end(arguments);\n        auto positional_argument_it = std::begin(m_positional_arguments);\n        for (auto it = std::next(std::begin(arguments)); it != end;) {\n            const auto &current_argument = *it;\n            if (Argument::is_positional(current_argument, m_prefix_chars)) {\n                if (positional_argument_it == std::end(m_positional_arguments)) {\n\n                    // Check sub-parsers\n                    auto subparser_it = m_subparser_map.find(current_argument);\n                    if (subparser_it != m_subparser_map.end()) {\n\n                        // build list of remaining args\n                        const auto unprocessed_arguments = std::vector<std::string>(it, end);\n\n                        // invoke subparser\n                        m_is_parsed = true;\n                        m_subparser_used[current_argument] = true;\n                        return subparser_it->second->get().parse_args(unprocessed_arguments);\n                    }\n\n                    if (m_positional_arguments.empty()) {\n\n                        // Ask the user if they argument they provided was a typo\n                        // for some sub-parser,\n                        // e.g., user provided `git totes` instead of `git notes`\n                        if (!m_subparser_map.empty()) {\n                            throw std::runtime_error(\n                                \"Failed to parse '\" + current_argument + \"', did you mean '\" +\n                                std::string{details::get_most_similar_string(m_subparser_map, current_argument)} + \"'\");\n                        }\n\n                        // Ask the user if they meant to use a specific optional argument\n                        if (!m_optional_arguments.empty()) {\n                            for (const auto &opt : m_optional_arguments) {\n                                if (!opt.m_implicit_value.has_value()) {\n                                    // not a flag, requires a value\n                                    if (!opt.m_is_used) {\n                                        throw std::runtime_error(\"Zero positional arguments expected, did you mean \" +\n                                                                 opt.get_usage_full());\n                                    }\n                                }\n                            }\n\n                            throw std::runtime_error(\"Zero positional arguments expected\");\n                        } else {\n                            throw std::runtime_error(\"Zero positional arguments expected\");\n                        }\n                    } else {\n                        throw std::runtime_error(\"Maximum number of positional arguments \"\n                                                 \"exceeded, failed to parse '\" +\n                                                 current_argument + \"'\");\n                    }\n                }\n                auto argument = positional_argument_it++;\n\n                // Deal with the situation of <positional_arg1>... <positional_arg2>\n                if (argument->m_num_args_range.get_min() == 1 &&\n                    argument->m_num_args_range.get_max() == (std::numeric_limits<std::size_t>::max)() &&\n                    positional_argument_it != std::end(m_positional_arguments) &&\n                    std::next(positional_argument_it) == std::end(m_positional_arguments) &&\n                    positional_argument_it->m_num_args_range.get_min() == 1 &&\n                    positional_argument_it->m_num_args_range.get_max() == 1) {\n                    if (std::next(it) != end) {\n                        positional_argument_it->consume(std::prev(end), end);\n                        end = std::prev(end);\n                    } else {\n                        throw std::runtime_error(\"Missing \" + positional_argument_it->m_names.front());\n                    }\n                }\n\n                it = argument->consume(it, end);\n                continue;\n            }\n\n            auto arg_map_it = m_argument_map.find(current_argument);\n            if (arg_map_it != m_argument_map.end()) {\n                auto argument = arg_map_it->second;\n                it = argument->consume(std::next(it), end, arg_map_it->first);\n            } else if (const auto &compound_arg = current_argument; compound_arg.size() > 1 &&\n                                                                    is_valid_prefix_char(compound_arg[0]) &&\n                                                                    !is_valid_prefix_char(compound_arg[1])) {\n                ++it;\n                for (std::size_t j = 1; j < compound_arg.size(); j++) {\n                    auto hypothetical_arg = std::string{'-', compound_arg[j]};\n                    auto arg_map_it2 = m_argument_map.find(hypothetical_arg);\n                    if (arg_map_it2 != m_argument_map.end()) {\n                        auto argument = arg_map_it2->second;\n                        it = argument->consume(it, end, arg_map_it2->first);\n                    } else {\n                        throw std::runtime_error(\"Unknown argument: \" + current_argument);\n                    }\n                }\n            } else {\n                throw std::runtime_error(\"Unknown argument: \" + current_argument);\n            }\n        }\n        m_is_parsed = true;\n    }\n\n    /*\n     * Like parse_args_internal but collects unused args into a vector<string>\n     */\n    std::vector<std::string> parse_known_args_internal(const std::vector<std::string> &raw_arguments) {\n        auto arguments = preprocess_arguments(raw_arguments);\n\n        std::vector<std::string> unknown_arguments{};\n\n        if (m_program_name.empty() && !arguments.empty()) {\n            m_program_name = arguments.front();\n        }\n        auto end = std::end(arguments);\n        auto positional_argument_it = std::begin(m_positional_arguments);\n        for (auto it = std::next(std::begin(arguments)); it != end;) {\n            const auto &current_argument = *it;\n            if (Argument::is_positional(current_argument, m_prefix_chars)) {\n                if (positional_argument_it == std::end(m_positional_arguments)) {\n\n                    // Check sub-parsers\n                    auto subparser_it = m_subparser_map.find(current_argument);\n                    if (subparser_it != m_subparser_map.end()) {\n\n                        // build list of remaining args\n                        const auto unprocessed_arguments = std::vector<std::string>(it, end);\n\n                        // invoke subparser\n                        m_is_parsed = true;\n                        m_subparser_used[current_argument] = true;\n                        return subparser_it->second->get().parse_known_args_internal(unprocessed_arguments);\n                    }\n\n                    // save current argument as unknown and go to next argument\n                    unknown_arguments.push_back(current_argument);\n                    ++it;\n                } else {\n                    // current argument is the value of a positional argument\n                    // consume it\n                    auto argument = positional_argument_it++;\n                    it = argument->consume(it, end);\n                }\n                continue;\n            }\n\n            auto arg_map_it = m_argument_map.find(current_argument);\n            if (arg_map_it != m_argument_map.end()) {\n                auto argument = arg_map_it->second;\n                it = argument->consume(std::next(it), end, arg_map_it->first);\n            } else if (const auto &compound_arg = current_argument; compound_arg.size() > 1 &&\n                                                                    is_valid_prefix_char(compound_arg[0]) &&\n                                                                    !is_valid_prefix_char(compound_arg[1])) {\n                ++it;\n                for (std::size_t j = 1; j < compound_arg.size(); j++) {\n                    auto hypothetical_arg = std::string{'-', compound_arg[j]};\n                    auto arg_map_it2 = m_argument_map.find(hypothetical_arg);\n                    if (arg_map_it2 != m_argument_map.end()) {\n                        auto argument = arg_map_it2->second;\n                        it = argument->consume(it, end, arg_map_it2->first);\n                    } else {\n                        unknown_arguments.push_back(current_argument);\n                        break;\n                    }\n                }\n            } else {\n                // current argument is an optional-like argument that is unknown\n                // save it and move to next argument\n                unknown_arguments.push_back(current_argument);\n                ++it;\n            }\n        }\n        m_is_parsed = true;\n        return unknown_arguments;\n    }\n\n    // Used by print_help.\n    std::size_t get_length_of_longest_argument() const {\n        if (m_argument_map.empty()) {\n            return 0;\n        }\n        std::size_t max_size = 0;\n        for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) {\n            max_size = std::max<std::size_t>(max_size, argument->get_arguments_length());\n        }\n        for ([[maybe_unused]] const auto &[command, unused] : m_subparser_map) {\n            max_size = std::max<std::size_t>(max_size, command.size());\n        }\n        return max_size;\n    }\n\n    using argument_it = std::list<Argument>::iterator;\n    using mutex_group_it = std::vector<MutuallyExclusiveGroup>::iterator;\n    using argument_parser_it = std::list<std::reference_wrapper<ArgumentParser>>::iterator;\n\n    void index_argument(argument_it it) {\n        for (const auto &name : std::as_const(it->m_names)) {\n            m_argument_map.insert_or_assign(name, it);\n        }\n    }\n\n    std::string m_program_name;\n    std::string m_version;\n    std::string m_description;\n    std::string m_epilog;\n    bool m_exit_on_default_arguments = true;\n    std::string m_prefix_chars{\"-\"};\n    std::string m_assign_chars{\"=\"};\n    bool m_is_parsed = false;\n    std::list<Argument> m_positional_arguments;\n    std::list<Argument> m_optional_arguments;\n    std::map<std::string, argument_it> m_argument_map;\n    std::string m_parser_path;\n    std::list<std::reference_wrapper<ArgumentParser>> m_subparsers;\n    std::map<std::string, argument_parser_it> m_subparser_map;\n    std::map<std::string, bool> m_subparser_used;\n    std::vector<MutuallyExclusiveGroup> m_mutually_exclusive_groups;\n    bool m_suppress = false;\n    std::size_t m_usage_max_line_width = std::numeric_limits<std::size_t>::max();\n    bool m_usage_break_on_mutex = false;\n    int m_usage_newline_counter = 0;\n    std::vector<std::string> m_group_names;\n};\n\n} // namespace argparse\n"
  },
  {
    "path": "candy-cli/src/config.cc",
    "content": "// SPDX-License-Identifier: MIT\n#include \"config.h\"\n#include \"argparse.h\"\n#include \"candy/candy.h\"\n#include <Poco/JSON/Object.h>\n#include <Poco/Platform.h>\n#include <Poco/String.h>\n#include <filesystem>\n#include <fstream>\n#include <functional>\n#include <iostream>\n#include <map>\n#include <spdlog/spdlog.h>\n#include <sstream>\n#include <string>\n\nPoco::JSON::Object arguments::json() {\n    Poco::JSON::Object config;\n    config.set(\"mode\", this->mode);\n    config.set(\"websocket\", this->websocket);\n    config.set(\"password\", this->password);\n\n    if (this->mode == \"client\") {\n        config.set(\"name\", this->name);\n        config.set(\"tun\", this->tun);\n        config.set(\"stun\", this->stun);\n        config.set(\"localhost\", this->localhost);\n        config.set(\"discovery\", this->discovery);\n        config.set(\"route\", this->routeCost);\n        config.set(\"mtu\", this->mtu);\n        config.set(\"port\", this->port);\n        config.set(\"vmac\", virtualMac(this->name));\n        config.set(\"expt\", loadTunAddress(this->name));\n    }\n\n    if (this->mode == \"server\") {\n        config.set(\"dhcp\", this->dhcp);\n        config.set(\"sdwan\", this->sdwan);\n    }\n\n    return config;\n}\n\nint arguments::parse(int argc, char *argv[]) {\n    argparse::ArgumentParser program(\"candy\", candy::version());\n\n    program.add_argument(\"-c\", \"--config\").help(\"config file path\");\n    program.add_argument(\"-m\", \"--mode\").help(\"working mode\");\n    program.add_argument(\"-w\", \"--websocket\").help(\"websocket address\");\n    program.add_argument(\"-p\", \"--password\").help(\"authorization password\");\n    program.add_argument(\"-d\", \"--dhcp\").help(\"dhcp address range\");\n    program.add_argument(\"--sdwan\").help(\"software-defined wide area network\");\n    program.add_argument(\"-n\", \"--name\").help(\"network interface name\");\n    program.add_argument(\"-t\", \"--tun\").help(\"static address\");\n    program.add_argument(\"-s\", \"--stun\").help(\"stun address\");\n    program.add_argument(\"--port\").help(\"p2p listen port\").scan<'i', int>();\n    program.add_argument(\"--mtu\").help(\"maximum transmission unit\").scan<'i', int>();\n    program.add_argument(\"-r\", \"--route\").help(\"routing cost\").scan<'i', int>();\n    program.add_argument(\"--discovery\").help(\"discovery interval\").scan<'i', int>();\n    program.add_argument(\"--localhost\").help(\"local ip\");\n\n    program.add_argument(\"--no-timestamp\").implicit_value(true);\n    program.add_argument(\"--debug\").implicit_value(true);\n\n    try {\n        program.parse_args(argc, argv);\n        if (program.is_used(\"--config\")) {\n            parseFile(program.get<std::string>(\"--config\"));\n        }\n\n        if (program.is_used(\"--mode\")) {\n            this->mode = program.get<std::string>(\"--mode\");\n        }\n\n        program.set_if_used(\"--mode\", this->mode);\n        program.set_if_used(\"--websocket\", this->websocket);\n        program.set_if_used(\"--password\", this->password);\n        program.set_if_used(\"--no-timestamp\", this->noTimestamp);\n        program.set_if_used(\"--debug\", this->debug);\n        program.set_if_used(\"--dhcp\", this->dhcp);\n        program.set_if_used(\"--sdwan\", this->sdwan);\n        program.set_if_used(\"--name\", this->name);\n        program.set_if_used(\"--tun\", this->tun);\n        program.set_if_used(\"--stun\", this->stun);\n        program.set_if_used(\"--localhost\", this->localhost);\n        program.set_if_used(\"--port\", this->port);\n        program.set_if_used(\"--mtu\", this->mtu);\n        program.set_if_used(\"--discovery\", this->discovery);\n        program.set_if_used(\"--route\", this->routeCost);\n\n        bool needShowUsage = [&]() {\n            if (this->mode != \"client\" && this->mode != \"server\")\n                return true;\n            if (this->websocket.empty())\n                return true;\n\n            return false;\n        }();\n\n        if (needShowUsage) {\n            std::cout << program.usage() << std::endl;\n            exit(1);\n        }\n\n        if (this->noTimestamp) {\n            spdlog::set_pattern(\"[%^%l%$] %v\");\n        }\n        if (this->debug) {\n            spdlog::set_level(spdlog::level::debug);\n        }\n        return 0;\n    } catch (const std::exception &e) {\n        std::cout << program.usage() << std::endl;\n        exit(1);\n    }\n}\n\nvoid arguments::parseFile(std::string cfgFile) {\n    try {\n        std::map<std::string, std::function<void(const std::string &)>> cfgHandlers = {\n            {\"mode\", [&](const std::string &value) { this->mode = value; }},\n            {\"websocket\", [&](const std::string &value) { this->websocket = value; }},\n            {\"password\", [&](const std::string &value) { this->password = value; }},\n            {\"debug\", [&](const std::string &value) { this->debug = (value == \"true\"); }},\n            {\"dhcp\", [&](const std::string &value) { this->dhcp = value; }},\n            {\"sdwan\", [&](const std::string &value) { this->sdwan = value; }},\n            {\"tun\", [&](const std::string &value) { this->tun = value; }},\n            {\"stun\", [&](const std::string &value) { this->stun = value; }},\n            {\"name\", [&](const std::string &value) { this->name = value; }},\n            {\"discovery\", [&](const std::string &value) { this->discovery = std::stoi(value); }},\n            {\"route\", [&](const std::string &value) { this->routeCost = std::stoi(value); }},\n            {\"port\", [&](const std::string &value) { this->port = std::stoi(value); }},\n            {\"mtu\", [&](const std::string &value) { this->mtu = std::stoi(value); }},\n            {\"localhost\", [&](const std::string &value) { this->localhost = value; }},\n        };\n        auto trim = [](std::string str) {\n            if (str.length() >= 2 && str.front() == '\\\"' && str.back() == '\\\"') {\n                return str.substr(1, str.length() - 2);\n            }\n            return str;\n        };\n        auto configs = fileToKvMap(cfgFile);\n        for (auto cfg : configs) {\n            auto handler = cfgHandlers.find(cfg.first);\n            if (handler != cfgHandlers.end()) {\n                handler->second(trim(cfg.second));\n            } else {\n                spdlog::warn(\"unknown config: {}={}\", cfg.first, cfg.second);\n            }\n        }\n    } catch (std::exception &e) {\n        spdlog::error(\"parse config file failed: {}\", e.what());\n        exit(1);\n    }\n}\n\nstd::map<std::string, std::string> arguments::fileToKvMap(const std::string &filename) {\n    std::map<std::string, std::string> config;\n    std::ifstream file(filename);\n    std::string line;\n\n    while (std::getline(file, line)) {\n        line = Poco::trimLeft(line);\n        if (line.empty() || line.front() == '#')\n            continue;\n        line.erase(line.find_last_not_of(\" \\t;\") + 1);\n        std::size_t delimiterPos = line.find('=');\n        if (delimiterPos != std::string::npos) {\n            std::string key = Poco::trim(line.substr(0, delimiterPos));\n            std::string value = Poco::trim(line.substr(delimiterPos + 1));\n            config[key] = value;\n        }\n    }\n    return config;\n}\n\nint saveTunAddress(const std::string &name, const std::string &cidr) {\n    try {\n        std::string cache = storageDirectory(\"address/\");\n        cache += name.empty() ? \"__noname__\" : name;\n        std::filesystem::create_directories(std::filesystem::path(cache).parent_path());\n        std::ofstream ofs(cache);\n        if (ofs.is_open()) {\n            ofs << cidr;\n            ofs.close();\n        }\n        return 0;\n    } catch (std::exception &e) {\n        spdlog::critical(\"save latest address failed: {}\", e.what());\n        return -1;\n    }\n}\n\nstd::string loadTunAddress(const std::string &name) {\n    std::string cache = storageDirectory(\"address/\");\n    cache += name.empty() ? \"__noname__\" : name;\n    std::ifstream ifs(cache);\n    if (ifs.is_open()) {\n        std::stringstream ss;\n        ss << ifs.rdbuf();\n        ifs.close();\n        return ss.str();\n    }\n    return \"0.0.0.0/0\";\n}\n\nstd::string virtualMacHelper(std::string name = \"\") {\n    try {\n        std::string path = storageDirectory(\"vmac/\");\n        path += name.empty() ? \"__noname__\" : name;\n        char buffer[candy::VMAC_SIZE];\n        std::stringstream ss;\n        std::ifstream ifs(path);\n        if (ifs.is_open()) {\n            ifs.read(buffer, sizeof(buffer));\n            if (ifs) {\n                for (int i = 0; i < (int)sizeof(buffer); i++) {\n                    ss << std::hex << buffer[i];\n                }\n            }\n            ifs.close();\n            return ss.str();\n        }\n        return \"\";\n    } catch (std::exception &e) {\n        return \"\";\n    }\n}\n\nstd::string initVirtualMac() {\n    try {\n        std::string path = storageDirectory(\"vmac/__noname__\");\n        std::filesystem::create_directories(std::filesystem::path(path).parent_path());\n        std::string vmac = candy::create_vmac();\n        std::ofstream ofs(path);\n        if (ofs.is_open()) {\n            ofs << vmac;\n            ofs.close();\n        }\n        return vmac;\n    } catch (std::exception &e) {\n        spdlog::critical(\"init vmac failed: {}\", e.what());\n        return \"\";\n    }\n}\n\nstd::string virtualMac(const std::string &name) {\n    std::string path;\n    // 兼容老版本,优先获取与配置网卡名对应的 vmac\n    path = virtualMacHelper(name);\n    if (!path.empty()) {\n        return path;\n    }\n    // 获取网卡名无关的全局 vmac\n    path = virtualMacHelper();\n    if (!path.empty()) {\n        return path;\n    }\n    // 初次启动,生成全局 vmac\n    return initVirtualMac();\n}\n\nbool starts_with(const std::string &str, const std::string &prefix) {\n    return str.size() >= prefix.size() && std::equal(prefix.begin(), prefix.end(), str.begin());\n}\n\n#if POCO_OS == POCO_OS_WINDOWS_NT\nstd::string storageDirectory(std::string subdir) {\n    return \"C:/ProgramData/Candy/\" + subdir;\n}\n#else\nstd::string storageDirectory(std::string subdir) {\n    return \"/var/lib/candy/\" + subdir;\n}\n#endif\n"
  },
  {
    "path": "candy-cli/src/config.h",
    "content": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CLI_CONFIG_H\n#define CANDY_CLI_CONFIG_H\n\n#include <Poco/JSON/Object.h>\n#include <map>\n#include <string>\n\nstruct arguments {\n    int parse(int argc, char *argv[]);\n    Poco::JSON::Object json();\n\nprivate:\n    void parseFile(std::string cfgFile);\n    std::map<std::string, std::string> fileToKvMap(const std::string &filename);\n\n    std::string mode;\n    std::string websocket;\n    std::string password;\n    bool noTimestamp = false;\n    bool debug = false;\n\n    std::string dhcp;\n    std::string sdwan;\n\n    std::string name;\n    std::string tun;\n    std::string stun;\n    std::string localhost;\n    int port = 0;\n    int discovery = 0;\n    int routeCost = 0;\n    int mtu = 1400;\n};\n\nint saveTunAddress(const std::string &name, const std::string &cidr);\nstd::string loadTunAddress(const std::string &name);\nstd::string virtualMac(const std::string &name);\nstd::string storageDirectory(std::string subdir = \"\");\n\n#endif\n"
  },
  {
    "path": "candy-cli/src/main.cc",
    "content": "#include \"candy/candy.h\"\n#include \"config.h\"\n#include <signal.h>\n#include <spdlog/spdlog.h>\n\nint main(int argc, char *argv[]) {\n    arguments args;\n    args.parse(argc, argv);\n    auto config = args.json();\n\n    if (config.getValue<std::string>(\"mode\") == \"client\") {\n        static const std::string id = \"cli\";\n\n        auto handler = [](int) -> void { candy::client::shutdown(id); };\n\n        signal(SIGINT, handler);\n        signal(SIGTERM, handler);\n\n        std::thread([&]() {\n            while (true) {\n                std::this_thread::sleep_for(std::chrono::seconds(1));\n                auto status = candy::client::status(id);\n                if (status && (*status).has(\"address\")) {\n                    std::string address = (*status).getValue<std::string>(\"address\");\n                    if (!address.empty()) {\n                        saveTunAddress(config.getValue<std::string>(\"name\"), address);\n                        break;\n                    }\n                }\n            }\n        }).detach();\n\n        candy::client::run(id, config);\n        return 0;\n    }\n\n    if (config.getValue<std::string>(\"mode\") == \"server\") {\n        auto handler = [](int) -> void { candy::server::shutdown(); };\n\n        signal(SIGINT, handler);\n        signal(SIGTERM, handler);\n\n        candy::server::run(config);\n        return 0;\n    }\n\n    return -1;\n}\n"
  },
  {
    "path": "candy-service/CMakeLists.txt",
    "content": "file(GLOB_RECURSE SOURCES \"src/*.cc\")\nadd_executable(candy-service ${SOURCES})\n\ntarget_include_directories(candy-service PUBLIC \n    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>\n    $<INSTALL_INTERFACE:include>\n)\n\nset_target_properties(candy-service PROPERTIES OUTPUT_NAME \"candy-service\")\n\ntarget_link_libraries(candy-service PRIVATE spdlog::spdlog)\ntarget_link_libraries(candy-service PRIVATE Poco::Foundation Poco::Net Poco::JSON Poco::Util)\ntarget_link_libraries(candy-service PRIVATE Threads::Threads)\ntarget_link_libraries(candy-service PRIVATE Candy::Library)\n\ninstall(TARGETS candy-service)\n\nadd_executable(Candy::Service ALIAS candy-service)\n"
  },
  {
    "path": "candy-service/README.md",
    "content": "# candy-service\n\nCandy 客户端的另一个实现.\n\n- **无状态**: 进程本身不持久化任何数据, 进程重启后数据丢失，需要外部维护网络配置信息\n- **API 交互**: 对外提供 HTTP API 交互接口,可以远程控制和访问\n\n## API\n\n### 帮助\n\nLinux\n\n```bash\ncandy-service --help\n```\n\nWindows \n  \n```bat\ncandy-service /help\n```\n\n请求响应中的 **id** 用于标识网络连接, 通过不同标识可以同时加入多个网络, 这个标识用于查看状态和关闭网络.\n\n### Run\n\n启动参数的含义与[配置文件](../candy.cfg)相同,此外还有两个额外的配置项.\n\n- vmac: 用于标识唯一设备,当同一网络中有两台不同 vmac 的设备申请相同 IP 地址时, 后者会报告 IP 冲突. 为 16 个字符的随机数字字母字符串, 需要持久化存储, 建议在首次启动进程时生成.\n- expt: 期望使用的 IP 地址, 这个参数用于实现有优先分配曾经使用过的地址, 由客户端主动向服务器报告, 可以为空. 建议由服务端随机分配地址的情况下, 通过 `/api/status` 查看分配的地址并保存, 下次连接时携带这个地址.\n\n`POST /api/run`\n\n```json\n{\n  \"id\": \"test\",\n  \"config\": {\n    \"mode\": \"client\",\n    \"websocket\": \"wss://canets.org\",\n    \"password\": \"\",\n    \"name\": \"\",\n    \"tun\": \"\",\n    \"stun\": \"stun://stun.canets.org\",\n    \"discovery\": 300,\n    \"route\": 5,\n    \"port\": 0,\n    \"localhost\": \"\",\n    \"mtu\": 1400,\n    \"expt\": \"\",\n    \"vmac\": \"16-char rand str\"\n  }\n}\n```\n\n```json\n{\n    \"id\": \"test\",\n    \"message\": \"success\"\n}\n```\n\n### Status\n\n`POST /api/status`\n\n```json\n{\n  \"id\": \"test\"\n}\n```\n\n```json\n{\n  \"id\": \"test\",\n  \"message\": \"success\",\n  \"status\": {\n    \"address\": \"192.168.202.1/24\"\n  }\n}\n```\n\n### Shutdown\n\n`POST /api/shutdown`\n\n```json\n{\n  \"id\": \"test\"\n}\n```\n\n```json\n{\n    \"id\": \"test\",\n    \"message\": \"success\"\n}\n```\n"
  },
  {
    "path": "candy-service/src/main.cc",
    "content": "#include \"candy/client.h\"\n#include <Poco/Exception.h>\n#include <Poco/File.h>\n#include <Poco/JSON/Object.h>\n#include <Poco/JSON/Parser.h>\n#include <Poco/Net/HTTPRequestHandler.h>\n#include <Poco/Net/HTTPRequestHandlerFactory.h>\n#include <Poco/Net/HTTPServer.h>\n#include <Poco/Net/HTTPServerRequest.h>\n#include <Poco/Net/HTTPServerResponse.h>\n#include <Poco/Net/ServerSocket.h>\n#include <Poco/Net/SocketAddress.h>\n#include <Poco/StreamCopier.h>\n#include <Poco/Timestamp.h>\n#include <Poco/Util/HelpFormatter.h>\n#include <Poco/Util/Option.h>\n#include <Poco/Util/OptionSet.h>\n#include <Poco/Util/ServerApplication.h>\n#include <iostream>\n#include <iterator>\n#include <map>\n#include <mutex>\n#include <spdlog/sinks/rotating_file_sink.h>\n#include <spdlog/spdlog.h>\n#include <sstream>\n#include <thread>\n\nstd::mutex threadMutex;\nstd::map<std::string, std::thread> threadMap;\n\nclass BaseJSONHandler : public Poco::Net::HTTPRequestHandler {\nprotected:\n    Poco::JSON::Object::Ptr readRequest(Poco::Net::HTTPServerRequest &request) {\n        Poco::JSON::Parser parser;\n        Poco::Dynamic::Var result = parser.parse(request.stream());\n        return result.extract<Poco::JSON::Object::Ptr>();\n    }\n\n    void sendResponse(Poco::Net::HTTPServerResponse &response, const Poco::JSON::Object::Ptr &json) {\n        response.setChunkedTransferEncoding(true);\n        response.setContentType(\"application/json\");\n        Poco::JSON::Stringifier::stringify(json, response.send());\n    }\n};\n\nclass RunHandler : public BaseJSONHandler {\npublic:\n    void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::HTTPServerResponse &response) override {\n        if (request.getMethod() != Poco::Net::HTTPRequest::HTTP_POST) {\n            response.setStatus(Poco::Net::HTTPResponse::HTTP_METHOD_NOT_ALLOWED);\n            return;\n        }\n\n        auto json = readRequest(request);\n        auto id = json->getValue<std::string>(\"id\");\n        auto config = json->getObject(\"config\");\n        json->remove(\"config\");\n\n        std::lock_guard lock(threadMutex);\n        auto it = threadMap.find(id);\n        if (it != threadMap.end()) {\n            json->set(\"message\", \"id already exists\");\n        } else {\n            auto thread = std::thread([=]() { candy::client::run(id, *config); });\n            threadMap.insert({id, std::move(thread)});\n            json->set(\"message\", \"success\");\n        }\n\n        sendResponse(response, json);\n    }\n};\n\nclass StatusHandler : public BaseJSONHandler {\npublic:\n    void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::HTTPServerResponse &response) override {\n        if (request.getMethod() != Poco::Net::HTTPRequest::HTTP_POST) {\n            response.setStatus(Poco::Net::HTTPResponse::HTTP_METHOD_NOT_ALLOWED);\n            return;\n        }\n\n        auto json = readRequest(request);\n        auto id = json->getValue<std::string>(\"id\");\n\n        std::lock_guard lock(threadMutex);\n        auto it = threadMap.find(id);\n        if (it != threadMap.end()) {\n            if (auto status = candy::client::status(id)) {\n                json->set(\"status\", *status);\n                json->set(\"message\", \"success\");\n            } else {\n                json->set(\"message\", \"unable to get status\");\n            }\n        } else {\n            json->set(\"message\", \"id does not exist\");\n        }\n\n        sendResponse(response, json);\n    }\n};\n\nclass ShutdownHandler : public BaseJSONHandler {\npublic:\n    void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::HTTPServerResponse &response) override {\n        if (request.getMethod() != Poco::Net::HTTPRequest::HTTP_POST) {\n            response.setStatus(Poco::Net::HTTPResponse::HTTP_METHOD_NOT_ALLOWED);\n            return;\n        }\n\n        auto json = readRequest(request);\n        auto id = json->getValue<std::string>(\"id\");\n        candy::client::shutdown(id);\n\n        std::lock_guard lock(threadMutex);\n        auto it = threadMap.find(id);\n        if (it != threadMap.end()) {\n            it->second.detach();\n            threadMap.erase(it);\n            json->set(\"message\", \"success\");\n        } else {\n            json->set(\"message\", \"id does not exist\");\n        }\n\n        sendResponse(response, json);\n    }\n};\n\nclass JSONRequestHandlerFactory : public Poco::Net::HTTPRequestHandlerFactory {\npublic:\n    Poco::Net::HTTPRequestHandler *createRequestHandler(const Poco::Net::HTTPServerRequest &request) override {\n        const std::string &uri = request.getURI();\n\n        if (uri == \"/api/run\") {\n            return new RunHandler;\n        } else if (uri == \"/api/status\") {\n            return new StatusHandler;\n        } else if (uri == \"/api/shutdown\") {\n            return new ShutdownHandler;\n        }\n\n        return nullptr;\n    }\n};\n\nclass CandyServiceApp : public Poco::Util::ServerApplication {\nprotected:\n    std::string bindAddress;\n    int port = 0;\n    bool helpRequested = false;\n    std::string logdir;\n    std::string loglevel;\n\n    void initialize(Poco::Util::Application &self) override {\n        loadConfiguration();\n        Poco::Util::ServerApplication::initialize(self);\n    }\n\n    void defineOptions(Poco::Util::OptionSet &options) override {\n        Poco::Util::ServerApplication::defineOptions(options);\n\n        options.addOption(Poco::Util::Option(\"help\", \"\", \"Display help information\")\n                              .required(false)\n                              .repeatable(false)\n                              .callback(Poco::Util::OptionCallback<CandyServiceApp>(this, &CandyServiceApp::handleHelp)));\n\n        options.addOption(Poco::Util::Option(\"bind\", \"\", \"Bind address and port (address:port)\")\n                              .required(false)\n                              .repeatable(false)\n                              .argument(\"address:port\")\n                              .callback(Poco::Util::OptionCallback<CandyServiceApp>(this, &CandyServiceApp::handleBind)));\n        options.addOption(Poco::Util::Option(\"logdir\", \"\", \"Specify log directory\")\n                              .required(false)\n                              .repeatable(false)\n                              .argument(\"path\")\n                              .callback(Poco::Util::OptionCallback<CandyServiceApp>(this, &CandyServiceApp::handleLogDir)));\n        options.addOption(Poco::Util::Option(\"loglevel\", \"\", \"Specify log level\")\n                              .required(false)\n                              .repeatable(false)\n                              .argument(\"level\")\n                              .callback(Poco::Util::OptionCallback<CandyServiceApp>(this, &CandyServiceApp::handleLogLevel)));\n    }\n\n    void handleHelp(const std::string &name, const std::string &value) {\n        helpRequested = true;\n        displayHelp();\n        stopOptionsProcessing();\n    }\n\n    void handleBind(const std::string &name, const std::string &value) {\n        size_t pos = value.find(':');\n        if (pos == std::string::npos) {\n            std::cerr << \"Invalid bind format. Use address:port (e.g., 0.0.0.0:26817)\" << std::endl;\n            std::exit(EXIT_FAILURE);\n        }\n\n        bindAddress = value.substr(0, pos);\n        try {\n            port = std::stoi(value.substr(pos + 1));\n        } catch (const std::exception &e) {\n            std::cerr << \"Invalid port number: \" << e.what() << std::endl;\n            std::exit(EXIT_FAILURE);\n        }\n    }\n\n    void handleLogDir(const std::string &name, const std::string &dir) {\n        this->logdir = dir;\n    }\n\n    void handleLogLevel(const std::string &name, const std::string &level) {\n        this->loglevel = level;\n    }\n\n    void displayHelp() {\n        Poco::Util::HelpFormatter helpFormatter(options());\n        helpFormatter.setCommand(commandName());\n        helpFormatter.format(std::cout);\n    }\n\n    int main(const std::vector<std::string> &args) override {\n        if (helpRequested) {\n            return Poco::Util::Application::EXIT_OK;\n        }\n\n        if (!logdir.empty()) {\n            Poco::File(logdir).createDirectories();\n            auto logger = spdlog::rotating_logger_mt(\"app\", logdir + \"/app.log\", 10 * 1024 * 1024, 5, true);\n            spdlog::set_default_logger(logger);\n        }\n\n        if (!loglevel.empty()) {\n            spdlog::set_level(spdlog::level::from_str(loglevel));\n        }\n\n        if (bindAddress.empty()) {\n            bindAddress = \"localhost\";\n            port = 26817;\n        }\n\n        try {\n            Poco::Net::ServerSocket socket;\n            socket.bind(Poco::Net::SocketAddress(bindAddress, port));\n            socket.listen();\n\n            Poco::Net::HTTPServerParams *params = new Poco::Net::HTTPServerParams;\n            params->setMaxQueued(10);\n            params->setMaxThreads(1);\n\n            Poco::Net::HTTPServer server(new JSONRequestHandlerFactory, socket, params);\n\n            server.start();\n            spdlog::info(\"bind: {}:{}\", bindAddress, port);\n\n            waitForTerminationRequest();\n            spdlog::info(\"exit signal detected\");\n\n            server.stop();\n\n            std::lock_guard lock(threadMutex);\n            for (auto &[id, thread] : threadMap) {\n                candy::client::shutdown(id);\n                if (thread.joinable()) {\n                    thread.join();\n                }\n            }\n\n        } catch (const Poco::Exception &exc) {\n            std::cerr << \"Fatal error: \" << exc.displayText() << std::endl;\n            return Poco::Util::Application::EXIT_SOFTWARE;\n        } catch (const std::exception &e) {\n            std::cerr << \"Fatal error: \" << e.what() << std::endl;\n            return Poco::Util::Application::EXIT_SOFTWARE;\n        }\n\n        return Poco::Util::Application::EXIT_OK;\n    }\n};\n\nint main(int argc, char **argv) {\n    CandyServiceApp app;\n    return app.run(argc, argv);\n}\n"
  },
  {
    "path": "candy.cfg",
    "content": "############################## Client and Server ##############################\n# [Required] Working mode, \"client\" or \"server\"\nmode = \"client\"\n\n# [Required] The address that the server listens on\n# Server only supports ws and needs to provide wss through an external web\n# service. Client supports ws and wss.\nwebsocket = \"wss://canets.org\"\n\n# [Optional] Password used to verify identity\n# Only the hashed content of the password and timestamp is transmitted on the\n# network, and the password cannot be obtained from the message.\n#password = \"this is the password\"\n\n# [Optional] Show debug log\n#debug = false\n\n################################# Server Only #################################\n# [Optional] The range of addresses automatically assigned by the server\n# Server address allocation is not enabled by default, and the client needs to\n# configure a static address through tun.\n#dhcp = \"192.168.202.0/24\"\n\n# [Optional] software-defined wide area network\n# IP packets entering 192.168.202.1/32 with the destination address 172.17.0.0/16\n# will be forwarded to 192.168.202.2. Multiple rules are separated by semicolons.\n# Extraneous whitespace characters are prohibited.\n#sdwan = \"192.168.202.1/32,172.17.0.0/16,192.168.202.2;192.168.202.2/32,172.16.0.0/16,192.168.202.1\"\n\n################################# Client Only #################################\n# [Optional] Network interface name\n# Used to differentiate networks when running multiple clients.\n#name = \"\"\n\n# [Optional] Static address\n# If dhcp is not configured, tun must be configured. When there is an address\n# conflict, the previous client will be kicked out.\n#tun = \"192.168.202.1/24\"\n\n# [Optional] STUN server address\nstun = \"stun://stun.canets.org\"\n\n# [Optional] Active discovery interval\n# Periodically sends broadcasts to try to establish P2P with devices on the\n# network. The default configuration is 0, which means disabled.\ndiscovery = 300\n\n# [Optional] The cost of routing through this machine\n# Use all nodes in the network to establish the link with the lowest latency.\n# This configuration represents the cost of using this node as a relay. The\n# default configuration is 0 which means disabled.\nroute = 5\n\n# [Optional] Local UDP port used for P2P\n# The default configuration is 0, which means it is allocated by the operating\n# system. This configuration can be used when the firewall is strict and can\n# only open specific ports.\n#port = 0\n\n# [Optional] Local IPv4 address used for peering connections\n# By default the IPv4 address of the local physical network card will be\n# detected. When there are multiple physical network cards, the detection\n# results may not be the best. You can specify it manually.\n#localhost = \"127.0.0.1\"\n\n# [Optional] Maximum Transmission Unit\n#mtu=1400\n"
  },
  {
    "path": "candy.initd",
    "content": "#!/sbin/openrc-run\n# Copyright 2024 Gentoo Authors\n# Distributed under the terms of the GNU General Public License v2\n\nname=\"candy daemon\"\ndescription=\"A simple networking tool\"\nCANDY_NAME=${SVCNAME##*.}\nif [ -n \"${CANDY_NAME}\" -a \"${SVCNAME}\" != \"candy\" ]; then\n\tCANDY_PIDFILE=\"/run/candy.${CANDY_NAME}.pid\"\n\tCANDY_CONFIG=\"/etc/candy.d/${CANDY_NAME}.cfg\"\n\tCANDY_LOG=\"/var/log/candy/${CANDY_NAME}.log\"\nelse\n\tCANDY_PIDFILE=\"/run/candy.pid\"\n\tCANDY_CONFIG=\"/etc/candy.cfg\"\n\tCANDY_LOG=\"/var/log/candy/candy.log\"\nfi\ndepend() {\n    need net\n}\n\nstart_pre() {\n\tif [ ! -d \"/tmp/candy/\" ]; then\n\t\tmkdir \"/tmp/candy\"\n\tfi\n\tif [ ! -L \"/var/log/candy\" ]; then\n\t\tln -s \"/tmp/candy\" \"/var/log/\"\n\tfi\n}\n\nstart() {\n\tebegin \"Starting Candy, Log File: ${CANDY_LOG}\"\n\tstart-stop-daemon --start --background \\\n\t\t--stdout \"${CANDY_LOG}\" --stderr \"${CANDY_LOG}\" \\\n\t\t--make-pidfile --pidfile \"${CANDY_PIDFILE}\" \\\n\t\t--exec /usr/bin/candy -- -c \"${CANDY_CONFIG}\"\n\teend $?\n}\n\nstop() {\n\tebegin \"Stopping Candy\"\n\tstart-stop-daemon --stop \\\n\t\t--pidfile \"${CANDY_PIDFILE}\"\n\teend $?\n}\n"
  },
  {
    "path": "candy.service",
    "content": "[Unit]\nDescription=A simple networking tool\nStartLimitIntervalSec=0\n\n[Service]\nExecStart=/usr/bin/candy --no-timestamp -c /etc/candy.cfg\nRestart=always\nRestartSec=3\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "candy@.service",
    "content": "[Unit]\nDescription=A simple networking tool\nStartLimitIntervalSec=0\n\n[Service]\nExecStart=/usr/bin/candy --no-timestamp -c /etc/candy.d/%i.cfg\nRestart=always\nRestartSec=3\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "cmake/Fetch.cmake",
    "content": "macro(Fetch NAME GIT_REPOSITORY GIT_TAG)\n    include(FetchContent)\n    if(${CMAKE_VERSION} VERSION_GREATER_EQUAL \"3.28\")\n        FetchContent_Declare(\n            ${NAME}\n            GIT_REPOSITORY ${GIT_REPOSITORY}\n            GIT_TAG        ${GIT_TAG}\n            EXCLUDE_FROM_ALL\n        )\n        FetchContent_MakeAvailable(${NAME})\n    else()\n        FetchContent_Declare(\n            ${NAME}\n            GIT_REPOSITORY ${GIT_REPOSITORY}\n            GIT_TAG        ${GIT_TAG}\n        )\n        FetchContent_GetProperties(${NAME})\n        if(NOT ${NAME}_POPULATED)\n            FetchContent_Populate(${NAME})\n            add_subdirectory(${${NAME}_SOURCE_DIR} ${${NAME}_BINARY_DIR} EXCLUDE_FROM_ALL)\n        endif()\n    endif()\nendmacro()\n"
  },
  {
    "path": "cmake/openssl/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.16)\nif(POLICY CMP0135)\n  cmake_policy(SET CMP0135 NEW)\nendif()\nproject(openssl)\n\ninclude(ExternalProject)\nExternalProject_Add(\n    openssl\n    PREFIX                      ${CMAKE_CURRENT_BINARY_DIR}/openssl\n    URL                         https://www.openssl.org/source/openssl-3.5.4.tar.gz\n    LOG_BUILD                   ON\n    BUILD_IN_SOURCE             YES\n    CONFIGURE_COMMAND \n        COMMAND sed -i \"s/disable('static', 'pic', 'threads')/disable('static', 'pic')/\" Configure\n        COMMAND ./config --release no-unit-test no-shared ${TARGET_OPENSSL} CFLAGS=$ENV{CFLAGS} LDFLAGS=$ENV{LDFLAGS}\n    BUILD_COMMAND               \"\"\n    INSTALL_COMMAND             \"\"\n    TEST_COMMAND                \"\"\n)\n"
  },
  {
    "path": "dockerfile",
    "content": "FROM alpine AS base\nRUN apk update\nRUN apk add spdlog openssl poco\n\nFROM base AS build\nRUN apk add git cmake ninja pkgconf g++ spdlog-dev openssl-dev poco-dev linux-headers\nCOPY . candy\nRUN cd candy && cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build build && cmake --install build\n\nFROM base AS product\nRUN install -D /dev/null /var/lib/candy/lost\nCOPY --from=build /usr/local/bin/candy /usr/bin/candy\nCOPY candy.cfg /etc/candy.cfg\nENTRYPOINT [\"/usr/bin/candy\"]\nCMD [\"-c\", \"/etc/candy.cfg\"]\n"
  },
  {
    "path": "docs/CNAME",
    "content": "docs.canets.org\n"
  },
  {
    "path": "docs/_config.yml",
    "content": "title: Candy\n"
  },
  {
    "path": "docs/deploy-cli-server.md",
    "content": "# 部署 CLI 服务端\n\n根据帮助信息 `candy --help` 和配置文件描述部署.\n\n非专业用户请[部署 Web 服务端](https://docs.canets.org/deploy-web-server).\n"
  },
  {
    "path": "docs/deploy-web-server.md",
    "content": "# 部署 Web 服务端\n\n## 前置条件\n\n知道如何部署 Web 服务,并能够申请证书后对外提供 HTTPS 服务.\n\n否则使用明文传输将导致数据泄漏,存在安全隐患.此时建议使用社区服务器构建私有网络.\n\n## 一键部署服务端\n\n```bash\ndocker run --name=cacao --detach --volume /var/lib/cacao:/var/lib/cacao --publish 8080:80 docker.io/lanthora/cacao:latest\n```\n\n## 使用\n\n假设你的域名为 `example.com`, 此时通过 `https://example.com` 应该能够正常访问服务.如果不是 `https` 请回到最开始解决前置条件.\n\n服务器启动后的第一个注册用户默认被设置为管理员.管理员无法创建网络,且无权查看其他用户的网络.\n\n管理员配置页面,能够配置是否允许注册,以及允许注册时的注册间隔(避免脚本小子刷注册用户).同时可以配置自动清理不活跃用户.\n\n![](images/cacao-admin-setting.png)\n\n### 单网络模式\n\n在不允许注册时,管理员可以手动添加用户.其中名为 @ 的用户是一个特殊用户,这个用户只能创建一个名为 @ 的网络.用户名和网络名的作用在后面说明.先创建这个用户.\n\n![](images/cacao-admin-user.png)\n\n退出管理员,并以 @ 用户登录.此时已经默认添加了 @ 网络.默认网络生成了随机密码 `ZrhaUcz1`\n\n![](images/cacao-network.png)\n\n此时连接这个网络的客户端仅需要修改以下配置:\n\n```cfg\nwebsocket = \"wss://example.com\"\npassword = \"ZrhaUcz1\"\n```\n\n除非你知道自己在做什么,否则请不要修改任何其他配置项.\n\n### 多用户多网络模式\n\n如果只是创建一个网络,单网络模式已经足够了.如果要允许多个用户使用,且每个用户可以创建多个网络.则可以使用多用户多网络模式.\n\n假设由管理员创建或自行注册的普通用户名为 `${username}`, 这个用户拥有的一个网络名是 `${netname}`,那么客户端对应的配置仅需要修改为:\n\n```cfg\nwebsocket = \"wss://example.com/${username}/${netname}\"\n```\n\n当用户名或者网络名为 @ 时,在客户端的配置中需要留空.当用户名和网络名都为空时,就是所谓的单网络模式\n"
  },
  {
    "path": "docs/index.md",
    "content": "# Candy\n\n<p>\n<a href=\"https://github.com/lanthora/candy/releases/latest\"><img src=\"https://img.shields.io/github/release/lanthora/candy\" /></a>\n<a href=\"https://github.com/lanthora/candy/actions/workflows/release.yaml\"><img src=\"https://img.shields.io/github/actions/workflow/status/lanthora/candy/release.yaml\" /></a>\n<a href=\"https://github.com/lanthora/candy/graphs/contributors\"><img src=\"https://img.shields.io/github/contributors-anon/lanthora/candy\" /></a>\n<a href=\"https://github.com/lanthora/candy/issues\"><img src=\"https://img.shields.io/github/issues-raw/lanthora/candy\" /></a>\n<a href=\"https://github.com/lanthora/candy/pulls\"><img src=\"https://img.shields.io/github/issues-pr-raw/lanthora/candy\" /></a>\n</p>\n\n一个高可用,低时延,反审查的组网工具.\n\n## 如何使用\n\n- [安装 Windows 客户端](install-client-for-windows)\n- [安装 macOS 客户端](install-client-for-macos)\n- [安装 Linux 客户端](install-client-for-linux)\n- [部署 Web 服务端](deploy-web-server)\n- [部署 CLI 服务端](deploy-cli-server)\n- [使用社区服务器](use-the-community-server)\n- [多局域网组网](software-defined-wide-area-network)\n\n## 相关项目\n\n- [Cacao](https://github.com/lanthora/cacao): WebUI 版的 Candy 服务器\n- [Cake](https://github.com/lanthora/cake): Qt 实现的 Candy GUI 桌面应用程序\n- [Candy Android](https://github.com/Jercrox/Candy_Android_Client): Android 客户端\n- [EasyTier](https://github.com/EasyTier/EasyTier): 一个简单、安全、去中心化的内网穿透 VPN 组网方案，使用 Rust 语言和 Tokio 框架实现\n- [candygo](https://github.com/SoraKasvgano/candygo): 一个简单的与candy原项目配置文件兼容的go版本\n\n## 交流群\n\n- QQ: 768305206\n- TG: [Click to Join](https://t.me/CandyUserGroup)\n"
  },
  {
    "path": "docs/install-client-for-linux.md",
    "content": "# 安装 Linux 客户端\n\n我们针对不同 Linux 发行版提供了多种格式的安装包.对于暂未支持的发行版,可以选择容器部署或者静态链接的可执行文件.\n我们致力于支持所有架构的 Linux 系统.\n\n## Docker\n\n镜像已上传 [Docker Hub](https://hub.docker.com/r/lanthora/candy) 和 [Github Packages](https://github.com/lanthora/candy/pkgs/container/candy).\n\n获取最新镜像\n\n```bash\ndocker pull docker.io/lanthora/candy:latest\n```\n\n容器需要管理员权限读取设备创建虚拟网卡并设置路由,同时需要 Host 网络命名空间共享虚拟网卡.\n\n以默认配置文件启动将加入社区网络.指定的参数为 `--rm` 当进程结束时会自动销毁容器,且日志会在控制台输出,这有利于初次运行调试.\n\n```bash\ndocker run --rm --privileged=true --net=host --volume /var/lib/candy:/var/lib/candy docker.io/lanthora/candy:latest\n```\n\n以自定义配置文件启动.请在[默认配置](https://raw.githubusercontent.com/lanthora/candy/refs/heads/master/candy.cfg)基础上自定义配置文件.\n\n```bash\ndocker run --rm --privileged=true --net=host --volume /var/lib/candy:/var/lib/candy --volume /path/to/candy.cfg:/etc/candy.cfg docker.io/lanthora/candy:latest\n```\n\n一切正常后,以守护进程的形式启动.\n\n```bash\ndocker run --detach --restart=always --privileged=true --net=host --volume /var/lib/candy:/var/lib/candy --volume /path/to/candy.cfg:/etc/candy.cfg docker.io/lanthora/candy:latest\n```\n\n## Arch Linux\n\n使用 [AUR](https://aur.archlinux.org/packages/candy) 或者 [archlinuxcn](https://github.com/archlinuxcn/repo/tree/master/archlinuxcn/candy) 仓库\n\n```bash\n# AUR\nparu candy\n# archlinuxcn\npacman -S candy\n```\n\n## Gentoo\n\n```bash\nemerge --sync gentoo && emerge -av candy\n```\n\n## 单文件可执行程序\n\n当上述所有方式都不适用时,尝试[单文件可执行程序](https://github.com/lanthora/candy/releases/latest).\n\n该程序由[交叉编译脚本](https://github.com/lanthora/candy/tree/master/scripts/build-standalone.sh)构建.\n\n如果你的系统在使用 Systemd 管理进程.请复制以下文件到指定目录.\n\n```bash\ncp candy.service /usr/lib/systemd/system/candy.service\ncp candy@.service /usr/lib/systemd/system/candy@.service\ncp candy.cfg /etc/candy.cfg\n```\n\n然后按照后续进程管理的方式管理进程.\n\n判断 Systemd 的方法: 检查 `ps -p 1 -o comm=` 输出的内容里是否为 systemd \n\n## 进程管理\n\n各发行版安装后自带 Service 文件,强烈建议使用 Systemd 管理进程,不要使用自己编写的脚本.\n\n对于自定义配置的用户,可以通过以下方式启动进程,不要修改默认配置.\n\n```bash\nmkdir /etc/candy.d\n# 复制一份默认配置,并修改.文件名为 one.cfg\ncp /etc/candy.cfg /etc/candy.d/one.cfg\n# 以 one.cfg 为配置启动进程\nsystemctl start candy@one\n\n# 复制一份默认配置,并修改.文件名为 two.cfg\n# 需要注意不同配置文件中的 name 字段不能重复\ncp /etc/candy.cfg /etc/candy.d/two.cfg\n# 以 two.cfg 为配置启动进程\nsystemctl start candy@two\n```\n"
  },
  {
    "path": "docs/install-client-for-macos.md",
    "content": "# 安装 macOS 客户端\n\nmacOS 客户端通过 [Homebrew](https://brew.sh) 安装并提供服务.\n\n## 安装 Homebrew\n\n```bash\n/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n```\n\n## 添加第三方仓库\n\n```bash\nbrew tap lanthora/repo\n```\n\n## 安装 Candy\n\n```bash\nbrew install candy\n```\n\n## 修改配置\n\n对于 M 系列处理器,配置文件在 `/opt/homebrew/etc/candy.cfg`, Intel 系列处理器,配置文件在 `/usr/local/etc/candy.cfg`\n\n通过以下命令进行测试:\n\n```bash\nsudo candy -c /path/to/candy.cfg\n```\n\n## 启动服务\n\n测试成功后以服务的形式运行.\n\n```bash\nsudo brew services start lanthora/repo/candy\n```\n"
  },
  {
    "path": "docs/install-client-for-windows.md",
    "content": "# 安装 Windows 客户端\n\n## 图形用户界面\n\n对于 Windows 10 及以上的用户,请使用[图形界面版本](https://github.com/lanthora/cake/releases/latest).此版本支持同时配置多个网络.\n\n在没有任何配置时,点击 \"文件\" => \"新建\" 将填充默认配置,点击 \"保存\" 后配置生效,客户端此时才开始连接服务端. \n\n图形界面的配置与[默认配置](https://raw.githubusercontent.com/lanthora/candy/refs/heads/master/candy.cfg)对应.\n\n日志保存在 `C:/ProgramData/Cake/logs`, 反馈 Windows 相关问题请带着日志和配置截图.\n\n![cake](images/cake.png)\n\n## 命令行\n\n使用命令行版本请自行解决遇到的任何问题,我们不对 Windows 命令行提供任何技术支持.\n\nWindows 7 用户只能使用[命令行版本](https://github.com/lanthora/candy/releases/latest)\n"
  },
  {
    "path": "docs/software-defined-wide-area-network.md",
    "content": "# 多局域网组网\n\n## 需求\n\n在多地有多个局域网时,希望能够让本局域网内的设备通过其他局域网的地址直接访问对方的设备,并且无需在所有设备上部署 Candy 客户端.\n\n## 示例\n\n首先你需要:\n\n- 有一个独立的网络.可以自建服务端或者使用社区服务器\n- 在网关 (Gateway) 上部署 Candy 并成功加入自己创建的网络\n\n以 LAN A 为例解释表格含义.\n\n- 局域网 (Network) 地址为 `172.16.1.0/24`, 这个地址不能与 B,C 冲突\n- 网关 (Gateway) 可以是路由器,也可以是局域网中任意一台 Linux 系统,但需要能够部署 Candy 客户端,假设它在局域网中的地址是 `172.16.1.1`. 通过给局域网中的设备配置路由,确保流量能够进入网关\n- Candy 客户端部署在网关上,它在虚拟网络中的地址是 `192.168.202.1`\n\n| LAN     | A             | B             | C             |\n| :------ | :------------ | :------------ | :------------ |\n| Network | 172.16.1.0/24 | 172.16.2.0/24 | 172.16.3.0/24 |\n| Gateway | 172.16.1.1    | 172.16.2.1    | 172.16.3.1    |\n| Candy   | 192.168.202.1 | 192.168.202.2 | 192.168.202.3 |\n\n当 `172.16.1.0/24` 的设备访问 `172.16.2.0/24` 的设备时,希望流量可以通过以下方式送达:\n\n```txt\n172.16.1.0/24 <=> 172.16.1.1 <=> 192.168.202.1 <=> 192.168.202.2 <=> 172.16.2.1 <=> 172.16.2.0/24\n```\n\n### 流量转发到网关 (172.16.1.0/24 => 172.16.1.1)\n\n如果网关是路由器,不需要任何操作,流量就应该能够进入网关.否则需要在非网关设备上配置流量转发到网关的路由.\n\n给 172.16.1.0/24 的设备配置路由:\n\n- dst: 172.16.2.0/24; gw: 172.16.1.1\n- dst: 172.16.3.0/24; gw: 172.16.1.1\n\n需要用同样的方式给另外两个局域网做配置.\n\n### 允许网关转发流量 (172.16.1.1 <=> 192.168.202.1)\n\n#### Linux\n\n如果你的网关是路由器,应该能够轻易的配置出允许转发.否则需要手动添加转发相关的配置.\n\n开启内核流量转发功能\n\n```bash\nsysctl -w net.ipv4.ip_forward=1\n```\n\n开启动态伪装并接受转发报文.\n\n```bash\niptables -t nat -A POSTROUTING -j MASQUERADE\niptables -A FORWARD -j ACCEPT\n```\n\n#### Windows\n\n查看的网卡名,应该与配置文件中写的名称相同,对于 GUI 版本客户端的默认配置网卡名应该为 `candy`\n\n```ps\nGet-NetAdapter\n```\n\n允许转发,注意要把网卡名替换成上一步查出来的网卡名\n\n```ps\nSet-NetIPInterface -ifAlias 'candy' -Forwarding Enabled\n```\n\n#### macOS\n\n应该不会有人拿 macOS 做网关吧, Windows 应该都没有多少人,有需要再补充这部分文档\n\n### 创建虚拟链路 (172.16.1.0/24 <=> 172.16.2.0/24)\n\n所有 Candy 客户端 `192.168.202.0/24` 收到发往 `172.16.1.0/24` 的 IP 报文时,将其转发到 `192.168.202.1`;\n所有 Candy 客户端 `192.168.202.0/24` 收到发往 `172.16.2.0/24` 的 IP 报文时,将其转发到 `192.168.202.2`;\n所有 Candy 客户端 `192.168.202.0/24` 收到发往 `172.16.3.0/24` 的 IP 报文时,将其转发到 `192.168.202.3`;\n\n策略会发下给属于 `192.168.202.0/24` 网络的客户端,上面的配置下发给了虚拟网络中的所有设备,能够满足大部分用户场景.\n\n此外支持更细粒度的控制供用户选择,例如 `192.168.202.1/32` 就表示仅把路由策略下发给 `192.168.202.1` 这台设备.\n\n#### Cacao 配置\n\n如果你在使用 Cacao 服务端(例如社区服务端),配置如下.\n\n![sdwan](images/sdwan.png)\n\n#### Candy 配置\n\n如果你在使用命令行版本的 Candy 服务端,等效配置如下. \n\n```ini\nsdwan = \"192.168.202.0/24,172.16.1.0/24,192.168.202.1;192.168.202.0/24,172.16.2.0/24,192.168.202.2;192.168.202.0/24,172.16.3.0/24,192.168.202.3;\"\n```\n\n### 测试\n\n此时局域网设备之间应当可以相互 ping 通.\n\n## 常见问题\n\n### 能 ping 通网关,但 ping 不通网关下的目标设备\n\n- 检查 iptables 配置的动态伪装是否生效.如果生效,抓包可以看到发往目标设备的源地址已经改成了网关地址\n- 检查目标设备防火墙.例如 Windows 系统防火墙默认禁止 ping, 此时直接尝试访问 Windows 提供出的服务,例如远程桌面, SSH, Web 服务等\n\n### 能 ping 通目标设备,但不能访问服务\n\n- 检查 iptables 配置的动态伪装是否生效.动态伪装不生效的情况下,某种路由配置规则也可以实现 ping 通目标设备,但是防火墙会拦截对应报文.\n\n### 关于源进源出\n\n通过合理的路由配置和对防火墙策略的调整,在不使用动态伪装的情况下,可以做到在目标设备看到请求的真实源地址.想要达成这个效果需要有足够的计算机网络知识储备,请自行探索.\n\n"
  },
  {
    "path": "docs/use-the-community-server.md",
    "content": "# 使用社区服务器\n\n社区服务器支持用户级别的隔离,同时支持一个用户创建多个网络.\n\n__服务器将定期清理不活跃用户,请确保短期内至少有一台设备连接过服务器,或手动登录过服务器管理页面.__\n\n## 注册\n\n在社区服务器[注册](https://canets.org/register),示例中的用户名为 `username`.\n\n![](images/cacao-register.png)\n\n## 使用默认网络\n\n查看网络,可以注意到已经有一个名称为 @ 的默认网络,密码是 `ZrhaUcz1`\n\n![](images/cacao-network.png)\n\n连接到这个网络的客户端仅需要修改以下配置,关于配置文件的位置请参考客户端安装的相关文档:\n\n```cfg\nwebsocket = \"wss://canets.org/username\"\npassword = \"ZrhaUcz1\"\n```\n\n## 多个网络\n\n点击左上角 `Add` 可以创建多个网络,例如:\n\n![](images/cacao-network-another.png)\n\n这个新网络,网络名为 `netname`, 这会体现到 `websocket` 参数中; 密码为空; 网络范围是 `10.0.0.0/24`; 不允许广播; 且租期为 3 天, 即超过 3 天不活跃的客户端将被自动从网络中移除, 配置为 0 时表示不自动移除.\n\n客户端的配置应该为:\n\n```cfg\nwebsocket = \"wss://canets.org/username/netname\"\npassword = \"\"\n```\n\n如果要给某个客户端指定静态地址 `10.0.0.1/24`, 只需要修改配置中的:\n\n```cfg\ntun = \"10.0.0.1/24\"\n```\n"
  },
  {
    "path": "scripts/build-standalone.sh",
    "content": "#!/bin/bash -e\n\nif [ -z $CANDY_WORKSPACE ];then echo \"CANDY_WORKSPACE is not exist\";exit 1;fi\n\nif [[ -z $TARGET || -z $TARGET_OPENSSL ]];then\n    if [ -z $CANDY_ARCH ];then echo \"CANDY_ARCH is not exist\";exit 1;fi\n    if [ -z $CANDY_OS ];then echo \"CANDY_OS is not exist\";exit 1;fi\n    echo \"CANDY_ARCH: $CANDY_ARCH\"\n    echo \"CANDY_OS: $CANDY_OS\"\n    if [[ \"$CANDY_OS\" == \"linux\" ]]; then\n        if [[ \"$CANDY_ARCH\" == \"aarch64\" ]]; then TARGET=\"aarch64-unknown-linux-musl\";TARGET_OPENSSL=\"linux-aarch64\";UPX=1\n        elif [[ \"$CANDY_ARCH\" == \"arm\" ]]; then TARGET=\"arm-unknown-linux-musleabi\";TARGET_OPENSSL=\"linux-armv4\";UPX=1\n        elif [[ \"$CANDY_ARCH\" == \"armhf\" ]]; then TARGET=\"arm-unknown-linux-musleabihf\";TARGET_OPENSSL=\"linux-armv4\";UPX=1\n        elif [[ \"$CANDY_ARCH\" == \"loongarch64\" ]]; then TARGET=\"loongarch64-unknown-linux-musl\";TARGET_OPENSSL=\"linux64-loongarch64\";UPX=0\n        elif [[ \"$CANDY_ARCH\" == \"mips\" ]]; then TARGET=\"mips-unknown-linux-musl\";TARGET_OPENSSL=\"linux-mips32\";UPX=1\n        elif [[ \"$CANDY_ARCH\" == \"mipssf\" ]]; then TARGET=\"mips-unknown-linux-muslsf\";TARGET_OPENSSL=\"linux-mips32\";UPX=1\n        elif [[ \"$CANDY_ARCH\" == \"mipsel\" ]]; then TARGET=\"mipsel-unknown-linux-musl\";TARGET_OPENSSL=\"linux-mips32\";UPX=1\n        elif [[ \"$CANDY_ARCH\" == \"mipselsf\" ]]; then TARGET=\"mipsel-unknown-linux-muslsf\";TARGET_OPENSSL=\"linux-mips32\";UPX=1\n        elif [[ \"$CANDY_ARCH\" == \"mips64\" ]]; then TARGET=\"mips64-unknown-linux-musl\";TARGET_OPENSSL=\"linux64-mips64\";UPX=0\n        elif [[ \"$CANDY_ARCH\" == \"mips64el\" ]]; then TARGET=\"mips64el-unknown-linux-musl\";TARGET_OPENSSL=\"linux64-mips64\";UPX=0\n        elif [[ \"$CANDY_ARCH\" == \"riscv32\" ]]; then TARGET=\"riscv32-unknown-linux-musl\";TARGET_OPENSSL=\"linux32-riscv32\";UPX=0\n        elif [[ \"$CANDY_ARCH\" == \"riscv64\" ]]; then TARGET=\"riscv64-unknown-linux-musl\";TARGET_OPENSSL=\"linux64-riscv64\";UPX=0\n        elif [[ \"$CANDY_ARCH\" == \"x86_64\" ]]; then TARGET=\"x86_64-unknown-linux-musl\";TARGET_OPENSSL=\"linux-x86_64\";UPX=1\n        else echo \"Unknown CANDY_ARCH: $CANDY_ARCH\";exit 1;fi\n    elif [[ \"$CANDY_OS\" == \"macos\" ]]; then\n        echo \"macos is not supported yet\";exit 1\n    elif [[ \"$CANDY_OS\" == \"windows\" ]]; then\n        echo \"windows is not supported yet\";exit 1\n    else echo \"Unknown CANDY_OS: $CANDY_OS\";exit 1;fi\nfi\n\necho \"CANDY_WORKSPACE: $CANDY_WORKSPACE\"\necho \"TARGET: $TARGET\"\necho \"TARGET_OPENSSL: $TARGET_OPENSSL\"\n\nTOOLCHAINS=\"$CANDY_WORKSPACE/toolchains\"\nCOMPILER_ROOT=\"$TOOLCHAINS/$TARGET\"\n\nif [ ! -d \"$COMPILER_ROOT\" ]; then\n    mkdir -p $TOOLCHAINS\n    VERSION=20250206\n    wget -c https://github.com/musl-cross/musl-cross/releases/download/$VERSION/$TARGET.tar.xz -P $TOOLCHAINS\n    tar xvf $COMPILER_ROOT.tar.xz -C $TOOLCHAINS\nfi\n\nexport CC=\"$COMPILER_ROOT/bin/$TARGET-gcc\"\nexport CXX=\"$COMPILER_ROOT/bin/$TARGET-g++\"\nexport AR=\"$COMPILER_ROOT/bin/$TARGET-ar\"\nexport LD=\"$COMPILER_ROOT/bin/$TARGET-ld\"\nexport RANLIB=\"$COMPILER_ROOT/bin/$TARGET-ranlib\"\nexport STRIP=\"$COMPILER_ROOT/bin/$TARGET-strip\"\nexport CFLAGS=\"-I $COMPILER_ROOT/$TARGET/include\"\nexport LDFLAGS=\"-static -Wl,--whole-archive -latomic -Wl,--no-whole-archive -L $COMPILER_ROOT/$TARGET/lib\"\n\nif [[ $CANDY_OS && $CANDY_ARCH ]];then\n    BUILD_DIR=\"$CANDY_WORKSPACE/build/$CANDY_OS-$CANDY_ARCH\"\n    OUTPUT_DIR=\"$CANDY_WORKSPACE/output/$CANDY_OS-$CANDY_ARCH\"\nelse\n    BUILD_DIR=\"$CANDY_WORKSPACE/build/$TARGET\"\n    OUTPUT_DIR=\"$CANDY_WORKSPACE/output/$TARGET\"\nfi\n\nif which ninja >/dev/null 2>&1;then GENERATOR=\"Ninja\";else GENERATOR=\"Unix Makefiles\";fi\nSOURCE_DIR=\"$(dirname $(readlink -f \"$0\"))/../\"\ncmake -G \"$GENERATOR\" -B \"$BUILD_DIR\" -DCMAKE_BUILD_TYPE=Release -DCANDY_STATIC=1 -DTARGET_OPENSSL=$TARGET_OPENSSL $SOURCE_DIR\ncmake --build $BUILD_DIR --parallel $(nproc)\nmkdir -p $OUTPUT_DIR\ncp $BUILD_DIR/candy-cli/candy $OUTPUT_DIR/candy\ncp $BUILD_DIR/candy-service/candy-service $OUTPUT_DIR/candy-service\n\nif [[ $CANDY_STRIP && $CANDY_STRIP -eq 1 ]];then\n    $STRIP $OUTPUT_DIR/candy\n    $STRIP $OUTPUT_DIR/candy-service\nfi\n\nif [[ $CANDY_UPX && $CANDY_UPX -eq 1 && $UPX -eq 1 ]];then\n    upx --lzma --best -q $OUTPUT_DIR/candy\n    upx --lzma --best -q $OUTPUT_DIR/candy-service\nfi\n\nif [[ $CANDY_TGZ && $CANDY_TGZ -eq 1 && $CANDY_OS && $CANDY_ARCH ]];then\n    cp $SOURCE_DIR/{candy.cfg,candy.service,candy@.service,candy.initd} $OUTPUT_DIR\n    tar zcvf $CANDY_WORKSPACE/output/candy-$CANDY_OS-$CANDY_ARCH.tar.gz -C $OUTPUT_DIR .\nfi\n"
  },
  {
    "path": "scripts/search-deps.sh",
    "content": "#!/bin/bash -e\n\n# Define an array to store the processed dependencies\ndeclare -a processed\n\n# Define a function to get the whole list of dependencies recursively\nrecursive_search_deps () {\n  # Use ldd to list the dependencies and filter out the ones that are not absolute paths\n  local list=$(ldd \"$1\" | awk '/=> \\// {print $3}')\n\n  # Loop through the dependencies\n  for dep in $list; do\n    # Check if the dependency has been processed before\n    if [[ ! \" ${processed[@]} \" =~ \" ${dep} \" ]]; then\n      # Check if the dependency contains /c/Windows in its path\n      if [[ \"$dep\" =~ \"/c/Windows\" ]]; then\n        # Ignore the dependency and continue the loop\n        continue\n      fi\n\n      # Copy the dependency to the specified directory\n      cp -n \"$dep\" \"$2\"\n      # Output the copied file path and name\n      echo \"Copied $dep to $2\"\n\n      # Add the dependency to the processed array\n      processed+=(\"$dep\")\n\n      # Recursively call the function to process the dependency's dependencies\n      recursive_search_deps \"$dep\" \"$2\"\n    fi\n  done\n}\n\n# Check if the executable file is given as an argument\nif [ -z \"$2\" ]; then\n  echo \"Usage: $0 <PATH> <DESTINATION>\"\n  exit 1\nfi\n\n# Create the directory if it does not exist\nif [ ! -d \"$2\" ]; then\n  mkdir -p $2\nfi\n\n# Get the absolute path of the executable file\nexe=$(readlink -f \"$1\")\n\n# Copy the executable file to the target directory\ncp \"$exe\" \"$2\"\nexe=$2/$(basename \"$exe\")\nexe=$(readlink -f \"$exe\")\n\n# Call the function to get the whole list of dependencies recursively\nrecursive_search_deps \"$exe\" \"$2\"\n"
  },
  {
    "path": "scripts/standalone.json",
    "content": "{\n    \"include\": [\n        {\n            \"os\": \"linux\",\n            \"arch\": \"aarch64\"\n        },\n        {\n            \"os\": \"linux\",\n            \"arch\": \"arm\"\n        },\n        {\n            \"os\": \"linux\",\n            \"arch\": \"armhf\"\n        },\n        {\n            \"os\": \"linux\",\n            \"arch\": \"loongarch64\"\n        },\n        {\n            \"os\": \"linux\",\n            \"arch\": \"mips\"\n        },\n        {\n            \"os\": \"linux\",\n            \"arch\": \"mipssf\"\n        },\n        {\n            \"os\": \"linux\",\n            \"arch\": \"mipsel\"\n        },\n        {\n            \"os\": \"linux\",\n            \"arch\": \"mipselsf\"\n        },\n        {\n            \"os\": \"linux\",\n            \"arch\": \"mips64\"\n        },\n        {\n            \"os\": \"linux\",\n            \"arch\": \"mips64el\"\n        },\n        {\n            \"os\": \"linux\",\n            \"arch\": \"riscv32\"\n        },\n        {\n            \"os\": \"linux\",\n            \"arch\": \"riscv64\"\n        },\n        {\n            \"os\": \"linux\",\n            \"arch\": \"x86_64\"\n        }\n    ]\n}\n"
  }
]