Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package libfzssh for openSUSE:Factory checked in at 2026-05-05 15:16:55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/libfzssh (Old) and /work/SRC/openSUSE:Factory/.libfzssh.new.30200 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "libfzssh" Tue May 5 15:16:55 2026 rev:2 rq:1350937 version:1.2.0 Changes: -------- --- /work/SRC/openSUSE:Factory/libfzssh/libfzssh.changes 2026-04-20 16:11:02.104505099 +0200 +++ /work/SRC/openSUSE:Factory/.libfzssh.new.30200/libfzssh.changes 2026-05-05 15:18:12.742149919 +0200 @@ -1,0 +2,39 @@ +Mon May 4 10:12:26 UTC 2026 - ecsos <[email protected]> - 1.2.0 + +- Update to 1.2.0 + * New features: + - Added support for the [email protected] extension + * Bugfixes and minor changes: + - Send sftp_client::ready_event after obtaining peer version + - Small improvements to channel log messages +- Changes from 1.1.10 + * Bugfixes and minor changes: + - Default to a maximum large receive window of 2^31 - 1 octets + that safely fits into a signed 32-bit integer use by specification + violating servers, and white-list known-good servers for use + of 2^32 - 1 octet receive windows + - Allow larger incoming SFTP packets to match what OpenSSH accepts, + as there are servers that unfortunately send packets larger than + the safe 32768 octet limit from the specs +- Changes from 1.1.9 + * Bugfixes and minor changes: + - Handle nonsensical SSH_MSG_USERAUTH_INFO_REQUEST messages + with no name, no instructions and no prompt + - SFTP: Since the SSH_MSG_USERAUTH_BANNER packet can be safely + ignored, only loudly complain if it is malformed instead of + dropping the connection + - Require libfilezilla 0.55.3 +- Changes from 1.1.8 + * Bugfixes and minor changes: + - SFTP: Allow empty longname in listing entries + - Some servers cannot handle large receive windows of up to 2^32 - 1 bytes, + despite RFC 4254 mandating that all implementations MUST support windows + of up to 2^32 - 1 bytes. If we believe to connect to such a server in + single-channel mode, use a smaller window of 2^31 - 1 bytes. + +------------------------------------------------------------------- +Mon Apr 20 08:38:35 UTC 2026 - Jan Engelhardt <[email protected]> + +- Trim redundancies from descriptions. + +------------------------------------------------------------------- Old: ---- fzssh-1.1.7.tar.xz New: ---- fzssh-1.2.0.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ libfzssh.spec ++++++ --- /var/tmp/diff_new_pack.p60JNX/_old 2026-05-05 15:18:13.322173538 +0200 +++ /var/tmp/diff_new_pack.p60JNX/_new 2026-05-05 15:18:13.326173699 +0200 @@ -16,17 +16,17 @@ # -%define sover 9 +%define sover 11 %define soname %{sover}_0_0 %define libname %{name}%{soname} %define develname %{name}-devel -%define libfilezillaversion 0.55.0 +%define libfilezillaversion 0.55.3 Name: libfzssh -Version: 1.1.7 +Version: 1.2.0 Release: 0 -Summary: The fzssh is a SSH/SFTP library based on libfilezilla +Summary: A C++ SSH/SFTP library based on libfilezilla License: AGPL-3.0-or-later Group: Development/Libraries/C and C++ URL: https://fzssh.filezilla-project.org/ @@ -42,21 +42,15 @@ BuildRequires: pkgconfig(libargon2) %description -fzssh is a SSH/SFTP library based on libfilezilla -This library is free software, it is distributed under the terms and conditions of -the GNU Affero General Public License v3+ with attribution, see the library's README file for details. -fzssh is a cross-platform library for all major operating systems, including but not limited to Linux, *BSD, macOS and Windows. +fzssh is a SSH/SFTP library based on libfilezilla. %package -n %{libname} -Summary: C++ library for libfzsh +Summary: A C++ SSH/SFTP library based on libfilezilla Group: System/Libraries Provides: %{name} = %{version} %description -n %{libname} fzssh is a SSH/SFTP library based on libfilezilla -This library is free software, it is distributed under the terms and conditions of -the GNU Affero General Public License v3+ with attribution, see the library's README file for details. -fzssh is a cross-platform library for all major operating systems, including but not limited to Linux, *BSD, macOS and Windows. %package -n %{develname} Summary: Development package for %{name} @@ -76,8 +70,7 @@ %install %meson_install -%post -n %{libname} -p /sbin/ldconfig -%postun -n %{libname} -p /sbin/ldconfig +%ldconfig_scriptlets -n %{libname} %files %license agpl3.txt ++++++ fzssh-1.1.7.tar.xz -> fzssh-1.2.0.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/NEWS new/fzssh-1.2.0/NEWS --- old/fzssh-1.1.7/NEWS 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/NEWS 2026-04-16 15:54:25.000000000 +0200 @@ -1,3 +1,25 @@ +1.2.0 (2026-04-16) + ++ Added support for the [email protected] extension +- Send sftp_client::ready_event after obtaining peer version +- Small improvements to channel log messages + +1.1.10 (2026-04-14) + +- Default to a maximum large receive window of 2^31 - 1 octets that safely fits into a signed 32-bit integer use by specification violating servers, and white-list known-good servers for use of 2^32 - 1 octet receive windows +- Allow larger incoming SFTP packets to match what OpenSSH accepts, as there are servers that unfortunately send packets larger than the safe 32768 octet limit from the specs + +1.1.9 (2026-04-14) + +- Handle nonsensical SSH_MSG_USERAUTH_INFO_REQUEST messages with no name, no instructions and no prompt +- SFTP: Since the SSH_MSG_USERAUTH_BANNER packet can be safely ignored, only loudly complain if it is malformed instead of dropping the connection +- Require libfilezilla 0.55.3 + +1.1.8 (2026-04-13) + +- SFTP: Allow empty longname in listing entries +- Some servers cannot handle large receive windows of up to 2^32 - 1 bytes, despite RFC 4254 mandating that all implementations MUST support windows of up to 2^32 - 1 bytes. If we believe to connect to such a server in single-channel mode, use a smaller window of 2^31 - 1 bytes. + 1.1.7 (2026-03-24) - Fix incorrect uploads in SFTP client due to a superflous length field diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/lib/buffer_util.cpp new/fzssh-1.2.0/lib/buffer_util.cpp --- old/fzssh-1.1.7/lib/buffer_util.cpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/lib/buffer_util.cpp 2026-04-16 15:54:25.000000000 +0200 @@ -128,12 +128,6 @@ } } break; - for (auto const c : *s) { - if (!c) { - return {extract_fail, "Bad character in string"sv}; - } - } - break; case string_type::blob: break; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/lib/client/connection_protocol.cpp new/fzssh-1.2.0/lib/client/connection_protocol.cpp --- old/fzssh-1.1.7/lib/client/connection_protocol.cpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/lib/client/connection_protocol.cpp 2026-04-16 15:54:25.000000000 +0200 @@ -176,6 +176,47 @@ return continuation::next; } +uint32_t client_connection_protocol::calculate_window_in_max() +{ + // RFC 4254 mandates that every implementation MUST be able to handle + // receive windows of 2^32 - 1 octets + uint32_t constexpr large_window_max = 0xffffffffu; + + // Yet some servers violate the spec and seemingly use signed 32-bit integers, + // so only windows of up to 2^31 - 1 octets can be used + uint32_t constexpr large_window_signed_int_safe = 0x7fffffffu; + + // If there are going to be multiple channels, use a very small value + // so that one channel cannot choke the others + uint32_t constexpr multi_channel_window = max_payload_size * 16; + + if (no_flow_control_) { + return large_window_max; + } + if (single_channel_) { + auto peer_version = transport_.peer_version(); + + // Check for known-good + if (fz::starts_with(peer_version, "SSH-2.0-fzssh_"sv) || + fz::starts_with(peer_version, "SSH-2.0-OpenSSH"sv)) + { + return large_window_max; + } + // And known-bad servers + else if ( + fz::starts_with(peer_version, "SSH-2.0-CrushFTPSSHD"sv) || + fz::starts_with(peer_version, "SSH-2.0-MOVEit "sv)) + { + logger_.log(logmsg::debug_warning, "Broken server detected. This server cannot large receive windows up to 2^32 - 1. As per RFC 4254, implementations MUST correctly handle window sizes of up to 2^32 - 1 bytes"sv); + return large_window_signed_int_safe; + } + + return large_window_signed_int_safe; + } + + return multi_channel_window; +} + std::unique_ptr<socket_interface> client_connection_protocol::open_channel(channel_type type, std::string_view const& cmd) { auto tname = to_string(type); @@ -220,7 +261,7 @@ channel.type_ = type; channel.cmd_ = cmd; channel.own_id_ = own_id; - channel.window_in_max_ = (no_flow_control_ || single_channel_) ? 0xffffffffu : (max_payload_size * 16); // TODO + channel.window_in_max_ = calculate_window_in_max(); channel.window_in_ = channel.window_in_max_; if (transport_.service_ != service_type::connection) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/lib/client/connection_protocol.hpp new/fzssh-1.2.0/lib/client/connection_protocol.hpp --- old/fzssh-1.1.7/lib/client/connection_protocol.hpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/lib/client/connection_protocol.hpp 2026-04-16 15:54:25.000000000 +0200 @@ -22,6 +22,7 @@ continuation on_auth_success(); private: + uint32_t calculate_window_in_max(); bool send_channel_open(channel_data & channel); }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/lib/client/sftp_client.cpp new/fzssh-1.2.0/lib/client/sftp_client.cpp --- old/fzssh-1.1.7/lib/client/sftp_client.cpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/lib/client/sftp_client.cpp 2026-04-16 15:54:25.000000000 +0200 @@ -15,6 +15,10 @@ using fz::ssh::extract_uint64; using fz::ssh::extract_string; +namespace { +size_t constexpr max_in_payload_size{256*1024}; +} + class sftp_client_impl final : public sftp_base { public: @@ -22,13 +26,20 @@ virtual ~sftp_client_impl(); - virtual void stop(bool send_sone) override; + virtual void stop(bool send_done) override; + void unblock_read(); private: friend class sftp_client; - sftp_client* c_{}; + sftp_client* const c_{}; + struct dequeue_on_unblock_read_event_type; + typedef simple_event<dequeue_on_unblock_read_event_type> dequeue_on_unblock_read_event; + void on_dequeue_on_unblock_read_event(); + + virtual void operator()(event_base const & ev) override; + virtual void on_connect() override; virtual void on_can_send_packets() override; virtual continuation process_packet(message_type type, uint32_t id, std::string_view data) override; @@ -41,6 +52,8 @@ continuation process_names(response_handler* handler, message_type request_type, std::string_view data); continuation process_attributes(response_handler* handler, std::string_view data); + continuation process_pending_responses(); + uint32_t next_id_{}; struct pending @@ -61,6 +74,7 @@ std::deque<pending> pending_; event_handler* waiting_for_on_can_send_{}; + bool is_retrying_process_in_queue_{}; friend class request_builder; }; @@ -166,11 +180,9 @@ } sftp_client_impl::sftp_client_impl(std::unique_ptr<socket_interface> && channel, event_handler & handler, logger_interface & logger, sftp_client* c) - : sftp_base(std::move(channel), handler, logger, true) + : sftp_base(std::move(channel), handler, logger, max_in_payload_size, true) , c_(c) { - request_builder b(*this, message_type::SSH_FXP_INIT); - write_uint32(b.buf_, 3); } sftp_client_impl::~sftp_client_impl() @@ -186,6 +198,9 @@ if (ev.derived_type() == outbuf_empty_event::type()) { return std::get<0>(static_cast<outbuf_empty_event const&>(ev).v_) == c_; } + else if (ev.derived_type() == sftp_client::ready_event::type()) { + return std::get<0>(static_cast<sftp_client::ready_event const&>(ev).v_) == c_; + } return false; }; event_loop_.filter_events(event_filter); @@ -197,6 +212,57 @@ socket_.reset(); } +void sftp_client_impl::unblock_read() +{ + if (wait_read_) { + if (!pending_.empty() && pending_.front().response_type_ != message_type::none) { + send_event<dequeue_on_unblock_read_event>(); + return; + } + + sftp_base::unblock_read(); + } +} + +void sftp_client_impl::on_dequeue_on_unblock_read_event() +{ + is_retrying_process_ = is_retrying_process_in_queue_; + is_retrying_process_in_queue_ = false; + + auto c = process_pending_responses(); + + is_retrying_process_ = false; + + if (c == continuation::next) { + sftp_base::unblock_read(); + return; + } + + if (c == continuation::error) { + stop(true); + return; + } + + // Else, keep waiting. +} + +void sftp_client_impl::operator()(const event_base &ev) +{ + if (dispatch<dequeue_on_unblock_read_event>(ev, this, &sftp_client_impl::on_dequeue_on_unblock_read_event)) { + return; + } + sftp_base::operator()(ev); +} + +void sftp_client_impl::on_connect() +{ + if (!peer_version_) { + request_builder b(*this, message_type::SSH_FXP_INIT); + write_uint32(b.buf_, 3); + } + sftp_base::on_connect(); +} + continuation sftp_client_impl::process_packet(message_type type, uint32_t id, std::string_view data) { if (type == message_type::SSH_FXP_VERSION) { @@ -218,8 +284,6 @@ return continuation::error; } - - // Re-order responses to match request order if (pending_.empty()) { @@ -253,39 +317,28 @@ pending_.pop_front(); if (handler) { auto ret = process_packet(handler, req_type, type, data); + if (ret == continuation::wait_and_retry) { + pending_.emplace_front(id, req_type, handler); + } if (ret != continuation::next) { return ret; } } - - // TODO: in case of wait: unblock_read should consider pending + auto ret = process_pending_responses(); + if (ret == continuation::wait_and_retry) { + // The original packet process_packet has been called with got + // processed already, hence convert wait_and_retry into wait + // so that the buffer gets properly consumed. + ret = continuation::wait; + } + return ret; } else { p.data_ = data; p.response_type_ = type; + return continuation::next; } - while (!pending_.empty()) { - pending &pref = pending_.front(); - if (pref.data_.empty()) { - return continuation::next; - } - - pending p = std::move(pref); - pending_.pop_front(); - if (!p.handler_) { - continue; - } - - auto ret = process_packet(p.handler_, p.request_type_, p.response_type_, p.data_); - if (ret != continuation::next) { - return ret; - } - - // TODO: in case of wait: unblock_read should consider pending - } - - return continuation::next; } continuation sftp_client_impl::process_packet(response_handler* handler, message_type request_type, message_type response_type, std::string_view data) @@ -311,17 +364,17 @@ { uint32_t count; if (!extract_uint32(data, count)) { - logger_.log(logmsg::error, "Could not extract count of names"sv); + logger_.log(logmsg::error, "Could not extract count of names from received SSH_FXP_NAME packet"sv); return handler->failure(); } if (count > data.size() / 12) { - logger_.log(logmsg::error, "Nonsensical name count"sv); + logger_.log(logmsg::error, "Nonsensical name count in received SSH_FXP_NAME packet"sv); return handler->failure(); } if (request_type == message_type::SSH_FXP_REALPATH && count != 1) { - logger_.log(logmsg::error, "Name count must be one in SSH_FXP_REALPATH response"sv); + logger_.log(logmsg::error, "Received a SSH_FXP_NAME packet with a name count different from 1 in a reply to a SSH_FXP_REALPATH packet"sv); return handler->failure(); } @@ -329,14 +382,14 @@ entry e; auto name = extract_string(data, string_type::text, false); if (!name) { - logger_.log(fz::logmsg::error, "Could not extract name"sv); + logger_.log(fz::logmsg::error, "Could not extract name %u from received SSH_FXP_NAME packet: %s"sv, i, *name); return handler->failure(); } e.name_ = *name; - auto longname = extract_string(data, string_type::text, false); + auto longname = extract_string(data, string_type::text, true); if (!longname) { - logger_.log(fz::logmsg::error, "Could not extract longname"sv); + logger_.log(fz::logmsg::error, "Could not extract longname %u from received SSH_FXP_NAME packet: %s"sv, i, *longname); return handler->failure(); } e.longname_ = *longname; @@ -366,6 +419,34 @@ return handler->process_attributes(*attrs); } +continuation sftp_client_impl::process_pending_responses() +{ + auto ret = continuation::next; + + while (ret == continuation::next && !pending_.empty()) { + if (pending_.front().response_type_ == message_type::none) { + break; + } + + auto p = std::move(pending_.front()); + pending_.pop_front(); + + if (!p.handler_) { + is_retrying_process_ = false; + continue; + } + + ret = process_packet(p.handler_, p.request_type_, p.response_type_, p.data_); + is_retrying_process_ = false; + if (ret == continuation::wait_and_retry) { + is_retrying_process_in_queue_ = true; + pending_.push_front(std::move(p)); + } + } + + return ret; +} + continuation sftp_client_impl::process_version(std::string_view data) { if (!extract_uint32(data, peer_version_)) { @@ -388,6 +469,7 @@ logger_.log(logmsg::debug_info, "Peer SFTP supports extension %s", *name); } + event_handler_.send_event<sftp_client::ready_event>(c_); return continuation::next; } @@ -488,6 +570,11 @@ return impl_->unblock_read(); } +bool sftp_client::is_retrying_process() +{ + return impl_->is_retrying_process(); +} + bool sftp_client::can_send_packets() { return impl_->can_send_packets(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/lib/client/transport.cpp new/fzssh-1.2.0/lib/client/transport.cpp --- old/fzssh-1.1.7/lib/client/transport.cpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/lib/client/transport.cpp 2026-04-16 15:54:25.000000000 +0200 @@ -286,6 +286,15 @@ } } } + else if (name == "[email protected]"sv) { + if (value == "0"sv) { + logger_.log(logmsg::debug_info, "Extension version is \"0\", enabling use of this extension."sv); + static_cast<client_userauth&>(*auth_).hostbound_pubkey_auth_ = true; + } + else { + logger_.log(logmsg::debug_warning, "Unsupported extension version, cannot use this extension."sv); + } + } return continuation::next; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/lib/client/userauth.cpp new/fzssh-1.2.0/lib/client/userauth.cpp --- old/fzssh-1.1.7/lib/client/userauth.cpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/lib/client/userauth.cpp 2026-04-16 15:54:25.000000000 +0200 @@ -1,3 +1,4 @@ +#include "transport.hpp" #include "userauth.hpp" #include "../buffer_util.hpp" #include "../transport.hpp" @@ -5,6 +6,7 @@ #include "../fzssh/privkey.hpp" #include <libfilezilla/logger.hpp> +#include <libfilezilla/translate.hpp> namespace fz::ssh { @@ -116,10 +118,13 @@ sigdata.append(static_cast<uint8_t>(message_id::SSH_MSG_USERAUTH_REQUEST)); write_string(sigdata, user_); write_string(sigdata, "ssh-connection"sv); - write_string(sigdata, "publickey"sv); + write_string(sigdata, hostbound_pubkey_auth_ ? "[email protected]"sv : "publickey"sv); sigdata.append(1); write_string(sigdata, signature_algorithm); write_string(sigdata, privkey_->pubkey_blob()); + if (hostbound_pubkey_auth_) { + write_string(sigdata, static_cast<client_transport&>(transport_).previous_hostkey_); + } if (privkey_->sign(sigdata.to_view(), signature_algorithm, *this)) { signature_algorithm_ = signature_algorithm; } @@ -183,10 +188,15 @@ continuation client_userauth::process_auth_banner(std::string_view packet) { - auto msg = extract_string(packet, string_type::utf8, false); + auto msg = extract_string(packet, string_type::utf8, true); + if (!msg) { + logger_.log(logmsg::error, fztranslate("Received malformed SSH_MSG_USERAUTH_BANNER, cannot extract banner: %s"), *msg); + return continuation::next; + } auto lang = extract_string(packet, string_type::ascii, true); - if (!msg || !lang) { - return transport_.send_disconnect(disconnect_reason::SSH_DISCONNECT_PROTOCOL_ERROR, "Malformed SSH_MSG_USERAUTH_BANNER, cannot extract banner"sv); + if (!lang) { + logger_.log(logmsg::error, fztranslate("Received malformed SSH_MSG_USERAUTH_BANNER, cannot extract language tag: %s"), *lang); + return continuation::next; } // TODO: Filter and display message @@ -269,13 +279,16 @@ return; } - packet_builder b(transport_, message_id::SSH_MSG_USERAUTH_REQUEST, sprintf("method=publickey, with %s signature"sv, signature_algorithm_)); + packet_builder b(transport_, message_id::SSH_MSG_USERAUTH_REQUEST, sprintf("method=%s, with %s signature"sv, hostbound_pubkey_auth_ ? "[email protected]"sv : "publickey"sv, signature_algorithm_)); write_string(b.buf_, user_); write_string(b.buf_, "ssh-connection"sv); - write_string(b.buf_, "publickey"sv); + write_string(b.buf_, hostbound_pubkey_auth_ ? "[email protected]"sv : "publickey"sv); b.buf_.append(1); write_string(b.buf_, signature_algorithm_); write_string(b.buf_, privkey_->pubkey_blob()); + if (hostbound_pubkey_auth_) { + write_string(b.buf_, static_cast<client_transport&>(transport_).previous_hostkey_); + } write_string(b.buf_, sig); b.commit(); privkey_.reset(); @@ -322,7 +335,7 @@ std::vector<keyboard_interactive_prompt> prompts; if (count > 10) { - return transport_.send_disconnect(disconnect_reason::SSH_DISCONNECT_PROTOCOL_ERROR, "Recieved SSH_MSG_USERAUTH_INFO_REQUEST with more than 10 prompts"sv); + return transport_.send_disconnect(disconnect_reason::SSH_DISCONNECT_PROTOCOL_ERROR, "Received SSH_MSG_USERAUTH_INFO_REQUEST with more than 10 prompts"sv); } for (size_t i = 0; i < count; ++i) { @@ -339,6 +352,19 @@ prompts.emplace_back(std::move(prompt)); } + // For reasons unknown, OpenSSH sends an empty request + if (prompts.empty() && name->empty() && instruction->empty()) { + if (pending_auths_.front().flags_) { + return transport_.send_disconnect(disconnect_reason::SSH_DISCONNECT_PROTOCOL_ERROR, "Received multiple SSH_MSG_USERAUTH_INFO_REQUEST messages with no name, no instructions and no prompt."sv); + } + pending_auths_.front().request_only_ = false; + pending_auths_.front().flags_ = 1; + + packet_builder b(transport_, message_id::SSH_MSG_USERAUTH_INFO_RESPONSE); + write_uint32(b.buf_, 0); + return b.commit() ? continuation::next : continuation::error; + } + transport_.handler_.send_event<auth_keyboard_interactive_prompt_event>(static_cast<client*>(&transport_.session_), *name, *instruction, std::move(prompts)); return continuation::next; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/lib/client/userauth.hpp new/fzssh-1.2.0/lib/client/userauth.hpp --- old/fzssh-1.1.7/lib/client/userauth.hpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/lib/client/userauth.hpp 2026-04-16 15:54:25.000000000 +0200 @@ -25,6 +25,8 @@ void auth_keyboard_interactive_response(std::vector<std::string> const& responses); private: + friend class client_transport; + void auth_with_key(std::string_view const& pubblob, std::string_view const& signature_algorithm); virtual void operator()(event_base const& ev) override; @@ -43,10 +45,12 @@ {} std::string_view name_{}; + size_t flags_{}; bool request_only_{}; }; std::list<pending_auth> pending_auths_; + bool hostbound_pubkey_auth_{}; }; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/lib/connection_protocol.cpp new/fzssh-1.2.0/lib/connection_protocol.cpp --- old/fzssh-1.1.7/lib/connection_protocol.cpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/lib/connection_protocol.cpp 2026-04-16 15:54:25.000000000 +0200 @@ -318,8 +318,6 @@ return continuation::next; } - logger_.log(logmsg::debug_verbose, "Request is for channel %u"sv, own_id); - auto & channel = *it->second; return process_channel_packet(id, channel, packet); } @@ -439,7 +437,7 @@ return continuation::wait; } #if DUMP_CHANNEL_DATA - logger_.log(logmsg::debug_debug, "Incoming data on channel %u: %s"sv, id, fz::hex_encode<std::string>(data)); + logger_.log(logmsg::debug_debug, "Incoming data on channel %u: %s"sv, channel.own_id_, fz::hex_encode<std::string>(*data)); #endif channel.in_buf_.append(*data); @@ -494,7 +492,7 @@ // For now, discard the extended data. #if DUMP_CHANNEL_DATA - logger_.log(logmsg::debug_debug, "Incoming extended data on channel %u: %s", id, fz::hex_encode<std::string>(*data)); + logger_.log(logmsg::debug_debug, "Incoming extended data on channel %u: %s", channel.own_id_, fz::hex_encode<std::string>(*data)); #endif if (!no_flow_control_) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/lib/fzssh/sftp/sftp.hpp new/fzssh-1.2.0/lib/fzssh/sftp/sftp.hpp --- old/fzssh-1.1.7/lib/fzssh/sftp/sftp.hpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/lib/fzssh/sftp/sftp.hpp 2026-04-16 15:54:25.000000000 +0200 @@ -60,6 +60,7 @@ std::optional<uint32_t> gid_; bool is_directory() const; + bool is_symlink() const; }; inline bool operator&(file_flags lhs, file_flags rhs) { @@ -87,6 +88,7 @@ enum class continuation { next, wait, + wait_and_retry, error }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/lib/fzssh/sftp/sftp_client.hpp new/fzssh-1.2.0/lib/fzssh/sftp/sftp_client.hpp --- old/fzssh-1.1.7/lib/fzssh/sftp/sftp_client.hpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/lib/fzssh/sftp/sftp_client.hpp 2026-04-16 15:54:25.000000000 +0200 @@ -51,6 +51,7 @@ ~sftp_client(); void unblock_read(); + bool is_retrying_process(); void cancel(response_handler* handler); void cancel_wait(fz::event_handler* handler); @@ -90,6 +91,11 @@ struct done_event_type; typedef simple_event<done_event_type, sftp_client*> done_event; + + /// Sent once protocol version has been negotiated + struct ready_event_type; + typedef simple_event<ready_event_type, sftp_client*> ready_event; + private: std::unique_ptr<sftp_client_impl> impl_; }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/lib/sftp.cpp new/fzssh-1.2.0/lib/sftp.cpp --- old/fzssh-1.1.7/lib/sftp.cpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/lib/sftp.cpp 2026-04-16 15:54:25.000000000 +0200 @@ -1,4 +1,3 @@ -#include "fzssh/sftp/sftp.hpp" #include "fzssh/channel.hpp" #include "buffer_util.hpp" @@ -12,12 +11,12 @@ #define idtos(id) \ case message_type:: id: \ - return #id ""sv \ + return #id ""sv \ std::string_view FZSSH_PUBLIC_SYMBOL to_string(message_type id) { switch (id) { - idtos(SSH_FXP_INIT); + idtos(SSH_FXP_INIT); idtos(SSH_FXP_VERSION); idtos(SSH_FXP_OPEN); idtos(SSH_FXP_CLOSE); @@ -53,12 +52,12 @@ #define idtos(id) \ case status_code:: id: \ - return #id ""sv \ + return #id ""sv \ std::string_view to_string(status_code id) { switch (id) { - idtos(SSH_FX_OK); + idtos(SSH_FX_OK); idtos(SSH_FX_EOF); idtos(SSH_FX_NO_SUCH_FILE); idtos(SSH_FX_PERMISSION_DENIED); @@ -76,18 +75,22 @@ bool attributes::is_directory() const { - if (!perms_) { - return false; - } + // Intentionally not using S_IFDIR + return perms_ && (*perms_ & 040000); +} - return *perms_ & 040000; +bool attributes::is_symlink() const +{ + // Intentionally not using S_IFLNK + return perms_ && (*perms_ & 0120000); } -sftp_base::sftp_base(std::unique_ptr<socket_interface> && channel, event_handler & handler, logger_interface & logger, bool server) +sftp_base::sftp_base(std::unique_ptr<socket_interface> && channel, event_handler & handler, logger_interface & logger, size_t max_in_payload_size, bool server) : event_handler(handler, child_event_handler) , event_handler_(handler) , logger_(logger) , socket_(std::move(channel)) + , max_in_payload_size_(max_in_payload_size) , server_(server) { socket_->set_event_handler(this); @@ -96,12 +99,14 @@ void sftp_base::dump() { + auto [type, len] = in_packet_ ? std::pair((int)in_packet_->type, in_packet_->len) : std::pair(-1, size_t{}); if (channel_is_fzssh_) { + logger_.log(logmsg::error, "wait_read_=%u wait_write_=%u outbuf_.size()=%u", wait_read_, wait_write_, outbuf_.size()); } else { logger_.log(logmsg::error, "wait_read_=%u inbuf_.size()=%u wait_write_=%u outbuf_.size()=%u", wait_read_, inbuf_.size(), wait_write_, outbuf_.size()); - logger_.log(logmsg::error, "%d %u", in_type_ ? (int)*in_type_ : -1, in_packet_len_); } + logger_.log(logmsg::error, "%d %u", type, len); } void sftp_base::operator()(event_base const& ev) @@ -125,15 +130,22 @@ if (type == socket_event_flag::read) { on_read(); } - else if (type == socket_event_flag::write || type == socket_event_flag::connection) { + else if (type == socket_event_flag::write) { on_send(); } + else if (type == socket_event_flag::connection) { + on_connect(); + } } void sftp_base::on_read() { wait_read_ = false; + if (!in_packet_) { + in_packet_.emplace(); + } + std::string_view packet; if (channel_is_fzssh_) { @@ -145,6 +157,12 @@ wait_read_ = true; } else { + if (!disconnecting_ && !server_) { + logger_.log(logmsg::error, "SSH channel got unexpectedly closed by server"sv); + } + else { + logger_.log(logmsg::debug_info, "SSH channel closed by peer"sv); + } stop(true); } return; @@ -161,11 +179,11 @@ } } else { - bool need_recv = disconnecting_ || inbuf_.size() < (in_packet_len_ + 4); + bool need_recv = disconnecting_ || inbuf_.size() < (in_packet_->len + 4); logger_.log(logmsg::debug_debug, "sftp_base::on_read (need_recv=%d)"sv, need_recv); if (need_recv) { int err{}; - int res = socket_->read(inbuf_.get(max_in_payload_size + 5), max_in_payload_size + 5, err); + int res = socket_->read(inbuf_.get(max_in_payload_size_ + 5), max_in_payload_size_ + 5, err); if (res > 0 && !disconnecting_) { inbuf_.add(res); } @@ -174,6 +192,12 @@ wait_read_ = true; } else { + if (!disconnecting_ && !server_) { + logger_.log(logmsg::error, "SSH channel got unexpectedly closed by server"sv); + } + else { + logger_.log(logmsg::debug_info, "SSH channel closed by peer"sv); + } stop(true); } return; @@ -192,9 +216,30 @@ packet = inbuf_.to_view(); } - // Fetch the type - if (!in_type_) { - if (packet.size() < 5) { + std::string_view payload; + if (!is_retrying_process_) { + // Fetch the type + if (in_packet_->len == 0) { + if (packet.size() < 5) { + if (channel_is_fzssh_) { + static_cast<ssh_channel&>(*socket_).want_more(); + } + else { + resend_current_event(); + } + return; + } + in_packet_->len = read_uint32(packet.data()); + in_packet_->type = static_cast<message_type>(packet[4]); + if (!in_packet_->len || in_packet_->len > max_in_payload_size_) { + logger_.log(logmsg::error, "Received SFTP packet header with invalid length %u for packet type %u"sv, in_packet_->len, in_packet_->type); + stop(true); + return; + } + } + + // Do we have full packet? + if (packet.size() < (4 + in_packet_->len)) { if (channel_is_fzssh_) { static_cast<ssh_channel&>(*socket_).want_more(); } @@ -203,67 +248,65 @@ } return; } - in_packet_len_ = read_uint32(packet.data()); - in_type_ = static_cast<message_type>(packet[4]); - if (!in_packet_len_ || in_packet_len_ > max_in_payload_size) { - logger_.log(logmsg::debug_verbose, "Received packet with invalid length %u"sv, in_packet_len_); + + payload = packet.substr(5, in_packet_->len - 1); + + auto t = to_string(in_packet_->type); + if (t.empty()) { + logger_.log(logmsg::error, "Received SFTP packet of unknown type %u", in_packet_->type); stop(true); return; } - } - - // Do we have full packet? - if (packet.size() < (4 + in_packet_len_)) { - if (channel_is_fzssh_) { - static_cast<ssh_channel&>(*socket_).want_more(); - } else { - resend_current_event(); + if (in_packet_->type > message_type::SSH_FXP_VERSION) { + if (!extract_uint32(payload, in_packet_->id)) { + logger_.log(logmsg::error, "Could not extract id from incoming %s packet"sv, t); + stop(true); + return; + } + else { + logger_.log(logmsg::debug_verbose, "Processing %s, id=%u, len=%u"sv, t, in_packet_->id, in_packet_->len); + } + } + else { + logger_.log(logmsg::debug_verbose, "Processing %s, len=%u"sv, t, in_packet_->len); + } } - return; } + else { + payload = packet.substr(in_packet_->type > message_type::SSH_FXP_VERSION ? 5+4 : 5); - packet = packet.substr(5, in_packet_len_ - 1); - - uint32_t id{}; - auto t = to_string(*in_type_); - if (t.empty()) { - logger_.log(logmsg::debug_verbose, "Received packet of unknown type %u", *in_type_); - stop(true); - return; + logger_.log(logmsg::debug_verbose, L"Retrying %s, id=%u, len=%u"sv, to_string(in_packet_->type), in_packet_->id, in_packet_->len); } - else { - if (*in_type_ > message_type::SSH_FXP_VERSION) { - if (!extract_uint32(packet, id)) { - logger_.log(logmsg::debug_verbose, "Could not extract id on incoming packet of type %s"sv, t); - stop(true); - return; - } - else { - logger_.log(logmsg::debug_verbose, "Received packet of type %s, id=%u, len=%u"sv, t, id, in_packet_len_); + + auto consume_buffer = [&] { + if (channel_is_fzssh_) { + if (socket_) { + static_cast<ssh_channel&>(*socket_).consume_inbuf(in_packet_->len + 4); } } else { - logger_.log(logmsg::debug_verbose, "Received packet of type %s, len=%u"sv, t, in_packet_len_); + inbuf_.consume(in_packet_->len + 4); } - } - continuation c = process_packet(*in_type_, id, packet); - in_type_.reset(); - if (channel_is_fzssh_) { - if (socket_) { - static_cast<ssh_channel&>(*socket_).consume_inbuf(in_packet_len_ + 4); - } - } - else { - inbuf_.consume(in_packet_len_ + 4); - } - in_packet_len_ = 0; + in_packet_.reset(); + is_retrying_process_ = false; + }; + + continuation c = process_packet(in_packet_->type, in_packet_->id, payload); if (c == continuation::wait) { + consume_buffer(); + + wait_read_ = true; + } + else if (c == continuation::wait_and_retry) { wait_read_ = true; + is_retrying_process_ = true; } else if (c == continuation::next) { + consume_buffer(); + if (!server_ || outbuf_.empty()) { // Server-side, send out what we have before reading more. resend_current_event(); @@ -285,7 +328,7 @@ int sent = socket_->write(outbuf_.get(), outbuf_.size(), error); if (!sent) { if (!disconnecting_) { - logger_.log(logmsg::error, "Could not write to socket, got eof"sv); + logger_.log(logmsg::error, "Could not write to SSH channel, got eof"sv); } stop(true); return; @@ -296,7 +339,7 @@ } else { if (!disconnecting_) { - logger_.log(logmsg::error, "Could not write to socket, error %d"sv, error); + logger_.log(logmsg::error, "Could not write to SSH channel, error %d"sv, error); } stop(true); } @@ -310,6 +353,11 @@ } } +void sftp_base::on_connect() +{ + on_send(); +} + void sftp_base::on_send() { logger_.log(logmsg::debug_debug, "sftp_base::on_send"); @@ -378,6 +426,11 @@ } } +bool sftp_base::is_retrying_process() +{ + return is_retrying_process_; +} + void write_attributes(buffer& buf, attributes const& attrs) { attribute_flags flags{}; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/lib/sftp.hpp new/fzssh-1.2.0/lib/sftp.hpp --- old/fzssh-1.1.7/lib/sftp.hpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/lib/sftp.hpp 2026-04-16 15:54:25.000000000 +0200 @@ -8,6 +8,7 @@ #include <libfilezilla/socket.hpp> #include "fzssh/visibility.hpp" +#include "fzssh/sftp/sftp.hpp" namespace fz { @@ -15,7 +16,6 @@ namespace ssh::sftp { -size_t constexpr max_in_payload_size{128*1024}; size_t constexpr max_out_payload_size{64*1024}; enum class message_type : uint8_t @@ -56,13 +56,14 @@ class FZSSH_PUBLIC_SYMBOL sftp_base : public event_handler { public: - sftp_base(std::unique_ptr<socket_interface> && channel, event_handler & handler, logger_interface & logger, bool server); + sftp_base(std::unique_ptr<socket_interface> && channel, event_handler & handler, logger_interface & logger, size_t max_in_payload_size, bool server); virtual ~sftp_base() = default; void dump(); bool can_send_packets(); void unblock_read(); + bool is_retrying_process(); // Immediate virtual void stop(bool send_done) = 0; @@ -73,7 +74,7 @@ protected: virtual continuation process_packet(message_type type, uint32_t id, std::string_view data) = 0; bool can_send_packets(bool wait); - virtual void on_can_send_packets() {}; + virtual void on_can_send_packets() {} event_handler & event_handler_; logger_interface & logger_; @@ -81,12 +82,14 @@ virtual void operator()(event_base const& ev) override; void on_socket_event(socket_event_source *s, fz::socket_event_flag type, int error); + virtual void on_connect(); void on_read(); void on_send(); void do_send(); std::unique_ptr<socket_interface> socket_; + size_t const max_in_payload_size_{}; bool const server_{}; bool wait_read_{true}; bool wait_write_{true}; @@ -98,8 +101,17 @@ buffer outbuf_; buffer inbuf_; - std::optional<message_type> in_type_; - size_t in_packet_len_{}; + bool is_retrying_process_{}; + +private: + struct packet_type + { + size_t len{}; + message_type type{}; + uint32_t id{}; + }; + + std::optional<packet_type> in_packet_; bool channel_is_fzssh_{}; }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/lib/transport.cpp new/fzssh-1.2.0/lib/transport.cpp --- old/fzssh-1.1.7/lib/transport.cpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/lib/transport.cpp 2026-04-16 15:54:25.000000000 +0200 @@ -755,7 +755,15 @@ { auto name = to_string(id); if (!name.empty()) { - logger_.log(logmsg::debug_info, "Processing binary packet with type %s and size %u"sv, name, packet.size()); + + // If channel packet, snoop channel id for better log readability + if (id >= message_id::SSH_MSG_CHANNEL_OPEN_CONFIRMATION && id <= message_id::SSH_MSG_CHANNEL_FAILURE && packet.size() >= 4) { + uint32_t channel = read_uint32(packet.data()); + logger_.log(logmsg::debug_info, "Processing %s, channel=%s, size=%u"sv, name, channel, packet.size() - 4); + } + else { + logger_.log(logmsg::debug_info, "Processing %s, size=%u"sv, name, packet.size()); + } } else { logger_.log(logmsg::debug_warning, "Received binary packet with unimplemented type %d and size %u"sv, id, packet.size()); @@ -1009,8 +1017,6 @@ return send_disconnect(disconnect_reason::SSH_DISCONNECT_PROTOCOL_ERROR, "Malformed SSH_MSG_KEXINIT, could not extract reserved field"sv); } - logger_.log(logmsg::debug_info, "Received SSH_MSG_KEXINIT is valid"sv); - logger_.log(logmsg::debug_info, "Kex algorithms offered by peer: %s"sv, peer_kex_init_data.kex_); logger_.log(logmsg::debug_info, "Host key signature algorithms offered by peer: %s"sv, peer_kex_init_data.hostkey_); logger_.log(logmsg::debug_info, "Ciphers c2s offered by peer: %s"sv, peer_kex_init_data.enc_c2s_); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/meson.build new/fzssh-1.2.0/meson.build --- old/fzssh-1.1.7/meson.build 2026-03-24 10:32:59.726321200 +0100 +++ new/fzssh-1.2.0/meson.build 2026-04-16 15:54:33.412575700 +0200 @@ -1,6 +1,6 @@ project('fzssh', ['cpp'], meson_version: '>=1.3', - version: '1.1.7', + version: '1.2.0', default_options: ['cpp_std=c++20,c++2a', 'b_pie=true', 'warning_level=3'] ) @@ -10,7 +10,7 @@ # If any interfaces have been added since the last public release, then increment age. # If any interfaces have been removed or changed since the last public release, then set age to 0. # CURRENT:REVISION:AGE -soversion = '9.0.0' +soversion = '11.0.0' if host_machine.system() == 'windows' platform = 'windows' @@ -20,7 +20,7 @@ platform = 'unix' endif -lfz_dep = dependency('libfilezilla', version: '>= 0.55.2') +lfz_dep = dependency('libfilezilla', version: '>= 0.55.3') nettle_dep = dependency('nettle', version: '>= 3.10') hogweed_dep = dependency('hogweed', version: '>= 3.10') gmp_dep = dependency('gmp', version: '>= 6.2') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/src/benchmark.cpp new/fzssh-1.2.0/src/benchmark.cpp --- old/fzssh-1.1.7/src/benchmark.cpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/src/benchmark.cpp 2026-04-16 15:54:25.000000000 +0200 @@ -215,6 +215,8 @@ int main(int argc, char *argv[]) { + std::setlocale(LC_ALL, ""); + fz::stdout_logger log; //log.set_all(fz::logmsg::type(0)); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/src/chatter_client.cpp new/fzssh-1.2.0/src/chatter_client.cpp --- old/fzssh-1.1.7/src/chatter_client.cpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/src/chatter_client.cpp 2026-04-16 15:54:25.000000000 +0200 @@ -174,6 +174,8 @@ (void)argc; (void)argv; + std::setlocale(LC_ALL, ""); + fz::event_loop loop(fz::event_loop::threadless); runner r(loop); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/fzssh-1.1.7/src/client.cpp new/fzssh-1.2.0/src/client.cpp --- old/fzssh-1.1.7/src/client.cpp 2026-03-24 10:32:05.000000000 +0100 +++ new/fzssh-1.2.0/src/client.cpp 2026-04-16 15:54:25.000000000 +0200 @@ -29,8 +29,6 @@ , client_(std::move(client)) , log_(log) { - - prompt(); } void prompt() @@ -127,7 +125,11 @@ virtual void operator()(fz::event_base const& ev) override { - fz::dispatch<fz::socket_event, fz::ssh::auth_done_event, fz::ssh::session_done_event, fz::ssh::hostkey_verification_event, fz::ssh::auth_requested_event, fz::ssh::available_keys_event, fz::ssh::auth_public_key_okay_event, fz::ssh::auth_signature_failure_event, fz::ssh::sftp::sftp_client::done_event>(ev, this, + fz::dispatch< + fz::socket_event, + fz::ssh::auth_done_event, + fz::ssh::session_done_event, fz::ssh::hostkey_verification_event, fz::ssh::auth_requested_event, fz::ssh::available_keys_event, fz::ssh::auth_public_key_okay_event, fz::ssh::auth_signature_failure_event, fz::ssh::sftp::sftp_client::ready_event, fz::ssh::sftp::sftp_client::done_event + >(ev, this, &runner::on_socket_event, &runner::on_auth_done, &runner::on_session_done, @@ -136,6 +138,7 @@ &runner::on_agent_keys, &runner::on_auth_pubkey_ok, &runner::on_auth_signature_failure, + &runner::on_sftp_ready, &runner::on_sftp_done); } @@ -294,6 +297,11 @@ ask_method(); } + void on_sftp_ready(fz::ssh::sftp::sftp_client*) + { + sftp_->prompt(); + } + void on_sftp_done(fz::ssh::sftp::sftp_client*) { sftp_.reset(); @@ -311,6 +319,8 @@ int main(int argc, char *argv[]) { + std::setlocale(LC_ALL, ""); + fz::stdout_logger log; log.set_all(fz::logmsg::type(-1));
