Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test LibGit2 SSH authentication #17651

Merged
merged 1 commit into from
Aug 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not libgit2 but libgit2-online

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, these tests are in libgit2, since they don't require a network connection but they do require lo. I suppose we could split them out yet again, but not sure it's worth it. @tkelman?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems fine, I rarely see net_on not true. WSL is one of the few places that's the case

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

World Surf League?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

windows subsystem for linux - aka bash on ubuntu on windows

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`))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which is not always installed. how about success(sshd) inside a try catch?

#17586 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not the purpose of this command. sshd refuses to be invoked as anything other than it's absolute path.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh. add a comment that says that.

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)"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this test just going to fail completely on freebsd then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, unless you a) tell me the value of TIOCSCTTY on freebsd and other relevant platforms or b) prevent @vtjnash from merging the patch that makes this necessary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although given that most of Apple's userland is freebsd, I wouldn't be surprised if the value is the same.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mhm, looks like it's the same as on OS X


# 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