Skip to content

Commit

Permalink
Test LibGit2 SSH authentication (#17651)
Browse files Browse the repository at this point in the history
  • Loading branch information
Keno authored and tkelman committed Aug 7, 2016
1 parent d3df8e7 commit 7f074e9
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 44 deletions.
17 changes: 7 additions & 10 deletions base/libgit2/callbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ function authenticate_ssh(creds::SSHCredentials, libgit2credptr::Ptr{Ptr{Void}},
else
keydefpath = creds.prvkey # check if credentials were already used
keydefpath === nothing && (keydefpath = "")
if !isempty(keydefpath) && !isusedcreds
keydefpath # use cached value
else
if isempty(keydefpath) || isusedcreds
defaultkeydefpath = joinpath(homedir(),".ssh","id_rsa")
if isempty(keydefpath) && isfile(defaultkeydefpath)
keydefpath = defaultkeydefpath
Expand All @@ -75,6 +73,7 @@ function authenticate_ssh(creds::SSHCredentials, libgit2credptr::Ptr{Ptr{Void}},
prompt("Private key location for '$schema$username@$host'", default=keydefpath)
end
end
keydefpath
end

# If the private key changed, invalidate the cached public key
Expand All @@ -87,18 +86,16 @@ function authenticate_ssh(creds::SSHCredentials, libgit2credptr::Ptr{Ptr{Void}},
ENV["SSH_PUB_KEY_PATH"]
else
keydefpath = creds.pubkey # check if credentials were already used
if keydefpath !== nothing && !isusedcreds
keydefpath # use cached value
else
if keydefpath === nothing || isempty(keydefpath)
keydefpath === nothing && (keydefpath = "")
if isempty(keydefpath) || isusedcreds
if isempty(keydefpath)
keydefpath = privatekey*".pub"
end
if isfile(keydefpath)
keydefpath
else
if !isfile(keydefpath)
prompt("Public key location for '$schema$username@$host'", default=keydefpath)
end
end
keydefpath
end
creds.pubkey = publickey # save credentials

Expand Down
1 change: 1 addition & 0 deletions base/libgit2/error.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export GitError
ECERTIFICATE = Cint(-17), # server certificate is invalid
EAPPLIED = Cint(-18), # patch/merge has already been applied
EPEEL = Cint(-19), # the requested peel operation is not possible
EEOF = Cint(-20), # Unexpted EOF
PASSTHROUGH = Cint(-30), # internal only
ITEROVER = Cint(-31)) # signals end of iteration

Expand Down
27 changes: 27 additions & 0 deletions test/TestHelpers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,31 @@ Base.Terminals.hascolor(t::FakeTerminal) = t.hascolor
Base.Terminals.raw!(t::FakeTerminal, raw::Bool) = t.raw = raw
Base.Terminals.size(t::FakeTerminal) = (24, 80)

function open_fake_pty()
const O_RDWR = Base.Filesystem.JL_O_RDWR
const O_NOCTTY = Base.Filesystem.JL_O_NOCTTY

fdm = ccall(:posix_openpt, Cint, (Cint,), O_RDWR|O_NOCTTY)
fdm == -1 && error("Failed to open PTY master")
rc = ccall(:grantpt, Cint, (Cint,), fdm)
rc != 0 && error("grantpt failed")
rc = ccall(:unlockpt, Cint, (Cint,), fdm)
rc != 0 && error("unlockpt")

fds = ccall(:open, Cint, (Ptr{UInt8}, Cint),
ccall(:ptsname, Ptr{UInt8}, (Cint,), fdm), O_RDWR|O_NOCTTY)

# slave
slave = RawFD(fds)
master = Base.TTY(RawFD(fdm); readable = true)
slave, master
end

function with_fake_pty(f)
slave, master = open_fake_pty()
f(slave, master)
ccall(:close,Cint,(Cint,),slave) # XXX: this causes the kernel to throw away all unread data on the pty
close(master)
end

end
2 changes: 1 addition & 1 deletion test/choosetests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ function choosetests(choices = [])
prepend!(tests, linalgtests)
end

net_required_for = ["socket", "parallel"]
net_required_for = ["socket", "parallel", "libgit2"]
net_on = true
try
getipaddr()
Expand Down
177 changes: 177 additions & 0 deletions test/libgit2.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

#@testset "libgit2" begin

isdefined(:TestHelpers) || include(joinpath(dirname(@__FILE__), "TestHelpers.jl"))
using TestHelpers

const LIBGIT2_MIN_VER = v"0.23.0"

#########
Expand Down Expand Up @@ -567,6 +570,180 @@ mktempdir() do dir
@test creds.user == creds_user
@test creds.pass == creds_pass
#end

#@testset "SSH" begin
sshd_command = ""
ssh_repo = joinpath(dir, "Example.SSH")
if !is_windows()
try
# SSHD needs to be executed by its full absolute path
sshd_command = strip(readstring(`which sshd`))
catch
warn("Skipping SSH tests (Are `which` and `sshd` installed?)")
end
end
if !isempty(sshd_command)
mktempdir() do fakehomedir
mkdir(joinpath(fakehomedir,".ssh"))
# Unsetting the SSH agent serves two purposes. First, we make
# sure that we don't accidentally pick up an existing agent,
# and second we test that we fall back to using a key file
# if the agent isn't present.
withenv("HOME"=>fakehomedir,"SSH_AUTH_SOCK"=>nothing) do
# Generate user file, first an unencrypted one
wait(spawn(`ssh-keygen -N "" -C juliatest@localhost -f $fakehomedir/.ssh/id_rsa`))

# Generate host keys
wait(spawn(`ssh-keygen -f $fakehomedir/ssh_host_rsa_key -N '' -t rsa`))
wait(spawn(`ssh-keygen -f $fakehomedir/ssh_host_dsa_key -N '' -t dsa`))

our_ssh_port = rand(13000:14000) # Chosen arbitrarily

key_option = "AuthorizedKeysFile $fakehomedir/.ssh/id_rsa.pub"
pidfile_option = "PidFile $fakehomedir/sshd.pid"
sshp = agentp = nothing
logfile = tempname()
ssh_debug = false
function spawn_sshd()
debug_flags = ssh_debug ? `-d -d` : ``
_p = open(logfile, "a") do logfilestream
spawn(pipeline(pipeline(`$sshd_command
-e -f /dev/null $debug_flags
-h $fakehomedir/ssh_host_rsa_key
-h $fakehomedir/ssh_host_dsa_key -p $our_ssh_port
-o $pidfile_option
-o 'Protocol 2'
-o $key_option
-o 'UsePrivilegeSeparation no'
-o 'StrictModes no'`,STDOUT),stderr=logfilestream))
end
# Give the SSH server 5 seconds to start up
yield(); sleep(5)
_p
end
sshp = spawn_sshd()

TIOCSCTTY_str = "ccall(:ioctl, Void, (Cint, Cint, Int64), 0,
(is_bsd() || is_apple()) ? 0x20007461 : is_linux() ? 0x540E :
error(\"Fill in TIOCSCTTY for this OS here\"), 0)"

# To fail rather than hang
function killer_task(p, master)
@async begin
sleep(10)
kill(p)
if isopen(master)
nb_available(master) > 0 &&
write(logfile,
readavailable(master))
close(master)
end
end
end

try
function try_clone(challenges = [])
cmd = """
repo = nothing
try
$TIOCSCTTY_str
reponame = "ssh://$(ENV["USER"])@localhost:$our_ssh_port$cache_repo"
repo = LibGit2.clone(reponame, "$ssh_repo")
catch err
open("$logfile","a") do f
println(f,"HOME: ",ENV["HOME"])
println(f, err)
end
finally
finalize(repo)
end
"""
# We try to be helpful by desparately looking for
# a way to prompt the password interactively. Pretend
# to be a TTY to suppress those shenanigans. Further, we
# need to detach and change the controlling terminal with
# TIOCSCTTY, since getpass opens the controlling terminal
TestHelpers.with_fake_pty() do slave, master
err = Base.Pipe()
let p = spawn(detach(
`$(Base.julia_cmd()) --startup-file=no -e $cmd`),slave,slave,STDERR)
killer_task(p, master)
for (challenge, response) in challenges
readuntil(master, challenge)
sleep(1)
print(master, response)
end
sleep(2)
wait(p)
close(master)
end
end
@test isfile(joinpath(ssh_repo,"testfile"))
rm(ssh_repo, recursive = true)
end

# Should use the default files, no interaction required.
try_clone()
ssh_debug && (kill(sshp); sshp = spawn_sshd())

# Ok, now encrypt the file and test with that (this also
# makes sure that we don't accidentally fall back to the
# unencrypted version)
wait(spawn(`ssh-keygen -p -N "xxxxx" -f $fakehomedir/.ssh/id_rsa`))

# Try with the encrypted file. Needs a password.
try_clone(["Passphrase"=>"xxxxx\r\n"])
ssh_debug && (kill(sshp); sshp = spawn_sshd())

# Move the file. It should now ask for the location and
# then the passphrase
mv("$fakehomedir/.ssh/id_rsa","$fakehomedir/.ssh/id_rsa2")
cp("$fakehomedir/.ssh/id_rsa.pub","$fakehomedir/.ssh/id_rsa2.pub")
try_clone(["location"=>"$fakehomedir/.ssh/id_rsa2\n",
"Passphrase"=>"xxxxx\n"])
mv("$fakehomedir/.ssh/id_rsa2","$fakehomedir/.ssh/id_rsa")
rm("$fakehomedir/.ssh/id_rsa2.pub")

# Ok, now start an agent
agent_sock = tempname()
agentp = spawn(`ssh-agent -a $agent_sock -d`)
while stat(agent_sock).mode == 0 # Wait until the agent is started
sleep(1)
end

# fake pty is required for the same reason as in try_clone
# above
withenv("SSH_AUTH_SOCK" => agent_sock) do
TestHelpers.with_fake_pty() do slave, master
cmd = """
$TIOCSCTTY_str
run(pipeline(`ssh-add $fakehomedir/.ssh/id_rsa`,
stderr = DevNull))
"""
addp = spawn(detach(`$(Base.julia_cmd()) --startup-file=no -e $cmd`),
slave, slave, STDERR)
killer_task(addp, master)
sleep(2)
write(master, "xxxxx\n")
wait(addp)
end

# Should now use the agent
try_clone()
end
catch err
println("SSHD logfile contents follows:")
println(readstring(logfile))
rethrow(err)
finally
rm(logfile)
sshp !== nothing && kill(sshp)
agentp !== nothing && kill(agentp)
end
end
end
end
#end
end

#end
51 changes: 18 additions & 33 deletions test/repl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -444,40 +444,25 @@ let exename = Base.julia_cmd()

# Test REPL in dumb mode
if !is_windows()
const O_RDWR = Base.Filesystem.JL_O_RDWR
const O_NOCTTY = Base.Filesystem.JL_O_NOCTTY

fdm = ccall(:posix_openpt, Cint, (Cint,), O_RDWR|O_NOCTTY)
fdm == -1 && error("Failed to open PTY master")
rc = ccall(:grantpt, Cint, (Cint,), fdm)
rc != 0 && error("grantpt failed")
rc = ccall(:unlockpt, Cint, (Cint,), fdm)
rc != 0 && error("unlockpt")

fds = ccall(:open, Cint, (Ptr{UInt8}, Cint),
ccall(:ptsname, Ptr{UInt8}, (Cint,), fdm), O_RDWR|O_NOCTTY)

# slave
slave = RawFD(fds)
master = Base.TTY(RawFD(fdm); readable = true)

nENV = copy(ENV)
nENV["TERM"] = "dumb"
p = spawn(setenv(`$exename --startup-file=no --quiet`,nENV),slave,slave,slave)
output = readuntil(master,"julia> ")
if ccall(:jl_running_on_valgrind,Cint,()) == 0
# If --trace-children=yes is passed to valgrind, we will get a
# valgrind banner here, not just the prompt.
@test output == "julia> "
TestHelpers.with_fake_pty() do slave, master

nENV = copy(ENV)
nENV["TERM"] = "dumb"
p = spawn(setenv(`$exename --startup-file=no --quiet`,nENV),slave,slave,slave)
output = readuntil(master,"julia> ")
if ccall(:jl_running_on_valgrind,Cint,()) == 0
# If --trace-children=yes is passed to valgrind, we will get a
# valgrind banner here, not just the prompt.
@test output == "julia> "
end
write(master,"1\nquit()\n")

wait(p)
output = readuntil(master,' ')
@test output == "1\r\nquit()\r\n1\r\n\r\njulia> "
@test nb_available(master) == 0

end
write(master,"1\nquit()\n")

wait(p)
output = readuntil(master,' ')
@test output == "1\r\nquit()\r\n1\r\n\r\njulia> "
@test nb_available(master) == 0
ccall(:close,Cint,(Cint,),fds) # XXX: this causes the kernel to throw away all unread data on the pty
close(master)
end

# Test stream mode
Expand Down

0 comments on commit 7f074e9

Please sign in to comment.