Skip to content

Commit 7f074e9

Browse files
Kenotkelman
authored andcommitted
Test LibGit2 SSH authentication (#17651)
1 parent d3df8e7 commit 7f074e9

File tree

6 files changed

+231
-44
lines changed

6 files changed

+231
-44
lines changed

base/libgit2/callbacks.jl

+7-10
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,7 @@ function authenticate_ssh(creds::SSHCredentials, libgit2credptr::Ptr{Ptr{Void}},
6464
else
6565
keydefpath = creds.prvkey # check if credentials were already used
6666
keydefpath === nothing && (keydefpath = "")
67-
if !isempty(keydefpath) && !isusedcreds
68-
keydefpath # use cached value
69-
else
67+
if isempty(keydefpath) || isusedcreds
7068
defaultkeydefpath = joinpath(homedir(),".ssh","id_rsa")
7169
if isempty(keydefpath) && isfile(defaultkeydefpath)
7270
keydefpath = defaultkeydefpath
@@ -75,6 +73,7 @@ function authenticate_ssh(creds::SSHCredentials, libgit2credptr::Ptr{Ptr{Void}},
7573
prompt("Private key location for '$schema$username@$host'", default=keydefpath)
7674
end
7775
end
76+
keydefpath
7877
end
7978

8079
# If the private key changed, invalidate the cached public key
@@ -87,18 +86,16 @@ function authenticate_ssh(creds::SSHCredentials, libgit2credptr::Ptr{Ptr{Void}},
8786
ENV["SSH_PUB_KEY_PATH"]
8887
else
8988
keydefpath = creds.pubkey # check if credentials were already used
90-
if keydefpath !== nothing && !isusedcreds
91-
keydefpath # use cached value
92-
else
93-
if keydefpath === nothing || isempty(keydefpath)
89+
keydefpath === nothing && (keydefpath = "")
90+
if isempty(keydefpath) || isusedcreds
91+
if isempty(keydefpath)
9492
keydefpath = privatekey*".pub"
9593
end
96-
if isfile(keydefpath)
97-
keydefpath
98-
else
94+
if !isfile(keydefpath)
9995
prompt("Public key location for '$schema$username@$host'", default=keydefpath)
10096
end
10197
end
98+
keydefpath
10299
end
103100
creds.pubkey = publickey # save credentials
104101

base/libgit2/error.jl

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export GitError
2323
ECERTIFICATE = Cint(-17), # server certificate is invalid
2424
EAPPLIED = Cint(-18), # patch/merge has already been applied
2525
EPEEL = Cint(-19), # the requested peel operation is not possible
26+
EEOF = Cint(-20), # Unexpted EOF
2627
PASSTHROUGH = Cint(-30), # internal only
2728
ITEROVER = Cint(-31)) # signals end of iteration
2829

test/TestHelpers.jl

+27
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,31 @@ Base.Terminals.hascolor(t::FakeTerminal) = t.hascolor
1616
Base.Terminals.raw!(t::FakeTerminal, raw::Bool) = t.raw = raw
1717
Base.Terminals.size(t::FakeTerminal) = (24, 80)
1818

19+
function open_fake_pty()
20+
const O_RDWR = Base.Filesystem.JL_O_RDWR
21+
const O_NOCTTY = Base.Filesystem.JL_O_NOCTTY
22+
23+
fdm = ccall(:posix_openpt, Cint, (Cint,), O_RDWR|O_NOCTTY)
24+
fdm == -1 && error("Failed to open PTY master")
25+
rc = ccall(:grantpt, Cint, (Cint,), fdm)
26+
rc != 0 && error("grantpt failed")
27+
rc = ccall(:unlockpt, Cint, (Cint,), fdm)
28+
rc != 0 && error("unlockpt")
29+
30+
fds = ccall(:open, Cint, (Ptr{UInt8}, Cint),
31+
ccall(:ptsname, Ptr{UInt8}, (Cint,), fdm), O_RDWR|O_NOCTTY)
32+
33+
# slave
34+
slave = RawFD(fds)
35+
master = Base.TTY(RawFD(fdm); readable = true)
36+
slave, master
37+
end
38+
39+
function with_fake_pty(f)
40+
slave, master = open_fake_pty()
41+
f(slave, master)
42+
ccall(:close,Cint,(Cint,),slave) # XXX: this causes the kernel to throw away all unread data on the pty
43+
close(master)
44+
end
45+
1946
end

test/choosetests.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ function choosetests(choices = [])
8080
prepend!(tests, linalgtests)
8181
end
8282

83-
net_required_for = ["socket", "parallel"]
83+
net_required_for = ["socket", "parallel", "libgit2"]
8484
net_on = true
8585
try
8686
getipaddr()

test/libgit2.jl

+177
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
#@testset "libgit2" begin
44

5+
isdefined(:TestHelpers) || include(joinpath(dirname(@__FILE__), "TestHelpers.jl"))
6+
using TestHelpers
7+
58
const LIBGIT2_MIN_VER = v"0.23.0"
69

710
#########
@@ -567,6 +570,180 @@ mktempdir() do dir
567570
@test creds.user == creds_user
568571
@test creds.pass == creds_pass
569572
#end
573+
574+
#@testset "SSH" begin
575+
sshd_command = ""
576+
ssh_repo = joinpath(dir, "Example.SSH")
577+
if !is_windows()
578+
try
579+
# SSHD needs to be executed by its full absolute path
580+
sshd_command = strip(readstring(`which sshd`))
581+
catch
582+
warn("Skipping SSH tests (Are `which` and `sshd` installed?)")
583+
end
584+
end
585+
if !isempty(sshd_command)
586+
mktempdir() do fakehomedir
587+
mkdir(joinpath(fakehomedir,".ssh"))
588+
# Unsetting the SSH agent serves two purposes. First, we make
589+
# sure that we don't accidentally pick up an existing agent,
590+
# and second we test that we fall back to using a key file
591+
# if the agent isn't present.
592+
withenv("HOME"=>fakehomedir,"SSH_AUTH_SOCK"=>nothing) do
593+
# Generate user file, first an unencrypted one
594+
wait(spawn(`ssh-keygen -N "" -C juliatest@localhost -f $fakehomedir/.ssh/id_rsa`))
595+
596+
# Generate host keys
597+
wait(spawn(`ssh-keygen -f $fakehomedir/ssh_host_rsa_key -N '' -t rsa`))
598+
wait(spawn(`ssh-keygen -f $fakehomedir/ssh_host_dsa_key -N '' -t dsa`))
599+
600+
our_ssh_port = rand(13000:14000) # Chosen arbitrarily
601+
602+
key_option = "AuthorizedKeysFile $fakehomedir/.ssh/id_rsa.pub"
603+
pidfile_option = "PidFile $fakehomedir/sshd.pid"
604+
sshp = agentp = nothing
605+
logfile = tempname()
606+
ssh_debug = false
607+
function spawn_sshd()
608+
debug_flags = ssh_debug ? `-d -d` : ``
609+
_p = open(logfile, "a") do logfilestream
610+
spawn(pipeline(pipeline(`$sshd_command
611+
-e -f /dev/null $debug_flags
612+
-h $fakehomedir/ssh_host_rsa_key
613+
-h $fakehomedir/ssh_host_dsa_key -p $our_ssh_port
614+
-o $pidfile_option
615+
-o 'Protocol 2'
616+
-o $key_option
617+
-o 'UsePrivilegeSeparation no'
618+
-o 'StrictModes no'`,STDOUT),stderr=logfilestream))
619+
end
620+
# Give the SSH server 5 seconds to start up
621+
yield(); sleep(5)
622+
_p
623+
end
624+
sshp = spawn_sshd()
625+
626+
TIOCSCTTY_str = "ccall(:ioctl, Void, (Cint, Cint, Int64), 0,
627+
(is_bsd() || is_apple()) ? 0x20007461 : is_linux() ? 0x540E :
628+
error(\"Fill in TIOCSCTTY for this OS here\"), 0)"
629+
630+
# To fail rather than hang
631+
function killer_task(p, master)
632+
@async begin
633+
sleep(10)
634+
kill(p)
635+
if isopen(master)
636+
nb_available(master) > 0 &&
637+
write(logfile,
638+
readavailable(master))
639+
close(master)
640+
end
641+
end
642+
end
643+
644+
try
645+
function try_clone(challenges = [])
646+
cmd = """
647+
repo = nothing
648+
try
649+
$TIOCSCTTY_str
650+
reponame = "ssh://$(ENV["USER"])@localhost:$our_ssh_port$cache_repo"
651+
repo = LibGit2.clone(reponame, "$ssh_repo")
652+
catch err
653+
open("$logfile","a") do f
654+
println(f,"HOME: ",ENV["HOME"])
655+
println(f, err)
656+
end
657+
finally
658+
finalize(repo)
659+
end
660+
"""
661+
# We try to be helpful by desparately looking for
662+
# a way to prompt the password interactively. Pretend
663+
# to be a TTY to suppress those shenanigans. Further, we
664+
# need to detach and change the controlling terminal with
665+
# TIOCSCTTY, since getpass opens the controlling terminal
666+
TestHelpers.with_fake_pty() do slave, master
667+
err = Base.Pipe()
668+
let p = spawn(detach(
669+
`$(Base.julia_cmd()) --startup-file=no -e $cmd`),slave,slave,STDERR)
670+
killer_task(p, master)
671+
for (challenge, response) in challenges
672+
readuntil(master, challenge)
673+
sleep(1)
674+
print(master, response)
675+
end
676+
sleep(2)
677+
wait(p)
678+
close(master)
679+
end
680+
end
681+
@test isfile(joinpath(ssh_repo,"testfile"))
682+
rm(ssh_repo, recursive = true)
683+
end
684+
685+
# Should use the default files, no interaction required.
686+
try_clone()
687+
ssh_debug && (kill(sshp); sshp = spawn_sshd())
688+
689+
# Ok, now encrypt the file and test with that (this also
690+
# makes sure that we don't accidentally fall back to the
691+
# unencrypted version)
692+
wait(spawn(`ssh-keygen -p -N "xxxxx" -f $fakehomedir/.ssh/id_rsa`))
693+
694+
# Try with the encrypted file. Needs a password.
695+
try_clone(["Passphrase"=>"xxxxx\r\n"])
696+
ssh_debug && (kill(sshp); sshp = spawn_sshd())
697+
698+
# Move the file. It should now ask for the location and
699+
# then the passphrase
700+
mv("$fakehomedir/.ssh/id_rsa","$fakehomedir/.ssh/id_rsa2")
701+
cp("$fakehomedir/.ssh/id_rsa.pub","$fakehomedir/.ssh/id_rsa2.pub")
702+
try_clone(["location"=>"$fakehomedir/.ssh/id_rsa2\n",
703+
"Passphrase"=>"xxxxx\n"])
704+
mv("$fakehomedir/.ssh/id_rsa2","$fakehomedir/.ssh/id_rsa")
705+
rm("$fakehomedir/.ssh/id_rsa2.pub")
706+
707+
# Ok, now start an agent
708+
agent_sock = tempname()
709+
agentp = spawn(`ssh-agent -a $agent_sock -d`)
710+
while stat(agent_sock).mode == 0 # Wait until the agent is started
711+
sleep(1)
712+
end
713+
714+
# fake pty is required for the same reason as in try_clone
715+
# above
716+
withenv("SSH_AUTH_SOCK" => agent_sock) do
717+
TestHelpers.with_fake_pty() do slave, master
718+
cmd = """
719+
$TIOCSCTTY_str
720+
run(pipeline(`ssh-add $fakehomedir/.ssh/id_rsa`,
721+
stderr = DevNull))
722+
"""
723+
addp = spawn(detach(`$(Base.julia_cmd()) --startup-file=no -e $cmd`),
724+
slave, slave, STDERR)
725+
killer_task(addp, master)
726+
sleep(2)
727+
write(master, "xxxxx\n")
728+
wait(addp)
729+
end
730+
731+
# Should now use the agent
732+
try_clone()
733+
end
734+
catch err
735+
println("SSHD logfile contents follows:")
736+
println(readstring(logfile))
737+
rethrow(err)
738+
finally
739+
rm(logfile)
740+
sshp !== nothing && kill(sshp)
741+
agentp !== nothing && kill(agentp)
742+
end
743+
end
744+
end
745+
end
746+
#end
570747
end
571748

572749
#end

test/repl.jl

+18-33
Original file line numberDiff line numberDiff line change
@@ -444,40 +444,25 @@ let exename = Base.julia_cmd()
444444

445445
# Test REPL in dumb mode
446446
if !is_windows()
447-
const O_RDWR = Base.Filesystem.JL_O_RDWR
448-
const O_NOCTTY = Base.Filesystem.JL_O_NOCTTY
449-
450-
fdm = ccall(:posix_openpt, Cint, (Cint,), O_RDWR|O_NOCTTY)
451-
fdm == -1 && error("Failed to open PTY master")
452-
rc = ccall(:grantpt, Cint, (Cint,), fdm)
453-
rc != 0 && error("grantpt failed")
454-
rc = ccall(:unlockpt, Cint, (Cint,), fdm)
455-
rc != 0 && error("unlockpt")
456-
457-
fds = ccall(:open, Cint, (Ptr{UInt8}, Cint),
458-
ccall(:ptsname, Ptr{UInt8}, (Cint,), fdm), O_RDWR|O_NOCTTY)
459-
460-
# slave
461-
slave = RawFD(fds)
462-
master = Base.TTY(RawFD(fdm); readable = true)
463-
464-
nENV = copy(ENV)
465-
nENV["TERM"] = "dumb"
466-
p = spawn(setenv(`$exename --startup-file=no --quiet`,nENV),slave,slave,slave)
467-
output = readuntil(master,"julia> ")
468-
if ccall(:jl_running_on_valgrind,Cint,()) == 0
469-
# If --trace-children=yes is passed to valgrind, we will get a
470-
# valgrind banner here, not just the prompt.
471-
@test output == "julia> "
447+
TestHelpers.with_fake_pty() do slave, master
448+
449+
nENV = copy(ENV)
450+
nENV["TERM"] = "dumb"
451+
p = spawn(setenv(`$exename --startup-file=no --quiet`,nENV),slave,slave,slave)
452+
output = readuntil(master,"julia> ")
453+
if ccall(:jl_running_on_valgrind,Cint,()) == 0
454+
# If --trace-children=yes is passed to valgrind, we will get a
455+
# valgrind banner here, not just the prompt.
456+
@test output == "julia> "
457+
end
458+
write(master,"1\nquit()\n")
459+
460+
wait(p)
461+
output = readuntil(master,' ')
462+
@test output == "1\r\nquit()\r\n1\r\n\r\njulia> "
463+
@test nb_available(master) == 0
464+
472465
end
473-
write(master,"1\nquit()\n")
474-
475-
wait(p)
476-
output = readuntil(master,' ')
477-
@test output == "1\r\nquit()\r\n1\r\n\r\njulia> "
478-
@test nb_available(master) == 0
479-
ccall(:close,Cint,(Cint,),fds) # XXX: this causes the kernel to throw away all unread data on the pty
480-
close(master)
481466
end
482467

483468
# Test stream mode

0 commit comments

Comments
 (0)