├── .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 | ![DESCRIPTION](LINK.png) 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] - <title>" 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 | $<$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>:OpenSSL::SSL> 33 | $<$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>:OpenSSL::Crypto> 34 | hv_static 35 | libssh2_static 36 | atomic 37 | spdlog $<$<BOOL:${MINGW}>: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 <string> 6 | #include <cstring> 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 <functional> 6 | 7 | namespace ohtoai 8 | { 9 | namespace detail { 10 | #if __cplusplus >= 202002L 11 | struct scope_guard { 12 | std::function<void()>f; 13 | template<typename Func, typename...Args> requires std::invocable<Func, std::unwrap_reference_t<Args>...> 14 | scope_guard(Func&& func, Args&&...args) :f{ [func = std::forward<Func>(func), ...args = std::forward<Args>(args)]() mutable { 15 | std::invoke(std::forward<std::decay_t<Func>>(func), std::unwrap_reference_t<Args>(std::forward<Args>(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<typename F, typename...Args> 23 | struct scope_guard { 24 | F f; 25 | std::tuple<Args...>values; 26 | 27 | template<typename Fn, typename...Ts> 28 | scope_guard(Fn&& func, Ts&&...args) :f{ std::forward<Fn>(func) }, values{ std::forward<Ts>(args)... } {} 29 | ~scope_guard() { 30 | std::apply(f, values); 31 | } 32 | scope_guard(const scope_guard&) = delete; 33 | }; 34 | 35 | template<typename F, typename...Args>//推导指引非常重要 36 | scope_guard(F&&, Args&&...) -> scope_guard<std::decay_t<F>, std::decay_t<Args>...>; 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 <spdlog/spdlog.h> 4 | #include <chrono> 5 | #include <thread> 6 | #include <set> 7 | #include <hv/WebSocketServer.h> 8 | #include <hv/HttpServer.h> 9 | #include <hv/EventLoop.h> 10 | #include <hv/hasync.h> 11 | 12 | class ssh_context : public std::mutex { 13 | public: 14 | ohtoai::ssh::channel_id_t ssh_channel_id; 15 | std::set<WebSocketChannelPtr> channels_read; 16 | std::set<WebSocketChannelPtr> 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<ohtoai::ssh::channel_id_t, std::weak_ptr<ssh_context>> 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<ssh_context>(); 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<ssh_context>(); 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<std::string>(); 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<ssh_context>(); 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 <memory> 4 | #include <libssh2.h> 5 | #include <sys/socket.h> 6 | #include <arpa/inet.h> 7 | #include <netinet/in.h> 8 | #include <netdb.h> 9 | #include <stdexcept> 10 | #include <spdlog/spdlog.h> 11 | #include <spdlog/fmt/fmt.h> 12 | #include <unistd.h> 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 <typename Result, typename Func, typename... Args> 22 | auto wrapSSHFunctionImpl(const DebugInfo &dbg_info, LIBSSH2_SESSION* session, Func func, Args&&... args) -> decltype(func(std::forward<Args>(args)...)){ 23 | if constexpr (std::is_same_v<Result, void>) { 24 | return func(std::forward<Args>(args)...); 25 | } 26 | else { 27 | Result rc {}; 28 | while (true) { 29 | rc = func(std::forward<Args>(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<decltype(func(__VA_ARGS__)), decltype(func), decltype(__VA_ARGS__)>({__FILE__, __func__, __LINE__}, session, func, __VA_ARGS__) 44 | 45 | ohtoai::ssh::detail::ssh_channel::ssh_channel(): 46 | id(std::to_string(reinterpret_cast<uintptr_t>(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<const byte*>(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<struct sockaddr*>(&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<ssh_channel>(); 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<std::mutex> 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<std::mutex> 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<detail::ssh_session>(); 446 | session->connect(host, port); 447 | session->authenticate(username, password); 448 | std::lock_guard<std::mutex> 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 <vector> 5 | #include <string> 6 | #include <memory> 7 | #include <map> 8 | #include <atomic> 9 | #include <mutex> 10 | 11 | typedef struct _LIBSSH2_SESSION LIBSSH2_SESSION; 12 | typedef struct _LIBSSH2_CHANNEL LIBSSH2_CHANNEL; 13 | namespace ohtoai::ssh 14 | { 15 | namespace detail 16 | { 17 | using byte = char; 18 | using channel_id_t = std::string; 19 | using session_id_t = std::string; 20 | class mini_buffer; 21 | 22 | class ssh_channel; 23 | class ssh_session; 24 | using ssh_channel_ptr = std::shared_ptr<ssh_channel>; 25 | using ssh_channel_weak_ptr = std::weak_ptr<ssh_channel>; 26 | using ssh_session_ptr = std::shared_ptr<ssh_session>; 27 | 28 | class ssh_channel { 29 | friend class ssh_session; 30 | friend class ssh_pty_connection_manager; 31 | public: 32 | ssh_channel(); 33 | ~ssh_channel(); 34 | 35 | void reserve_buffer(size_t size); 36 | const ohtoai::mini_buffer& get_buffer(); 37 | bool is_open(); 38 | long read(); 39 | void write(const byte* data, size_t size); 40 | void write(const std::string &data); 41 | void shell(); 42 | void set_env(const std::string &name, const std::string &value); 43 | void request_pty(const std::string &pty_type = "vanilla"); 44 | void resize_pty(int width, int height); 45 | void send_eof(); 46 | void close(); 47 | const channel_id_t id; 48 | protected: 49 | LIBSSH2_CHANNEL *channel = nullptr; 50 | ssh_session *session = nullptr; 51 | ohtoai::mini_buffer buffer {4096}; 52 | }; 53 | 54 | 55 | class ssh_session { 56 | friend class ssh_channel; 57 | friend class ssh_pty_connection_manager; 58 | public: 59 | ssh_session(); 60 | ~ssh_session(); 61 | 62 | static session_id_t generate_id(const std::string &host, int port, const std::string &username, const std::string &custom = ""); 63 | session_id_t get_id() const; 64 | void connect(const std::string &host, int port); 65 | void authenticate(const std::string &username, const std::string &password); 66 | ssh_channel_ptr open_channel(); 67 | void disconnect(); 68 | void close_channel(const channel_id_t &id); 69 | void wait_socket(); 70 | protected: 71 | std::string host; 72 | int port; 73 | std::string username; 74 | LIBSSH2_SESSION *session = nullptr; 75 | std::map<channel_id_t, ssh_channel_ptr> channels; 76 | int sock; 77 | inline static std::atomic_size_t counter = 0; 78 | }; 79 | 80 | class ssh_pty_connection_manager { 81 | public: 82 | 83 | static ssh_pty_connection_manager& get_instance(); 84 | 85 | void set_max_channel_in_session(size_t max_channel_in_session); // 0 means no limit, default is 3 86 | size_t get_max_channel_in_session() const; 87 | 88 | size_t get_channel_count(detail::session_id_t) const; 89 | size_t get_channel_count() const; 90 | size_t get_channel_alive_count() const; 91 | size_t get_session_count() const; 92 | 93 | detail::ssh_channel_ptr get_channel(const std::string &host, int port, const std::string &username, const std::string &password); 94 | detail::ssh_channel_ptr get_channel(const detail::session_id_t &id); 95 | void close_channel(const detail::channel_id_t &id); 96 | protected: 97 | std::multimap<detail::session_id_t, detail::ssh_session_ptr> sessions; 98 | std::map<detail::channel_id_t, detail::ssh_channel_weak_ptr> channels; 99 | size_t max_channel_in_session = 3; 100 | 101 | mutable std::mutex sessions_mutex; 102 | protected: 103 | ssh_pty_connection_manager() = default; 104 | ssh_pty_connection_manager(const ssh_pty_connection_manager&) = delete; 105 | ssh_pty_connection_manager(ssh_pty_connection_manager&&) = delete; 106 | ssh_pty_connection_manager& operator=(const ssh_pty_connection_manager&) = delete; 107 | ssh_pty_connection_manager& operator=(ssh_pty_connection_manager&&) = delete; 108 | virtual ~ssh_pty_connection_manager(); 109 | }; 110 | } 111 | 112 | using detail::ssh_channel; 113 | using detail::ssh_session; 114 | using detail::ssh_channel_ptr; 115 | using detail::ssh_session_ptr; 116 | using detail::channel_id_t; 117 | using detail::session_id_t; 118 | using detail::ssh_pty_connection_manager; 119 | } 120 | -------------------------------------------------------------------------------- /static/css/fonts/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/static/css/fonts/.gitignore -------------------------------------------------------------------------------- /static/css/fonts/MesloLGS NF Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/static/css/fonts/MesloLGS NF Regular.ttf -------------------------------------------------------------------------------- /static/css/fullscreen.min.css: -------------------------------------------------------------------------------- 1 | .xterm.fullscreen{position:fixed;top:0;bottom:0;left:0;right:0;width:auto;height:auto;z-index:255} 2 | /*# sourceMappingURL=fullscreen.min.css.map */ -------------------------------------------------------------------------------- /static/css/xterm.min.css: -------------------------------------------------------------------------------- 1 | .xterm{font-feature-settings:"liga" 0;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#FFF;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm{cursor:text}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:0.5}.xterm-underline{text-decoration:underline} -------------------------------------------------------------------------------- /static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/static/img/favicon.png -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <title> WebSSH 6 | 7 | 8 | 9 | 10 | 36 | 37 | 38 | 39 | 40 | 75 | 76 |
77 |
78 |
79 |
80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.3.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},Ae="show",Ne="out",Oe={HIDE:"hide"+Ce,HIDDEN:"hidden"+Ce,SHOW:"show"+Ce,SHOWN:"shown"+Ce,INSERTED:"inserted"+Ce,CLICK:"click"+Ce,FOCUSIN:"focusin"+Ce,FOCUSOUT:"focusout"+Ce,MOUSEENTER:"mouseenter"+Ce,MOUSELEAVE:"mouseleave"+Ce},ke="fade",Pe="show",Le=".tooltip-inner",je=".arrow",He="hover",Re="focus",Ue="click",We="manual",xe=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Pe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(ke);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:je},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Pe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===Ne&&e._leave(null,e)};if(g(this.tip).hasClass(ke)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==Ae&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Pe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ue]=!1,this._activeTrigger[Re]=!1,this._activeTrigger[He]=!1,g(this.tip).hasClass(ke)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Se+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Le)),this.getTitle()),g(t).removeClass(ke+" "+Pe)},t.setElementContent=function(t,e){var n=this.config.html;"object"==typeof e&&(e.nodeType||e.jquery)?n?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text()):t[n?"html":"text"](e)},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return De[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==We){var e=t===He?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===He?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Re:He]=!0),g(e.getTipElement()).hasClass(Pe)||e._hoverState===Ae?e._hoverState=Ae:(clearTimeout(e._timeout),e._hoverState=Ae,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===Ae&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Re:He]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=Ne,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===Ne&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){return"number"==typeof(t=l({},this.constructor.Default,g(this.element).data(),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(ye,t,this.constructor.DefaultType),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(be);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(ke),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ee),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ee,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.0"}},{key:"Default",get:function(){return we}},{key:"NAME",get:function(){return ye}},{key:"DATA_KEY",get:function(){return Ee}},{key:"Event",get:function(){return Oe}},{key:"EVENT_KEY",get:function(){return Ce}},{key:"DefaultType",get:function(){return Ie}}]),i}();g.fn[ye]=xe._jQueryInterface,g.fn[ye].Constructor=xe,g.fn[ye].noConflict=function(){return g.fn[ye]=Te,xe._jQueryInterface};var Fe="popover",qe="bs.popover",Me="."+qe,Ke=g.fn[Fe],Qe="bs-popover",Be=new RegExp("(^|\\s)"+Qe+"\\S+","g"),Ve=l({},xe.Default,{placement:"right",trigger:"click",content:"",template:''}),Ye=l({},xe.DefaultType,{content:"(string|element|function)"}),Xe="fade",ze="show",Ge=".popover-header",Je=".popover-body",Ze={HIDE:"hide"+Me,HIDDEN:"hidden"+Me,SHOW:"show"+Me,SHOWN:"shown"+Me,INSERTED:"inserted"+Me,CLICK:"click"+Me,FOCUSIN:"focusin"+Me,FOCUSOUT:"focusout"+Me,MOUSEENTER:"mouseenter"+Me,MOUSELEAVE:"mouseleave"+Me},$e=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Qe+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(Ge),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(Je),e),t.removeClass(Xe+" "+ze)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Be);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t= 0) { 139 | form_map[key] = val; 140 | } else if (opts_keys.indexOf(key) >=0) { 141 | opts_map[key] = val; 142 | } 143 | } 144 | 145 | if (form_map.password) { 146 | form_map.password = decode_password(form_map.password); 147 | } 148 | } 149 | 150 | 151 | function parse_xterm_style() { 152 | var text = $('.xterm-helpers style').text(); 153 | var arr = text.split('xterm-normal-char{width:'); 154 | style.width = parseFloat(arr[1]); 155 | arr = text.split('div{height:'); 156 | style.height = parseFloat(arr[1]); 157 | } 158 | 159 | 160 | function get_cell_size(term) { 161 | style.width = term._core._renderService._renderer.dimensions.actualCellWidth; 162 | style.height = term._core._renderService._renderer.dimensions.actualCellHeight; 163 | } 164 | 165 | 166 | function toggle_fullscreen(term) { 167 | $('#terminal .terminal').toggleClass('fullscreen'); 168 | term.fitAddon.fit(); 169 | } 170 | 171 | 172 | function current_geometry(term) { 173 | if (!style.width || !style.height) { 174 | try { 175 | get_cell_size(term); 176 | } catch (TypeError) { 177 | parse_xterm_style(); 178 | } 179 | } 180 | 181 | var cols = parseInt(window.innerWidth / style.width, 10) - 1; 182 | var rows = parseInt(window.innerHeight / style.height, 10); 183 | return {'cols': cols, 'rows': rows}; 184 | } 185 | 186 | 187 | function resize_terminal(term) { 188 | var geometry = current_geometry(term); 189 | term.on_resize(geometry.cols, geometry.rows); 190 | } 191 | 192 | 193 | function set_backgound_color(term, color) { 194 | term.setOption('theme', { 195 | background: color 196 | }); 197 | } 198 | 199 | function set_font_color(term, color) { 200 | term.setOption('theme', { 201 | foreground: color 202 | }); 203 | } 204 | 205 | function custom_font_is_loaded() { 206 | if (!custom_font) { 207 | console.log('No custom font specified.'); 208 | } else { 209 | console.log('Status of custom font ' + custom_font.family + ': ' + custom_font.status); 210 | if (custom_font.status === 'loaded') { 211 | return true; 212 | } 213 | if (custom_font.status === 'unloaded') { 214 | return false; 215 | } 216 | } 217 | } 218 | 219 | function update_font_family(term) { 220 | if (term.font_family_updated) { 221 | console.log('Already using custom font family'); 222 | return; 223 | } 224 | 225 | if (!default_fonts) { 226 | default_fonts = term.getOption('fontFamily'); 227 | } 228 | 229 | if (custom_font_is_loaded()) { 230 | var new_fonts = custom_font.family + ', ' + default_fonts; 231 | term.setOption('fontFamily', new_fonts); 232 | term.font_family_updated = true; 233 | console.log('Using custom font family ' + new_fonts); 234 | } 235 | } 236 | 237 | 238 | function reset_font_family(term) { 239 | if (!term.font_family_updated) { 240 | console.log('Already using default font family'); 241 | return; 242 | } 243 | 244 | if (default_fonts) { 245 | term.setOption('fontFamily', default_fonts); 246 | term.font_family_updated = false; 247 | console.log('Using default font family ' + default_fonts); 248 | } 249 | } 250 | 251 | 252 | function format_geometry(cols, rows) { 253 | return JSON.stringify({'cols': cols, 'rows': rows}); 254 | } 255 | 256 | 257 | function read_as_text_with_decoder(file, callback, decoder) { 258 | var reader = new window.FileReader(); 259 | 260 | if (decoder === undefined) { 261 | decoder = new window.TextDecoder('utf-8', {'fatal': true}); 262 | } 263 | 264 | reader.onload = function() { 265 | var text; 266 | try { 267 | text = decoder.decode(reader.result); 268 | } catch (TypeError) { 269 | console.log('Decoding error happened.'); 270 | } finally { 271 | if (callback) { 272 | callback(text); 273 | } 274 | } 275 | }; 276 | 277 | reader.onerror = function (e) { 278 | console.error(e); 279 | }; 280 | 281 | reader.readAsArrayBuffer(file); 282 | } 283 | 284 | 285 | function read_as_text_with_encoding(file, callback, encoding) { 286 | var reader = new window.FileReader(); 287 | 288 | if (encoding === undefined) { 289 | encoding = 'utf-8'; 290 | } 291 | 292 | reader.onload = function() { 293 | if (callback) { 294 | callback(reader.result); 295 | } 296 | }; 297 | 298 | reader.onerror = function (e) { 299 | console.error(e); 300 | }; 301 | 302 | reader.readAsText(file, encoding); 303 | } 304 | 305 | 306 | function read_file_as_text(file, callback, decoder) { 307 | if (!window.TextDecoder) { 308 | read_as_text_with_encoding(file, callback, decoder); 309 | } else { 310 | read_as_text_with_decoder(file, callback, decoder); 311 | } 312 | } 313 | 314 | 315 | function reset_wssh() { 316 | var name; 317 | 318 | for (name in wssh) { 319 | if (wssh.hasOwnProperty(name) && name !== 'connect') { 320 | delete wssh[name]; 321 | } 322 | } 323 | } 324 | 325 | 326 | function log_status(text, to_populate) { 327 | console.log(text); 328 | status.html(text.split('\n').join('
')); 329 | 330 | if (to_populate && validated_form_data) { 331 | populate_form(validated_form_data); 332 | validated_form_data = undefined; 333 | } 334 | 335 | if (waiter.css('display') !== 'none') { 336 | waiter.hide(); 337 | } 338 | 339 | if (form_container.css('display') === 'none') { 340 | form_container.show(); 341 | } 342 | } 343 | 344 | 345 | function ajax_complete_callback(resp) { 346 | button.prop('disabled', false); 347 | 348 | if (resp.status !== 200) { 349 | log_status(resp.status + ': ' + resp.statusText, true); 350 | state = DISCONNECTED; 351 | return; 352 | } 353 | 354 | var msg = resp.responseJSON; 355 | if (!msg.id) { 356 | log_status(msg.status, true); 357 | state = DISCONNECTED; 358 | return; 359 | } 360 | 361 | var ws_url = window.location.href.split(/\?|#/, 1)[0].replace('http', 'ws'), 362 | join = (ws_url[ws_url.length-1] === '/' ? '' : '/'), 363 | url = ws_url + join + 'ws?id=' + msg.id, 364 | sock = new window.WebSocket(url), 365 | encoding = 'utf-8', 366 | decoder = window.TextDecoder ? new window.TextDecoder(encoding) : encoding, 367 | terminal = document.getElementById('terminal'), 368 | termOptions = { 369 | cursorBlink: true, 370 | theme: { 371 | background: url_opts_data.bgcolor || 'black', 372 | foreground: url_opts_data.fontcolor || 'white', 373 | cursor: url_opts_data.cursor || url_opts_data.fontcolor || 'white' 374 | } 375 | }; 376 | 377 | if (url_opts_data.fontsize) { 378 | var fontsize = window.parseInt(url_opts_data.fontsize); 379 | if (fontsize && fontsize > 0) { 380 | termOptions.fontSize = fontsize; 381 | } 382 | } 383 | 384 | var term = new window.Terminal(termOptions); 385 | 386 | term.fitAddon = new window.FitAddon.FitAddon(); 387 | term.loadAddon(term.fitAddon); 388 | 389 | console.log(url); 390 | if (!msg.encoding) { 391 | console.log('Unable to detect the default encoding of your server'); 392 | msg.encoding = encoding; 393 | } else { 394 | console.log('The deault encoding of your server is ' + msg.encoding); 395 | } 396 | 397 | function term_write(text) { 398 | if (term) { 399 | term.write(text); 400 | if (!term.resized) { 401 | resize_terminal(term); 402 | term.resized = true; 403 | } 404 | } 405 | } 406 | 407 | function set_encoding(new_encoding) { 408 | // for console use 409 | if (!new_encoding) { 410 | console.log('An encoding is required'); 411 | return; 412 | } 413 | 414 | if (!window.TextDecoder) { 415 | decoder = new_encoding; 416 | encoding = decoder; 417 | console.log('Set encoding to ' + encoding); 418 | } else { 419 | try { 420 | decoder = new window.TextDecoder(new_encoding); 421 | encoding = decoder.encoding; 422 | console.log('Set encoding to ' + encoding); 423 | } catch (RangeError) { 424 | console.log('Unknown encoding ' + new_encoding); 425 | return false; 426 | } 427 | } 428 | } 429 | 430 | wssh.set_encoding = set_encoding; 431 | 432 | if (url_opts_data.encoding) { 433 | if (set_encoding(url_opts_data.encoding) === false) { 434 | set_encoding(msg.encoding); 435 | } 436 | } else { 437 | set_encoding(msg.encoding); 438 | } 439 | 440 | 441 | wssh.geometry = function() { 442 | // for console use 443 | var geometry = current_geometry(term); 444 | console.log('Current window geometry: ' + JSON.stringify(geometry)); 445 | }; 446 | 447 | wssh.send = function(data) { 448 | // for console use 449 | if (!sock) { 450 | console.log('Websocket was already closed'); 451 | return; 452 | } 453 | 454 | if (typeof data !== 'string') { 455 | console.log('Only string is allowed'); 456 | return; 457 | } 458 | 459 | try { 460 | JSON.parse(data); 461 | sock.send(data); 462 | } catch (SyntaxError) { 463 | data = data.trim() + '\r'; 464 | sock.send(JSON.stringify({'data': data})); 465 | } 466 | }; 467 | 468 | wssh.reset_encoding = function() { 469 | // for console use 470 | if (encoding === msg.encoding) { 471 | console.log('Already reset to ' + msg.encoding); 472 | } else { 473 | set_encoding(msg.encoding); 474 | } 475 | }; 476 | 477 | wssh.resize = function(cols, rows) { 478 | // for console use 479 | if (term === undefined) { 480 | console.log('Terminal was already destroryed'); 481 | return; 482 | } 483 | 484 | var valid_args = false; 485 | 486 | if (cols > 0 && rows > 0) { 487 | var geometry = current_geometry(term); 488 | if (cols <= geometry.cols && rows <= geometry.rows) { 489 | valid_args = true; 490 | } 491 | } 492 | 493 | if (!valid_args) { 494 | console.log('Unable to resize terminal to geometry: ' + format_geometry(cols, rows)); 495 | } else { 496 | term.on_resize(cols, rows); 497 | } 498 | }; 499 | 500 | wssh.set_bgcolor = function(color) { 501 | set_backgound_color(term, color); 502 | }; 503 | 504 | wssh.set_fontcolor = function(color) { 505 | set_font_color(term, color); 506 | }; 507 | 508 | wssh.custom_font = function() { 509 | update_font_family(term); 510 | }; 511 | 512 | wssh.default_font = function() { 513 | reset_font_family(term); 514 | }; 515 | 516 | term.on_resize = function(cols, rows) { 517 | if (cols !== this.cols || rows !== this.rows) { 518 | console.log('Resizing terminal to geometry: ' + format_geometry(cols, rows)); 519 | this.resize(cols, rows); 520 | sock.send(JSON.stringify({'resize': [cols, rows]})); 521 | } 522 | }; 523 | 524 | term.onData(function(data) { 525 | // console.log(data); 526 | sock.send(JSON.stringify({'data': data})); 527 | }); 528 | 529 | sock.onopen = function() { 530 | term.open(terminal); 531 | toggle_fullscreen(term); 532 | update_font_family(term); 533 | term.focus(); 534 | state = CONNECTED; 535 | title_element.text = url_opts_data.title || default_title; 536 | if (url_opts_data.command) { 537 | setTimeout(function () { 538 | sock.send(JSON.stringify({'data': url_opts_data.command+'\r'})); 539 | }, 500); 540 | } 541 | }; 542 | 543 | sock.onmessage = function(msg) { 544 | read_file_as_text(msg.data, term_write, decoder); 545 | }; 546 | 547 | sock.onerror = function(e) { 548 | console.error(e); 549 | }; 550 | 551 | sock.onclose = function(e) { 552 | term.dispose(); 553 | term = undefined; 554 | sock = undefined; 555 | reset_wssh(); 556 | log_status(e.reason, true); 557 | state = DISCONNECTED; 558 | default_title = 'WebSSH'; 559 | title_element.text = default_title; 560 | }; 561 | 562 | $(window).resize(function(){ 563 | if (term) { 564 | resize_terminal(term); 565 | } 566 | }); 567 | } 568 | 569 | 570 | function wrap_object(opts) { 571 | var obj = {}; 572 | 573 | obj.get = function(attr) { 574 | return opts[attr] || ''; 575 | }; 576 | 577 | obj.set = function(attr, val) { 578 | opts[attr] = val; 579 | }; 580 | 581 | return obj; 582 | } 583 | 584 | 585 | function clean_data(data) { 586 | var i, attr, val; 587 | var attrs = form_keys.concat(['privatekey', 'passphrase']); 588 | 589 | for (i = 0; i < attrs.length; i++) { 590 | attr = attrs[i]; 591 | val = data.get(attr); 592 | if (typeof val === 'string') { 593 | data.set(attr, val.trim()); 594 | } 595 | } 596 | } 597 | 598 | 599 | function validate_form_data(data) { 600 | clean_data(data); 601 | 602 | var hostname = data.get('hostname'), 603 | port = data.get('port'), 604 | username = data.get('username'), 605 | pk = data.get('privatekey'), 606 | channel = data.get('channel'), 607 | result = { 608 | valid: false, 609 | data: data, 610 | title: '' 611 | }, 612 | errors = [], size; 613 | 614 | if (!channel) { 615 | if (!hostname) { 616 | errors.push('Value of hostname is required.'); 617 | } else { 618 | if (!hostname_tester.test(hostname)) { 619 | errors.push('Invalid hostname: ' + hostname); 620 | } 621 | } 622 | 623 | if (!port) { 624 | port = 22; 625 | } else { 626 | if (!(port > 0 && port <= 65535)) { 627 | errors.push('Invalid port: ' + port); 628 | } 629 | } 630 | 631 | if (!username) { 632 | errors.push('Value of username is required.'); 633 | } 634 | 635 | if (pk) { 636 | size = pk.size || pk.length; 637 | if (size > key_max_size) { 638 | errors.push('Invalid private key: ' + pk.name || ''); 639 | } 640 | } 641 | } 642 | 643 | if (!errors.length || debug) { 644 | result.valid = true; 645 | if (channel) { 646 | result.title = channel; 647 | } 648 | else { 649 | result.title = username + '@' + hostname + ':' + port; 650 | } 651 | } 652 | result.errors = errors; 653 | 654 | return result; 655 | } 656 | 657 | // Fix empty input file ajax submission error for safari 11.x 658 | function disable_file_inputs(inputs) { 659 | var i, input; 660 | 661 | for (i = 0; i < inputs.length; i++) { 662 | input = inputs[i]; 663 | if (input.files.length === 0) { 664 | input.setAttribute('disabled', ''); 665 | } 666 | } 667 | } 668 | 669 | 670 | function enable_file_inputs(inputs) { 671 | var i; 672 | 673 | for (i = 0; i < inputs.length; i++) { 674 | inputs[i].removeAttribute('disabled'); 675 | } 676 | } 677 | 678 | 679 | function connect_without_options() { 680 | // use data from the form 681 | var form = document.querySelector(form_id), 682 | inputs = form.querySelectorAll('input[type="file"]'), 683 | url = form.action, 684 | data, pk; 685 | 686 | disable_file_inputs(inputs); 687 | data = new FormData(form); 688 | pk = data.get('privatekey'); 689 | enable_file_inputs(inputs); 690 | 691 | function ajax_post() { 692 | status.text(''); 693 | button.prop('disabled', true); 694 | 695 | $.ajax({ 696 | url: url, 697 | type: 'post', 698 | data: data, 699 | complete: ajax_complete_callback, 700 | cache: false, 701 | contentType: false, 702 | processData: false 703 | }); 704 | } 705 | 706 | var result = validate_form_data(data); 707 | if (!result.valid) { 708 | log_status(result.errors.join('\n')); 709 | return; 710 | } 711 | 712 | if (pk && pk.size && !debug) { 713 | read_file_as_text(pk, function(text) { 714 | if (text === undefined) { 715 | log_status('Invalid private key: ' + pk.name); 716 | } else { 717 | ajax_post(); 718 | } 719 | }); 720 | } else { 721 | ajax_post(); 722 | } 723 | 724 | return result; 725 | } 726 | 727 | 728 | function connect_with_options(data) { 729 | // use data from the arguments 730 | var form = document.querySelector(form_id), 731 | url = data.url || form.action; 732 | 733 | var result = validate_form_data(wrap_object(data)); 734 | if (!result.valid) { 735 | log_status(result.errors.join('\n')); 736 | return; 737 | } 738 | 739 | data.term = term_type.val(); 740 | if (event_origin) { 741 | data._origin = event_origin; 742 | } 743 | 744 | status.text(''); 745 | button.prop('disabled', true); 746 | 747 | $.ajax({ 748 | url: url, 749 | type: 'post', 750 | data: data, 751 | complete: ajax_complete_callback 752 | }); 753 | 754 | return result; 755 | } 756 | 757 | 758 | function connect(hostname, port, username, password, privatekey, passphrase, totp, channel) { 759 | // for console use 760 | var result, opts; 761 | 762 | if (state !== DISCONNECTED) { 763 | console.log(messages[state]); 764 | return; 765 | } 766 | 767 | if (hostname === undefined) { 768 | result = connect_without_options(); 769 | } else { 770 | if (typeof hostname === 'string') { 771 | opts = { 772 | hostname: hostname, 773 | port: port, 774 | username: username, 775 | password: password, 776 | privatekey: privatekey, 777 | passphrase: passphrase, 778 | totp: totp, 779 | channel: channel 780 | }; 781 | } else { 782 | opts = hostname; 783 | } 784 | 785 | result = connect_with_options(opts); 786 | } 787 | 788 | if (result) { 789 | state = CONNECTING; 790 | default_title = result.title; 791 | if (hostname) { 792 | validated_form_data = result.data; 793 | } 794 | store_items(fields, result.data); 795 | } 796 | } 797 | 798 | wssh.connect = connect; 799 | 800 | $(form_id).submit(function(event){ 801 | event.preventDefault(); 802 | connect(); 803 | }); 804 | 805 | 806 | function cross_origin_connect(event) 807 | { 808 | console.log(event.origin); 809 | var prop = 'connect', 810 | args; 811 | 812 | try { 813 | args = JSON.parse(event.data); 814 | } catch (SyntaxError) { 815 | args = event.data.split('|'); 816 | } 817 | 818 | if (!Array.isArray(args)) { 819 | args = [args]; 820 | } 821 | 822 | try { 823 | event_origin = event.origin; 824 | wssh[prop].apply(wssh, args); 825 | } finally { 826 | event_origin = undefined; 827 | } 828 | } 829 | 830 | window.addEventListener('message', cross_origin_connect, false); 831 | 832 | if (document.fonts) { 833 | document.fonts.ready.then( 834 | function () { 835 | if (custom_font_is_loaded() === false) { 836 | document.body.style.fontFamily = custom_font.family; 837 | } 838 | } 839 | ); 840 | } 841 | 842 | 843 | parse_url_data( 844 | decode_uri_component(window.location.search.substring(1)) + '&' + decode_uri_component(window.location.hash.substring(1)), 845 | form_keys, opts_keys, url_form_data, url_opts_data 846 | ); 847 | // console.log(url_form_data); 848 | // console.log(url_opts_data); 849 | 850 | if (url_opts_data.term) { 851 | term_type.val(url_opts_data.term); 852 | } 853 | 854 | if (url_form_data.password === null) { 855 | log_status('Password via url must be encoded in base64.'); 856 | } else { 857 | if (get_object_length(url_form_data)) { 858 | waiter.show(); 859 | connect(url_form_data); 860 | } else { 861 | restore_items(fields); 862 | form_container.show(); 863 | } 864 | } 865 | 866 | }); 867 | -------------------------------------------------------------------------------- /static/js/popper.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) Federico Zivolo 2019 3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). 4 | */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=e.ownerDocument.defaultView,n=o.getComputedStyle(e,null);return t?n[t]:n}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll|overlay)/.test(r+s+p)?e:n(o(e))}function r(e){return 11===e?pe:10===e?se:pe||se}function p(e){if(!e)return document.documentElement;for(var o=r(10)?document.body:null,n=e.offsetParent||null;n===o&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TH','TD','TABLE'].indexOf(n.nodeName)&&'static'===t(n,'position')?p(n):n:e?e.ownerDocument.documentElement:document.documentElement}function s(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||p(e.firstElementChild)===e)}function d(e){return null===e.parentNode?e:d(e.parentNode)}function a(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,r=document.createRange();r.setStart(n,0),r.setEnd(i,0);var l=r.commonAncestorContainer;if(e!==l&&t!==l||n.contains(i))return s(l)?l:p(l);var f=d(e);return f.host?a(f.host,t):a(e,d(t).host)}function l(e){var t=1=o.clientWidth&&n>=o.clientHeight}),l=0a[e]&&!t.escapeWithReference&&(n=Q(f[o],a[e]-('right'===e?f.width:f.height))),le({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=fe({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=Z,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!K(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-us[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,w=t(e.instance.popper),y=parseFloat(w['margin'+f],10),E=parseFloat(w['border'+f+'Width'],10),v=b-e.offsets.popper[m]-y-E;return v=ee(Q(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},le(n,m,$(v)),le(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case ge.FLIP:p=[n,i];break;case ge.CLOCKWISE:p=G(n);break;case ge.COUNTERCLOCKWISE:p=G(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=Z,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)f(l.top)||'bottom'===n&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,w=-1!==['top','bottom'].indexOf(n),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&g||!w&&'end'===r&&u);(m||b||y)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),y&&(r=z(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=fe({},e.offsets.popper,D(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!K(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=C(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.rightwindow.devicePixelRatio||!me),c='bottom'===o?'top':'bottom',g='right'===n?'left':'right',b=H('transform');if(d='bottom'==c?'HTML'===l.nodeName?-l.clientHeight+h.bottom:-f.height+h.bottom:h.top,s='right'==g?'HTML'===l.nodeName?-l.clientWidth+h.right:-f.width+h.right:h.left,a&&b)m[b]='translate3d('+s+'px, '+d+'px, 0)',m[c]=0,m[g]=0,m.willChange='transform';else{var w='bottom'==c?-1:1,y='right'==g?-1:1;m[c]=d*w,m[g]=s*y,m.willChange=c+', '+g}var E={"x-placement":e.placement};return e.attributes=fe({},E,e.attributes),e.styles=fe({},m,e.styles),e.arrowStyles=fe({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:function(e){return j(e.instance.popper,e.styles),V(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&j(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,o,n,i){var r=L(i,t,e,o.positionFixed),p=O(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),j(t,{position:o.positionFixed?'fixed':'absolute'}),o},gpuAcceleration:void 0}}},ue}); 5 | //# sourceMappingURL=popper.min.js.map 6 | -------------------------------------------------------------------------------- /static/js/xterm-addon-fit.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(window,function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(){function e(){}return e.prototype.activate=function(e){this._terminal=e},e.prototype.dispose=function(){},e.prototype.fit=function(){var e=this.proposeDimensions();if(e&&this._terminal){var t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}},e.prototype.proposeDimensions=function(){if(this._terminal&&this._terminal.element&&this._terminal.element.parentElement){var e=this._terminal._core,t=window.getComputedStyle(this._terminal.element.parentElement),r=parseInt(t.getPropertyValue("height")),n=Math.max(0,parseInt(t.getPropertyValue("width"))),o=window.getComputedStyle(this._terminal.element),i=r-(parseInt(o.getPropertyValue("padding-top"))+parseInt(o.getPropertyValue("padding-bottom"))),a=n-(parseInt(o.getPropertyValue("padding-right"))+parseInt(o.getPropertyValue("padding-left")))-e.viewport.scrollBarWidth;return{cols:Math.max(2,Math.floor(a/e._renderService.dimensions.actualCellWidth)),rows:Math.max(1,Math.floor(i/e._renderService.dimensions.actualCellHeight))}}},e}();t.FitAddon=n}])}); 2 | //# sourceMappingURL=xterm-addon-fit.js.map -------------------------------------------------------------------------------- /test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohto-ai/webssh_cpp/00097b3e9d013016cb4d1b05cf509206d2bd4c75/test/.gitkeep -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(test) 3 | 4 | enable_testing() 5 | 6 | add_custom_target(${PROJECT_NAME} ALL) 7 | 8 | # Add test files 9 | file(GLOB test_files "*.test.cpp") 10 | foreach (test_file ${test_files}) 11 | set(SOURCES ${test_file}) 12 | get_filename_component(test_name ${test_file} NAME_WLE) 13 | add_executable(${test_name} ${SOURCES}) 14 | target_compile_definitions(${test_name} PRIVATE CATCH_CONFIG_MAIN) 15 | target_include_directories(${test_name} PRIVATE ${CMAKE_BINARY_DIR}/src/generated/inc) 16 | target_link_libraries(${test_name} PRIVATE 17 | Catch2 Catch2WithMain 18 | Threads::Threads 19 | $<$:OpenSSL::SSL> 20 | $<$:OpenSSL::Crypto> 21 | spdlog $<$:ws2_32>) 22 | target_compile_options(${test_name} 23 | PRIVATE 24 | $<$:/bigobj>) 25 | add_test(NAME ${test_name} COMMAND ${test_name}) 26 | add_dependencies(${PROJECT_NAME} ${test_name}) 27 | list(APPEND test_list ${test_name}) 28 | endforeach () 29 | 30 | if(GENERATE_CODE_COVERAGE AND LINUX AND CMAKE_BUILD_TYPE MATCHES Debug) 31 | setup_target_for_coverage_lcov(NAME coverage 32 | EXECUTABLE ctest --build-config Debug --output-on-failure 33 | BASE_DIRECTORY ${SOURCE_FOLDER} 34 | EXCLUDE "/usr/*" "${CMAKE_SOURCE_DIR}/inc/*" "${CMAKE_SOURCE_DIR}/build/*" "${CMAKE_SOURCE_DIR}/3rd/*" 35 | DEPENDENCIES ${test_list}) 36 | endif(GENERATE_CODE_COVERAGE AND LINUX AND CMAKE_BUILD_TYPE MATCHES Debug) 37 | -------------------------------------------------------------------------------- /test/fake.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | TEST_CASE("NOT A TEST", "[single-file]" ) { 4 | REQUIRE(true == true); 5 | } 6 | --------------------------------------------------------------------------------