diff --git a/docs/src/changelog.md b/docs/src/changelog.md index dd03822..881f824 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -9,9 +9,14 @@ Changelog](https://keepachangelog.com). ## Unreleased +### Added + +- Added support for setting the file descriptor for a [`Session`](@ref) during + construction ([#21]). + ### Fixed -- Improved handling of possible errors in [`Base.readdir()`](@ref). +- Improved handling of possible errors in [`Base.readdir()`](@ref) ([#20]). ## [v0.6.0] - 2024-10-11 diff --git a/src/channel.jl b/src/channel.jl index f2d6468..132b724 100644 --- a/src/channel.jl +++ b/src/channel.jl @@ -925,7 +925,7 @@ function Base.show(io::IO, f::Forwarder) if !isopen(f) print(io, Forwarder, "()") else - if isnothing(forwarder.out) + if isnothing(f.out) print(io, Forwarder, "($(f.localinterface):$(f.localport) → $(f.remotehost):$(f.remoteport))") else print(io, Forwarder, "($(f.out) → $(f.remotehost):$(f.remoteport))") diff --git a/src/session.jl b/src/session.jl index 4b1b9d5..23c1bcc 100644 --- a/src/session.jl +++ b/src/session.jl @@ -144,6 +144,9 @@ server. # Arguments - `host`: The host to connect to. - `port=22`: The port to connect to. +- `socket=nothing`: Can be an open `TCPSocket` or `RawFD` to connect to + directly. If this is not `nothing` it will be used instead of `port`. You will + need to close the socket afterwards, the `Session` will not do it for you. - `user=nothing`: Set the user to connect as. If unset the current username will be used. - `log_verbosity=nothing`: Set the log verbosity for the session. @@ -159,6 +162,7 @@ julia> session = ssh.Session(ip"12.34.56.78", 2222) ``` """ function Session(host::Union{AbstractString, Sockets.IPAddr}, port=22; + socket::Union{Sockets.TCPSocket, RawFD, Nothing}=nothing, user=nothing, log_verbosity=nothing, auto_connect=true) session_ptr = lib.ssh_new() if session_ptr == C_NULL @@ -173,7 +177,12 @@ function Session(host::Union{AbstractString, Sockets.IPAddr}, port=22; # if initialization fails for some reason. try session.host = host_str - session.port = port + + if isnothing(socket) + session.port = port + else + session.fd = socket isa RawFD ? socket : Base._fd(socket) + end if isnothing(user) # Explicitly initialize the user, otherwise an error will be thrown when @@ -281,6 +290,7 @@ end # Mapping from option name to the corresponding enum and C type const SESSION_PROPERTY_OPTIONS = Dict(:host => (SSH_OPTIONS_HOST, Cstring), :port => (SSH_OPTIONS_PORT, Cuint), + :fd => (SSH_OPTIONS_FD, Cint), :user => (SSH_OPTIONS_USER, Cstring), :ssh_dir => (SSH_OPTIONS_SSH_DIR, Cstring), :known_hosts => (SSH_OPTIONS_KNOWNHOSTS, Cstring), @@ -312,11 +322,13 @@ function Base.getproperty(session::Session, name::Symbol) value = nothing is_string = false - if name == :port + if name === :port # The port is a special option with its own function port = Ref{Cuint}(0) ret = lib.ssh_options_get_port(session, port; throw=false) value = UInt(port[]) + elseif name === :fd + value = RawFD(lib.ssh_get_fd(session)) else # All properties supported by ssh_options_get() are strings, so we know # that this option must be a string. @@ -434,9 +446,8 @@ function _wait_loop(session::Session) readable = (poll_flags & lib.SSH_READ_PENDING) > 0 writable = (poll_flags & lib.SSH_WRITE_PENDING) > 0 - fd = RawFD(lib.ssh_get_fd(session)) while !(@atomic session._waiter_stop_flag) && isopen(session) - result = @lock session _safe_poll_fd(fd, 0.1; readable, writable) + result = @lock session _safe_poll_fd(session.fd, 0.1; readable, writable) if isnothing(result) # This means the session's file descriptor has been closed (see the # comments for _safe_poll_fd()). diff --git a/test/LibSSHTests.jl b/test/LibSSHTests.jl index d570176..c8c70e9 100644 --- a/test/LibSSHTests.jl +++ b/test/LibSSHTests.jl @@ -354,6 +354,7 @@ end @test session.known_hosts == "/tmp/foo" session.gssapi_server_identity = "foo.com" @test session.gssapi_server_identity == "foo.com" + @test session.fd == RawFD(-1) # Test setting an initial user ssh.Session("localhost"; user="foo", auto_connect=false) do session2 @@ -375,6 +376,24 @@ end # And we shouldn't be able to wait on a closed session @test_throws InvalidStateException wait(session) + # Test initializing with a socket instead of a port. We do this by setting + # up two dummy servers, the one on port 2222 is what we want to connect to + # and the one on port 2223 is the simulated jump host we have to go + # through. It has to be done this way because we only know whether setting + # the socket worked after connecting. + demo_server_with_session(2222) do server_session + demo_server_with_session(2223; verbose=false) do jump_session + # Make the jump session forward the desired server port and connect + # to it directly by its socket. + ssh.Forwarder(jump_session, "localhost", 2222) do forwarder + client_session = ssh.Session("localhost"; socket=forwarder.out) + @test ssh.isconnected(client_session) + @test client_session.fd == Base._fd(forwarder.out) + close(client_session) + end + end + end + @testset "Password authentication" begin # Test connecting to a server and doing password authentication DemoServer(2222; password="foo") do @@ -555,6 +574,9 @@ end # Test forwarding to a port demo_server_with_session(2222) do session ssh.Forwarder(session, 8080, "localhost", 9090) do forwarder + # Smoke test + show(IOBuffer(), forwarder) + http_server(9090) do curl_proc = run(ignorestatus(`$(curl()) localhost:8080`); wait=false) try @@ -571,6 +593,9 @@ end # Test forwarding to a socket demo_server_with_session(2222) do session ssh.Forwarder(session, "localhost", 9090) do forwarder + # Smoke test + show(IOBuffer(), forwarder) + http_server(9090) do socket = forwarder.out write(socket, "foo")