Skip to content

Commit

Permalink
Merge pull request #16308 from wildart/credentials
Browse files Browse the repository at this point in the history
LibGit2 `credentials` callback overhaul
  • Loading branch information
tkelman committed May 21, 2016
2 parents 316ce61 + 3684b1f commit e2f59bc
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 67 deletions.
170 changes: 127 additions & 43 deletions base/libgit2/callbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,78 +27,162 @@ end
"""Credentials callback function
Function provides different credential acquisition functionality w.r.t. a connection protocol.
If payload is provided then `payload_ptr` should contain `LibGit2.AbstractPayload` object.
For `LibGit2.Consts.CREDTYPE_USERPASS_PLAINTEXT` type, if payload contains fields: `user` & `pass` they are used to create authentication credentials.
In addition, if payload has field `used` it can be set to `true` to indicate that payload was used and abort callback with error. This behavior is required to avoid repeated authentication calls with incorrect credentials.
If payload is provided then `payload_ptr` should contain `LibGit2.AbstractCredentials` object.
For `LibGit2.Consts.CREDTYPE_USERPASS_PLAINTEXT` type, if payload contains fields:
`user` & `pass`, they are used to create authentication credentials.
Empty `user` name and `pass`word trigger authentication error.
For `LibGit2.Consts.CREDTYPE_SSH_KEY` type, if payload contains fields:
`user`, `prvkey`, `pubkey` & `pass`, they are used to create authentication credentials.
Empty `user` name triggers authentication error.
Order of credential checks (if supported):
- ssh key pair (ssh-agent if specified in payload's `usesshagent` field)
- plain text
**Note**: Due to the specifics of `libgit2` authentication procedure, when authentication fails,
this function is called again without any indication whether authentication was successful or not.
To avoid an infinite loop from repeatedly using the same faulty credentials,
`checkused!` function can be called to test if credentials were used if call returns `true` value.
Used credentials trigger 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,
username_ptr::Cstring,
allowed_types::Cuint, payload_ptr::Ptr{Void})
err = 1
err = 0
url = String(url_ptr)

if isset(allowed_types, Cuint(Consts.CREDTYPE_USERPASS_PLAINTEXT))
username = userpass = ""
if payload_ptr != C_NULL
payload = unsafe_pointer_to_objref(payload_ptr)
if isa(payload, AbstractPayload)
isused(payload) && return Cint(-1)
username = user(payload)
userpass = password(payload)
setused!(payload, true)
end
end
if isempty(username)
username = prompt("Username for '$url'")
# parse url for schema and host
urlparts = match(urlmatcher, url)
schema = urlparts.captures[1]
host = urlparts.captures[5]
schema = schema === nothing ? "" : schema*"://"

# get credentials object from payload pointer
creds = EmptyCredentials()
if payload_ptr != C_NULL
tmpobj = unsafe_pointer_to_objref(payload_ptr)
if isa(tmpobj, AbstractCredentials)
creds = tmpobj
end
if isempty(userpass)
userpass = prompt("Password for '$username@$url'", password=true)
end
isusedcreds = checkused!(creds)

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

# set ssh-agent trigger for first use
if creds[:usesshagent, credid] === nothing
creds[:usesshagent, credid] = "Y"
end

isempty(username) && isempty(userpass) && return Cint(-1)
# first try ssh-agent if credentials support its usage
if 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

err = ccall((:git_cred_userpass_plaintext_new, :libgit2), Cint,
(Ptr{Ptr{Void}}, Cstring, Cstring),
cred, username, userpass)
err == 0 && return Cint(0)
elseif isset(allowed_types, Cuint(Consts.CREDTYPE_SSH_KEY)) && err > 0
# use ssh-agent
err = ccall((:git_cred_ssh_key_from_agent, :libgit2), Cint,
(Ptr{Ptr{Void}}, Cstring), cred, username_ptr)
err == 0 && return Cint(0)
elseif isset(allowed_types, Cuint(Consts.CREDTYPE_SSH_CUSTOM)) && err > 0
# for SSH we need key info, look for environment vars SSH_* as well
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_NULL
prompt("Username for '$url'")
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
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
if keydefpath !== nothing && !isusedcreds
keydefpath # use cached value
else
keydefpath = if keydefpath === nothing
homedir()*"/.ssh/id_rsa"
end
prompt("Private key location for '$schema$username@$host'", default=keydefpath)
end
end
creds[:prvkey, credid] = privatekey # save credentials

publickey = if "SSH_PUB_KEY" in keys(ENV)
ENV["GITHUB_PUB_KEY"]
# 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
keydef = homedir()*"/.ssh/id_rsa.pub"
prompt("Public key location", default=keydef)
keydefpath = creds[:pubkey, credid] # check if credentials were already used
if keydefpath !== nothing && !isusedcreds
keydefpath # use cached value
else
keydefpath = if keydefpath === nothing
privatekey*".pub"
end
if isfile(keydefpath)
keydefpath
else
prompt("Public key location for '$schema$username@$host'", default=keydefpath)
end
end
end
creds[:pubkey, credid] = publickey # save credentials

privatekey = if "SSH_PRV_KEY" in keys(ENV)
ENV["GITHUB_PRV_KEY"]
passphrase = if haskey(ENV,"SSH_KEY_PASS")
ENV["SSH_KEY_PASS"]
else
keydef = homedir()*"/.ssh/id_rsa"
prompt("Private key location", default=keydef)
passdef = creds[:pass, credid] # check if credentials were already used
passdef !== nothing && !isusedcreds ? passdef : prompt("Passphrase for $privatekey", password=true)
end
creds[:pass, credid] = passphrase # save credentials

passphrase= get(ENV,"SSH_PRV_KEY_PASS","0") == "0" ? "" : prompt("Private key passphrase", password=true)
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))
credid = "$schema$host"
username = creds[:user, credid]
if username === nothing || isusedcreds
username = prompt("Username for '$schema$host'")
creds[:user, credid] = username # save credentials
end

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

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

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

return Cint(err)
end

Expand Down
5 changes: 5 additions & 0 deletions base/libgit2/consts.jl
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,11 @@ Option flags for `GitRepo`.
CREDTYPE_USERNAME = Cuint(1 << 5),
CREDTYPE_SSH_MEMORY = Cuint(1 << 6))

@enum(GIT_FEATURE, FEATURE_THREADS = Cuint(1 << 0),
FEATURE_HTTPS = Cuint(1 << 1),
FEATURE_SSH = Cuint(1 << 2),
FEATURE_NSEC = Cuint(1 << 3))

if LibGit2.version() >= v"0.24.0"
"""
Priority level of a config file.
Expand Down
101 changes: 90 additions & 11 deletions base/libgit2/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,29 @@ end

"Abstract payload type for callback functions"
abstract AbstractPayload
user(p::AbstractPayload) = throw(AssertionError("Function 'user' is not implemented for type $(typeof(p))"))
password(p::AbstractPayload) = throw(AssertionError("Function 'password' is not implemented for type $(typeof(p))"))
isused(p::AbstractPayload) = throw(AssertionError("Function 'isused' is not implemented for type $(typeof(p))"))
setused!(p::AbstractPayload,v::Bool) = throw(AssertionError("Function 'setused!' is not implemented for type $(typeof(p))"))

"Abstract credentials payload"
abstract AbstractCredentials <: AbstractPayload
"Returns a credentials parameter"
function Base.getindex(p::AbstractCredentials, keys...)
for k in keys
ks = Symbol(k)
isdefined(p, ks) && return getfield(p, ks)
end
return nothing
end
"Sets credentials with `key` parameter with value"
function Base.setindex!(p::AbstractCredentials, val, keys...)
for k in keys
ks = Symbol(k)
isdefined(p, ks) && setfield!(p, ks, val)
end
return p
end
"Checks if credentials were used"
checkused!(p::AbstractCredentials) = true
"Resets credentials for another usage"
reset!(p::AbstractCredentials, cnt::Int=3) = nothing

immutable CheckoutOptions
version::Cuint
Expand Down Expand Up @@ -628,13 +647,73 @@ function getobjecttype(obj_type::Cint)
end
end

type UserPasswordCredentials <: AbstractPayload
"Empty credentials"
type EmptyCredentials <: AbstractCredentials end

"Credentials that support only `user` and `password` parameters"
type UserPasswordCredentials <: AbstractCredentials
user::AbstractString
pass::AbstractString
used::Bool
UserPasswordCredentials(u::AbstractString,p::AbstractString) = new(u,p,false)
usesshagent::AbstractString # used for ssh-agent authentication
count::Int # authentication failure protection count
UserPasswordCredentials(u::AbstractString,p::AbstractString) = new(u,p,"Y",3)
end
"Checks if credentials were used or failed authentication, see `LibGit2.credentials_callback`"
function checkused!(p::UserPasswordCredentials)
p.count <= 0 && return true
p.count -= 1
return false
end
user(p::UserPasswordCredentials) = p.user
password(p::UserPasswordCredentials) = p.pass
isused(p::UserPasswordCredentials) = p.used
setused!(p::UserPasswordCredentials, val::Bool) = (p.used = val)
"Resets authentication failure protection count"
reset!(p::UserPasswordCredentials, cnt::Int=3) = (p.count = cnt)

"SSH credentials type"
type SSHCredentials <: AbstractCredentials
user::AbstractString
pass::AbstractString
pubkey::AbstractString
prvkey::AbstractString
usesshagent::AbstractString # used for ssh-agent authentication

SSHCredentials(u::AbstractString,p::AbstractString) = new(u,p,"","","Y")
SSHCredentials() = SSHCredentials("","")
end

"Credentials that support caching"
type CachedCredentials <: AbstractCredentials
cred::Dict{AbstractString,SSHCredentials}
count::Int # authentication failure protection count
CachedCredentials() = new(Dict{AbstractString,SSHCredentials}(),3)
end
"Returns specific credential parameter value: first index is a credential parameter name, second index is a host name (with schema)"
function Base.getindex(p::CachedCredentials, keys...)
length(keys) != 2 && return nothing
key, host = keys
if haskey(p.cred, host)
creds = p.cred[host]
if isdefined(creds,key)
kval = getfield(creds, key)
!isempty(kval) && return kval
end
end
return nothing
end
"Sets specific credential parameter value: first index is a credential parameter name, second index is a host name (with schema)"
function Base.setindex!(p::CachedCredentials, val, keys...)
length(keys) != 2 && return nothing
key, host = keys
if !haskey(p.cred, host)
p.cred[host] = SSHCredentials()
end
creds = p.cred[host]
isdefined(creds,key) && setfield!(creds, key, val)
return p
end
"Checks if credentials were used or failed authentication, see `LibGit2.credentials_callback`"
function checkused!(p::CachedCredentials)
p.count <= 0 && return true
p.count -= 1
return false
end
"Resets authentication failure protection count"
reset!(p::CachedCredentials, cnt::Int=3) = (p.count = cnt)
15 changes: 14 additions & 1 deletion base/libgit2/utils.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# This file is a part of Julia. License is MIT: http://julialang.org/license

const urlmatcher = r"^(http[s]?|git|ssh)?(:\/\/)?((\w+)@)?([A-Za-z0-9\-\.]+)(:[0-9]+)?(.*)$"

function version()
major = Ref{Cint}(0)
minor = Ref{Cint}(0)
Expand All @@ -10,14 +12,25 @@ function version()
end

isset(val::Integer, flag::Integer) = (val & flag == flag)
reset(val::Integer, flag::Integer) = (val &= ~flag)
toggle(val::Integer, flag::Integer) = (val |= flag)

function prompt(msg::AbstractString; default::AbstractString="", password::Bool=false)
msg = !isempty(default) ? msg*" [$default]:" : msg*":"
uinput = if password
String(ccall(:getpass, Cstring, (Cstring,), msg))
Base.getpass(msg)
else
print(msg)
chomp(readline(STDIN))
end
isempty(uinput) ? default : uinput
end

function features()
feat = ccall((:git_libgit2_features, :libgit2), Cint, ())
res = Consts.GIT_FEATURE[]
for f in instances(Consts.GIT_FEATURE)
isset(feat, Cuint(f)) && push!(res, f)
end
return res
end
Loading

0 comments on commit e2f59bc

Please sign in to comment.