From 5538292fa95126afe75e951a11bfaf0b2c7d1ff4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:49:38 +0000 Subject: [PATCH 1/5] Fix critical bugs, security issues, and enhance webssh_cpp - Fix log-after-erase UB in ssh_session::close_channel() - Fix map modification during iteration in ssh_session::disconnect() - Fix ssh_session::sock uninitialized (init to LIBSSH2_INVALID_SOCKET) - Fix libssh2_init thread safety with std::call_once - Fix partial writes in ssh_channel::write() with retry loop - Fix set modification during iteration in async read loop - Fix channels_write.empty() check without holding ctx mutex - Add try/catch around ssh_channel::read() in async loop - Fix ws.onclose: remove channel from read/write sets properly - Fix ws.onclose: close SSH when last write channel disconnects - Add ssh_contexts_mutex for thread-safe static map access - Enforce read-only semantics in ws.onmessage - Wrap all JSON parsing and SSH writes in try/catch - Remove password from debug log (security fix) - Add command-line options: -p port, -a address, -h help - Improve HTTP error responses with proper status codes - Default PTY to xterm-256color when not specified - Add startup log showing listen address and port - Add mini_buffer move semantics (Rule of Five) - Add kSshPollIntervalMs constant with explanation - Reset sock=LIBSSH2_INVALID_SOCKET after closing in disconnect() Agent-Logs-Url: https://github.com/ohto-ai/webssh_cpp/sessions/181dda9e-cb49-417a-943b-0a144be7153c Co-authored-by: ohto-ai <46275725+ohto-ai@users.noreply.github.com> --- src/inc/mini_buffer.hpp | 21 +++ src/main.cpp | 194 ++++++++++++++++++-------- src/ssh_connection/ssh_connection.cpp | 47 ++++--- src/ssh_connection/ssh_connection.h | 2 +- 4 files changed, 192 insertions(+), 72 deletions(-) diff --git a/src/inc/mini_buffer.hpp b/src/inc/mini_buffer.hpp index 945aea8..b646a56 100644 --- a/src/inc/mini_buffer.hpp +++ b/src/inc/mini_buffer.hpp @@ -18,6 +18,27 @@ namespace ohtoai data = new char[capacity]; } + mini_buffer(const mini_buffer&) = delete; + mini_buffer& operator=(const mini_buffer&) = delete; + + mini_buffer(mini_buffer&& other) noexcept + : data(other.data), size(other.size), capacity(other.capacity) { + other.data = nullptr; + other.size = 0; + other.capacity = 0; + } + mini_buffer& operator=(mini_buffer&& other) noexcept { + if (this != &other) { + delete[] data; + data = other.data; + size = other.size; + capacity = other.capacity; + other.data = nullptr; + other.size = 0; + other.capacity = 0; + } + return *this; + } ~mini_buffer() { delete[] data; diff --git a/src/main.cpp b/src/main.cpp index c397664..57c9b2d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,9 +1,15 @@ #include "ssh_connection/ssh_connection.h" #include +#include #include +#include +#include +#include #include #include +#include +#include #include #include #include @@ -32,41 +38,79 @@ class ssh_context : public std::mutex { close(); } inline static std::map> ssh_contexts; + inline static std::mutex ssh_contexts_mutex; }; int main(int argc, char *argv[]) { - if (argc > 1 && strcmp(argv[1], "-d") == 0 ) { - spdlog::set_level(spdlog::level::debug); + int port = 8080; + std::string host = "0.0.0.0"; + + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "-d") == 0) { + spdlog::set_level(spdlog::level::debug); + } else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) { + try { + port = std::stoi(argv[++i]); + if (port <= 0 || port > 65535) throw std::out_of_range("port out of range"); + } catch (const std::exception& e) { + fmt::print(stderr, "Invalid port: {}\n", argv[i]); + return 1; + } + } else if (strcmp(argv[i], "-a") == 0 && i + 1 < argc) { + host = argv[++i]; + } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + fmt::print("Usage: {} [-d] [-p port] [-a address]\n", argv[0]); + fmt::print(" -d Enable debug logging\n"); + fmt::print(" -p port Listen port (default: 8080)\n"); + fmt::print(" -a address Listen address (default: 0.0.0.0)\n"); + fmt::print(" -h Show this help\n"); + return 0; + } } + // Poll interval when no SSH data is available. + // 10ms gives good responsiveness without busy-looping. + constexpr int kSshPollIntervalMs = 10; + hv::WebSocketService ws; ws.onopen = [](const WebSocketChannelPtr& channel, const HttpRequestPtr& req) { spdlog::debug("{} {}", channel->peeraddr(), req->Path()); auto channel_id = req->GetParam("id"); if (channel_id.empty()) { spdlog::error("channel_id is empty"); + channel->close(); return; } - // find ssh_context by channel_id - auto it = ssh_context::ssh_contexts.find(channel_id); - if (it != ssh_context::ssh_contexts.end()) { - auto ctx = it->second.lock(); - if (ctx != nullptr) { - ctx->channels_read.emplace(channel); - spdlog::info("[{}] Add channel to existing ssh_context", channel_id); - return; + // Find or create ssh_context for this channel_id + { + std::lock_guard guard(ssh_context::ssh_contexts_mutex); + auto it = ssh_context::ssh_contexts.find(channel_id); + if (it != ssh_context::ssh_contexts.end()) { + auto ctx = it->second.lock(); + if (ctx != nullptr) { + // Attach as read-only observer to an existing session + std::lock_guard lock(*ctx); + ctx->channels_read.emplace(channel); + spdlog::info("[{}] Attached read-only WebSocket to existing ssh_context", channel_id); + return; + } } } auto ctx = channel->newContextPtr(); - ssh_context::ssh_contexts.emplace(channel_id, ctx); + { + std::lock_guard guard(ssh_context::ssh_contexts_mutex); + ssh_context::ssh_contexts.emplace(channel_id, ctx); + } ctx->ssh_channel_id = channel_id; ctx->channels_read.emplace(channel); ctx->channels_write.emplace(channel); + auto ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(ctx->ssh_channel_id); if (ssh_channel == nullptr) { spdlog::error("[{}] ssh_channel is null", channel_id); + channel->close(); return; } @@ -77,23 +121,41 @@ int main(int argc, char *argv[]) { spdlog::error("[{}] ssh_channel is null", ctx->ssh_channel_id); break; } - if (ctx->channels_write.empty()) { - spdlog::error("[{}] channels_write is empty", ctx->ssh_channel_id); + + { + std::lock_guard lock(*ctx); + if (ctx->channels_write.empty()) { + spdlog::info("[{}] All write channels gone, stopping read loop", ctx->ssh_channel_id); + break; + } + } + + long rc = 0; + try { + rc = ssh_channel->read(); + } + catch (const std::exception& e) { + spdlog::error("[{}] SSH read failed: {}", ctx->ssh_channel_id, e.what()); break; } - if (ssh_channel->read() <= 0) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + if (rc == 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(kSshPollIntervalMs)); continue; } std::lock_guard lock(*ctx); - for (auto& channel : ctx->channels_read) { - if (!channel->isConnected()) { - ctx->channels_read.erase(channel); - ctx->channels_write.erase(channel); + std::vector to_remove; + for (auto& ws_channel : ctx->channels_read) { + if (!ws_channel->isConnected()) { + to_remove.push_back(ws_channel); continue; } - channel->send(ssh_channel->get_buffer().data, ssh_channel->get_buffer().size); + ws_channel->send(ssh_channel->get_buffer().data, ssh_channel->get_buffer().size); + } + for (auto& ws_channel : to_remove) { + ctx->channels_read.erase(ws_channel); + ctx->channels_write.erase(ws_channel); } } ctx->close(); @@ -106,6 +168,16 @@ int main(int argc, char *argv[]) { spdlog::error("ctx is null"); return; } + + // Read-only channels must not send data + { + std::lock_guard lock(*ctx); + if (ctx->channels_write.find(channel) == ctx->channels_write.end()) { + spdlog::debug("[{}] Ignoring message from read-only channel", ctx->ssh_channel_id); + return; + } + } + auto ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(ctx->ssh_channel_id); if (ssh_channel == nullptr) { spdlog::error("ssh_channel is null"); @@ -113,35 +185,38 @@ int main(int argc, char *argv[]) { return; } - hv::Json j = hv::Json::parse(msg); - if (j.contains("resize")) { - int width = j["resize"][0]; - int height = j["resize"][1]; - ssh_channel->resize_pty(width, height); - } else if (j.contains("data")) { - auto data = j["data"].get(); - try { + try { + hv::Json j = hv::Json::parse(msg); + if (j.contains("resize")) { + int width = j["resize"][0]; + int height = j["resize"][1]; + ssh_channel->resize_pty(width, height); + } else if (j.contains("data")) { + auto data = j["data"].get(); ssh_channel->write(data); } - catch (const std::exception& e) { - spdlog::error("[{}] Try to write {} bytes, but failed.", ssh_channel->id, data.size()); - spdlog::error("{}", e.what()); - } + } + catch (const std::exception& e) { + spdlog::error("[{}] Failed to handle message: {}", ctx->ssh_channel_id, e.what()); } }; ws.onclose = [](const WebSocketChannelPtr& channel) { spdlog::debug("{}", channel->peeraddr()); auto ctx = channel->getContextPtr(); if (ctx == nullptr) { - spdlog::error("ctx is null"); return; } - auto ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(ctx->ssh_channel_id); - if (ssh_channel == nullptr) { - spdlog::error("ssh_channel is null"); - ctx->close(); - return; + { + std::lock_guard lock(*ctx); + ctx->channels_read.erase(channel); + ctx->channels_write.erase(channel); + if (!ctx->channels_write.empty()) { + // Other write-capable connections remain; keep SSH alive + channel->deleteContextPtr(); + return; + } } + ctx->close(); channel->deleteContextPtr(); }; @@ -156,33 +231,39 @@ int main(int argc, char *argv[]) { auto term = ctx->get("term"); auto channel_id = ctx->get("channel"); - spdlog::debug("Receive login request: hostname={}, port={}, username={}, password={}, term={}, channel_id={}", - hostname, port, username, password, term, channel_id); + spdlog::debug("Receive login request: hostname={}, port={}, username={}, term={}, channel_id={}", + hostname, port, username, term, channel_id); ohtoai::ssh::ssh_channel_ptr ssh_channel {}; if (!channel_id.empty()) { ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(channel_id); if (ssh_channel == nullptr) { - ctx->setStatus(HTTP_STATUS_FORBIDDEN); - return ctx->send("ssh_channel is null"); + ctx->setStatus(HTTP_STATUS_NOT_FOUND); + return ctx->send("channel not found"); } - // todo: Need attach to original ssh_channel } else { - if (hostname.empty() || username.empty() || password.empty()) { - ctx->setStatus(HTTP_STATUS_FORBIDDEN); - return ctx->send("hostname, username, password are required"); + if (hostname.empty() || username.empty()) { + ctx->setStatus(HTTP_STATUS_BAD_REQUEST); + return ctx->send("hostname and username are required"); } - ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(hostname, port, username, password); - if (ssh_channel == nullptr) { + try { + ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(hostname, port, username, password); + if (ssh_channel == nullptr) { + ctx->setStatus(HTTP_STATUS_INTERNAL_SERVER_ERROR); + return ctx->send("failed to create ssh channel"); + } + ssh_channel->set_env("LC_WSSH_WEBSOCKET_HOST", ctx->host()); + ssh_channel->set_env("LC_WSSH_WEBSOCKET_URL", ctx->url()); + ssh_channel->set_env("LC_WSSH_WEBSOCKET_CLIENT_IP", ctx->header("X-Real-IP", ctx->ip())); + ssh_channel->request_pty(term.empty() ? "xterm-256color" : term); + ssh_channel->shell(); + } + catch (const std::exception& e) { + spdlog::error("SSH connection failed: {}", e.what()); ctx->setStatus(HTTP_STATUS_FORBIDDEN); - return ctx->send("ssh_channel is null"); + return ctx->send(e.what()); } - ssh_channel->set_env("LC_WSSH_WEBSOCKET_HOST", ctx->host()); - ssh_channel->set_env("LC_WSSH_WEBSOCKET_URL", ctx->url()); - ssh_channel->set_env("LC_WSSH_WEBSOCKET_CLIENT_IP", ctx->header("X-Real-IP", ctx->ip())); - ssh_channel->request_pty(term); - ssh_channel->shell(); } hv::Json resp; @@ -192,10 +273,13 @@ int main(int argc, char *argv[]) { }); hv::WebSocketServer server; - server.port = 8080; + server.port = port; + server.host = host.c_str(); server.registerHttpService(&http); server.registerWebSocketService(&ws); + + spdlog::info("WebSSH server starting on {}:{}", host, port); server.run(); return 0; } diff --git a/src/ssh_connection/ssh_connection.cpp b/src/ssh_connection/ssh_connection.cpp index 9651329..f8733fb 100644 --- a/src/ssh_connection/ssh_connection.cpp +++ b/src/ssh_connection/ssh_connection.cpp @@ -1,6 +1,7 @@ #include "ssh_connection.h" #include +#include #include #include #include @@ -92,13 +93,21 @@ void ohtoai::ssh::detail::ssh_channel::write(const byte* data, size_t size) { if (channel == nullptr) { throw std::runtime_error(fmt::format("[{}] Channel is not opened", id)); } - ssize_t rc = libssh2_channel_write(channel, data, size); - if (rc < 0 && rc != LIBSSH2_ERROR_EAGAIN) { - char *error_msg = nullptr; - libssh2_session_last_error(session->session, &error_msg, nullptr, 0); - throw std::runtime_error(fmt::format("[{}]({}) <{}> {}", id, __LINE__, rc, error_msg)); + size_t total_written = 0; + while (total_written < size) { + ssize_t rc = libssh2_channel_write(channel, data + total_written, size - total_written); + if (rc == LIBSSH2_ERROR_EAGAIN) { + session->wait_socket(); + continue; + } + if (rc < 0) { + char *error_msg = nullptr; + libssh2_session_last_error(session->session, &error_msg, nullptr, 0); + throw std::runtime_error(fmt::format("[{}]({}) <{}> {}", id, __LINE__, rc, error_msg)); + } + total_written += static_cast(rc); } - spdlog::debug("[{}] Wrote {} bytes", id, rc); + spdlog::debug("[{}] Wrote {} bytes", id, total_written); } void ohtoai::ssh::detail::ssh_channel::write(const std::string &data) { @@ -176,20 +185,22 @@ void ohtoai::ssh::detail::ssh_channel::close() { if (channel != nullptr) { libssh2_channel_free(channel); channel = nullptr; - if (session) { - session->close_channel(id); + if (session != nullptr) { + auto* sess = session; + session = nullptr; + sess->close_channel(id); } } } ohtoai::ssh::detail::ssh_session::ssh_session() { - if (counter == 0) { - // init sshlib + static std::once_flag libssh2_init_flag; + std::call_once(libssh2_init_flag, []() { spdlog::debug("libssh2 init"); if (int rc = libssh2_init(0)) { throw std::runtime_error(fmt::format("Failed to initialize ssh library <{}>", rc)); } - } + }); ++counter; session = nullptr; } @@ -312,10 +323,8 @@ ohtoai::ssh::detail::ssh_channel_ptr ohtoai::ssh::detail::ssh_session::open_chan void ohtoai::ssh::detail::ssh_session::close_channel(const channel_id_t &id) { auto iter = channels.find(id); if (iter != channels.end()) { - iter->second->close(); channels.erase(iter); - spdlog::info("[{}] Channel closed", iter->second->id); - spdlog::info("[{}] Session channels opened {}", get_id(), channels.size()); + spdlog::info("[{}] Channel deregistered from session [{}], remaining {}", id, get_id(), channels.size()); } if (channels.empty()) { disconnect(); @@ -324,8 +333,13 @@ void ohtoai::ssh::detail::ssh_session::close_channel(const channel_id_t &id) { void ohtoai::ssh::detail::ssh_session::disconnect() { if (session != nullptr) { - for (auto &channel : channels) { - channel.second->close(); + // Move channels out to avoid modification-during-iteration and + // prevent recursive disconnect() calls triggered by close_channel(). + auto channels_to_close = std::move(channels); + channels.clear(); + for (auto &[cid, ch] : channels_to_close) { + ch->session = nullptr; // Prevent ch->close() from calling close_channel() again + ch->close(); } libssh2_session_disconnect(session, "Bye bye"); libssh2_session_free(session); @@ -335,6 +349,7 @@ void ohtoai::ssh::detail::ssh_session::disconnect() { if (sock != LIBSSH2_INVALID_SOCKET) { ::shutdown(sock, 2); ::close(sock); + sock = LIBSSH2_INVALID_SOCKET; } } diff --git a/src/ssh_connection/ssh_connection.h b/src/ssh_connection/ssh_connection.h index 327ff79..68a8f67 100644 --- a/src/ssh_connection/ssh_connection.h +++ b/src/ssh_connection/ssh_connection.h @@ -73,7 +73,7 @@ namespace ohtoai::ssh std::string username; LIBSSH2_SESSION *session = nullptr; std::map channels; - int sock; + int sock = LIBSSH2_INVALID_SOCKET; inline static std::atomic_size_t counter = 0; }; From 8c987e5c3f391244dd1465fdce0b8360091d8f01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 03:32:49 +0000 Subject: [PATCH 2/5] Fix CI build errors: undefined LIBSSH2_INVALID_SOCKET in header, lambda capture, server.host assignment - Use -1 as sock initializer in ssh_connection.h (LIBSSH2_INVALID_SOCKET is only available after including libssh2.h which the header doesn't include) - Move kSshPollIntervalMs to file scope as static constexpr so it is accessible inside the lambda without needing to be captured - Use strncpy to copy host string into server.host char[64] array instead of direct pointer assignment - Add include for strncpy Agent-Logs-Url: https://github.com/ohto-ai/webssh_cpp/sessions/af1c7525-36ea-4286-9653-a69c2c1aa849 Co-authored-by: ohto-ai <46275725+ohto-ai@users.noreply.github.com> --- src/main.cpp | 12 +++++++----- src/ssh_connection/ssh_connection.h | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 57c9b2d..62a743d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,10 @@ class ssh_context : public std::mutex { inline static std::mutex ssh_contexts_mutex; }; +// Poll interval when no SSH data is available. +// 10ms gives good responsiveness without busy-looping. +static constexpr int kSshPollIntervalMs = 10; + int main(int argc, char *argv[]) { int port = 8080; std::string host = "0.0.0.0"; @@ -68,10 +73,6 @@ int main(int argc, char *argv[]) { } } - // Poll interval when no SSH data is available. - // 10ms gives good responsiveness without busy-looping. - constexpr int kSshPollIntervalMs = 10; - hv::WebSocketService ws; ws.onopen = [](const WebSocketChannelPtr& channel, const HttpRequestPtr& req) { spdlog::debug("{} {}", channel->peeraddr(), req->Path()); @@ -274,7 +275,8 @@ int main(int argc, char *argv[]) { hv::WebSocketServer server; server.port = port; - server.host = host.c_str(); + strncpy(server.host, host.c_str(), sizeof(server.host) - 1); + server.host[sizeof(server.host) - 1] = '\0'; server.registerHttpService(&http); server.registerWebSocketService(&ws); diff --git a/src/ssh_connection/ssh_connection.h b/src/ssh_connection/ssh_connection.h index 68a8f67..2198dc5 100644 --- a/src/ssh_connection/ssh_connection.h +++ b/src/ssh_connection/ssh_connection.h @@ -73,7 +73,7 @@ namespace ohtoai::ssh std::string username; LIBSSH2_SESSION *session = nullptr; std::map channels; - int sock = LIBSSH2_INVALID_SOCKET; + int sock = -1; inline static std::atomic_size_t counter = 0; }; From 4af82f1c9de711c8e8e6140b501cb0fd93bab5ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:35:45 +0000 Subject: [PATCH 3/5] Apply code review suggestions: write loop guard, sock init, ssh_contexts map fixes, error disclosure - ssh_connection.cpp write(): guard rc==0 (channel closed, no forward progress) to prevent infinite spin; throw instead of looping - ssh_session constructor: explicitly set sock=LIBSSH2_INVALID_SOCKET so the correct platform value is used (not the header's -1 fallback) - main.cpp onopen: use insert_or_assign so stale/expired weak_ptr entries in ssh_contexts are replaced on reconnect - main.cpp onclose: erase ssh_contexts entry under mutex when SSH session tears down, preventing unbounded map growth and stale keys - main.cpp HTTP handler: return generic "SSH connection failed" to client; keep detailed exception in server log only (prevents info disclosure) Agent-Logs-Url: https://github.com/ohto-ai/webssh_cpp/sessions/13c3502d-09e9-447e-8e7b-ba91d19c3da5 Co-authored-by: ohto-ai <46275725+ohto-ai@users.noreply.github.com> --- src/main.cpp | 8 ++++++-- src/ssh_connection/ssh_connection.cpp | 4 ++++ src/ssh_connection/ssh_connection.h | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 62a743d..d091f4f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -102,7 +102,7 @@ int main(int argc, char *argv[]) { auto ctx = channel->newContextPtr(); { std::lock_guard guard(ssh_context::ssh_contexts_mutex); - ssh_context::ssh_contexts.emplace(channel_id, ctx); + ssh_context::ssh_contexts.insert_or_assign(channel_id, ctx); } ctx->ssh_channel_id = channel_id; ctx->channels_read.emplace(channel); @@ -218,6 +218,10 @@ int main(int argc, char *argv[]) { } } ctx->close(); + { + std::lock_guard guard(ssh_context::ssh_contexts_mutex); + ssh_context::ssh_contexts.erase(ctx->ssh_channel_id); + } channel->deleteContextPtr(); }; @@ -263,7 +267,7 @@ int main(int argc, char *argv[]) { catch (const std::exception& e) { spdlog::error("SSH connection failed: {}", e.what()); ctx->setStatus(HTTP_STATUS_FORBIDDEN); - return ctx->send(e.what()); + return ctx->send("SSH connection failed"); } } diff --git a/src/ssh_connection/ssh_connection.cpp b/src/ssh_connection/ssh_connection.cpp index f8733fb..f73f6a2 100644 --- a/src/ssh_connection/ssh_connection.cpp +++ b/src/ssh_connection/ssh_connection.cpp @@ -100,6 +100,9 @@ void ohtoai::ssh::detail::ssh_channel::write(const byte* data, size_t size) { session->wait_socket(); continue; } + if (rc == 0) { + throw std::runtime_error(fmt::format("[{}] Channel write returned 0 — channel may be closed", id)); + } if (rc < 0) { char *error_msg = nullptr; libssh2_session_last_error(session->session, &error_msg, nullptr, 0); @@ -194,6 +197,7 @@ void ohtoai::ssh::detail::ssh_channel::close() { } ohtoai::ssh::detail::ssh_session::ssh_session() { + sock = LIBSSH2_INVALID_SOCKET; static std::once_flag libssh2_init_flag; std::call_once(libssh2_init_flag, []() { spdlog::debug("libssh2 init"); diff --git a/src/ssh_connection/ssh_connection.h b/src/ssh_connection/ssh_connection.h index 2198dc5..b871c61 100644 --- a/src/ssh_connection/ssh_connection.h +++ b/src/ssh_connection/ssh_connection.h @@ -73,7 +73,7 @@ namespace ohtoai::ssh std::string username; LIBSSH2_SESSION *session = nullptr; std::map channels; - int sock = -1; + int sock = -1; // initialized to LIBSSH2_INVALID_SOCKET in constructor inline static std::atomic_size_t counter = 0; }; From 7dc1d55b71502513642a55aa133dd3ec305872e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:49:27 +0000 Subject: [PATCH 4/5] Fix observer channel context, idempotent close, and password validation - main.cpp onopen observer path: call channel->setContext(ctx) so that onclose/onmessage for read-only observers have a valid ssh_context and are correctly cleaned up when the observer WebSocket disconnects - ssh_context::close(): add closed_ flag to make close() idempotent; prevents double-close of SSH channel and observer WebSocket channels when observer onclose fires after primary has already torn down the session - HTTP login handler: add password to required fields check; return 400 with a clear message instead of silently failing authentication Agent-Logs-Url: https://github.com/ohto-ai/webssh_cpp/sessions/87d1d1db-00b6-4689-872a-4945e8d6ae31 Co-authored-by: ohto-ai <46275725+ohto-ai@users.noreply.github.com> --- src/main.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index d091f4f..27c996c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,8 @@ class ssh_context : public std::mutex { std::set channels_write; void close() { std::lock_guard lock(*this); + if (closed_) return; + closed_ = true; for (auto& channel : channels_read) { channel->close(); } @@ -40,6 +42,8 @@ class ssh_context : public std::mutex { } inline static std::map> ssh_contexts; inline static std::mutex ssh_contexts_mutex; +private: + bool closed_ = false; }; // Poll interval when no SSH data is available. @@ -93,6 +97,7 @@ int main(int argc, char *argv[]) { // Attach as read-only observer to an existing session std::lock_guard lock(*ctx); ctx->channels_read.emplace(channel); + channel->setContext(ctx); spdlog::info("[{}] Attached read-only WebSocket to existing ssh_context", channel_id); return; } @@ -248,9 +253,9 @@ int main(int argc, char *argv[]) { } } else { - if (hostname.empty() || username.empty()) { + if (hostname.empty() || username.empty() || password.empty()) { ctx->setStatus(HTTP_STATUS_BAD_REQUEST); - return ctx->send("hostname and username are required"); + return ctx->send("hostname, username and password are required"); } try { ssh_channel = ohtoai::ssh::ssh_pty_connection_manager::get_instance().get_channel(hostname, port, username, password); From 5395a08eb89623fc968f4a0d5863674ead130be2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 03:25:00 +0000 Subject: [PATCH 5/5] Fix build error: use setContextPtr instead of setContext for shared_ptr setContext(void*) cannot accept a std::shared_ptr. Use setContextPtr(const std::shared_ptr&) which is the libhv API for storing a shared_ptr as the channel context. Agent-Logs-Url: https://github.com/ohto-ai/webssh_cpp/sessions/6922ed93-bceb-4efa-9cb5-70701bdcf66c Co-authored-by: ohto-ai <46275725+ohto-ai@users.noreply.github.com> --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 27c996c..034ef97 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -97,7 +97,7 @@ int main(int argc, char *argv[]) { // Attach as read-only observer to an existing session std::lock_guard lock(*ctx); ctx->channels_read.emplace(channel); - channel->setContext(ctx); + channel->setContextPtr(ctx); spdlog::info("[{}] Attached read-only WebSocket to existing ssh_context", channel_id); return; }