From f694eda4a6ebc2feefcc0cfeca539d6e7f9012e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano?= Date: Fri, 7 Aug 2026 15:08:45 +0900 Subject: [PATCH 1/2] Store the Docker logs after the specs --- .gitignore | 9 +++++---- spec/spec_helper.rb | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 1117264..1035295 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ /coverage/ /doc/ /pkg/ -/spec/reports/ /tmp/ *.bundle *.so @@ -13,6 +12,8 @@ *.a mkmf.log -spec/examples.txt -spec/known_hosts.valid -spec/known_hosts.invalid +/spec/docker.log +/spec/examples.txt +/spec/known_hosts.valid +/spec/known_hosts.invalid +/spec/reports/ diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 067341e..fae9204 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -12,6 +12,7 @@ module DockerHelper -e PASSWORD_ACCESS=true \ -e USER_NAME=alice \ -e USER_PASSWORD=alice \ + -e LOG_STDOUT=true \ -v ./spec:/spec:ro \ -v ./spec/sshd_config:/config/sshd/sshd_config.d/99-override.conf:ro \ --publish-all \ @@ -33,6 +34,7 @@ def start end def stop + system("docker", "logs", @container_id, out: "spec/docker.log", err: [:child, :out]) unless system('docker', 'stop', '-t', '0', @container_id, out: File::NULL) warn "Cannot stop Docker container #{@container_id}" end From 587e34f36a0d326dd9584beb82dcb497a9e03f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano?= Date: Fri, 7 Aug 2026 13:56:20 +0900 Subject: [PATCH 2/2] Add support for SSH_OPTIONS_PROXYJUMP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To make life easier in the future I have added a low-level binding to ssh_options_set and a high-level wrapper class for Session in Ruby. I haved merged the C initialize method into the allocator and defined a new Ruby initialize method. Though evil, `Session.allocate.connect` should not make the whole Ruby process crash like it does now. Without authentication the test is not convincing yet, but I could confirm that `session.add_proxy_jump "localhost"` establishes a connection to my host’s SSH server using my user’s default keys. Calling ssh_options_set every time a new proxy jump is added is a bit wasteful but multiple jumps are rare and the intended API is: session.add_proxy_jump "foo", authenticate: -> (jump_session) { … } session.add_proxy_jump "bar", authenticate: -> (jump_session) { … } Rather than the counter-intuitive libssh way: session.proxy_jumps = ["foo", "bar"] session.append_proxy_jump_callback authenticate: -> (jump_session) { … } session.append_proxy_jump_callback authenticate: -> (jump_session) { … } -Wno-unused-parameter silences warnings in Ruby’s headers, which used to cause noise when building. --- ext/libssh_ruby/extconf.rb | 2 +- ext/libssh_ruby/libssh_ruby.c | 2 ++ ext/libssh_ruby/libssh_ruby.h | 1 + ext/libssh_ruby/session.c | 51 +++++++++++++++++++++----------- lib/libssh.rb | 1 + lib/libssh/session.rb | 23 ++++++++++++++ spec/integration/session_spec.rb | 18 +++++++++++ 7 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 lib/libssh/session.rb diff --git a/ext/libssh_ruby/extconf.rb b/ext/libssh_ruby/extconf.rb index cf0909b..1947d62 100644 --- a/ext/libssh_ruby/extconf.rb +++ b/ext/libssh_ruby/extconf.rb @@ -3,7 +3,7 @@ if ENV['LIBSSH_CFLAGS'] $CFLAGS = ENV['LIBSSH_CFLAGS'] else - $CFLAGS << ' -Wall -W -Wno-deprecated-declarations -Wno-missing-field-initializers' + $CFLAGS << ' -Wall -W -Wno-deprecated-declarations -Wno-missing-field-initializers -Wno-unused-parameter' end unless have_header('libssh/libssh.h') diff --git a/ext/libssh_ruby/libssh_ruby.c b/ext/libssh_ruby/libssh_ruby.c index b892076..636fed4 100644 --- a/ext/libssh_ruby/libssh_ruby.c +++ b/ext/libssh_ruby/libssh_ruby.c @@ -2,6 +2,7 @@ #include VALUE rb_mLibSSH; +VALUE rb_mLibSSHC; /* * @overload version(req_version = 0) @@ -31,6 +32,7 @@ void Init_libssh_ruby(void) { ssh_init(); rb_mLibSSH = rb_define_module("LibSSH"); + rb_mLibSSHC = rb_define_module_under(rb_mLibSSH, "C"); /* @see Session#server_known */ rb_define_const(rb_mLibSSH, "SERVER_KNOWN_OK", INT2FIX(SSH_SERVER_KNOWN_OK)); diff --git a/ext/libssh_ruby/libssh_ruby.h b/ext/libssh_ruby/libssh_ruby.h index f161873..78727af 100644 --- a/ext/libssh_ruby/libssh_ruby.h +++ b/ext/libssh_ruby/libssh_ruby.h @@ -8,6 +8,7 @@ #include extern VALUE rb_mLibSSH; +extern VALUE rb_mLibSSHC; extern VALUE rb_cLibSSHKey; void Init_libssh_ruby(void); diff --git a/ext/libssh_ruby/session.c b/ext/libssh_ruby/session.c index 452dbfe..0305459 100644 --- a/ext/libssh_ruby/session.c +++ b/ext/libssh_ruby/session.c @@ -29,9 +29,10 @@ SessionHolder *libssh_ruby_session_holder(VALUE session) { } static VALUE session_alloc(VALUE klass) { - SessionHolder *holder = ALLOC(SessionHolder); - holder->session = NULL; - return TypedData_Wrap_Struct(klass, &session_type, holder); + SessionHolder *holder; + VALUE object = TypedData_Make_Struct(klass, SessionHolder, &session_type, holder); + holder->session = ssh_new(); + return object; } static void session_mark(RB_UNUSED_VAR(void *arg)) {} @@ -49,19 +50,6 @@ static size_t session_memsize(RB_UNUSED_VAR(const void *arg)) { return sizeof(SessionHolder); } -/* - * @overload initialize - * Create a new SSH session. - * @see http://api.libssh.org/stable/group__libssh__session.html ssh_new - */ -static VALUE m_initialize(VALUE self) { - SessionHolder *holder; - - TypedData_Get_Struct(self, SessionHolder, &session_type, holder); - holder->session = ssh_new(); - return self; -} - /* * @overload log_verbosity=(verbosity) * Set the session logging verbosity. @@ -468,6 +456,27 @@ static VALUE m_add_identity(VALUE self, VALUE path) { return Qnil; } +static VALUE c_ssh_options_set(VALUE module, VALUE session, VALUE type, VALUE value) { + SessionHolder *holder; + TypedData_Get_Struct(session, SessionHolder, &session_type, holder); + + int c_type = NUM2INT(type); + const void *c_value; + + switch (c_type) { + // const char* + case SSH_OPTIONS_PROXYJUMP: + c_value = NIL_P(value) ? NULL : StringValueCStr(value); + break; + + default: + rb_raise(rb_eTypeError, "unsupported option"); + } + + RAISE_IF_ERROR(ssh_options_set(holder->session, c_type, c_value)); + return Qnil; +} + struct nogvl_session_args { ssh_session session; int rc; @@ -773,8 +782,6 @@ void Init_libssh_session() { I(gssapi_mic); #undef I - rb_define_method(rb_cLibSSHSession, "initialize", m_initialize, 0); - rb_define_method(rb_cLibSSHSession, "log_verbosity=", m_set_log_verbosity, 1); rb_define_method(rb_cLibSSHSession, "host=", m_set_host, 1); rb_define_method(rb_cLibSSHSession, "user=", m_set_user, 1); @@ -815,4 +822,12 @@ void Init_libssh_session() { rb_define_method(rb_cLibSSHSession, "userauth_kbdint_setanswer", m_userauth_kbdint_setanswer, 2); rb_define_method(rb_cLibSSHSession, "get_publickey", m_get_publickey, 0); rb_define_method(rb_cLibSSHSession, "write_knownhost", m_write_knownhost, 0); + + /* + * LibSSH::C constants and low-level functions. + */ + + rb_define_const(rb_mLibSSHC, "SSH_OPTIONS_PROXYJUMP", INT2FIX(SSH_OPTIONS_PROXYJUMP)); + + rb_define_module_function(rb_mLibSSHC, "ssh_options_set", c_ssh_options_set, 3); } diff --git a/lib/libssh.rb b/lib/libssh.rb index c4776c3..dc45a8d 100644 --- a/lib/libssh.rb +++ b/lib/libssh.rb @@ -1,3 +1,4 @@ require 'libssh/version' require 'libssh/libssh_ruby' require 'libssh/key' +require 'libssh/session' diff --git a/lib/libssh/session.rb b/lib/libssh/session.rb new file mode 100644 index 0000000..d6024f0 --- /dev/null +++ b/lib/libssh/session.rb @@ -0,0 +1,23 @@ +require "libssh/libssh_ruby" + +module LibSSH + class Session + def initialize + @proxyjump_hosts = [] + end + + def add_proxy_jump(host) + raise ArgumentError, "Jump host must not contain commas: #{host.inspect}" if host.include?(",") + @proxyjump_hosts << host + set_option("proxy jump", C::SSH_OPTIONS_PROXYJUMP, @proxyjump_hosts.join(",")) + end + + private + + def set_option(name, type, value) + C::ssh_options_set(self, type, value) + rescue Error + raise ArgumentError, "Invalid #{name}: #{value.inspect}" + end + end +end diff --git a/spec/integration/session_spec.rb b/spec/integration/session_spec.rb index 63be1d4..6104072 100644 --- a/spec/integration/session_spec.rb +++ b/spec/integration/session_spec.rb @@ -247,4 +247,22 @@ def kbdint(password) end end end + + describe "proxy jump" do + before do + session.host = SshHelper.host + session.port = DockerHelper.port + session.user = SshHelper.user + session.add_proxy_jump "#{SshHelper.user}@#{SshHelper.host}:#{DockerHelper.port}" + end + + specify "when lacking authentication" do + expect { session.connect }.to raise_error LibSSH::Error + end + + specify "with a bad host" do + expect { session.add_proxy_jump ":" }.to raise_error ArgumentError, /Invalid proxy jump/ + expect { session.add_proxy_jump "," }.to raise_error ArgumentError, /must not contain commas/ + end + end end