From 423bc9d40243da03f25cfaae3e4afd653f983c10 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sat, 23 Jul 2016 17:37:42 -0400 Subject: [PATCH] Test LibGit2 SSH authentication --- base/libgit2/callbacks.jl | 17 ++-- base/libgit2/error.jl | 1 + test/TestHelpers.jl | 27 ++++++ test/libgit2.jl | 175 ++++++++++++++++++++++++++++++++++++++ test/repl.jl | 51 ++++------- 5 files changed, 228 insertions(+), 43 deletions(-) diff --git a/base/libgit2/callbacks.jl b/base/libgit2/callbacks.jl index 81aa56c56d78a..0143125c3c2f7 100644 --- a/base/libgit2/callbacks.jl +++ b/base/libgit2/callbacks.jl @@ -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 @@ -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 @@ -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 diff --git a/base/libgit2/error.jl b/base/libgit2/error.jl index 823b4e3907ded..1d6c904a62e57 100644 --- a/base/libgit2/error.jl +++ b/base/libgit2/error.jl @@ -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 diff --git a/test/TestHelpers.jl b/test/TestHelpers.jl index 609962e68e389..b57b0d10f77ee 100644 --- a/test/TestHelpers.jl +++ b/test/TestHelpers.jl @@ -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 diff --git a/test/libgit2.jl b/test/libgit2.jl index c5ba7a9082e9d..d4ece755b89c5 100644 --- a/test/libgit2.jl +++ b/test/libgit2.jl @@ -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" ######### @@ -567,6 +570,178 @@ 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_command = strip(readstring(`which sshd`)) + catch + warn("Skipping SSH tests because (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 = spawn(pipeline(pipeline(`$sshd_command -f /dev/null + $debug_flags + -h $fakehomedir/ssh_host_rsa_key + -h $fakehomedir/ssh_host_dsa_key -p $our_ssh_port + -E $logfile + -o $pidfile_option + -o 'Protocol 2' + -o $key_option + -o 'UsePrivilegeSeparation no' + -o 'StrictModes no'`,STDOUT),stderr=STDERR)) + # 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_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()) -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()) -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 diff --git a/test/repl.jl b/test/repl.jl index f75411b165b58..09653bcb8422a 100644 --- a/test/repl.jl +++ b/test/repl.jl @@ -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