Skip to content

Commit d9ce7ad

Browse files
authored
Merge pull request #28 from JuliaWeb/distributednext
Changes for DistributedNext.jl
2 parents d560e14 + 87fdd2b commit d9ce7ad

File tree

7 files changed

+173
-40
lines changed

7 files changed

+173
-40
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "LibSSH"
22
uuid = "00483490-30f8-4353-8aba-35b82f51f4d0"
33
authors = ["James Wrigley <james@puiterwijk.org> and contributors"]
4-
version = "0.6.1"
4+
version = "0.7.0"
55

66
[deps]
77
CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82"

docs/src/changelog.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ CurrentModule = LibSSH
77
This documents notable changes in LibSSH.jl. The format is based on [Keep a
88
Changelog](https://keepachangelog.com).
99

10+
## [v0.7.0] - 2024-10-25
11+
12+
### Added
13+
14+
- [`Demo.DemoServer`](@ref) now supports passing `allow_auth_none=true` to allow
15+
easily setting up passwordless authentication ([#28]).
16+
17+
### Fixed
18+
19+
- Previously the [`Demo.DemoServer`](@ref)'s command execution implementation
20+
would only send the command output after it had finished. Now the output gets
21+
sent as soon as it's printed by the command ([#28]).
22+
23+
### Changed
24+
25+
- **Breaking**: [`set_channel_callbacks()`](@ref) will remove any existing
26+
callbacks ([#28]).
27+
1028
## [v0.6.1] - 2024-10-20
1129

1230
### Added

src/callbacks.jl

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -248,14 +248,14 @@ $(TYPEDEF)
248248
Wrapper around `lib.ssh_channel_callbacks_struct`.
249249
"""
250250
mutable struct ChannelCallbacks
251-
cb_struct::Union{lib.ssh_channel_callbacks_struct, Nothing}
252251
userdata::Any
253252
functions::Dict{Symbol, Function}
254253
c_result_types::Dict{Symbol, DataType}
255254
c_arg_types::Dict{Symbol}
256255
jl_result_types::Dict{Symbol}
257256
jl_result_defaults::Dict{Symbol}
258257
jl_result_to_ctype::Dict{Symbol}
258+
cb_struct::lib.ssh_channel_callbacks_struct
259259

260260
@doc """
261261
$(TYPEDSIGNATURES)
@@ -329,26 +329,30 @@ mutable struct ChannelCallbacks
329329
on_write_wontblock::Union{Function, Nothing}=Returns(0),
330330
on_open_response::Union{Function, Nothing}=Returns(nothing),
331331
on_request_response::Union{Function, Nothing}=Returns(nothing))
332-
self = new(nothing, userdata,
332+
self = new(userdata,
333333
Dict{Symbol, Function}(),
334334
Dict{Symbol, DataType}(),
335335
Dict{Symbol, Any}(),
336336
Dict{Symbol, Any}(),
337337
Dict{Symbol, Any}(),
338338
Dict{Symbol, Any}())
339339

340-
self.cb_struct = lib.ssh_channel_callbacks_struct(sizeof(lib.ssh_channel_callbacks_struct), # size (see: ssh_callback_init())
341-
pointer_from_objref(self), # userdata points to self
342-
C_NULL, C_NULL,
343-
C_NULL, C_NULL,
344-
C_NULL, C_NULL,
345-
C_NULL, C_NULL,
346-
C_NULL, C_NULL,
347-
C_NULL, C_NULL,
348-
C_NULL, C_NULL,
349-
C_NULL,
350-
C_NULL,
351-
C_NULL)
340+
cb_struct = lib.ssh_channel_callbacks_struct(sizeof(lib.ssh_channel_callbacks_struct), # size (see: ssh_callback_init())
341+
pointer_from_objref(self), # userdata points to self
342+
C_NULL, C_NULL,
343+
C_NULL, C_NULL,
344+
C_NULL, C_NULL,
345+
C_NULL, C_NULL,
346+
C_NULL, C_NULL,
347+
C_NULL, C_NULL,
348+
C_NULL, C_NULL,
349+
C_NULL,
350+
C_NULL,
351+
C_NULL)
352+
# Call setfield!() explicitly to fully initialize the object so that all
353+
# other calls to setproperty!() will work.
354+
setfield!(self, :cb_struct, cb_struct)
355+
352356
self.on_data = on_data
353357
self.on_eof = on_eof
354358
self.on_close = on_close

src/channel.jl

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,17 +284,35 @@ end
284284
"""
285285
$(TYPEDSIGNATURES)
286286
287-
Wrapper around [`lib.ssh_set_channel_callbacks()`](@ref). Will throw a
288-
[`LibSSHException`](@ref) if setting the callbacks failed.
287+
Wrapper around [`lib.ssh_set_channel_callbacks()`](@ref) and
288+
[`lib.ssh_remove_channel_callbacks()`](@ref). Unlike
289+
[`lib.ssh_set_channel_callbacks()`](@ref) this will replace any existing
290+
callbacks.
291+
292+
# Throws
293+
- [`LibSSHException`](@ref): If setting the callbacks failed.
289294
"""
290295
function set_channel_callbacks(sshchan::SshChannel, callbacks::Callbacks.ChannelCallbacks)
296+
if !isnothing(sshchan.callbacks)
297+
remove_channel_callbacks(sshchan, sshchan.callbacks)
298+
end
299+
291300
ret = lib.ssh_set_channel_callbacks(sshchan, Ref(callbacks.cb_struct))
292301
if ret != SSH_OK
293302
throw(LibSSHException("Error when setting channel callbacks: $(ret)"))
294303
end
295304
sshchan.callbacks = callbacks
296305
end
297306

307+
# Undocumented for now because the API for setting callbacks isn't fleshed out yet
308+
function remove_channel_callbacks(sshchan::SshChannel, callbacks::Callbacks.ChannelCallbacks)
309+
ret = lib.ssh_remove_channel_callbacks(sshchan, Ref(callbacks.cb_struct))
310+
if ret != SSH_OK
311+
throw(LibSSHException("Error when removing channel callbacks: $(ret)"))
312+
end
313+
sshchan.callbacks = nothing
314+
end
315+
298316
"""
299317
$(TYPEDSIGNATURES)
300318

src/server.jl

Lines changed: 96 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,12 @@ end
554554

555555
function on_auth_none(session, user, client)::ssh.AuthStatus
556556
_add_log_event!(client, :auth_none, true)
557-
return ssh.AuthStatus_Denied
557+
558+
if client.allow_auth_none
559+
client.authenticated = true
560+
end
561+
562+
return client.authenticated ? ssh.AuthStatus_Success : ssh.AuthStatus_Denied
558563
end
559564

560565
function on_service_request(session, service, client)::Bool
@@ -580,9 +585,9 @@ function on_channel_env_request(session, sshchan, name, value, client)::Bool
580585
end
581586

582587
function on_channel_exec_request(session, sshchan, command, client)::Bool
583-
_add_log_event!(client, :channel_exec_request, command)
588+
_add_log_event!(client, :channel_exec_request, "'$command'")
584589
owning_sshchan = find_unclaimed_channel(client, sshchan)
585-
push!(client.channel_operations, CommandExecutor(command, owning_sshchan, client.env))
590+
push!(client.channel_operations, CommandExecutor(client, command, owning_sshchan, client.env))
586591

587592
return true
588593
end
@@ -695,6 +700,7 @@ end
695700
session::ssh.Session
696701
verbose::Bool
697702
password::Union{String, Nothing}
703+
allow_auth_none::Bool = false
698704
authenticated::Bool = false
699705

700706
session_event::Union{ssh.SessionEvent, Nothing} = nothing
@@ -773,6 +779,7 @@ $(TYPEDFIELDS)
773779
sshchan::Union{ssh.SshChannel, Nothing} = nothing
774780
verbose::Bool = false
775781
password::Union{String, Nothing} = nothing
782+
allow_auth_none::Bool = false
776783

777784
clients::Vector{Client} = Client[]
778785
end
@@ -794,13 +801,17 @@ Creates a [`DemoServer`](@ref).
794801
authentication etc. Useful for high-level debugging. The events can always be
795802
printed afterwards with [`Demo.print_timeline`](@ref).
796803
- `password=nothing`: The password to use if password authentication is enabled.
804+
- `allow_auth_none`: Whether to allow authentication without any credentials
805+
being presented.
797806
- `auth_methods=[AuthMethod_None, AuthMethod_Password]`: A list of
798807
authentication methods to enable. See [`ssh.AuthMethod`](@ref).
799808
- `log_verbosity=nothing`: Controls the logging of libssh itself. This could be
800809
e.g. `lib.SSH_LOG_WARNING` (see the [upstream
801810
documentation](https://api.libssh.org/stable/group__libssh__log.html#ga06fc87d81c62e9abb8790b6e5713c55b)).
802811
"""
803-
function DemoServer(port::Int; verbose::Bool=false, password::Union{String, Nothing}=nothing,
812+
function DemoServer(port::Int; verbose::Bool=false,
813+
password::Union{String, Nothing}=nothing,
814+
allow_auth_none=false,
804815
auth_methods=[ssh.AuthMethod_None, ssh.AuthMethod_Password],
805816
log_verbosity=ssh.SSH_LOG_NOLOG)
806817
if ssh.AuthMethod_Password in auth_methods && isnothing(password)
@@ -810,7 +821,7 @@ function DemoServer(port::Int; verbose::Bool=false, password::Union{String, Noth
810821
key = pki.generate(pki.KeyType_ed25519)
811822
bind = ssh.Bind(port; auth_methods, key, log_verbosity)
812823

813-
demo_server = DemoServer(; bind, verbose, password)
824+
demo_server = DemoServer(; bind, verbose, password, allow_auth_none)
814825

815826
ssh.set_message_callback(on_message, bind, demo_server)
816827

@@ -893,9 +904,10 @@ end
893904

894905
function _handle_client(session::ssh.Session, ds::DemoServer)
895906
client = Client(; id=length(ds.clients) + 1,
896-
session,
897-
password=ds.password,
898-
verbose=ds.verbose)
907+
session,
908+
password=ds.password,
909+
allow_auth_none=ds.allow_auth_none,
910+
verbose=ds.verbose)
899911
server_callbacks = ServerCallbacks(client;
900912
on_auth_password=on_auth_password,
901913
on_auth_none=on_auth_none,
@@ -1016,49 +1028,114 @@ end
10161028

10171029
## Execute commands
10181030

1031+
function on_exec_channel_eof(session, sshchan, executor)
1032+
_add_log_event!(executor.client, :exec_channel_eof, true)
1033+
end
1034+
1035+
function on_exec_channel_close(session, sshchan, executor)
1036+
_add_log_event!(executor.client, :exec_channel_close, true)
1037+
end
1038+
1039+
function on_exec_channel_data(session, sshchan, data, is_stderr, executor)
1040+
_add_log_event!(executor.client, :exec_channel_data, length(data))
1041+
1042+
# Wait for the command to have been started and the pipe to have been opened
1043+
timedwait(10) do
1044+
try
1045+
isopen(executor.stdin)
1046+
catch
1047+
false
1048+
end
1049+
end
1050+
1051+
write(executor.stdin, data)
1052+
return length(data)
1053+
end
1054+
10191055
function exec_command(executor)
10201056
sshchan = executor.sshchan
1021-
cmd_stdout = IOBuffer()
1022-
cmd_stderr = IOBuffer()
1057+
cmd_stdout = ChannelBuffer(sshchan, false)
1058+
cmd_stderr = ChannelBuffer(sshchan, true)
10231059

10241060
# Start the process and wait for it
10251061
cmd_str = join(Base.shell_split(executor.command), " ")
10261062
cmd = setenv(ignorestatus(`sh -c $(cmd_str)`), executor.env)
1027-
proc = run(pipeline(cmd; stdout=cmd_stdout, stderr=cmd_stderr); wait=false)
1063+
proc = run(pipeline(cmd; stdin=executor.stdin, stdout=cmd_stdout, stderr=cmd_stderr); wait=false)
10281064
executor.process = proc
10291065
notify(executor._started_event)
10301066
wait(proc)
10311067

1032-
# Write the output to the channel. We first check if the channel is open in
1033-
# case it's been killed suddenly in the meantime.
1034-
if isopen(sshchan)
1035-
write(sshchan, String(take!(cmd_stdout)))
1036-
write(sshchan, String(take!(cmd_stderr)); stderr=true)
1068+
close(executor.stdin)
1069+
close(cmd_stdout)
1070+
close(cmd_stderr)
10371071

1038-
# Clean up
1072+
# Clean up
1073+
if isopen(sshchan)
10391074
ssh.channel_request_send_exit_status(sshchan, proc.exitcode)
10401075
closewrite(sshchan)
10411076
end
10421077

10431078
close(sshchan)
10441079
end
10451080

1081+
# This is a helper IO type that exists for the sole purpose of asynchronously
1082+
# forwarding output from commands back to the SshChannel. Other containers like
1083+
# IOBuffer aren't thread-safe and can't be used so this implements a minimal,
1084+
# thread-safe IO type based on Channels.
1085+
mutable struct ChannelBuffer <: IO
1086+
channel::Channel{Vector{UInt8}}
1087+
sshchan::ssh.SshChannel
1088+
is_stderr::Bool
1089+
task::Task
1090+
1091+
function ChannelBuffer(sshchan, is_stderr)
1092+
self = new(Channel{Vector{UInt8}}(), sshchan, is_stderr)
1093+
self.task = Threads.@spawn for data in self.channel
1094+
# Write the output to the channel. We first check if the channel is open in
1095+
# case it's been killed suddenly in the meantime.
1096+
if isopen(sshchan)
1097+
write(self.sshchan, data; stderr=self.is_stderr)
1098+
end
1099+
end
1100+
errormonitor(self.task)
1101+
1102+
return self
1103+
end
1104+
end
1105+
1106+
function Base.write(chbuf::ChannelBuffer, data::Vector{UInt8})
1107+
put!(chbuf.channel, data)
1108+
return length(data)
1109+
end
1110+
1111+
function Base.close(chbuf::ChannelBuffer)
1112+
close(chbuf.channel)
1113+
wait(chbuf.task)
1114+
end
1115+
10461116
@kwdef mutable struct CommandExecutor
1117+
client::Client
10471118
command::String
10481119
sshchan::ssh.SshChannel
10491120
env::Dict{String, String}
10501121
task::Union{Task, Nothing} = nothing
10511122
process::Union{Base.Process, Nothing} = nothing
1123+
stdin::Base.PipeEndpoint = Base.PipeEndpoint()
10521124

10531125
_started_event::Base.Event = Base.Event()
10541126
end
10551127

1056-
function CommandExecutor(command::String, sshchan::ssh.SshChannel, env)
1128+
function CommandExecutor(client::Client, command::String, sshchan::ssh.SshChannel, env)
10571129
if !sshchan.owning
10581130
throw(ArgumentError("The passed SshChannel is non-owning, CommandExecutor requires an owning SshChannel"))
10591131
end
10601132

1061-
executor = CommandExecutor(; command, sshchan, env)
1133+
executor = CommandExecutor(; client, command, sshchan, env)
1134+
callbacks = ChannelCallbacks(executor;
1135+
on_data=on_exec_channel_data,
1136+
on_eof=on_exec_channel_eof,
1137+
on_close=on_exec_channel_close)
1138+
ssh.set_channel_callbacks(sshchan, callbacks)
10621139

10631140
executor.task = Threads.@spawn try
10641141
exec_command(executor)

test/LibSSHTests.jl

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,12 @@ end
177177
# https://github.com/JuliaLang/julia/issues/39282
178178
# Also note that we set `-F none` to disabling reading user config files.
179179
openssh_cmd = OpenSSH_jll.ssh()
180-
ssh_cmd(cmd::Cmd) = ignorestatus(Cmd(`sshpass -p bar $(openssh_cmd.exec) -F none -o NoHostAuthenticationForLocalhost=yes -p 2222 $cmd`; env=openssh_cmd.env))
180+
ssh_args = `-F none -o NoHostAuthenticationForLocalhost=yes -p 2222`
181+
ssh_cmd(cmd::Cmd) = ignorestatus(Cmd(`sshpass -p bar $(openssh_cmd.exec) $(ssh_args) $cmd`; env=openssh_cmd.env))
182+
passwordless_ssh_cmd(cmd::Cmd) = ignorestatus(Cmd(`$(openssh_cmd.exec) $(ssh_args) $cmd`; env=openssh_cmd.env))
181183

182184
@testset "Command execution" begin
183-
DemoServer(2222; password="bar") do
185+
DemoServer(2222; password="bar", verbose=false) do
184186
# Test exitcodes
185187
@test run(ssh_cmd(`foo@localhost exit 0`)).exitcode == 0
186188
@test run(ssh_cmd(`foo@localhost exit 42`)).exitcode == 42
@@ -192,6 +194,20 @@ end
192194
cmd_result = run(pipeline(cmd; stdout=cmd_out))
193195

194196
@test strip(String(take!(cmd_out))) == "bar"
197+
198+
# Test writing data to stdin
199+
read_cmd = "read var && echo \$var"
200+
cmd_out = IOBuffer()
201+
open(ssh_cmd(`foo@localhost $read_cmd`), cmd_out; write=true) do io
202+
write(io, "foo\n")
203+
end
204+
@test String(take!(cmd_out)) == "foo\n"
205+
end
206+
end
207+
208+
@testset "allow_auth_none" begin
209+
DemoServer(2222; auth_methods=[ssh.AuthMethod_None], allow_auth_none=true) do
210+
@test readchomp(passwordless_ssh_cmd(`foo@localhost whoami`)) == username()
195211
end
196212
end
197213

@@ -281,7 +297,7 @@ end
281297
@test client.authenticated
282298

283299
# And the command was executed
284-
@test client.callback_log[:channel_exec_request] == ["whoami"]
300+
@test client.callback_log[:channel_exec_request] == ["'whoami'"]
285301
end
286302

287303
@testset "Multiple connections" begin

test/runtests.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ import LibSSH
33

44
include("LibSSHTests.jl")
55

6-
retest(LibSSH, LibSSHTests)
6+
retest(LibSSH, LibSSHTests; stats=true)

0 commit comments

Comments
 (0)