Skip to content

Commit

Permalink
Refactor libgit2 credential callback
Browse files Browse the repository at this point in the history
Cleanly separate the SSHCredentials and UserPasswordCredentials to only use each type
when appropriate (the former for SSH keys, the latter for HTTPS for SSH password/keyboard-interactive
authentication). Previously the types would sometimes get confused and SSHCredentials
would be used for userpass authentication, since the field names happen to be a subset
(though they have different meaning, since the `pass` field in SSHCredentials is the
SSH key passphrase while the `pass` in UserPasswordCredentials is the remote user's
password).

Fix a couple issues in the process:
- Allow externally passed in credentials to reach the right place
- Don't skip using the SSH agent if there are two private packages
  • Loading branch information
Keno authored and mfasi committed Sep 5, 2016
1 parent 3df357e commit dcb6bfa
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 233 deletions.
316 changes: 175 additions & 141 deletions base/libgit2/callbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,155 @@ function mirror_callback(remote::Ptr{Ptr{Void}}, repo_ptr::Ptr{Void},
return Cint(0)
end

function authenticate_ssh(creds::SSHCredentials, libgit2credptr::Ptr{Ptr{Void}},
username_ptr, schema, host)
isusedcreds = checkused!(creds)

errcls, errmsg = Error.last_error()
if errcls != Error.None
# Check if we used ssh-agent
if creds.usesshagent == "U"
println("ERROR: $errmsg ssh-agent")
creds.usesshagent = "E" # reported ssh-agent error, disables ssh agent use for the future
else
println("ERROR: $errmsg")
end
flush(STDOUT)
end

# first try ssh-agent if credentials support its usage
if creds.usesshagent === nothing || creds.usesshagent == "Y" || creds.usesshagent == "U"
err = ccall((:git_cred_ssh_key_from_agent, :libgit2), Cint,
(Ptr{Ptr{Void}}, Cstring), libgit2credptr, username_ptr)
creds.usesshagent = "U" # used ssh-agent only one time
err == 0 && return Cint(0)
end

# if username is not provided, then prompt for it
username = if username_ptr == Cstring(C_NULL)
uname = creds.user # check if credentials were already used
uname !== nothing && !isusedcreds ? uname : prompt("Username for '$schema$host'")
else
unsafe_string(username_ptr)
end
creds.user = username # save credentials
isempty(username) && return Cint(Error.EAUTH)

# For SSH we need a private key location
privatekey = if haskey(ENV,"SSH_KEY_PATH")
ENV["SSH_KEY_PATH"]
else
keydefpath = creds.prvkey # check if credentials were already used
keydefpath === nothing && (keydefpath = "")
if !isempty(keydefpath) && !isusedcreds
keydefpath # use cached value
else
defaultkeydefpath = joinpath(homedir(),".ssh","id_rsa")
if isempty(keydefpath) && isfile(defaultkeydefpath)
keydefpath = defaultkeydefpath
else
keydefpath =
prompt("Private key location for '$schema$username@$host'", default=keydefpath)
end
end
end

# If the private key changed, invalidate the cached public key
(privatekey != creds.prvkey) &&
(creds.pubkey = "")
creds.prvkey = privatekey # save credentials

# For SSH we need a public key location, look for environment vars SSH_* as well
publickey = if haskey(ENV,"SSH_PUB_KEY_PATH")
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 = privatekey*".pub"
end
if isfile(keydefpath)
keydefpath
else
prompt("Public key location for '$schema$username@$host'", default=keydefpath)
end
end
end
creds.pubkey = publickey # save credentials

passphrase_required = true
if !isfile(privatekey)
warn("Private key not found")
else
# In encrypted private keys, the second line is "Proc-Type: 4,ENCRYPTED"
open(privatekey) do f
passphrase_required = (readline(f); chomp(readline(f)) == "Proc-Type: 4,ENCRYPTED")
end
end

passphrase = if haskey(ENV,"SSH_KEY_PASS")
ENV["SSH_KEY_PASS"]
else
passdef = creds.pass # check if credentials were already used
passdef === nothing && (passdef = "")
if passphrase_required && (isempty(passdef) || isusedcreds)
if is_windows()
passdef = Base.winprompt(
"Your SSH Key requires a password, please enter it now:",
"Passphrase required", privatekey; prompt_username = false)
isnull(passdef) && return Cint(Error.EAUTH)
passdef = Base.get(passdef)[2]
else
passdef = prompt("Passphrase for $privatekey", password=true)
end
end
passdef
end
creds.pass = passphrase

err = ccall((:git_cred_ssh_key_new, :libgit2), Cint,
(Ptr{Ptr{Void}}, Cstring, Cstring, Cstring, Cstring),
libgit2credptr, username, publickey, privatekey, passphrase)
return err
end

function authenticate_userpass(creds::UserPasswordCredentials, libgit2credptr::Ptr{Ptr{Void}},
schema, host, urlusername)
isusedcreds = checkused!(creds)

username = creds.user
userpass = creds.pass
if is_windows()
if username === nothing || userpass === nothing || isusedcreds
res = Base.winprompt("Please enter your credentials for '$schema$host'", "Credentials required",
username === nothing || isempty(username) ?
urlusername : username; prompt_username = true)
isnull(res) && return Cint(Error.EAUTH)
username, userpass = Base.get(res)
end
else
if username === nothing || isusedcreds
username = prompt("Username for '$schema$host'", default = urlusername)
end

if userpass === nothing || isusedcreds
userpass = prompt("Password for '$schema$username@$host'", password=true)
end
end
creds.user = username # save credentials
creds.pass = userpass # save credentials

isempty(username) && isempty(userpass) && return Cint(Error.EAUTH)

err = ccall((:git_cred_userpass_plaintext_new, :libgit2), Cint,
(Ptr{Ptr{Void}}, Cstring, Cstring),
libgit2credptr, username, userpass)
err == 0 && return Cint(0)
end


"""Credentials callback function
Function provides different credential acquisition functionality w.r.t. a connection protocol.
Expand All @@ -50,7 +199,7 @@ Using credentials triggers a user prompt for (re)entering required information.
`UserPasswordCredentials` and `CachedCredentials` are implemented using a call
counting strategy that prevents repeated usage of faulty credentials.
"""
function credentials_callback(cred::Ptr{Ptr{Void}}, url_ptr::Cstring,
function credentials_callback(libgit2credptr::Ptr{Ptr{Void}}, url_ptr::Cstring,
username_ptr::Cstring,
allowed_types::Cuint, payload_ptr::Ptr{Void})
err = 0
Expand All @@ -74,157 +223,42 @@ function credentials_callback(cred::Ptr{Ptr{Void}}, url_ptr::Cstring,
creds_are_temp = false
end
end
isusedcreds = checkused!(creds)

try
# use ssh key or ssh-agent
if isset(allowed_types, Cuint(Consts.CREDTYPE_SSH_KEY))
creds === nothing && (creds = SSHCredentials())
credid = "ssh://$host"

# first try ssh-agent if credentials support its usage
if creds[:usesshagent, credid] === nothing || creds[:usesshagent, credid] == "Y"
err = ccall((:git_cred_ssh_key_from_agent, :libgit2), Cint,
(Ptr{Ptr{Void}}, Cstring), cred, username_ptr)
creds[:usesshagent, credid] = "U" # used ssh-agent only one time
err == 0 && return Cint(0)
end

errcls, errmsg = Error.last_error()
if errcls != Error.None
# Check if we used ssh-agent
if creds[:usesshagent, credid] == "U"
println("ERROR: $errmsg ssh-agent")
creds[:usesshagent, credid] = "E" # reported ssh-agent error
else
println("ERROR: $errmsg")
end
flush(STDOUT)
end

# if username is not provided, then prompt for it
username = if username_ptr == Cstring(C_NULL)
uname = creds[:user, credid] # check if credentials were already used
uname !== nothing && !isusedcreds ? uname : prompt("Username for '$schema$host'")
else
unsafe_string(username_ptr)
end
creds[:user, credid] = username # save credentials

# For SSH we need a private key location
privatekey = if haskey(ENV,"SSH_KEY_PATH")
ENV["SSH_KEY_PATH"]
else
keydefpath = creds[:prvkey, credid] # check if credentials were already used
keydefpath === nothing && (keydefpath = "")
if !isempty(keydefpath) && !isusedcreds
keydefpath # use cached value
else
defaultkeydefpath = joinpath(homedir(),".ssh","id_rsa")
if isempty(keydefpath) && isfile(defaultkeydefpath)
keydefpath = defaultkeydefpath
else
keydefpath =
prompt("Private key location for '$schema$username@$host'", default=keydefpath)
end
end
end

# If the private key changed, invalidate the cached public key
(privatekey != creds[:prvkey, credid]) &&
(creds[:pubkey, credid] = "")
creds[:prvkey, credid] = privatekey # save credentials

# For SSH we need a public key location, look for environment vars SSH_* as well
publickey = if haskey(ENV,"SSH_PUB_KEY_PATH")
ENV["SSH_PUB_KEY_PATH"]
else
keydefpath = creds[:pubkey, credid] # check if credentials were already used
if keydefpath !== nothing && !isusedcreds
keydefpath # use cached value
else
if keydefpath === nothing || isempty(keydefpath)
keydefpath = privatekey*".pub"
end
if isfile(keydefpath)
keydefpath
else
prompt("Public key location for '$schema$username@$host'", default=keydefpath)
end
end
sshcreds = get_creds!(creds, "ssh://$host", SSHCredentials())
if isa(sshcreds, SSHCredentials)
creds = sshcreds # To make sure these get cleaned up below
err = authenticate_ssh(creds, libgit2credptr, username_ptr, schema, host)
err == 0 && return err
end
creds[:pubkey, credid] = publickey # save credentials

passphrase_required = true
if !isfile(privatekey)
warn("Private key not found")
else
# In encrypted private keys, the second line is "Proc-Type: 4,ENCRYPTED"
open(privatekey) do f
passphrase_required = (readline(f); chomp(readline(f)) == "Proc-Type: 4,ENCRYPTED")
end
end

passphrase = if haskey(ENV,"SSH_KEY_PASS")
ENV["SSH_KEY_PASS"]
else
passdef = creds[:pass, credid] # check if credentials were already used
passdef === nothing && (passdef = "")
if passphrase_required && (isempty(passdef) || isusedcreds)
if is_windows()
passdef = Base.winprompt(
"Your SSH Key requires a password, please enter it now:",
"Passphrase required", privatekey; prompt_username = false)
isnull(passdef) && return Cint(Error.EAUTH)
passdef = Base.get(passdef)[2]
else
passdef = prompt("Passphrase for $privatekey", password=true)
end
end
passdef
end
creds[:pass, credid] = passphrase

isempty(username) && return Cint(Error.EAUTH)

err = ccall((:git_cred_ssh_key_new, :libgit2), Cint,
(Ptr{Ptr{Void}}, Cstring, Cstring, Cstring, Cstring),
cred, username, publickey, privatekey, passphrase)
err == 0 && return Cint(0)
end

if isset(allowed_types, Cuint(Consts.CREDTYPE_USERPASS_PLAINTEXT))
creds === nothing && (creds = UserPasswordCredentials())
defaultcreds = UserPasswordCredentials()
credid = "$schema$host"

username = creds[:user, credid]
userpass = creds[:pass, credid]
if is_windows()
if username === nothing || userpass === nothing || isusedcreds
res = Base.winprompt("Please enter your credentials for '$schema$host'", "Credentials required",
username === nothing || isempty(username) ?
urlusername : username; prompt_username = true)
isnull(res) && return Cint(Error.EAUTH)
username, userpass = Base.get(res)
end
else
if username === nothing || isusedcreds
username = prompt("Username for '$schema$host'", default = urlusername)
end

if userpass === nothing || isusedcreds
userpass = prompt("Password for '$schema$username@$host'", password=true)
end
upcreds = get_creds!(creds, credid, defaultcreds)
# If there were stored SSH credentials, but we ended up here that must
# mean that something went wrong. Replace the SSH credentials by user/pass
# credentials
if !isa(upcreds, UserPasswordCredentials)
upcreds = defaultcreds
isa(creds, CachedCredentials) && (creds.creds[credid] = upcreds)
end
creds[:user, credid] = username # save credentials
creds[:pass, credid] = userpass # save credentials

isempty(username) && isempty(userpass) && return Cint(Error.EAUTH)
creds = upcreds # To make sure these get cleaned up below
return authenticate_userpass(creds, libgit2credptr, schema, host, urlusername)
end

err = ccall((:git_cred_userpass_plaintext_new, :libgit2), Cint,
(Ptr{Ptr{Void}}, Cstring, Cstring),
cred, username, userpass)
err == 0 && return Cint(0)
# No authentication method we support succeeded. The most likely cause is
# that explicit credentials were passed in, but said credentials are incompatible
# with the remote host.
if err == 0
if (creds != nothing && !isa(creds, CachedCredentials))
warn("The explicitly provided credentials were incompatible with " *
"the server's supported authentication methods")
end
err = Cint(Error.EAUTH)
end
finally
# if credentials are not passed back to caller via payload,
Expand Down
Loading

0 comments on commit dcb6bfa

Please sign in to comment.