├── .github
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── BUG-REPORT.yml
│ └── FEATURE-REQUEST.yml
└── workflows
│ └── ci.yaml
├── .gitignore
├── .gitmodules
├── 3rd
└── CMakeLists.txt
├── CMakeLists.txt
├── LICENSE
├── README.md
├── doc
├── .gitkeep
└── img
│ ├── package_windows_finish.png
│ ├── package_windows_install_options.png
│ ├── package_windows_install_path.png
│ ├── package_windows_installing.png
│ ├── package_windows_license.png
│ ├── package_windows_start_menu.png
│ ├── package_windows_thumb.png
│ └── package_windows_welcome.png
├── lib
└── .gitkeep
├── src
├── .gitkeep
├── CMakeLists.txt
├── inc
│ ├── .gitkeep
│ ├── mini_buffer.hpp
│ └── scope_guard.hpp
├── main.cpp
├── res
│ └── icon.ico
└── ssh_connection
│ ├── ssh_connection.cpp
│ └── ssh_connection.h
├── static
├── css
│ ├── bootstrap.min.css
│ ├── fonts
│ │ ├── .gitignore
│ │ └── MesloLGS NF Regular.ttf
│ ├── fullscreen.min.css
│ └── xterm.min.css
├── img
│ └── favicon.png
├── index.html
└── js
│ ├── bootstrap.min.js
│ ├── jquery.min.js
│ ├── main.js
│ ├── popper.min.js
│ ├── xterm-addon-fit.min.js
│ └── xterm.min.js
└── test
├── .gitkeep
├── CMakeLists.txt
└── fake.test.cpp
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Request for changes/ Pull Requests
4 | You first need to create a fork of the [CMakeProjectTemplate](https://github.com/Ohto-Ai/CMakeProjectTemplate/) repository to commit your changes to it. Methods to fork a repository can be found in the [GitHub Documentation](https://docs.github.com/en/get-started/quickstart/fork-a-repo).
5 |
6 | Then add your fork as a local project:
7 |
8 | ```sh
9 | # Using HTTPS
10 | git clone https://github.com/Ohto-Ai/CMakeProjectTemplate.git
11 |
12 | # Using SSH
13 | git clone git@github.com:Ohto-Ai/CMakeProjectTemplate.git
14 | ```
15 |
16 | > [Which remote URL should be used ?](https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories)
17 |
18 | Then, go to your local folder
19 |
20 | ```sh
21 | cd CMakeProjectTemplate
22 | ```
23 |
24 | Add git remote controls :
25 |
26 | ```sh
27 | # Using HTTPS
28 | git remote add fork https://github.com/YOUR-USERNAME/CMakeProjectTemplate.git
29 | git remote add upstream https://github.com/Ohto-Ai/CMakeProjectTemplate.git
30 |
31 |
32 | # Using SSH
33 | git remote add fork git@github.com:YOUR-USERNAME/CMakeProjectTemplate.git
34 | git remote add upstream git@github.com/Ohto-Ai/CMakeProjectTemplate.git
35 | ```
36 |
37 | You can now verify that you have your two git remotes:
38 |
39 | ```sh
40 | git remote -v
41 | ```
42 |
43 | ## Receive remote updates
44 | In view of staying up to date with the central repository :
45 |
46 | ```sh
47 | git pull upstream master
48 | ```
49 |
50 | ## Choose a base branch
51 | Before starting development, you need to know which branch to base your modifications/additions on. When in doubt, use master.
52 |
53 | | Type of change | | Branches |
54 | | :------------------ |:---------:| ---------------------:|
55 | | Documentation | | `master` |
56 | | Bug fixes | | `master` |
57 | | New features | | `master` |
58 | | New issues models | | `YOUR-USERNAME:patch` |
59 |
60 | ```sh
61 | # Switch to the desired branch
62 | git switch master
63 |
64 | # Pull down any upstream changes
65 | git pull
66 |
67 | # Create a new branch to work on
68 | git switch --create patch/1234-name-issue
69 | ```
70 |
71 | Commit your changes, then push the branch to your fork with `git push -u fork` and open a pull request on [the CMakeProjectTemplate repository](https://github.com/Ohto-Ai/CMakeProjectTemplate/) following the template provided.
72 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG-REPORT.yml:
--------------------------------------------------------------------------------
1 | name: "🐛 Bug Report"
2 | description: Create a new ticket for a bug.
3 | title: "🐛 [BUG] -
"
4 | labels: [
5 | "bug"
6 | ]
7 | body:
8 | - type: textarea
9 | id: description
10 | attributes:
11 | label: "Description"
12 | description: Please enter an explicit description of your issue
13 | placeholder: Short and explicit description of your incident...
14 | validations:
15 | required: true
16 | - type: input
17 | id: reprod-url
18 | attributes:
19 | label: "Reproduction URL"
20 | description: Please enter your GitHub URL to provide a reproduction of the issue
21 | placeholder: ex. https://github.com/USERNAME/REPO-NAME
22 | validations:
23 | required: false
24 | - type: textarea
25 | id: reprod
26 | attributes:
27 | label: "Reproduction steps"
28 | description: Please enter an explicit description of your issue
29 | value: |
30 | 1. Go to '...'
31 | 2. Click on '....'
32 | 3. Scroll down to '....'
33 | 4. See error
34 | render: bash
35 | validations:
36 | required: true
37 | - type: textarea
38 | id: screenshot
39 | attributes:
40 | label: "Screenshots"
41 | description: If applicable, add screenshots to help explain your problem.
42 | value: |
43 | 
44 | render: bash
45 | validations:
46 | required: false
47 | - type: textarea
48 | id: logs
49 | attributes:
50 | label: "Logs"
51 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
52 | render: bash
53 | validations:
54 | required: false
55 | - type: dropdown
56 | id: os
57 | attributes:
58 | label: "OS"
59 | description: What is the impacted environment ?
60 | multiple: true
61 | options:
62 | - Windows
63 | - Linux
64 | - Mac
65 | validations:
66 | required: false
67 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml:
--------------------------------------------------------------------------------
1 | name: "💡 Feature Request"
2 | description: Create a new ticket for a new feature request
3 | title: "💡 [REQUEST] - "
4 | labels: [
5 | "question"
6 | ]
7 | body:
8 | - type: input
9 | id: start_date
10 | attributes:
11 | label: "Start Date"
12 | description: Start of development
13 | placeholder: "month/day/year"
14 | validations:
15 | required: false
16 | - type: textarea
17 | id: implementation_pr
18 | attributes:
19 | label: "Implementation PR"
20 | description: Pull request used
21 | placeholder: "#Pull Request ID"
22 | validations:
23 | required: false
24 | - type: textarea
25 | id: reference_issues
26 | attributes:
27 | label: "Reference Issues"
28 | description: Common issues
29 | placeholder: "#Issues IDs"
30 | validations:
31 | required: false
32 | - type: textarea
33 | id: summary
34 | attributes:
35 | label: "Summary"
36 | description: Provide a brief explanation of the feature
37 | placeholder: Describe in a few lines your feature request
38 | validations:
39 | required: true
40 | - type: textarea
41 | id: basic_example
42 | attributes:
43 | label: "Basic Example"
44 | description: Indicate here some basic examples of your feature.
45 | placeholder: A few specific words about your feature request.
46 | validations:
47 | required: true
48 | - type: textarea
49 | id: drawbacks
50 | attributes:
51 | label: "Drawbacks"
52 | description: What are the drawbacks/impacts of your feature request ?
53 | placeholder: Identify the drawbacks and impacts while being neutral on your feature request
54 | validations:
55 | required: true
56 | - type: textarea
57 | id: unresolved_question
58 | attributes:
59 | label: "Unresolved questions"
60 | description: What questions still remain unresolved ?
61 | placeholder: Identify any unresolved issues.
62 | validations:
63 | required: false
64 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: Cross-platform CI
2 |
3 | on:
4 | push:
5 | paths:
6 | - '.github/workflows/ci.yaml'
7 | - 'src/**'
8 | - 'inc/**'
9 | - 'test/**'
10 | - 'CMakeLists.txt'
11 | - '*.cmake'
12 | pull_request:
13 | paths:
14 | - '.github/workflows/ci.yaml'
15 | - 'src/**'
16 | - 'inc/**'
17 | - 'test/**'
18 | - 'CMakeLists.txt'
19 | - '*.cmake'
20 | workflow_dispatch:
21 | inputs:
22 | debug_enabled:
23 | type: boolean
24 | description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
25 | required: false
26 | default: false
27 |
28 | jobs:
29 | build:
30 | name: ${{ matrix.build_target }} - ${{ matrix.os }} - build
31 | runs-on: ${{ matrix.os }}
32 | strategy:
33 | matrix:
34 | os: [ubuntu-20.04, ubuntu-22.04]
35 | build_type: [Debug, Release]
36 | build_target: [webssh_cpp]
37 | steps:
38 | - name: Checkout
39 | uses: actions/checkout@v3
40 | with:
41 | submodules: 'true'
42 | fetch-depth: 0
43 | - name: Set reusable strings
44 | id: strings
45 | shell: bash
46 | run: |
47 | unix_workspace_path=$(echo "${{ github.workspace }}" | sed 's/\\/\//g')
48 | echo "build-output-dir=${unix_workspace_path}/build" >> "$GITHUB_OUTPUT"
49 | echo "test-output-dir=${unix_workspace_path}/build/test" >> "$GITHUB_OUTPUT"
50 | echo "build-parallel=8" >> "$GITHUB_OUTPUT"
51 | - name: Config
52 | shell: bash
53 | run: |
54 | cmake -B${{ steps.strings.outputs.build-output-dir }} -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
55 | - name: Build
56 | shell: bash
57 | run: |
58 | cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} --target ${{ matrix.build_target }} -j${{ steps.strings.outputs.build-parallel }}
59 | # - name: Test
60 | # shell: bash
61 | # run: |
62 | # ctest --build-config ${{ matrix.build_type }} --test-dir ${{ steps.strings.outputs.test-output-dir }} --output-on-failure
63 | - name: Setup tmate session
64 | uses: mxschmitt/action-tmate@v3
65 | if: ${{ failure() || github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
66 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | CMakeLists.txt.user
2 | CMakeCache.txt
3 | CMakeFiles
4 | CMakeScripts
5 | Testing
6 | Makefile
7 | cmake_install.cmake
8 | install_manifest.txt
9 | compile_commands.json
10 | CTestTestfile.cmake
11 | _deps
12 | build
13 | bin
14 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "3rd/spdlog"]
2 | path = 3rd/spdlog
3 | url = https://github.com/gabime/spdlog
4 | [submodule "3rd/libssh2"]
5 | path = 3rd/libssh2
6 | url = https://github.com/libssh2/libssh2
7 | [submodule "3rd/libhv"]
8 | path = 3rd/libhv
9 | url = https://github.com/ithewei/libhv
10 | [submodule "3rd/catch2"]
11 | path = 3rd/catch2
12 | url = https://github.com/catchorg/Catch2
13 |
--------------------------------------------------------------------------------
/3rd/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | include(FetchContent)
2 |
3 | # libuv
4 | set(BUILD_SHARED OFF CACHE BOOL "" FORCE)
5 | set(BUILD_STATIC ON CACHE BOOL "" FORCE)
6 | set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
7 | set(BUILD_UNITTEST OFF CACHE BOOL "" FORCE)
8 | set(WITH_PROTOCOL OFF CACHE BOOL "" FORCE)
9 | set(WITH_PROTOCOL OFF CACHE BOOL "" FORCE)
10 |
11 | # libssh2
12 | set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
13 | set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
14 | set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
15 |
16 | FetchContent_Declare(
17 | spdlog
18 | SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/spdlog
19 | )
20 | FetchContent_Declare(
21 | libssh2
22 | SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libssh2
23 | )
24 | FetchContent_Declare(
25 | libhv
26 | SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libhv
27 | )
28 | FetchContent_Declare(
29 | catch2
30 | SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/catch2
31 | )
32 |
33 | FetchContent_MakeAvailable(spdlog)
34 | FetchContent_MakeAvailable(libssh2)
35 | FetchContent_MakeAvailable(libhv)
36 | FetchContent_MakeAvailable(catch2)
37 |
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.10)
2 |
3 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
4 | # set the project name with the current directory name
5 | get_filename_component(ProjectId ${CMAKE_SOURCE_DIR} NAME_WE)
6 | string(REPLACE " " "_" ProjectId ${ProjectId})
7 |
8 | # set the project name and version
9 | project(${ProjectId})
10 |
11 | set(CMAKE_CXX_STANDARD 17)
12 | set(SOURCE_FOLDER ${PROJECT_SOURCE_DIR}/src CACHE STRING "Source folder")
13 |
14 | if (CMAKE_BUILD_TYPE MATCHES Debug)
15 | add_compile_definitions(_DEBUG)
16 | add_compile_definitions(SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_DEBUG)
17 | endif()
18 |
19 | find_package(OpenSSL QUIET)
20 | find_package(Threads REQUIRED)
21 |
22 | # include top level include directory
23 | include_directories(${PROJECT_SOURCE_DIR}/inc)
24 |
25 | add_subdirectory(3rd)
26 | add_subdirectory(src)
27 | add_subdirectory(test)
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 请问墙和柱子能奶你吗
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # webssh_cpp
2 |
3 | ## Build
4 | ```shell
5 | cmake -Bbuild -DCMAKE_BUILD_TYPE=Release
6 | cmake --build build --target webssh_cpp -j8
7 | ```
8 |
9 | ## Run
10 | ```shell
11 | ./build/src/webssh_cpp
12 | ```
13 | Web: http://localhost:8080/
14 |
--------------------------------------------------------------------------------
/doc/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/doc/.gitkeep
--------------------------------------------------------------------------------
/doc/img/package_windows_finish.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/doc/img/package_windows_finish.png
--------------------------------------------------------------------------------
/doc/img/package_windows_install_options.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/doc/img/package_windows_install_options.png
--------------------------------------------------------------------------------
/doc/img/package_windows_install_path.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/doc/img/package_windows_install_path.png
--------------------------------------------------------------------------------
/doc/img/package_windows_installing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/doc/img/package_windows_installing.png
--------------------------------------------------------------------------------
/doc/img/package_windows_license.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/doc/img/package_windows_license.png
--------------------------------------------------------------------------------
/doc/img/package_windows_start_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/doc/img/package_windows_start_menu.png
--------------------------------------------------------------------------------
/doc/img/package_windows_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/doc/img/package_windows_thumb.png
--------------------------------------------------------------------------------
/doc/img/package_windows_welcome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/doc/img/package_windows_welcome.png
--------------------------------------------------------------------------------
/lib/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/lib/.gitkeep
--------------------------------------------------------------------------------
/src/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/src/.gitkeep
--------------------------------------------------------------------------------
/src/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.10)
2 |
3 | # set the project name and version
4 | project(${ProjectId} LANGUAGES CXX)
5 |
6 | # set project maintainer and contact
7 | set(PROJECT_MAINTAINER "OhtoAi")
8 | set(PROJECT_CONTACT "zhu.thatboy@outlook.com")
9 | set(PROJECT_DESCRIPTION "A cmake project template")
10 | set(PROJECT_DEBIAN_DEPENDENCIES "libssl1.1 (>= 1.1.0)")
11 | set(PROJECT_HOMEPAGE "https://github.com/Ohto-Ai/CMakeProjectTemplate/")
12 |
13 | # trans project name
14 | string(TOUPPER ${PROJECT_NAME} PROJECT_NAME_VAR)
15 | string(REPLACE "-" "_" PROJECT_NAME_VAR ${PROJECT_NAME_VAR})
16 |
17 | # Add source files
18 | set(SOURCES
19 | main.cpp
20 | ssh_connection/ssh_connection.cpp
21 | )
22 |
23 | # Build the executable
24 | add_executable(${PROJECT_NAME} ${SOURCES})
25 |
26 | # Add include directories
27 | target_include_directories(${PROJECT_NAME} PRIVATE inc)
28 | target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_BINARY_DIR}/generated/inc)
29 |
30 | target_link_libraries(${PROJECT_NAME} PRIVATE
31 | Threads::Threads
32 | $<$:OpenSSL::SSL>
33 | $<$:OpenSSL::Crypto>
34 | hv_static
35 | libssh2_static
36 | atomic
37 | spdlog $<$:ws2_32>)
38 |
--------------------------------------------------------------------------------
/src/inc/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/src/inc/.gitkeep
--------------------------------------------------------------------------------
/src/inc/mini_buffer.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #ifndef OHTOAI_MINI_BUFFER_HPP
3 | #define OHTOAI_MINI_BUFFER_HPP
4 |
5 | #include
6 | #include
7 |
8 | namespace ohtoai
9 | {
10 | namespace detail {
11 | struct mini_buffer {
12 | char *data;
13 | size_t size;
14 | size_t capacity;
15 |
16 | mini_buffer() : data(nullptr), size(0), capacity(0) {}
17 | mini_buffer(size_t capacity) : data(nullptr), size(0), capacity(capacity) {
18 | data = new char[capacity];
19 | }
20 |
21 |
22 | ~mini_buffer() {
23 | delete[] data;
24 | }
25 |
26 | operator char*(){
27 | return data;
28 | }
29 | operator const char*() const{
30 | return data;
31 | }
32 |
33 | void reserve(size_t capacity) {
34 | if (capacity <= this->capacity) {
35 | return;
36 | }
37 | char *new_data = new char[capacity];
38 | std::memcpy(new_data, data, size);
39 | delete[] data;
40 | data = new_data;
41 | this->capacity = capacity;
42 | }
43 |
44 | void resize(size_t size) {
45 | reserve(size);
46 | this->size = size;
47 | }
48 |
49 | void clear() {
50 | size = 0;
51 | }
52 |
53 | void append(const char *data, size_t size) {
54 | reserve(this->size + size);
55 | memcpy(this->data + this->size, data, size);
56 | this->size += size;
57 | }
58 |
59 | void append(const std::string &data) {
60 | append(data.data(), data.size());
61 | }
62 |
63 | void append(const mini_buffer &buffer) {
64 | append(buffer.data, buffer.size);
65 | }
66 | };
67 | }
68 | using detail::mini_buffer;
69 | }
70 |
71 | #endif //OHTOAI_MINI_BUFFER_HPP
72 |
--------------------------------------------------------------------------------
/src/inc/scope_guard.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #ifndef OHTOAI_SCOPE_GUARD
3 | #define OHTOAI_SCOPE_GUARD
4 |
5 | #include
6 |
7 | namespace ohtoai
8 | {
9 | namespace detail {
10 | #if __cplusplus >= 202002L
11 | struct scope_guard {
12 | std::functionf;
13 | template requires std::invocable...>
14 | scope_guard(Func&& func, Args&&...args) :f{ [func = std::forward(func), ...args = std::forward(args)]() mutable {
15 | std::invoke(std::forward>(func), std::unwrap_reference_t(std::forward(args))...);
16 | } }{}
17 | ~scope_guard() { f(); }
18 | scope_guard(const scope_guard&) = delete;
19 | scope_guard& operator=(const scope_guard&) = delete;
20 | };
21 | #elif __cplusplus >= 201703L
22 | template
23 | struct scope_guard {
24 | F f;
25 | std::tuplevalues;
26 |
27 | template
28 | scope_guard(Fn&& func, Ts&&...args) :f{ std::forward(func) }, values{ std::forward(args)... } {}
29 | ~scope_guard() {
30 | std::apply(f, values);
31 | }
32 | scope_guard(const scope_guard&) = delete;
33 | };
34 |
35 | template//推导指引非常重要
36 | scope_guard(F&&, Args&&...) -> scope_guard, std::decay_t...>;
37 | #else
38 | using scope_guard = void;
39 | #endif // !__cplusplus >= 202002L
40 | }
41 |
42 | using detail::scope_guard;
43 | } // namespace ohtoai
44 |
45 | #endif // !OHTOAI_SCOPE_GUARD
46 |
--------------------------------------------------------------------------------
/src/main.cpp:
--------------------------------------------------------------------------------
1 | #include "ssh_connection/ssh_connection.h"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | class ssh_context : public std::mutex {
13 | public:
14 | ohtoai::ssh::channel_id_t ssh_channel_id;
15 | std::set channels_read;
16 | std::set channels_write;
17 | void close() {
18 | std::lock_guard lock(*this);
19 | for (auto& channel : channels_read) {
20 | channel->close();
21 | }
22 | for (auto& channel : channels_write) {
23 | channel->close();
24 | }
25 | auto ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(ssh_channel_id);
26 | if (ssh_channel != nullptr) {
27 | ssh_channel->send_eof();
28 | ssh_channel->close();
29 | }
30 | }
31 | ~ssh_context() {
32 | close();
33 | }
34 | inline static std::map> ssh_contexts;
35 | };
36 |
37 | int main(int argc, char *argv[]) {
38 | if (argc > 1 && strcmp(argv[1], "-d") == 0 ) {
39 | spdlog::set_level(spdlog::level::debug);
40 | }
41 |
42 | hv::WebSocketService ws;
43 | ws.onopen = [](const WebSocketChannelPtr& channel, const HttpRequestPtr& req) {
44 | spdlog::debug("{} {}", channel->peeraddr(), req->Path());
45 | auto channel_id = req->GetParam("id");
46 | if (channel_id.empty()) {
47 | spdlog::error("channel_id is empty");
48 | return;
49 | }
50 |
51 | // find ssh_context by channel_id
52 | auto it = ssh_context::ssh_contexts.find(channel_id);
53 | if (it != ssh_context::ssh_contexts.end()) {
54 | auto ctx = it->second.lock();
55 | if (ctx != nullptr) {
56 | ctx->channels_read.emplace(channel);
57 | spdlog::info("[{}] Add channel to existing ssh_context", channel_id);
58 | return;
59 | }
60 | }
61 |
62 | auto ctx = channel->newContextPtr();
63 | ssh_context::ssh_contexts.emplace(channel_id, ctx);
64 | ctx->ssh_channel_id = channel_id;
65 | ctx->channels_read.emplace(channel);
66 | ctx->channels_write.emplace(channel);
67 | auto ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(ctx->ssh_channel_id);
68 | if (ssh_channel == nullptr) {
69 | spdlog::error("[{}] ssh_channel is null", channel_id);
70 | return;
71 | }
72 |
73 | hv::async([ctx] {
74 | while (true) {
75 | auto ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(ctx->ssh_channel_id);
76 | if (ssh_channel == nullptr) {
77 | spdlog::error("[{}] ssh_channel is null", ctx->ssh_channel_id);
78 | break;
79 | }
80 | if (ctx->channels_write.empty()) {
81 | spdlog::error("[{}] channels_write is empty", ctx->ssh_channel_id);
82 | break;
83 | }
84 | if (ssh_channel->read() <= 0) {
85 | std::this_thread::sleep_for(std::chrono::milliseconds(100));
86 | continue;
87 | }
88 |
89 | std::lock_guard lock(*ctx);
90 | for (auto& channel : ctx->channels_read) {
91 | if (!channel->isConnected()) {
92 | ctx->channels_read.erase(channel);
93 | ctx->channels_write.erase(channel);
94 | continue;
95 | }
96 | channel->send(ssh_channel->get_buffer().data, ssh_channel->get_buffer().size);
97 | }
98 | }
99 | ctx->close();
100 | });
101 | };
102 | ws.onmessage = [](const WebSocketChannelPtr& channel, const std::string& msg) {
103 | spdlog::debug("{} {}", channel->peeraddr(), msg);
104 | auto ctx = channel->getContextPtr();
105 | if (ctx == nullptr) {
106 | spdlog::error("ctx is null");
107 | return;
108 | }
109 | auto ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(ctx->ssh_channel_id);
110 | if (ssh_channel == nullptr) {
111 | spdlog::error("ssh_channel is null");
112 | ctx->close();
113 | return;
114 | }
115 |
116 | hv::Json j = hv::Json::parse(msg);
117 | if (j.contains("resize")) {
118 | int width = j["resize"][0];
119 | int height = j["resize"][1];
120 | ssh_channel->resize_pty(width, height);
121 | } else if (j.contains("data")) {
122 | auto data = j["data"].get();
123 | try {
124 | ssh_channel->write(data);
125 | }
126 | catch (const std::exception& e) {
127 | spdlog::error("[{}] Try to write {} bytes, but failed.", ssh_channel->id, data.size());
128 | spdlog::error("{}", e.what());
129 | }
130 | }
131 | };
132 | ws.onclose = [](const WebSocketChannelPtr& channel) {
133 | spdlog::debug("{}", channel->peeraddr());
134 | auto ctx = channel->getContextPtr();
135 | if (ctx == nullptr) {
136 | spdlog::error("ctx is null");
137 | return;
138 | }
139 | auto ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(ctx->ssh_channel_id);
140 | if (ssh_channel == nullptr) {
141 | spdlog::error("ssh_channel is null");
142 | ctx->close();
143 | return;
144 | }
145 | channel->deleteContextPtr();
146 | };
147 |
148 | HttpService http;
149 | http.Static("/", "static");
150 | http.POST("/", [](const HttpContextPtr& ctx) {
151 | spdlog::info("{}:{} {}", ctx->ip(), ctx->port(), ctx->path());
152 | auto hostname = ctx->get("hostname");
153 | auto port = ctx->get("port", 22);
154 | auto username = ctx->get("username");
155 | auto password = ctx->get("password");
156 | auto term = ctx->get("term");
157 | auto channel_id = ctx->get("channel");
158 |
159 | spdlog::debug("Receive login request: hostname={}, port={}, username={}, password={}, term={}, channel_id={}",
160 | hostname, port, username, password, term, channel_id);
161 |
162 | ohtoai::ssh::ssh_channel_ptr ssh_channel {};
163 | if (!channel_id.empty()) {
164 | ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(channel_id);
165 | if (ssh_channel == nullptr) {
166 | ctx->setStatus(HTTP_STATUS_FORBIDDEN);
167 | return ctx->send("ssh_channel is null");
168 | }
169 | // todo: Need attach to original ssh_channel
170 | }
171 | else {
172 | if (hostname.empty() || username.empty() || password.empty()) {
173 | ctx->setStatus(HTTP_STATUS_FORBIDDEN);
174 | return ctx->send("hostname, username, password are required");
175 | }
176 | ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(hostname, port, username, password);
177 | if (ssh_channel == nullptr) {
178 | ctx->setStatus(HTTP_STATUS_FORBIDDEN);
179 | return ctx->send("ssh_channel is null");
180 | }
181 | ssh_channel->set_env("LC_WSSH_WEBSOCKET_HOST", ctx->host());
182 | ssh_channel->set_env("LC_WSSH_WEBSOCKET_URL", ctx->url());
183 | ssh_channel->set_env("LC_WSSH_WEBSOCKET_CLIENT_IP", ctx->header("X-Real-IP", ctx->ip()));
184 | ssh_channel->request_pty(term);
185 | ssh_channel->shell();
186 | }
187 |
188 | hv::Json resp;
189 | resp["id"] = ssh_channel->id;
190 | resp["encoding"] = "utf-8";
191 | return ctx->send(resp.dump(2));
192 | });
193 |
194 | hv::WebSocketServer server;
195 | server.port = 8080;
196 |
197 | server.registerHttpService(&http);
198 | server.registerWebSocketService(&ws);
199 | server.run();
200 | return 0;
201 | }
202 |
--------------------------------------------------------------------------------
/src/res/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/src/res/icon.ico
--------------------------------------------------------------------------------
/src/ssh_connection/ssh_connection.cpp:
--------------------------------------------------------------------------------
1 | #include "ssh_connection.h"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | struct DebugInfo {
15 | const char *file = nullptr;
16 | const char *func_name = nullptr;
17 | int line = 0;
18 | };
19 |
20 | // Internal implementation for functions with non-void return type
21 | template
22 | auto wrapSSHFunctionImpl(const DebugInfo &dbg_info, LIBSSH2_SESSION* session, Func func, Args&&... args) -> decltype(func(std::forward(args)...)){
23 | if constexpr (std::is_same_v) {
24 | return func(std::forward(args)...);
25 | }
26 | else {
27 | Result rc {};
28 | while (true) {
29 | rc = func(std::forward(args)...);
30 | if (rc > 0) {
31 | return rc;
32 | }
33 | else if (rc != LIBSSH2_ERROR_EAGAIN) {
34 | char *error_msg = nullptr;
35 | libssh2_session_last_error(session, &error_msg, nullptr, 0);
36 | throw std::runtime_error(fmt::format("{}:{} ({}) {}", dbg_info.file, dbg_info.line, rc, error_msg));
37 | }
38 | }
39 | return rc;
40 | }
41 | }
42 |
43 | #define WRAP_SSH_FUNCTION(session, func, ...) wrapSSHFunctionImpl({__FILE__, __func__, __LINE__}, session, func, __VA_ARGS__)
44 |
45 | ohtoai::ssh::detail::ssh_channel::ssh_channel():
46 | id(std::to_string(reinterpret_cast(this))) {
47 | channel = nullptr;
48 | session = nullptr;
49 | }
50 |
51 | ohtoai::ssh::detail::ssh_channel::~ssh_channel() {
52 | close();
53 | spdlog::debug("[{}] Channel closed", id);
54 | }
55 |
56 | void ohtoai::ssh::detail::ssh_channel::reserve_buffer(size_t size) {
57 | buffer.reserve(size);
58 | }
59 |
60 | const ohtoai::mini_buffer& ohtoai::ssh::detail::ssh_channel::get_buffer() {
61 | return buffer;
62 | }
63 |
64 | bool ohtoai::ssh::detail::ssh_channel::is_open() {
65 | return channel != nullptr && libssh2_channel_eof(channel) == 0;
66 | }
67 |
68 | long ohtoai::ssh::detail::ssh_channel::read() {
69 | if (channel == nullptr) {
70 | throw std::runtime_error(fmt::format("[{}] Channel is not opened", id));
71 | }
72 | long rc = libssh2_channel_read(channel, buffer.data, buffer.capacity);
73 |
74 | if (rc == LIBSSH2_ERROR_EAGAIN) {
75 | rc = 0;
76 | }
77 |
78 | if (rc < 0) {
79 | char *error_msg = nullptr;
80 | libssh2_session_last_error(session->session, &error_msg, nullptr, 0);
81 | throw std::runtime_error(fmt::format("[{}]({}) <{}> {}", id, __LINE__, rc, error_msg));
82 | }
83 | buffer.resize(rc);
84 |
85 | if (rc >0) {
86 | spdlog::debug("[{}] Read {} bytes", id, rc);
87 | }
88 | return rc;
89 | }
90 |
91 | void ohtoai::ssh::detail::ssh_channel::write(const byte* data, size_t size) {
92 | if (channel == nullptr) {
93 | throw std::runtime_error(fmt::format("[{}] Channel is not opened", id));
94 | }
95 | ssize_t rc = libssh2_channel_write(channel, data, size);
96 | if (rc < 0 && rc != LIBSSH2_ERROR_EAGAIN) {
97 | char *error_msg = nullptr;
98 | libssh2_session_last_error(session->session, &error_msg, nullptr, 0);
99 | throw std::runtime_error(fmt::format("[{}]({}) <{}> {}", id, __LINE__, rc, error_msg));
100 | }
101 | spdlog::debug("[{}] Wrote {} bytes", id, rc);
102 | }
103 |
104 | void ohtoai::ssh::detail::ssh_channel::write(const std::string &data) {
105 | write(reinterpret_cast(data.data()), data.size());
106 | }
107 |
108 | void ohtoai::ssh::detail::ssh_channel::set_env(const std::string &name, const std::string &value) {
109 | if (channel == nullptr) {
110 | throw std::runtime_error(fmt::format("[{}] Channel is not opened", id));
111 | }
112 | while (int rc = libssh2_channel_setenv(channel, name.c_str(), value.c_str())) {
113 | if (rc != LIBSSH2_ERROR_EAGAIN) {
114 | char *error_msg = nullptr;
115 | libssh2_session_last_error(session->session, &error_msg, nullptr, 0);
116 | throw std::runtime_error(fmt::format("[{}]({}) <{}> {}", id, __LINE__, rc, error_msg));
117 | }
118 | session->wait_socket();
119 | }
120 | spdlog::debug("[{}] Env set {}={}", id, name, value);
121 | }
122 |
123 | void ohtoai::ssh::detail::ssh_channel::shell() {
124 | if (channel == nullptr) {
125 | throw std::runtime_error(fmt::format("[{}] Channel is not opened", id));
126 | }
127 | while (int rc = libssh2_channel_shell(channel)) {
128 | if (rc != LIBSSH2_ERROR_EAGAIN) {
129 | char *error_msg = nullptr;
130 | libssh2_session_last_error(session->session, &error_msg, nullptr, 0);
131 | throw std::runtime_error(fmt::format("[{}]({}) <{}> {}", id, __LINE__, rc, error_msg));
132 | }
133 | session->wait_socket();
134 | }
135 | spdlog::debug("[{}] Shell requested", id);
136 | }
137 |
138 | void ohtoai::ssh::detail::ssh_channel::request_pty(const std::string &pty_type) {
139 | if (channel == nullptr) {
140 | throw std::runtime_error(fmt::format("[{}] Channel is not opened", id));
141 | }
142 |
143 | while (int rc = libssh2_channel_request_pty(channel, pty_type.c_str())) {
144 | if (rc != LIBSSH2_ERROR_EAGAIN) {
145 | char *error_msg = nullptr;
146 | libssh2_session_last_error(session->session, &error_msg, nullptr, 0);
147 | throw std::runtime_error(fmt::format("[{}]({}) <{}> {}", id, __LINE__, rc, error_msg));
148 | }
149 | session->wait_socket();
150 | }
151 | spdlog::debug("[{}] Pty requested {}", id, pty_type);
152 | }
153 |
154 | void ohtoai::ssh::detail::ssh_channel::resize_pty(int width, int height) {
155 | if (channel == nullptr) {
156 | throw std::runtime_error(fmt::format("[{}] Channel is not opened", id));
157 | }
158 | spdlog::debug("[{}] Pty resized {}x{}", id, width, height);
159 | while (int rc = libssh2_channel_request_pty_size(channel, width, height)) {
160 | if (rc != LIBSSH2_ERROR_EAGAIN) {
161 | char *error_msg = nullptr;
162 | libssh2_session_last_error(session->session, &error_msg, nullptr, 0);
163 | throw std::runtime_error(fmt::format("[{}]({}) <{}> {}", id, __LINE__, rc, error_msg));
164 | }
165 | session->wait_socket();
166 | }
167 | }
168 |
169 | void ohtoai::ssh::detail::ssh_channel::send_eof() {
170 | if (channel != nullptr) {
171 | libssh2_channel_send_eof(channel);
172 | }
173 | }
174 |
175 | void ohtoai::ssh::detail::ssh_channel::close() {
176 | if (channel != nullptr) {
177 | libssh2_channel_free(channel);
178 | channel = nullptr;
179 | if (session) {
180 | session->close_channel(id);
181 | }
182 | }
183 | }
184 |
185 | ohtoai::ssh::detail::ssh_session::ssh_session() {
186 | if (counter == 0) {
187 | // init sshlib
188 | spdlog::debug("libssh2 init");
189 | if (int rc = libssh2_init(0)) {
190 | throw std::runtime_error(fmt::format("Failed to initialize ssh library <{}>", rc));
191 | }
192 | }
193 | ++counter;
194 | session = nullptr;
195 | }
196 |
197 | ohtoai::ssh::detail::ssh_session::~ssh_session() {
198 | disconnect();
199 | --counter;
200 | if (counter == 0) {
201 | // deinit sshlib
202 | spdlog::debug("libssh2 exit");
203 | libssh2_exit();
204 | }
205 | }
206 |
207 | void ohtoai::ssh::detail::ssh_session::connect(const std::string &host, int port) {
208 | this->host = host;
209 | this->port = port;
210 | if (session != nullptr) {
211 | throw std::runtime_error("Session is already opened");
212 | }
213 |
214 | sock = ::socket(AF_INET, SOCK_STREAM, 0);
215 | if (sock == LIBSSH2_INVALID_SOCKET) {
216 | throw std::runtime_error("Failed to create socket");
217 | }
218 | spdlog::debug("Socket created");
219 |
220 | struct addrinfo hints, *res;
221 | memset(&hints, 0, sizeof hints);
222 | hints.ai_family = AF_INET;
223 | hints.ai_socktype = SOCK_STREAM;
224 |
225 | if (getaddrinfo(host.c_str(), nullptr, &hints, &res) != 0) {
226 | throw std::runtime_error("Failed to resolve host");
227 | }
228 | // log host and its ip
229 | spdlog::debug("Host resolved {}", host);
230 |
231 | struct sockaddr_in sin {};
232 | memcpy(&sin, res->ai_addr, sizeof sin);
233 | sin.sin_port = htons(port);
234 | freeaddrinfo(res);
235 | if (sin.sin_addr.s_addr == INADDR_NONE) {
236 | throw std::runtime_error("Failed to parse host");
237 | }
238 | spdlog::debug("Host parsed {}", inet_ntoa(sin.sin_addr));
239 |
240 | if (::connect(sock, reinterpret_cast(&sin), sizeof(sin)) != 0) {
241 | throw std::runtime_error("Failed to connect");
242 | }
243 | spdlog::debug("Connected to {}", host);
244 |
245 | session = libssh2_session_init();
246 | if (session == nullptr) {
247 | throw std::runtime_error("Failed to initialize session");
248 | }
249 | spdlog::debug("Session initialized");
250 |
251 | libssh2_session_set_blocking(session, 0);
252 |
253 | while(int rc = libssh2_session_handshake(session, sock)) {
254 | if (rc != LIBSSH2_ERROR_EAGAIN) {
255 | char *error_msg = nullptr;
256 | libssh2_session_last_error(session, &error_msg, nullptr, 0);
257 | throw std::runtime_error(fmt::format("[{}]({}) <{}> {}", get_id(), __LINE__, rc, error_msg));
258 | }
259 | }
260 | spdlog::info("[{}] Session connected", get_id());
261 | }
262 |
263 | void ohtoai::ssh::detail::ssh_session::authenticate(const std::string &username, const std::string &password) {
264 | this->username = username;
265 | if (session == nullptr) {
266 | throw std::runtime_error("Session is not opened");
267 | }
268 | while(int rc = libssh2_userauth_password(session, username.c_str(), password.c_str())) {
269 | if (rc != LIBSSH2_ERROR_EAGAIN) {
270 | char *error_msg = nullptr;
271 | libssh2_session_last_error(session, &error_msg, nullptr, 0);
272 | throw std::runtime_error(fmt::format("[{}]({}) <{}> {}", get_id(), __LINE__, rc, error_msg));
273 | }
274 | }
275 | spdlog::info("[{}] Session authenticated", get_id());
276 | }
277 |
278 | ohtoai::ssh::detail::ssh_channel_ptr ohtoai::ssh::detail::ssh_session::open_channel() {
279 | if (session == nullptr) {
280 | throw std::runtime_error("Session is not opened");
281 | }
282 | ssh_channel_ptr channel = std::make_shared();
283 | channel->session = this;
284 | do {
285 | channel->channel = libssh2_channel_open_session(session);
286 | if (channel->channel) {
287 | break;
288 | }
289 | char *error_msg = nullptr;
290 | auto rc = libssh2_session_last_error(session, &error_msg, nullptr, 0);
291 | if (rc != LIBSSH2_ERROR_EAGAIN) {
292 | throw std::runtime_error(fmt::format("[{}]({}) <{}> {}", channel->id, __LINE__, rc, error_msg));
293 | }
294 | wait_socket();
295 | } while (true);
296 |
297 | try {
298 | channel->set_env("LC_WSSH_CHANNEL_ID", channel->id);
299 | }
300 | catch (const std::exception& e) {
301 | spdlog::error("[{}] Try to set env, but failed.", channel->id);
302 | spdlog::error("{}", e.what());
303 | }
304 |
305 | spdlog::info("[{}] Channel opened", channel->id);
306 |
307 | channels.emplace(channel->id, channel);
308 | spdlog::info("[{}] Session channels opened {}", get_id(), channels.size());
309 | return channel;
310 | }
311 |
312 | void ohtoai::ssh::detail::ssh_session::close_channel(const channel_id_t &id) {
313 | auto iter = channels.find(id);
314 | if (iter != channels.end()) {
315 | iter->second->close();
316 | channels.erase(iter);
317 | spdlog::info("[{}] Channel closed", iter->second->id);
318 | spdlog::info("[{}] Session channels opened {}", get_id(), channels.size());
319 | }
320 | if (channels.empty()) {
321 | disconnect();
322 | }
323 | }
324 |
325 | void ohtoai::ssh::detail::ssh_session::disconnect() {
326 | if (session != nullptr) {
327 | for (auto &channel : channels) {
328 | channel.second->close();
329 | }
330 | libssh2_session_disconnect(session, "Bye bye");
331 | libssh2_session_free(session);
332 | session = nullptr;
333 | spdlog::info("[{}] Session disconnected", get_id());
334 | }
335 | if (sock != LIBSSH2_INVALID_SOCKET) {
336 | ::shutdown(sock, 2);
337 | ::close(sock);
338 | }
339 | }
340 |
341 | void ohtoai::ssh::detail::ssh_session::wait_socket() {
342 | struct timeval timeout;
343 | int rc;
344 | fd_set fd;
345 | fd_set *writefd = nullptr;
346 | fd_set *readfd = nullptr;
347 | int dir;
348 |
349 | timeout.tv_sec = 10;
350 | timeout.tv_usec = 0;
351 |
352 | FD_ZERO(&fd);
353 |
354 | FD_SET(sock, &fd);
355 |
356 | /* now make sure we wait in the correct direction */
357 | dir = libssh2_session_block_directions(session);
358 |
359 | if(dir & LIBSSH2_SESSION_BLOCK_INBOUND)
360 | readfd = &fd;
361 |
362 | if(dir & LIBSSH2_SESSION_BLOCK_OUTBOUND)
363 | writefd = &fd;
364 |
365 | rc = select((int)(sock + 1), readfd, writefd, nullptr, &timeout);
366 | }
367 |
368 | ohtoai::ssh::detail::session_id_t ohtoai::ssh::detail::ssh_session::generate_id(const std::string &host, int port, const std::string &username, const std::string &custom) {
369 | if (username.empty())
370 | return fmt::format("{}:{}{}", host, port, custom);
371 | else
372 | return fmt::format("{}@{}:{}{}", username, host, port, custom);
373 | }
374 |
375 | ohtoai::ssh::detail::session_id_t ohtoai::ssh::detail::ssh_session::get_id() const {
376 | return generate_id(host, port, username);
377 | }
378 |
379 | ohtoai::ssh::detail::ssh_pty_connection_manager::~ssh_pty_connection_manager() {
380 | spdlog::debug("ssh_pty_connection_manager destroyed");
381 |
382 | for (auto &session : sessions) {
383 | session.second->disconnect();
384 | }
385 |
386 | spdlog::debug("ssh_pty_connection_manager sessions closed");
387 |
388 | sessions.clear();
389 | channels.clear();
390 | }
391 |
392 | ohtoai::ssh::detail::ssh_pty_connection_manager &ohtoai::ssh::ssh_pty_connection_manager::get_instance() {
393 | static ssh_pty_connection_manager instance;
394 | return instance;
395 | }
396 |
397 | void ohtoai::ssh::detail::ssh_pty_connection_manager::set_max_channel_in_session(size_t max_channel_in_session) {
398 | this->max_channel_in_session = max_channel_in_session;
399 | }
400 |
401 | size_t ohtoai::ssh::detail::ssh_pty_connection_manager::get_max_channel_in_session() const {
402 | return max_channel_in_session;
403 | }
404 |
405 | size_t ohtoai::ssh::detail::ssh_pty_connection_manager::get_channel_count(detail::session_id_t session_id) const {
406 | std::lock_guard lock(sessions_mutex);
407 | auto begin = sessions.lower_bound(session_id);
408 | auto end = sessions.upper_bound(session_id);
409 | size_t count = 0;
410 | for (auto iter = begin; iter != end; ++iter) {
411 | count += iter->second->channels.size();
412 | }
413 | return count;
414 | }
415 |
416 | size_t ohtoai::ssh::detail::ssh_pty_connection_manager::get_channel_count() const {
417 | return channels.size();
418 | }
419 |
420 | size_t ohtoai::ssh::detail::ssh_pty_connection_manager::get_channel_alive_count() const {
421 | return std::count_if(channels.begin(), channels.end(), [](const auto &pair) {
422 | return !pair.second.expired();
423 | });
424 | }
425 |
426 | size_t ohtoai::ssh::detail::ssh_pty_connection_manager::get_session_count() const {
427 | return sessions.size();
428 | }
429 |
430 | ohtoai::ssh::detail::ssh_channel_ptr ohtoai::ssh::detail::ssh_pty_connection_manager::get_channel(const std::string &host, int port, const std::string &username, const std::string &password) {
431 | auto session_id = detail::ssh_session::generate_id(host, port, username);
432 |
433 | auto session_iter = [this, &session_id]{
434 | std::lock_guard lock(sessions_mutex);
435 | // find first session that has less than max_channel_in_session channels
436 | auto begin = sessions.lower_bound(session_id);
437 | auto end = sessions.upper_bound(session_id);
438 | return std::find_if(begin, end, [this](const auto &pair) {
439 | return pair.second->channels.size() < max_channel_in_session;
440 | });
441 | }();
442 |
443 | // if no session found, create new session
444 | if (session_iter == sessions.end()) {
445 | auto session = std::make_shared();
446 | session->connect(host, port);
447 | session->authenticate(username, password);
448 | std::lock_guard lock(sessions_mutex);
449 | session_iter = sessions.emplace(session_id, session);
450 | }
451 | auto session = session_iter->second;
452 | auto channel = session->open_channel();
453 | channels.emplace(channel->id, channel);
454 | return channel;
455 | }
456 |
457 | ohtoai::ssh::detail::ssh_channel_ptr ohtoai::ssh::detail::ssh_pty_connection_manager::get_channel(const detail::session_id_t &id) {
458 | auto iter = channels.find(id);
459 | if (iter == channels.end()) {
460 | return nullptr;
461 | }
462 | return iter->second.lock();
463 | }
464 |
--------------------------------------------------------------------------------
/src/ssh_connection/ssh_connection.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "mini_buffer.hpp"
4 | #include
5 | #include
6 | #include
7 | #include