diff --git a/base/libgit2/callbacks.jl b/base/libgit2/callbacks.jl index 06ead82ea578e..b55e7e8125c9c 100644 --- a/base/libgit2/callbacks.jl +++ b/base/libgit2/callbacks.jl @@ -24,156 +24,213 @@ 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" - creds.usesshagent = "E" # reported ssh-agent error, disables ssh agent use for the future - end - end +function require_passphrase(private_key::AbstractString) + !isfile(private_key) && return false + + # In encrypted private keys, the second line is "Proc-Type: 4,ENCRYPTED" + return open(private_key) do f + readline(f) + readline(f) == "Proc-Type: 4,ENCRYPTED" + end +end + +function user_abort() + # TODO: Maybe just throw a Julia error + ccall((:giterr_set_str, :libgit2), Void, (Cint, Cstring), Cint(Error.Callback), "Aborting, user cancelled credential request.") + return Cint(Error.EAUTH) +end + +function authenticate_ssh(libgit2credptr::Ptr{Ptr{Void}}, username_ptr, p::RemotePayload) + c = Base.get(p.credential)::SSHCredential + c.passphrase = "" + state = p.state + modified = false + + prompt_url = git_url(protocol=p.protocol, username=c.username, host=p.host) # first try ssh-agent if credentials support its usage - if creds.usesshagent === nothing || creds.usesshagent == "Y" || creds.usesshagent == "U" + if get!(state, :ssh_agent, 'Y') == 'Y' 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 creds.prompt_if_incorrect - # 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 - !isusedcreds ? uname : prompt("Username for '$schema$host'", default=uname) + if err == 0 + state[:ssh_agent] = 'U' # used ssh-agent only one time + return Cint(0) else - unsafe_string(username_ptr) + state[:ssh_agent] = 'E' end - isempty(username) && return Cint(Error.EAUTH) + end - # 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 - 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 - keydefpath - end + assert(!isempty(c.username)) - # If the private key changed, invalidate the cached public key - (privatekey != creds.prvkey) && - (creds.pubkey = "") + # All or nothing. Cache should only contain valid credentials + if get!(state, :cache, 'Y') == 'Y' && !isnull(p.cache) && !isfilled(c) + cred_id = "$(isempty(p.protocol) ? "ssh" : p.protocol)://$(p.host)" + cached_cred = get_cred(unsafe_get(p.cache), cred_id, SSHCredential) - # 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 - keydefpath === nothing && (keydefpath = "") - if isempty(keydefpath) || isusedcreds - if isempty(keydefpath) - keydefpath = privatekey*".pub" - end - if !isfile(keydefpath) - prompt("Public key location for '$schema$username@$host'", default=keydefpath) - end + c.private_key = cached_cred.private_key + c.public_key = cached_cred.public_key + c.passphrase = cached_cred.passphrase + modified = true # Valid once modified + end + + if get!(state, :env, 'Y') == 'Y' && !isfilled(c) + c.private_key = Base.get(ENV, "SSH_KEY_PATH", joinpath(homedir(), ".ssh", "id_rsa")) + c.public_key = Base.get(ENV, "SSH_PUB_KEY_PATH", c.private_key * ".pub") + c.passphrase = Base.get(ENV, "SSH_KEY_PASS", "") + modified = true + + state[:env] = 'N' + end + + # Fallback to default public key + default_public_key = c.private_key * ".pub" + if !modified && c.public_key != default_public_key + c.public_key = default_public_key + modified = true + end + + # Since SSH authentication will raise an exception when credentials are incorrect we + # will prompt the user for confirmation if we need to gather any additional info. + prompt_for_keys = !modified || !isfile(c.private_key) || !isfile(c.public_key) + while get!(state, :prompt, 'Y') == 'Y' && (!modified || !isfilled(c)) + if prompt_for_keys + last_private_key = isfile(c.private_key) ? c.private_key : "" + private_key = prompt( + "Private key location for '$prompt_url'", + default=last_private_key) + isnull(private_key) && return user_abort() + c.private_key = unsafe_get(private_key) + + # Update the public key to reflect the change to the private key + if c.private_key != last_private_key + c.public_key = c.private_key * ".pub" end - keydefpath - end - 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 - readline(f) - passphrase_required = readline(f) == "Proc-Type: 4,ENCRYPTED" + # Avoid asking about the public key as typically this will just annoy users. + if isfile(c.private_key) && !isfile(c.public_key) + public_key = prompt("Public key location for '$prompt_url'") + isnull(public_key) && return user_abort() + c.public_key = unsafe_get(public_key) end + else + # Always prompt for private key on future iterations + prompt_for_keys = true 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 + if isempty(c.passphrase) && require_passphrase(c.private_key) + @static if is_windows() + res = Base.winprompt( + "Your SSH Key requires a password, please enter it now:", + "Passphrase required", c.private_key; prompt_username = false) + isnull(res) && return user_abort() + c.passphrase = unsafe_get(res)[2] + else + passphrase = prompt("Passphrase for $(c.private_key)", password=true) + isnull(passphrase) && return user_abort() + c.passphrase = unsafe_get(passphrase) + isempty(c.passphrase) && return user_abort() end - passdef end - ((creds.user != username) || (creds.pass != passphrase) || - (creds.prvkey != privatekey) || (creds.pubkey != publickey)) && reset!(creds) - - creds.user = username # save credentials - creds.prvkey = privatekey # save credentials - creds.pubkey = publickey # save credentials - creds.pass = passphrase - else - isusedcreds && return Cint(Error.EAUTH) + + modified = true + + p.prompts_remaining -= 1 + if p.prompts_remaining <= 0 + state[:prompt] = 'N' + ccall((:giterr_set_str, :libgit2), Void, (Cint, Cstring), Cint(Error.Callback), "Aborting, maximum number of user prompts reached.") + return Cint(Error.EAUTH) + end + end + + # LibGit2 will only complain about the public key being missing if both the private key and + # public key are missing. + if !isfile(c.private_key) + # Emulates the missing public key file error produced by LibGit2 + ccall((:giterr_set_str, :libgit2), Void, + (Cint, Cstring), Cint(Error.SSH), + "Failed to authenticate SSH session: Unable to open private key file") + # ccall((:giterr_set_str, :libgit2), Void, + # (Cint, Cstring), Cint(Error.Callback), + # "Unable to open private key file") + return Cint(Error.EAUTH) end - err = ccall((:git_cred_ssh_key_new, :libgit2), Cint, + # Note: Failure to authenticate with SSH credentials abend the callback loop. + # Note: Passing in missing credential files will abend the callback loop. + return ccall((:git_cred_ssh_key_new, :libgit2), Cint, (Ptr{Ptr{Void}}, Cstring, Cstring, Cstring, Cstring), - libgit2credptr, creds.user, creds.pubkey, creds.prvkey, creds.pass) - return err + libgit2credptr, c.username, c.public_key, c.private_key, c.passphrase) end -function authenticate_userpass(creds::UserPasswordCredentials, libgit2credptr::Ptr{Ptr{Void}}, - schema, host, urlusername) - isusedcreds = checkused!(creds) - - if creds.prompt_if_incorrect - username = creds.user - userpass = creds.pass - (username === nothing) && (username = "") - (userpass === nothing) && (userpass = "") - if is_windows() - if isempty(username) || isempty(userpass) || 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) +function authenticate_userpass(libgit2credptr::Ptr{Ptr{Void}}, p::RemotePayload) + + c = Base.get(p.credential)::UserPasswordCredential + c.password = "" + state = p.state + config = p.config + + if get!(state, :cache, 'Y') == 'Y' && !isnull(p.cache) && !isfilled(c) + cred_id = "$(isempty(p.protocol) ? "ssh" : p.protocol)://$(p.host)" + cached_cred = get_cred(unsafe_get(p.cache), cred_id, UserPasswordCredential) + + c.username = cached_cred.username + c.password = cached_cred.password + + state[:cache] == 'U' + end + + if get!(state, :git_credential_helper, 'Y') == 'Y' && !isfilled(c) + cred = GitCredential( + protocol=p.protocol, + host=p.host, + path=p.path, + username=c.username, + password=c.password, + ) + helpers = helpers!(config, cred) + fill!(helpers, cred) + + c.username = cred.username + c.password = cred.password + + state[:git_credential_helper] = 'U' # used git-credentials only one time + end + + if get!(state, :prompt, 'Y') == 'Y' && !isfilled(c) + prompt_url = git_url(protocol=p.protocol, host=p.host) + @static if is_windows() + res = Base.winprompt( + "Please enter your credentials for '$prompt_url'", + "Credentials required", + c.username; + prompt_username=true) + isnull(res) && return user_abort() + c.username, c.password = unsafe_get(res) + else + username = prompt("Username for '$prompt_url'", default=c.username) + isnull(username) && return user_abort() + c.username = unsafe_get(username) + + if !isempty(c.username) + prompt_url = git_url(protocol=p.protocol, host=p.host, username=c.username) + password = prompt("Password for '$prompt_url'", password=true) + isnull(password) && return user_abort() + c.password = unsafe_get(password) + isempty(c.password) && return user_abort() end - elseif isusedcreds - username = prompt("Username for '$schema$host'", default = isempty(username) ? - urlusername : username) - userpass = prompt("Password for '$schema$username@$host'", password=true) end - ((creds.user != username) || (creds.pass != userpass)) && reset!(creds) - creds.user = username # save credentials - creds.pass = userpass # save credentials - isempty(username) && isempty(userpass) && return Cint(Error.EAUTH) - else - isusedcreds && return Cint(Error.EAUTH) + p.prompts_remaining -= 1 + if p.prompts_remaining <= 0 + state[:prompt] = 'N' + ccall((:giterr_set_str, :libgit2), Void, (Cint, Cstring), Cint(Error.Callback), "Aborting, maximum number of user prompts reached.") + return Cint(Error.EAUTH) + end end - err = ccall((:git_cred_userpass_plaintext_new, :libgit2), Cint, + return ccall((:git_cred_userpass_plaintext_new, :libgit2), Cint, (Ptr{Ptr{Void}}, Cstring, Cstring), - libgit2credptr, creds.user, creds.pass) - err == 0 && return Cint(0) + libgit2credptr, c.username, c.password) end @@ -200,59 +257,118 @@ authentication was successful or not. To avoid an infinite loop from repeatedly using the same faulty credentials, the `checkused!` function can be called. This function returns `true` if the credentials were used. Using credentials triggers a user prompt for (re)entering required information. -`UserPasswordCredentials` and `CachedCredentials` are implemented using a call +`UserPasswordCredential` and `CachedCredentials` are implemented using a call counting strategy that prevents repeated usage of faulty credentials. """ function credentials_callback(libgit2credptr::Ptr{Ptr{Void}}, url_ptr::Cstring, username_ptr::Cstring, allowed_types::Cuint, payload_ptr::Ptr{Void}) - err = 0 - url = unsafe_string(url_ptr) - - # parse url for schema and host - urlparts = match(URL_REGEX, url) - schema = urlparts[:scheme] === nothing ? "" : urlparts[:scheme] * "://" - urlusername = urlparts[:user] === nothing ? "" : urlparts[:user] - host = urlparts[:host] + err = Cint(0) + handled = false # get credentials object from payload pointer @assert payload_ptr != C_NULL - creds = unsafe_pointer_to_objref(payload_ptr) - explicit = !isnull(creds[]) && !isa(Base.get(creds[]), CachedCredentials) + p = unsafe_pointer_to_objref(payload_ptr)[] + + # parse url for protocol and host + if isempty(p.host) + url = match(GIT_URL_REGEX, unsafe_string(url_ptr)) + + p.protocol = url[:protocol] === nothing ? "" : url[:protocol] + p.username = url[:user] === nothing ? "" : url[:user] + p.host = url[:host] + p.path = url[:path] + end + # use ssh key or ssh-agent if isset(allowed_types, Cuint(Consts.CREDTYPE_SSH_KEY)) - sshcreds = get_creds!(creds, "ssh://$host", reset!(SSHCredentials(true), -1)) - if isa(sshcreds, SSHCredentials) - err = authenticate_ssh(sshcreds, libgit2credptr, username_ptr, schema, host) - err == 0 && return err + handled = true + if isnull(p.credential) || !isa(Base.get(p.credential), SSHCredential) + p.credential = Nullable(SSHCredential(p.username)) end + err = authenticate_ssh(libgit2credptr, username_ptr, p) + err == 0 && return err end if isset(allowed_types, Cuint(Consts.CREDTYPE_USERPASS_PLAINTEXT)) - defaultcreds = reset!(UserPasswordCredentials(true), -1) - credid = "$schema$host" - 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(Base.get(creds[]), CachedCredentials) && (Base.get(creds[]).creds[credid] = upcreds) + handled = true + if isnull(p.credential) || !isa(Base.get(p.credential), UserPasswordCredential) + p.credential = Nullable(UserPasswordCredential(p.username)) end - return authenticate_userpass(upcreds, libgit2credptr, schema, host, urlusername) + err = authenticate_userpass(libgit2credptr, p) + err == 0 && return err end - # 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 explicit - warn("The explicitly provided credentials were incompatible with " * - "the server's supported authentication methods") - end + # Fail safe when we are unable to handle any of the allowed_types + if !handled + ccall((:giterr_set_str, :libgit2), Void, (Cint, Cstring), Cint(Error.Callback), + @sprintf("Aborting credential callback. Unable authenticate using allowed types 0x%08x", allowed_types)) err = Cint(Error.EAUTH) end - return Cint(err) + + return err +end + +""" + credentials_approve(payload::RemotePayload) -> Void + +Saves the payload credentials for future requests to avoid interative authentication. Should +only be called if the credential callback resulted in a successful operation. +""" +function credentials_approve(p::RemotePayload) + isnull(p.credential) && return # No credentials were used + + c = Base.get(p.credential) + cred_id = "$(isempty(p.protocol) ? "ssh" : p.protocol)://$(p.host)" + config = p.config + + if isa(c, UserPasswordCredential) + cred = GitCredential( + protocol=p.protocol, + host=p.host, + path=p.path, + username=c.username, + password=c.password, + ) + helpers = helpers!(config, cred) + approve(helpers, cred) + end + + if !isnull(p.cache) + cache = Base.get(p.cache) + approve(cache, cred_id, c) + end +end + +""" + credentials_reject(payload::RemotePayload) -> Void + +Removes the payload credentials if they were stored to avoid automatic re-use. Should only +be called if the credential callback resulted in a unsuccessful operation. +""" +function credentials_reject(p::RemotePayload) + isnull(p.credential) && return # No credentials were used + + c = Base.get(p.credential) + cred_id = "$(isempty(p.protocol) ? "ssh" : p.protocol)://$(p.host)" + config = p.config + + if isa(c, UserPasswordCredential) + cred = GitCredential( + protocol=p.protocol, + host=p.host, + path=p.path, + username=c.username, + password=c.password, + ) + helpers = helpers!(config, cred) + reject(helpers, cred) + end + + if !isnull(p.cache) + cache = Base.get(p.cache) + reject(cache, cred_id, c) + end end function fetchhead_foreach_callback(ref_name::Cstring, remote_url::Cstring, @@ -268,3 +384,49 @@ mirror_cb() = cfunction(mirror_callback, Cint, (Ptr{Ptr{Void}}, Ptr{Void}, Cstri credentials_cb() = cfunction(credentials_callback, Cint, (Ptr{Ptr{Void}}, Cstring, Cstring, Cuint, Ptr{Void})) "C function pointer for `fetchhead_foreach_callback`" fetchhead_foreach_cb() = cfunction(fetchhead_foreach_callback, Cint, (Cstring, Cstring, Ptr{GitHash}, Cuint, Ptr{Void})) + + +function credential_loop( + valid_credential::AbstractCredential, url::AbstractString, user::AbstractString, allowed_types::UInt32, + cache::Nullable{CachedCredentials}=Nullable{CachedCredentials}(), config::Nullable{GitConfig}=Nullable{GitConfig}(), +) + cb = credentials_cb() + libgitcred_ptr_ptr = Ref{Ptr{Void}}(C_NULL) + payload_ptr = Ref(isnull(config) ? RemotePayload(cache) : RemotePayload(cache, Base.get(config))) + payload = payload_ptr[] + + # Emulate how LibGit2 uses the credential callback by repeatedly calling the function + # until we find valid credentials or an exception is raised. + num_iterations = 1 + err = Cint(0) + while err == 0 + err = ccall(cb, Cint, (Ptr{Ptr{Void}}, Cstring, Cstring, Cuint, Ptr{Void}), + libgitcred_ptr_ptr, url, isempty(user) ? C_NULL : user, allowed_types, pointer_from_objref(payload_ptr)) + + # Check if the callback provided us with valid credentials + if !isnull(payload.credential) && Base.get(payload.credential) == valid_credential + break + end + + num_iterations += 1 + if num_iterations > 20 + error("Credential callback seems to be caught in an infinite loop") + end + end + + return err +end + +function credential_loop( + valid_credential::UserPasswordCredential; + url="https://github.com/test/package.jl", user="", cache=Nullable{CachedCredentials}(), + config=Nullable{GitConfig}()) + credential_loop(valid_credential, url, user, 0x000001, cache, config) +end + +function credential_loop( + valid_credential::SSHCredential; + url="git@github.com/test/package.jl", user="git", cache=Nullable{CachedCredentials}(), + config=Nullable{GitConfig}()) + credential_loop(valid_credential, url, user, 0x000046, cache, config) +end diff --git a/base/libgit2/config.jl b/base/libgit2/config.jl index 7c01664042cc4..8dc89359cde22 100644 --- a/base/libgit2/config.jl +++ b/base/libgit2/config.jl @@ -128,3 +128,61 @@ function set!(c::GitConfig, name::AbstractString, value::Int64) @check ccall((:git_config_set_int64, :libgit2), Cint, (Ptr{Void}, Cstring, Cintmax_t), c.ptr, name, value) end + +function GitConfigIter(cfg::GitConfig) + ci_ptr = Ref{Ptr{Void}}(C_NULL) + @check ccall((:git_config_iterator_new, :libgit2), Cint, + (Ptr{Ptr{Void}}, Ptr{Void}), ci_ptr, cfg.ptr) + return GitConfigIter(ci_ptr[]) +end + +function GitConfigIter(cfg::GitConfig, name::AbstractString) + ci_ptr = Ref{Ptr{Void}}(C_NULL) + @check ccall((:git_config_multivar_iterator_new, :libgit2), Cint, + (Ptr{Ptr{Void}}, Ptr{Void}, Cstring, Cstring), + ci_ptr, cfg.ptr, name, C_NULL) + return GitConfigIter(ci_ptr[]) +end + +function GitConfigIter(cfg::GitConfig, name::AbstractString, value::Regex) + ci_ptr = Ref{Ptr{Void}}(C_NULL) + @check ccall((:git_config_multivar_iterator_new, :libgit2), Cint, + (Ptr{Ptr{Void}}, Ptr{Void}, Cstring, Cstring), + ci_ptr, cfg.ptr, name, value.pattern) + return GitConfigIter(ci_ptr[]) +end + +function GitConfigIter(cfg::GitConfig, name::Regex) + ci_ptr = Ref{Ptr{Void}}(C_NULL) + @check ccall((:git_config_iterator_glob_new, :libgit2), Cint, + (Ptr{Ptr{Void}}, Ptr{Void}, Cstring), + ci_ptr, cfg.ptr, name.pattern) + return GitConfigIter(ci_ptr[]) +end + +function Base.start(ci::GitConfigIter) + entry_ptr_ptr = Ref{Ptr{ConfigEntry}}(C_NULL) + err = ccall((:git_config_next, :libgit2), Cint, + (Ptr{Ptr{ConfigEntry}}, Ptr{Void}), entry_ptr_ptr, ci.ptr) + if err == Int(Error.GIT_OK) + unsafe_load(entry_ptr_ptr[]) + else + nothing + end +end + +Base.done(ci::GitConfigIter, state) = state === nothing + +function Base.next(ci::GitConfigIter, state) + entry = state + entry_ptr_ptr = Ref{Ptr{ConfigEntry}}(C_NULL) + err = ccall((:git_config_next, :libgit2), Cint, + (Ptr{Ptr{ConfigEntry}}, Ptr{Void}), entry_ptr_ptr, ci.ptr) + if err == Int(Error.GIT_OK) + (entry, unsafe_load(entry_ptr_ptr[])) + else + (entry, nothing) + end +end + +Base.iteratorsize(::Type{GitConfigIter}) = Base.SizeUnknown() diff --git a/base/libgit2/error.jl b/base/libgit2/error.jl index 8e91bec65821c..908b899b0da31 100644 --- a/base/libgit2/error.jl +++ b/base/libgit2/error.jl @@ -89,6 +89,10 @@ function GitError(code::Integer) return GitError(err_class, err_code, err_msg) end +function Base.:(==)(a::GitError, b::GitError) + a.class == b.class && a.code == b.code && a.msg == b.msg +end + end # Error module macro check(git_func) diff --git a/base/libgit2/gitcredential.jl b/base/libgit2/gitcredential.jl new file mode 100644 index 0000000000000..06429088d9f4d --- /dev/null +++ b/base/libgit2/gitcredential.jl @@ -0,0 +1,188 @@ +function Base.parse(::Type{GitCredentialHelper}, helper::AbstractString) + if startswith(helper, "!") + cmd_str = helper[2:end] + elseif isabspath(first(Base.shell_split(helper))) + cmd_str = helper + else + cmd_str = "git credential-$helper" + end + + GitCredentialHelper(`$(Base.shell_split(cmd_str)...)`) +end + +function run!(helper::GitCredentialHelper, operation::AbstractString, cred::GitCredential) + cmd = `$(helper.cmd) $operation` + output, input, p = readandwrite(cmd) + + # Provide the helper with the credential information we know + Base.write(input, cred) + Base.write(input, "\n") + close(input) + + # Process the response from the helper + Base.read!(output, cred) + close(output) + + return cred +end + +function run(helper::GitCredentialHelper, operation::AbstractString, cred::GitCredential) + updated_cred = deepcopy(cred) + run!(helper, operation, updated_cred) +end + +function Base.parse(::Type{GitCredential}, url::AbstractString) + # TODO: It appears that the Git internals expect the contents to be URL encoded: + # https://github.com/git/git/blob/24321375cda79f141be72d1a842e930df6f41725/credential.c#L324 + # + # Match one of: + # (1) proto:///... + # (2) proto://@/... + # (3) proto://:@/... + m = match(URL_REGEX, url) + m === nothing && error("Unable to parse URL") + host = m[:host] * (m[:port] != nothing ? ":$(m[:port])" : "") + return GitCredential( + m[:protocol], + host, + m[:path] === nothing ? "" : m[:path], + m[:user] === nothing ? "" : m[:user], + m[:password] === nothing ? "" : m[:password], + ) +end + +function merge!(a::GitCredential, b::GitCredential) + !isempty(b.protocol) && (a.protocol = b.protocol) + !isempty(b.host) && (a.host = b.host) + !isempty(b.path) && (a.path = b.path) + !isempty(b.username) && (a.username = b.username) + !isempty(b.password) && (a.password = b.password) + return a +end + +function Base.:(==)(a::GitCredential, b::GitCredential) + return ( + a.protocol == b.protocol && + a.host == b.host && + a.path == b.path && + a.username == b.username && + a.password == b.password + ) +end + +function Base.contains(haystack::GitCredential, needle::GitCredential) + field_contains(h, n) = isempty(n) || (!isempty(h) && h == n) + return ( + field_contains(haystack.protocol, needle.protocol) && + field_contains(haystack.host, needle.host) && + field_contains(haystack.path, needle.path) && + field_contains(haystack.username, needle.username) + ) +end + +function Base.write(io::IO, cred::GitCredential) + !isempty(cred.protocol) && println(io, "protocol=", cred.protocol) + !isempty(cred.host) && println(io, "host=", cred.host) + !isempty(cred.path) && println(io, "path=", cred.path) + !isempty(cred.username) && println(io, "username=", cred.username) + !isempty(cred.password) && println(io, "password=", cred.password) + nothing +end + +function Base.read!(io::IO, cred::GitCredential) + # https://git-scm.com/docs/git-credential#IOFMT + while !eof(io) + key, value = split(readline(io), '=') + + if key == "protocol" + cred.protocol = value + elseif key == "host" + cred.host = value + elseif key == "path" + cred.path = value + elseif key == "username" + cred.username = value + elseif key == "password" + cred.password = value + elseif key == "url" + merge!(cred, parse(GitCredential, value)) + end + end + + return cred +end + +Base.read(io::IO, ::Type{GitCredential}) = Base.read!(io, GitCredential()) + +""" +Git credential helpers that are relevant to the given credentials. If the credentials do not +contain a username one may be added at this step by the configuration. +""" +function helpers!(cfg::GitConfig, cred::GitCredential) + # Note: Should be quoting user input but `\Q` and `\E` isn't supported by libgit2 + # ci = LibGit2.GitConfigIter(cfg, Regex("credential(\\.$protocol://$host)?\\.helper")) + + # Note: We will emulate the way Git reads the the configuration file which is from + # top to bottom with no precedence on specificity. + helpers = GitCredentialHelper[] + for entry in GitConfigIter(cfg, r"credential.*") + name, value = unsafe_string(entry.name), unsafe_string(entry.value) + + a, b = search(name, '.'), rsearch(name, '.') + url = SubString(name, a + 1, b - 1) + token = SubString(name, b + 1) + + if !isempty(url) + !contains(cred, parse(GitCredential, url)) && continue + end + + if token == "helper" + push!(helpers, parse(GitCredentialHelper, value)) + elseif token == "username" + if isempty(cred.username) + cred.username = value + end + end + end + + return helpers +end + +function filled(cred::GitCredential) + !isempty(cred.username) && !isempty(cred.password) +end + +function fill!(helpers::AbstractArray{GitCredentialHelper}, cred::GitCredential) + filled(cred) && return cred + for helper in helpers + run!(helper, "get", cred) + filled(cred) && break + end + return cred +end + +function fill(helpers::AbstractArray{GitCredentialHelper}, cred::GitCredential) + new_cred = deepcopy(cred) + fill!(helpers, new_cred) +end + +function approve(helpers::AbstractArray{GitCredentialHelper}, cred::GitCredential) + !filled(cred) && error("Credentials are not filled") + for helper in helpers + run(helper, "store", cred) + end +end + +function reject(helpers::AbstractArray{GitCredentialHelper}, cred::GitCredential) + !filled(cred) && return + + for helper in helpers + run(helper, "erase", cred) + end + + Base.securezero!(cred.username) + Base.securezero!(cred.password) + cred.username = "" + cred.password = "" + nothing +end diff --git a/base/libgit2/libgit2.jl b/base/libgit2/libgit2.jl index 113946fb3ac02..f029bd68c1838 100644 --- a/base/libgit2/libgit2.jl +++ b/base/libgit2/libgit2.jl @@ -32,6 +32,7 @@ include("diff.jl") include("rebase.jl") include("status.jl") include("tree.jl") +include("gitcredential.jl") include("callbacks.jl") using .Error @@ -202,10 +203,6 @@ function set_remote_url(path::AbstractString, url::AbstractString; remote::Abstr end end -function make_payload(payload::Nullable{<:AbstractCredentials}) - Ref{Nullable{AbstractCredentials}}(payload) -end - """ fetch(repo::GitRepo; kwargs...) @@ -218,24 +215,31 @@ The keyword arguments are: * `remoteurl::AbstractString=""`: the URL of `remote`. If not specified, will be assumed based on the given name of `remote`. * `refspecs=AbstractString[]`: determines properties of the fetch. - * `payload=Nullable{AbstractCredentials}()`: provides credentials, if necessary, + * `credentials=Nullable{CachedCredentials}()`: provides credentials, if necessary, for instance if `remote` is a private repository. Equivalent to `git fetch [|] []`. """ -function fetch(repo::GitRepo; remote::AbstractString="origin", +function fetch(repo::GitRepo; + remote::AbstractString="origin", remoteurl::AbstractString="", refspecs::Vector{<:AbstractString}=AbstractString[], - payload::Nullable{<:AbstractCredentials}=Nullable{AbstractCredentials}()) + credentials::Nullable{CachedCredentials}=Nullable{CachedCredentials}()) rmt = if isempty(remoteurl) get(GitRemote, repo, remote) else GitRemoteAnon(repo, remoteurl) end + + payload = RemotePayload(credentials, GitConfig(repo)) try - payload = make_payload(payload) fo = FetchOptions(callbacks=RemoteCallbacks(credentials_cb(), payload)) - fetch(rmt, refspecs, msg="from $(url(rmt))", options = fo) + result = fetch(rmt, refspecs, msg="from $(url(rmt))", options = fo) + credentials_approve(payload) + return result + catch err + isa(err, GitError) && err.code == Error.EAUTH && credentials_reject(payload) + rethrow() finally close(rmt) end @@ -252,25 +256,32 @@ The keyword arguments are: * `refspecs=AbstractString[]`: determines properties of the push. * `force::Bool=false`: determines if the push will be a force push, overwriting the remote branch. - * `payload=Nullable{AbstractCredentials}()`: provides credentials, if necessary, + * `credentials=Nullable{CachedCredentials}()`: provides credentials, if necessary, for instance if `remote` is a private repository. Equivalent to `git push [|] []`. """ -function push(repo::GitRepo; remote::AbstractString="origin", +function push(repo::GitRepo; + remote::AbstractString="origin", remoteurl::AbstractString="", refspecs::Vector{<:AbstractString}=AbstractString[], force::Bool=false, - payload::Nullable{<:AbstractCredentials}=Nullable{AbstractCredentials}()) + credentials::Nullable{CachedCredentials}=Nullable{CachedCredentials}()) rmt = if isempty(remoteurl) get(GitRemote, repo, remote) else GitRemoteAnon(repo, remoteurl) end + + payload = RemotePayload(credentials, GitConfig(repo)) try - payload = make_payload(payload) push_opts=PushOptions(callbacks=RemoteCallbacks(credentials_cb(), payload)) - push(rmt, refspecs, force=force, options=push_opts) + result = push(rmt, refspecs, force=force, options=push_opts) + credentials_approve(payload) + return result + catch err + isa(err, GitError) && err.code == Error.EAUTH && credentials_reject(payload) + rethrow() finally close(rmt) end @@ -429,7 +440,7 @@ The keyword arguments are: * `remote_cb::Ptr{Void}=C_NULL`: a callback which will be used to create the remote before it is cloned. If `C_NULL` (the default), no attempt will be made to create the remote - it will be assumed to already exist. - * `payload::Nullable{P<:AbstractCredentials}=Nullable{AbstractCredentials}()`: + * `credentials::Nullable{CachedCredentials}=Nullable{CachedCredentials}()`: provides credentials if necessary, for instance if the remote is a private repository. @@ -439,10 +450,10 @@ function clone(repo_url::AbstractString, repo_path::AbstractString; branch::AbstractString="", isbare::Bool = false, remote_cb::Ptr{Void} = C_NULL, - payload::Nullable{<:AbstractCredentials}=Nullable{AbstractCredentials}()) + credentials::Nullable{CachedCredentials}=Nullable{CachedCredentials}()) # setup clone options lbranch = Base.cconvert(Cstring, branch) - payload = make_payload(payload) + payload = RemotePayload(credentials) fetch_opts=FetchOptions(callbacks = RemoteCallbacks(credentials_cb(), payload)) clone_opts = CloneOptions( bare = Cint(isbare), @@ -450,7 +461,14 @@ function clone(repo_url::AbstractString, repo_path::AbstractString; fetch_opts=fetch_opts, remote_cb = remote_cb ) - return clone(repo_url, repo_path, clone_opts) + try + repo = clone(repo_url, repo_path, clone_opts) + credentials_approve(payload) + return repo + catch err + isa(err, GitError) && err.code == Error.EAUTH && credentials_reject(payload) + rethrow() + end end """ git reset [] [--] ... """ diff --git a/base/libgit2/types.jl b/base/libgit2/types.jl index 10423af8e0952..1ab1094c13634 100644 --- a/base/libgit2/types.jl +++ b/base/libgit2/types.jl @@ -1,6 +1,6 @@ # This file is a part of Julia. License is MIT: http://julialang.org/license import Base.@kwdef -import .Consts: GIT_SUBMODULE_IGNORE, GIT_MERGE_FILE_FAVOR, GIT_MERGE_FILE +import .Consts: GIT_SUBMODULE_IGNORE, GIT_MERGE_FILE_FAVOR, GIT_MERGE_FILE, GIT_CONFIG const OID_RAWSZ = 20 const OID_HEXSZ = OID_RAWSZ * 2 @@ -117,15 +117,6 @@ function free(buf_ref::Base.Ref{Buffer}) ccall((:git_buf_free, :libgit2), Void, (Ptr{Buffer},), buf_ref) end -"Abstract credentials payload" -abstract type AbstractCredentials end - -"Checks if credentials were used" -checkused!(p::AbstractCredentials) = true -checkused!(p::Void) = false -"Resets credentials for another use" -reset!(p::AbstractCredentials, cnt::Int=3) = nothing - """ LibGit2.CheckoutOptions @@ -184,10 +175,6 @@ Matches the [`git_remote_callbacks`](https://libgit2.github.com/libgit2/#HEAD/ty payload::Ptr{Void} end -function RemoteCallbacks(credentials::Ptr{Void}, payload::Ref{Nullable{AbstractCredentials}}) - RemoteCallbacks(credentials=credentials_cb(), payload=pointer_from_objref(payload)) -end - """ LibGit2.ProxyOptions @@ -449,6 +436,23 @@ struct FetchHead ismerge::Bool end +""" + LibGit2.ConfigEntry + +Matches the [`git_config_entry`](https://libgit2.github.com/libgit2/#HEAD/type/git_config_entry) struct. +""" +@kwdef struct ConfigEntry + name::Cstring + value::Cstring + level::GIT_CONFIG = Consts.CONFIG_LEVEL_DEFAULT + free::Ptr{Void} + payload::Ptr{Void} +end + +function Base.show(io::IO, ce::ConfigEntry) + print(io, "ConfigEntry(\"", unsafe_string(ce.name), "\", \"", unsafe_string(ce.value), "\")") +end + # Abstract object types abstract type AbstractGitObject end Base.isempty(obj::AbstractGitObject) = (obj.ptr == C_NULL) @@ -472,7 +476,8 @@ for (typ, reporef, sup, cname) in [ (:GitCommit, :GitRepo, :GitObject, :git_commit), (:GitBlob, :GitRepo, :GitObject, :git_blob), (:GitTree, :GitRepo, :GitObject, :git_tree), - (:GitTag, :GitRepo, :GitObject, :git_tag)] + (:GitTag, :GitRepo, :GitObject, :git_tag), + (:GitConfigIter, nothing, :AbstractGitObject, :git_config_iterator)] if reporef === nothing @eval mutable struct $typ <: $sup @@ -628,79 +633,76 @@ end import Base.securezero! +abstract type AbstractCredential end + "Credentials that support only `user` and `password` parameters" -mutable struct UserPasswordCredentials <: AbstractCredentials - user::String - pass::String - prompt_if_incorrect::Bool # Whether to allow interactive prompting if the credentials are incorrect - count::Int # authentication failure protection count - function UserPasswordCredentials(u::AbstractString,p::AbstractString,prompt_if_incorrect::Bool=false) - c = new(u,p,prompt_if_incorrect,3) +mutable struct UserPasswordCredential <: AbstractCredential + username::String + password::String + function UserPasswordCredential(username::AbstractString="", password::AbstractString="") + c = new(username, password) finalizer(c, securezero!) return c end - UserPasswordCredentials(prompt_if_incorrect::Bool=false) = UserPasswordCredentials("","",prompt_if_incorrect) end -function securezero!(cred::UserPasswordCredentials) - securezero!(cred.user) - securezero!(cred.pass) - cred.count = 0 +function isfilled(cred::UserPasswordCredential) + !isempty(cred.username) && !isempty(cred.password) +end + +function securezero!(cred::UserPasswordCredential) + securezero!(cred.username) + securezero!(cred.password) return cred end -"SSH credentials type" -mutable struct SSHCredentials <: AbstractCredentials - user::String - pass::String - pubkey::String - prvkey::String - usesshagent::String # used for ssh-agent authentication - prompt_if_incorrect::Bool # Whether to allow interactive prompting if the credentials are incorrect - count::Int +function Base.:(==)(a::UserPasswordCredential, b::UserPasswordCredential) + a.username == b.username && a.password == b.password +end - function SSHCredentials(u::AbstractString,p::AbstractString,prompt_if_incorrect::Bool=false) - c = new(u,p,"","","Y",prompt_if_incorrect,3) +mutable struct SSHCredential <: AbstractCredential + username::String + passphrase::String + private_key::String + public_key::String + function SSHCredential( + username::AbstractString="", passphrase::AbstractString="", + private_key::AbstractString="", public_key::AbstractString="") + c = new(username, passphrase, private_key, public_key) finalizer(c, securezero!) return c end - SSHCredentials(prompt_if_incorrect::Bool=false) = SSHCredentials("","",prompt_if_incorrect) -end -function securezero!(cred::SSHCredentials) - securezero!(cred.user) - securezero!(cred.pass) - securezero!(cred.pubkey) - securezero!(cred.prvkey) - cred.count = 0 +end + +function isfilled(c::SSHCredential) + !isempty(c.username) && isfile(c.private_key) && isfile(c.public_key) && + (!isempty(c.passphrase) || !require_passphrase(c.private_key)) +end + +function securezero!(cred::SSHCredential) + securezero!(cred.username) + securezero!(cred.passphrase) + securezero!(cred.private_key) + securezero!(cred.public_key) return cred end +function Base.:(==)(a::SSHCredential, b::SSHCredential) + return ( + a.username == b.username && + a.passphrase == b.passphrase && + a.private_key == b.private_key && + a.public_key == b.public_key + ) +end + "Credentials that support caching" -mutable struct CachedCredentials <: AbstractCredentials - cred::Dict{String,AbstractCredentials} - count::Int # authentication failure protection count - CachedCredentials() = new(Dict{String,AbstractCredentials}(),3) -end - -"Checks if credentials were used or failed authentication, see `LibGit2.credentials_callback`" -function checkused!(p::Union{UserPasswordCredentials, SSHCredentials}) - p.count <= 0 && return true - p.count -= 1 - return false -end -reset!(p::Union{UserPasswordCredentials, SSHCredentials}, cnt::Int=3) = (p.count = cnt; p) -reset!(p::CachedCredentials) = (foreach(reset!, values(p.cred)); p) - -"Obtain the cached credentials for the given host+protocol (credid), or return and store the default if not found" -get_creds!(collection::CachedCredentials, credid, default) = get!(collection.cred, credid, default) -get_creds!(creds::AbstractCredentials, credid, default) = creds -get_creds!(creds::Void, credid, default) = default -function get_creds!(creds::Ref{Nullable{AbstractCredentials}}, credid, default) - if isnull(creds[]) - creds[] = Nullable{AbstractCredentials}(default) - return default - else - get_creds!(Base.get(creds[]), credid, default) +mutable struct CachedCredentials + cred::Dict{String,AbstractCredential} + function CachedCredentials() + c = new(Dict{String,AbstractCredential}()) + finalizer(c, securezero!) + return c end end @@ -708,3 +710,77 @@ function securezero!(p::CachedCredentials) foreach(securezero!, values(p.cred)) return p end + +function get_cred{C<:AbstractCredential}(cache::CachedCredentials, id::AbstractString, ::Type{C}) + default = C() + credential = Base.get(cache.cred, id, default) + return isa(credential, C) ? credential : default +end + +function approve(cache::CachedCredentials, cred_id, cred::AbstractCredential) + cache.cred[cred_id] = cred + nothing +end + +function reject(cache::CachedCredentials, cred_id) + if haskey(cache.cred, credid) + securezero!(cache.cred[cred_id]) + delete!(cache.cred, credid) + end + nothing +end + +struct GitCredentialHelper + cmd::Cmd +end + +""" + GitCredential + +Git credential helpers [input/output fields](https://git-scm.com/docs/git-credential#IOFMT). +""" +mutable struct GitCredential + protocol::String + host::String + path::String + username::String + password::String +end + +function GitCredential(; + protocol::AbstractString="", host::AbstractString="", path::AbstractString="", + username::AbstractString="", password::AbstractString="", +) + GitCredential(protocol, host, path, username, password) +end + +mutable struct RemotePayload + cache::Nullable{CachedCredentials} + config::GitConfig + state::Dict{Symbol,Char} + prompts_remaining::Int + credential::Nullable{AbstractCredential} + protocol::String + host::String + path::String + username::String +end + +function RemotePayload(cache::Nullable{CachedCredentials}, config::GitConfig=GitConfig()) + RemotePayload( + cache, + config, + Dict{Symbol,Char}(), + 3, + Nullable{AbstractCredential}(), + "", "", "", "", + ) +end + +function RemoteCallbacks(credentials::Ptr{Void}, payload::Ref{RemotePayload}) + RemoteCallbacks(credentials=credentials, payload=pointer_from_objref(payload)) +end + +function RemoteCallbacks(credentials::Ptr{Void}, payload::RemotePayload) + RemoteCallbacks(credentials, Ref{RemotePayload}(payload)) +end diff --git a/base/libgit2/utils.jl b/base/libgit2/utils.jl index 31ccfbf6ff93e..d9a6d44259e34 100644 --- a/base/libgit2/utils.jl +++ b/base/libgit2/utils.jl @@ -1,13 +1,33 @@ # This file is a part of Julia. License is MIT: http://julialang.org/license -const URL_REGEX = r""" -^(?:(?https?|git|ssh)\:\/\/)? -(?:(?.*?)(?:\:(?.*?))?@)? +# Parse GIT URLs and scp-like syntax. +# https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a +const GIT_URL_REGEX = r""" +^(?:(?ssh|git|https?)://)? +(?: + (?.*?) + (?:\:(?.*?))?@ +)? (?[A-Za-z0-9\-\.]+) -(?:\:(?\d+)?)? +(?() + (?:\:(?\d+))? # only parse port when not using SCP-like syntax + | + :? +) (?.*?)$ """x +const URL_REGEX = r""" +^(?:(?.*?)://)? +(?: + (?.*?) + (?:\:(?.*?))?@ +)? +(?[A-Za-z0-9\-\.]+) +(?:\:(?\d+))? +(?/.*)?$ +"""x + function version() major = Ref{Cint}(0) minor = Ref{Cint}(0) @@ -23,17 +43,23 @@ reset(val::Integer, flag::Integer) = (val &= ~flag) toggle(val::Integer, flag::Integer) = (val |= flag) function prompt(msg::AbstractString; default::AbstractString="", password::Bool=false) - if is_windows() && password - error("Command line prompt not supported for password entry on windows. Use winprompt instead") + @static if is_windows() + if password + error("Command line prompt not supported for password entry on windows. Use winprompt instead") + end end msg = !isempty(default) ? msg*" [$default]:" : msg*":" uinput = if password - Base.getpass(msg) + Base.getpass(msg) # Automatically chomps. We cannot tell EOF from '\n'. else print(msg) - readline() + readline(chomp=false) + end + if !password + isempty(uinput) && return Nullable{String}() # Encountered EOF + uinput = chomp(uinput) end - isempty(uinput) ? default : uinput + Nullable{String}(isempty(uinput) ? default : uinput) end function features() @@ -56,3 +82,35 @@ if is_windows() else is_unix() posixpath(path) = path end + +function git_url(; + protocol::AbstractString="", + username::AbstractString="", + password::AbstractString="", + host::AbstractString="", + port::Union{AbstractString,Integer}="", + path::AbstractString="", +) + port_str = string(port) + scp_syntax = isempty(protocol) + + isempty(host) && error("A host needs to be specified") + scp_syntax && !isempty(port_str) && error("Port cannot be specified when using scp-like syntax") + + io = IOBuffer() + if !isempty(protocol) + print(io, protocol, "://") + end + + if !isempty(username) || !isempty(password) + print(io, username) + !isempty(password) && print(io, ':', password) + print(io, '@') + end + + print(io, host) + !isempty(port) && print(io, ':', port) + print(io, scp_syntax && !isempty(path) ? ":" : "", path) + + return readstring(seekstart(io)) +end diff --git a/base/pkg/entry.jl b/base/pkg/entry.jl index 0d53268f96d5a..eb0e453db67c3 100644 --- a/base/pkg/entry.jl +++ b/base/pkg/entry.jl @@ -427,7 +427,7 @@ function update(branch::AbstractString, upkgs::Set{String}) prev_sha = string(LibGit2.head_oid(repo)) success = true try - LibGit2.fetch(repo, payload = Nullable(creds)) + LibGit2.fetch(repo, credentials=Nullable(creds)) LibGit2.reset!(creds) LibGit2.merge!(repo, fastforward=true) catch err diff --git a/test/TestHelpers.jl b/test/TestHelpers.jl index c7393ba0f09a8..7e9774047676b 100644 --- a/test/TestHelpers.jl +++ b/test/TestHelpers.jl @@ -41,9 +41,64 @@ 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) + try + f(slave, master) + finally + ccall(:close,Cint,(Cint,),slave) # XXX: this causes the kernel to throw away all unread data on the pty + close(master) + end +end + +function challenge_prompt(code::AbstractString, challenges; timeout::Integer=10, debug::Bool=true) + output_file = tempname() + wrapped_code = """ + open("$output_file", "w") do fp + serialize(fp, begin $code end) + end + """ + cmd = `$(Base.julia_cmd()) --startup-file=no -e $wrapped_code` + try + challenge_prompt(cmd, challenges, timeout=timeout, debug=debug) + return open(output_file, "r") do fp + deserialize(fp) + end + finally + isfile(output_file) && rm(output_file) + end + return nothing +end + +function challenge_prompt(cmd::Cmd, challenges; timeout::Integer=10, debug::Bool=true) + function format_output(output) + debug ? "Process output found:\n\"\"\"\n$(readstring(seekstart(out)))\n\"\"\"" : "" + end + out = IOBuffer() + with_fake_pty() do slave, master + p = spawn(detach(cmd), slave, slave, slave) + @async begin + sleep(timeout) + kill(p) + close(master) + end + try + for (challenge, response) in challenges + process_exited(p) && error("Too few prompts. $(format_output(out))") + + write(out, readuntil(master, challenge)) + if !isopen(master) + error("Could not locate challenge: \"$challenge\". $(format_output(out))") + end + write(master, response) + end + wait(p) + finally + kill(p) + end + # Determine if the process was explicitly killed + killed = process_exited(p) && (p.exitcode != 0 || p.termsignal != 0) + killed && error("Too many prompts. $(format_output(out))") + end + nothing end # OffsetArrays (arrays with indexing that doesn't start at 1) diff --git a/test/libgit2-online.jl b/test/libgit2-online.jl index 590555a8958c9..64fabfcbda112 100644 --- a/test/libgit2-online.jl +++ b/test/libgit2-online.jl @@ -19,19 +19,6 @@ mktempdir() do dir close(repo) end end - - @testset "with incorrect url" begin - try - repo_path = joinpath(dir, "Example2") - # credentials are required because github tries to authenticate on unknown repo - cred = LibGit2.UserPasswordCredentials("","") # empty credentials cause authentication error - LibGit2.clone(repo_url*randstring(10), repo_path, payload=Nullable(cred)) - error("unexpected") - catch ex - @test isa(ex, LibGit2.Error.GitError) - @test ex.code == LibGit2.Error.EAUTH - end - end end end diff --git a/test/libgit2.jl b/test/libgit2.jl index 43a677fe800f4..d1eccd4f7b0f4 100644 --- a/test/libgit2.jl +++ b/test/libgit2.jl @@ -1,7 +1,8 @@ # This file is a part of Julia. License is MIT: http://julialang.org/license isdefined(Main, :TestHelpers) || @eval Main include(joinpath(dirname(@__FILE__), "TestHelpers.jl")) -using TestHelpers +import TestHelpers: challenge_prompt +import Base.Iterators: repeated const LIBGIT2_MIN_VER = v"0.23.0" @@ -57,56 +58,151 @@ end @test sig3.email == sig.email end -#@testset "URL parsing" begin - # HTTPS URL - m = match(LibGit2.URL_REGEX, "https://user:pass@server.com:80/org/project.git") - @test m[:scheme] == "https" - @test m[:user] == "user" - @test m[:password] == "pass" - @test m[:host] == "server.com" - @test m[:port] == "80" - @test m[:path] == "/org/project.git" - - # SSH URL - m = match(LibGit2.URL_REGEX, "ssh://user:pass@server:22/project.git") - @test m[:scheme] == "ssh" - @test m[:user] == "user" - @test m[:password] == "pass" - @test m[:host] == "server" - @test m[:port] == "22" - @test m[:path] == "/project.git" - - # SSH URL using scp-like syntax - m = match(LibGit2.URL_REGEX, "user@server:project.git") - @test m[:scheme] === nothing - @test m[:user] == "user" - @test m[:password] === nothing - @test m[:host] == "server" - @test m[:port] === nothing - @test m[:path] == "project.git" +@testset "Git URL parsing" begin + @testset "HTTPS URL" begin + m = match(LibGit2.GIT_URL_REGEX, "https://user:pass@server.com:80/org/project.git") + @test m[:protocol] == "https" + @test m[:user] == "user" + @test m[:password] == "pass" + @test m[:host] == "server.com" + @test m[:port] == "80" + @test m[:path] == "/org/project.git" + end + + @testset "SSH URL" begin + m = match(LibGit2.GIT_URL_REGEX, "ssh://user:pass@server:22/project.git") + @test m[:protocol] == "ssh" + @test m[:user] == "user" + @test m[:password] == "pass" + @test m[:host] == "server" + @test m[:port] == "22" + @test m[:path] == "/project.git" + end + + @testset "scp-like syntax" begin + m = match(LibGit2.GIT_URL_REGEX, "user@server:project.git") + @test m[:protocol] === nothing + @test m[:user] == "user" + @test m[:password] == nothing + @test m[:host] == "server" + @test m[:port] === nothing + @test m[:path] == "project.git" + end + + # scp-like syntax corner case. The SCP syntax does not support port so everything after + # the colon is part of the path. + @testset "scp-like syntax, no port" begin + m = match(LibGit2.GIT_URL_REGEX, "server:1234/repo") + @test m[:protocol] === nothing + @test m[:user] == nothing + @test m[:password] == nothing + @test m[:host] == "server" + @test m[:port] === nothing + @test m[:path] == "1234/repo" + end # Realistic example from GitHub using HTTPS - m = match(LibGit2.URL_REGEX, "https://github.com/JuliaLang/Example.jl.git") - @test m[:scheme] == "https" - @test m[:user] === nothing - @test m[:password] === nothing - @test m[:host] == "github.com" - @test m[:port] === nothing - @test m[:path] == "/JuliaLang/Example.jl.git" + @testset "HTTPS URL GitHub" begin + m = match(LibGit2.GIT_URL_REGEX, "https://github.com/JuliaLang/Example.jl.git") + @test m[:protocol] == "https" + @test m[:user] === nothing + @test m[:password] == nothing + @test m[:host] == "github.com" + @test m[:port] === nothing + @test m[:path] == "/JuliaLang/Example.jl.git" + end # Realistic example from GitHub using SSH - m = match(LibGit2.URL_REGEX, "git@github.com:JuliaLang/Example.jl.git") - @test m[:scheme] === nothing - @test m[:user] == "git" - @test m[:password] === nothing - @test m[:host] == "github.com" - @test m[:port] === nothing - @test m[:path] == "JuliaLang/Example.jl.git" + @testset "SSH URL GitHub" begin + m = match(LibGit2.GIT_URL_REGEX, "git@github.com:JuliaLang/Example.jl.git") + @test m[:protocol] === nothing + @test m[:user] == "git" + @test m[:password] == nothing + @test m[:host] == "github.com" + @test m[:port] === nothing + @test m[:path] == "JuliaLang/Example.jl.git" + end # Make sure usernames can contain special characters - m = match(LibGit2.URL_REGEX, "user-name@hostname.com") - @test m[:user] == "user-name" -#end + @testset "username special charaters" begin + m = match(LibGit2.GIT_URL_REGEX, "user-name@hostname.com:path") + @test m[:user] == "user-name" + end + + @testset "HTTPS URL, no path" begin + m = match(LibGit2.GIT_URL_REGEX, "https://user:pass@server.com:80") + @test m[:path] == "" + end + + @testset "scp-like syntax, no path" begin + m = match(LibGit2.GIT_URL_REGEX, "user@server:") + @test m[:path] == "" + + m = match(LibGit2.GIT_URL_REGEX, "user@server") + @test m[:path] == "" + end +end + +@testset "Git Credential" begin + @testset "empty" begin + str = "" + cred = read(IOBuffer(str), LibGit2.GitCredential) + @test cred == LibGit2.GitCredential() + buf = IOBuffer(); write(buf, cred) + @test readstring(seekstart(buf)) == str + end + + @testset "basic" begin + str = """ + protocol=https + host=example.com + username=alice + password=xxxxx + """ + cred = read(IOBuffer(str), LibGit2.GitCredential) + @test cred == LibGit2.GitCredential("https", "example.com", "", "alice", "xxxxx") + buf = IOBuffer(); write(buf, cred) + @test readstring(seekstart(buf)) == str + end + + @testset "url field" begin + str = """ + host=example.com + password=bar + url=https://a@b/c + username=foo + """ + expected = """ + protocol=https + host=b + path=/c + username=foo + password=bar + """ + cred = read(IOBuffer(str), LibGit2.GitCredential) + @test cred == LibGit2.GitCredential("https", "b", "/c", "foo", "bar") + buf = IOBuffer(); write(buf, cred) + @test readstring(seekstart(buf)) == expected + end + + @testset "contains" begin + @test contains( + LibGit2.GitCredential(), + LibGit2.GitCredential()) + @test contains( + LibGit2.GitCredential("https", "", "", "", ""), + LibGit2.GitCredential("", "", "", "", "")) + @test !contains( + LibGit2.GitCredential("", "", "", "", ""), + LibGit2.GitCredential("https", "", "", "", "")) + @test !contains( + LibGit2.GitCredential("ssh", "", "", "", ""), + LibGit2.GitCredential("https", "", "", "", "")) + @test contains( + LibGit2.GitCredential("https", "hostname", "", "user", ""), + LibGit2.GitCredential("https", "hostname", "", "", "")) + end +end mktempdir() do dir # test parameters @@ -141,6 +237,56 @@ mktempdir() do dir @test LibGit2.get(cfg, "tmp.int32", Int32(0)) == Int32(1) @test LibGit2.get(cfg, "tmp.int64", Int64(0)) == Int64(1) @test LibGit2.get(cfg, "tmp.bool", false) == true + + # Ordering of entries appears random when using `LibGit2.set!` + count = 0 + for entry in LibGit2.GitConfigIter(cfg, r"tmp.*") + count += 1 + name, value = unsafe_string(entry.name), unsafe_string(entry.value) + if name == "tmp.str" + @test value == "AAAA" + elseif name == "tmp.int32" + @test value == "1" + elseif name == "tmp.int64" + @test value == "1" + elseif name == "tmp.bool" + @test value == "true" + else + error("Found unexpected entry: $name") + end + end + @test count == 4 + finally + close(cfg) + end + end + + @testset "Configuration Iteration" begin + config_path = joinpath(dir, config_file) + + # Write config entries with duplicate names + open(config_path, "a") do fp + write(fp, """ + [credential] + \thelper = store + [credential] + \thelper = cache + """) + end + + cfg = LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP) + try + # Will only see the last entry + @test LibGit2.get(cfg, "credential.helper", "") == "cache" + + count = 0 + for entry in LibGit2.GitConfigIter(cfg, "credential.helper") + count += 1 + name, value = unsafe_string(entry.name), unsafe_string(entry.value) + @test name == "credential.helper" + @test value == (count == 1 ? "store" : "cache") + end + @test count == 2 finally close(cfg) end @@ -796,16 +942,323 @@ mktempdir() do dir end end - @testset "Credentials" begin - creds_user = "USER" - creds_pass = "PASS" - creds = LibGit2.UserPasswordCredentials(creds_user, creds_pass) - @test !LibGit2.checkused!(creds) - @test !LibGit2.checkused!(creds) - @test !LibGit2.checkused!(creds) - @test LibGit2.checkused!(creds) - @test creds.user == creds_user - @test creds.pass == creds_pass + @testset "SSH credentials" begin + KEY_DIR = joinpath(dirname(@__FILE__), "libgit2") + valid_key = joinpath(KEY_DIR, "valid") + invalid_key = joinpath(KEY_DIR, "invalid") + valid_p_key = joinpath(KEY_DIR, "valid-passphrase") + passphrase = "secret" + + abort_prompt = LibGit2.GitError(LibGit2.Error.Callback, LibGit2.Error.EAUTH, "Aborting, user cancelled credential request.") + max_prompts = LibGit2.GitError(LibGit2.Error.Callback, LibGit2.Error.EAUTH, "Aborting, maximum number of user prompts reached.") + + ssh_cmd = """ + valid_cred = LibGit2.SSHCredential("git", "", "$valid_key", "$valid_key.pub") + err = LibGit2.credential_loop(valid_cred) + err < 0 ? LibGit2.GitError(err) : err + """ + + ssh_p_cmd = """ + valid_cred = LibGit2.SSHCredential("git", "$passphrase", "$valid_p_key", "$valid_p_key.pub") + err = LibGit2.credential_loop(valid_cred) + err < 0 ? LibGit2.GitError(err) : err + """ + + # Note: We cannot use the default ~/.ssh/id_rsa for tests since we cannot be sure + # a users will actually have these files. Instead we will use the ENV variables to + # set the default values. + + # Default credentials are valid + withenv("SSH_KEY_PATH" => valid_key) do + @test challenge_prompt(ssh_cmd, []) == 0 + end + + # Default credentials are valid but requires a passphrase + withenv("SSH_KEY_PATH" => valid_p_key) do + challenges = [ + "Passphrase for $valid_p_key:" => "$passphrase\n", + ] + @test challenge_prompt(ssh_p_cmd, challenges) == 0 + + # User mistypes passphrase. + # Note: In reality LibGit2 will raise an error upon using the invalid + # SSH credentials. Since we don't control the internals of LibGit2 though they + # could also just re-call the credential callback like they do for HTTP. + challenges = [ + "Passphrase for $valid_p_key:" => "foo\n", + "Private key location for 'git@github.com' [$valid_p_key]:" => "\n", + "Passphrase for $valid_p_key:" => "$passphrase\n", + ] + @test challenge_prompt(ssh_p_cmd, challenges) == 0 + end + + withenv("SSH_KEY_PATH" => valid_p_key, "SSH_KEY_PASS" => passphrase) do + @test challenge_prompt(ssh_p_cmd, []) == 0 + end + + # Explicitly setting these env variables to be empty means the user will be given + # a prompt with no defaults set. + withenv("SSH_KEY_PATH" => "", "SSH_PUB_KEY_PATH" => "") do + # User provides valid credentials + challenges = [ + "Private key location for 'git@github.com':" => "$valid_key\n", + ] + @test challenge_prompt(ssh_cmd, challenges) == 0 + + # User provides valid credentials that requires a passphrase + challenges = [ + "Private key location for 'git@github.com':" => "$valid_p_key\n", + "Passphrase for $valid_p_key:" => "$passphrase\n", + ] + @test challenge_prompt(ssh_p_cmd, challenges) == 0 + + # User provides an invalid private key until prompt limit reached + challenges = [ + "Private key location for 'git@github.com':" => "foo\n", + ] + @test challenge_prompt(ssh_cmd, repmat(challenges, 3)) == max_prompts + + # User sends EOF in private key prompt which aborts the credential request + challenges = [ + "Private key location for 'git@github.com':" => "\x04", + ] + @test challenge_prompt(ssh_cmd, challenges) == abort_prompt + + # User sends EOF in passphrase prompt which aborts the credential request + challenges = [ + "Private key location for 'git@github.com':" => "$valid_p_key\n", + "Passphrase for $valid_p_key:" => "\x04", + ] + @test challenge_prompt(ssh_p_cmd, challenges) == abort_prompt + end + + # Explicitly setting these env variables to an existing but invalid key pair means + # the user will be given a prompt with that defaults to the specified values. + withenv("SSH_KEY_PATH" => invalid_key, "SSH_PUB_KEY_PATH" => invalid_key * ".pub") do + challenges = [ + "Private key location for 'git@github.com' [$invalid_key]:" => "$valid_key\n", + ] + @test challenge_prompt(ssh_cmd, challenges) == 0 + + # User repeatedly chooses the default invalid private key until prompt limit reached + # challenge = "Private key location for 'git@github.com' [$invalid_key]:" => "\n" + challenges = [ + "Private key location for 'git@github.com' [$invalid_key]:" => "\n", + ] + @test challenge_prompt(ssh_cmd, repmat(challenges, 3)) == max_prompts + end + + # withenv("SSH_KEY_PATH" => invalid_key) do + # challenges = [ + # "Private key location for 'git@github.com' [$invalid_key]:" => "\n", + # "Public key location for 'git@github.com':" => "$valid_key.pub\n" + # ] + # end + + withenv("SSH_KEY_PATH" => valid_key, "SSH_PUB_KEY_PATH" => valid_key * ".public") do + @test !isfile(ENV["SSH_PUB_KEY_PATH"]) + + # User explicitly sets the SSH_PUB_KEY_PATH incorrectly. + challenges = [ + "Private key location for 'git@github.com' [$valid_key]:" => "\n" + "Public key location for 'git@github.com':" => "$valid_key.pub\n" + ] + @test challenge_prompt(ssh_cmd, challenges) == 0 + + # User provides an empty public key which aborts the credential request + challenges = [ + "Private key location for 'git@github.com' [$valid_key]:" => "\n" + "Public key location for 'git@github.com':" => "\x04" + ] + @test challenge_prompt(ssh_cmd, challenges) == abort_prompt + end + end + + @testset "SSH credential cache" begin + KEY_DIR = joinpath(dirname(@__FILE__), "libgit2") + valid_key = joinpath(KEY_DIR, "valid") + invalid_key = joinpath(KEY_DIR, "invalid") + valid_p_key = joinpath(KEY_DIR, "valid-passphrase") + passphrase = "secret" + + ssh_cmd = """ + valid_cred = LibGit2.SSHCredential("git", "", "$valid_key", "$valid_key.pub") + cache = LibGit2.CachedCredentials() + LibGit2.approve(cache, "ssh://github.com", valid_cred) + err = LibGit2.credential_loop(valid_cred, cache=Nullable(cache)) + err < 0 ? LibGit2.GitError(err) : err + """ + + ssh_p_cmd = """ + valid_cred = LibGit2.SSHCredential("git", "$passphrase", "$valid_p_key", "$valid_p_key.pub") + cache = LibGit2.CachedCredentials() + LibGit2.approve(cache, "ssh://github.com", valid_cred) + err = LibGit2.credential_loop(valid_cred, cache=Nullable(cache)) + err < 0 ? LibGit2.GitError(err) : err + """ + + # Credential cache provides correct SSH credentials with no prompts + @test challenge_prompt(ssh_cmd, []) == 0 + + # Credential cache provides correct SSH credentials including passphrase with no prompts + @test challenge_prompt(ssh_p_cmd, []) == 0 + end + + @testset "HTTPS credential prompt" begin + valid_username = "julia" + valid_password = randstring(16) + + abort_prompt = LibGit2.GitError(LibGit2.Error.Callback, LibGit2.Error.EAUTH, "Aborting, user cancelled credential request.") + max_prompts = LibGit2.GitError(LibGit2.Error.Callback, LibGit2.Error.EAUTH, "Aborting, maximum number of user prompts reached.") + + https_cmd = """ + valid_cred = LibGit2.UserPasswordCredential("$valid_username", "$valid_password") + # Use of config which contains no git credential helpers + err = mktemp("$dir") do config_path, fp + cfg = LibGit2.GitConfig(config_path) + @assert isempty(LibGit2.helpers!(cfg, LibGit2.GitCredential())) + LibGit2.credential_loop(valid_cred, config=Nullable(cfg)) + end + err < 0 ? LibGit2.GitError(err) : err + """ + + # User provides a valid username and password + challenges = [ + "Username for 'https://github.com':" => "$valid_username\n", + "Password for 'https://$valid_username@github.com':" => "$valid_password\n", + ] + @test challenge_prompt(https_cmd, challenges) == 0 + + # User sends EOF in username prompt which aborts the credential request + challenges = [ + "Username for 'https://github.com':" => "\x04", + ] + @test challenge_prompt(https_cmd, challenges) == abort_prompt + + # User provides an empty username which triggers a re-prompt + challenges = [ + "Username for 'https://github.com':" => "\n", + ] + @test challenge_prompt(https_cmd, repmat(challenges, 3)) == max_prompts + + # User sends EOF in password prompt which aborts the credential request + challenges = [ + "Username for 'https://github.com':" => "foo\n", + "Password for 'https://foo@github.com':" => "\x04", + ] + @test challenge_prompt(https_cmd, challenges) == abort_prompt + + # User provides an empty password which aborts the credential request since we + # cannot tell it apart from an EOF. + challenges = [ + "Username for 'https://github.com':" => "foo\n", + "Password for 'https://foo@github.com':" => "\n", + ] + @test challenge_prompt(https_cmd, challenges) == abort_prompt + + # User repeatedly chooses invalid username/password until the prompt limit is reached + challenges = [ + "Username for 'https://github.com'" => "foo\n", + "Password for 'https://foo@github.com':" => "bar\n", + ] + @test challenge_prompt(https_cmd, repmat(challenges, 3)) == max_prompts + + # User repeatedly chooses invalid username/password until the prompt limit is reached + a = "Username for 'https://github.com':" => "foo\n" + b = "Password for 'https://foo@github.com':" => "bar\n" + c = "Username for 'https://github.com' [foo]:" => "\n" + challenges = [a, b, c, b, c, b] + @test challenge_prompt(https_cmd, challenges) == max_prompts + end + + @testset "HTTPS git credential helper" begin + valid_username = "julia" + valid_password = randstring(16) + + abort_prompt = LibGit2.GitError(LibGit2.Error.Callback, LibGit2.Error.EAUTH, "Aborting, user cancelled credential request.") + + # Setup a cache git credential helper. + config_path = joinpath(dir, "gitconfig") + @assert !isfile(config_path) + touch(config_path) + + # Make sure to use a different socket so we do not add random credentials into the + # user's cache. + socket_path = joinpath(dir, "git-credential-cache", "socket") + @assert !isfile(socket_path) + @assert !isdir(dirname(socket_path)) + mkdir(dirname(socket_path), 0o700) + + cfg = LibGit2.GitConfig(config_path) + LibGit2.set!(cfg, "credential.helper", "cache --socket $socket_path") + + # Add the fake credentials into the cache + git_cred = LibGit2.GitCredential( + protocol="https", + host="github.com", + path="/test/package.jl", + username=valid_username, + password=valid_password, + ) + helpers = LibGit2.helpers!(cfg, git_cred) + @assert length(helpers) == 1 + LibGit2.approve(helpers, git_cred) + + https_git_cmd(url) = """ + valid_cred = LibGit2.UserPasswordCredential("$valid_username", "$valid_password") + cfg = LibGit2.GitConfig("$config_path") + err = LibGit2.credential_loop(valid_cred, + url="$url", + config=Nullable(cfg)) + err < 0 ? LibGit2.GitError(err) : err + """ + + # Git credential helper provides correct credentials with no prompts + @test challenge_prompt(https_git_cmd("https://github.com/test/package.jl"), []) == 0 + + # Git credential helper doesn't know about credentials for "example.com" so we need + # to fall back to a prompt. + @test challenge_prompt( + https_git_cmd("https://example.com/test/package.jl"), + ["Username for 'https://example.com':" => "\x04"], + ) == abort_prompt + end + + @testset "HTTPS credential cache" begin + valid_username = "julia" + valid_password = randstring(16) + + https_cmd = """ + valid_cred = LibGit2.UserPasswordCredential("$valid_username", "$valid_password") + cache = LibGit2.CachedCredentials() + LibGit2.approve(cache, "https://github.com", valid_cred) + # Use of config which contains no git credential helpers + err = mktemp("$dir") do config_path, fp + cfg = LibGit2.GitConfig(config_path) + @assert isempty(LibGit2.helpers!(cfg, LibGit2.GitCredential())) + LibGit2.credential_loop(valid_cred, config=Nullable(cfg), cache=Nullable(cache)) + end + err < 0 ? LibGit2.GitError(err) : err + """ + + # Credential cache provides correct credentials with no prompts + @test challenge_prompt(https_cmd, []) == 0 + end + + @testset "Unhandled credential" begin + allowed_types = UInt32(0) # No allowed types + unhandled_exception = LibGit2.GitError( + LibGit2.Error.Callback, LibGit2.Error.EAUTH, + @sprintf("Aborting credential callback. Unable authenticate using allowed types 0x%08x", allowed_types) + ) + + err = LibGit2.credential_loop( + LibGit2.UserPasswordCredential(), + "https://github.com/test/package.jl", + "", allowed_types + ) + @test err < 0 + @test LibGit2.GitError(err) == unhandled_exception end #= temporarily disabled until working on the buildbots, ref https://github.com/JuliaLang/julia/pull/17651#issuecomment-238211150 diff --git a/test/libgit2/invalid b/test/libgit2/invalid new file mode 100644 index 0000000000000..7fa5389beb031 --- /dev/null +++ b/test/libgit2/invalid @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAs1L+Z8s5GdtzLXGM3+4UVQ7jc5q/uNPzQgGUl+nC+o2VFuHm +omkAIxxrS3zkVT0A4AnkrVq/H2l2AOlxKudFan4o87AJQsTHkj9LvFBhm+bBdHau +0SGSft8cCWAXv5lifFChYaxT78Y3QnSO4Q7AVEXQVAmGFiu3CgBKq7c1srQt8V4n +dH8PuMYdIiYuNG8HNAnErGjApdNHmosY63jybkRmTffvhuS/Df1FKLYY60QYThKM +50P+8HHX1jvH+JI5bWLyDhG97E8Vmh9bpgvHhdZ52WBVaf5i6qmYB2FBHHfb8Yuh +/EB39RQjPLEI9K5HP5LkBXAWxozv75qmKF7AiQIDAQABAoIBAFF1H89yRyvxva4k +6INIGMBSlQuMfg6taNDQ+vjO3R0Hd3P/hc68t607WZnez7HQljcvB0uf7zWLqGjd +oeSFTckEKgIYMvy7epR6YCLYuJ4jAjmvN9YT1AIBwzCXD/Ke8FfcIJLHvxEqDywG +2mhtVPxzAiFKejhT/z9xxnmeLaxQ3eE77XrFJiJR0M+fc6B2zyN8eVR/jdpoVFOw +Jb6TkzxDY1OWVY72EMf+7JBzpnNVqe36T0aJapF1c4qeqC+bklYtgnJ1GfohzbKU +ij8SF1yH1y+LX0qq67DLfxmm1qNMgBNhLC8eYm9yfAWkRi4eX9c7U2X/61S3Sm9a +CMGb8QECgYEA2x9d5uCeVdg595NutArea7sHVuJViF8kWYBwYgkAFgdEtjGhoEXX +YMa+11HQ+yUX4WfrLhcQm/eiSdaqKEcE0g/tz6ytuFCZvPHPZe2p2QIIHjqKYezc +mtcc95qYm1O3U5iTHsiL73TurdESOl7O1eSK/TgQwoes6cuELqZ5GBkCgYEA0YD2 +lJ7uW1MBCQwTENbe4ZapvwtEXsRIVad0dg6kzgaYgBWDIuNS0XBnB6sbD3B0TL6G +vhpfO7wMV8VQ1H2h0aMLSgpevPxHrD0uQsXhZ0cM8hzhAKmbAscBdQXv6MnXzr37 +8FL3nHyYc9qvk7qNcMqa1x975s5SlzR5UrA9ufECgYAydOeXiXOMSu0WMFHWjYYo +AnovLJELhPUG0weN24q0h9nvpkAUeuCcfUdmp4Vav3DfS6GhDFibwYsSO6cU/T2D +7X0STC34ej7cfkYGr5Jj0Q7zfwFsiTFHSm92KJgpdD6Ltl9yQDXQ3vky9yieXkR8 +hlLm+ikJ0lojv+RhJZ5OyQKBgQDLlVuIXszXF9jItuAMHBEn/GVlTYiZ4nk8KuaE +FoTV5BjrVnxBOc/v0B9+lypLnYaWUQrUzwG+JWWFISwD4TWPZrYAtAUN9sWbPTSo +FGyOFMAiwidvomEMmN/0nNleSE4bDAk3pxgRDA6FfnvhvYXWljtTUrfvOI2Pe6Ft +1e+VsQKBgFF1wW0E7L7ItpClFifF0tDZdOjfBwiUutusvFSj6RVtIv9s0WiSoc/R +qOc4Jfle2F0cw3Fwp5Qv/qgKItOT0zo4mOJKOzyHfER5UgI7zcIK9dBNuYCRBwwE +lXZzcMoTuRH8GrMdoTUTMyJ5DRC5vqRW422pM4K+ICVIYrZpfpDh +-----END RSA PRIVATE KEY----- diff --git a/test/libgit2/invalid.pub b/test/libgit2/invalid.pub new file mode 100644 index 0000000000000..d7abfc26e25c8 --- /dev/null +++ b/test/libgit2/invalid.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCzUv5nyzkZ23MtcYzf7hRVDuNzmr+40/NCAZSX6cL6jZUW4eaiaQAjHGtLfORVPQDgCeStWr8faXYA6XEq50VqfijzsAlCxMeSP0u8UGGb5sF0dq7RIZJ+3xwJYBe/mWJ8UKFhrFPvxjdCdI7hDsBURdBUCYYWK7cKAEqrtzWytC3xXid0fw+4xh0iJi40bwc0CcSsaMCl00eaixjrePJuRGZN9++G5L8N/UUothjrRBhOEoznQ/7wcdfWO8f4kjltYvIOEb3sTxWaH1umC8eF1nnZYFVp/mLqqZgHYUEcd9vxi6H8QHf1FCM8sQj0rkc/kuQFcBbGjO/vmqYoXsCJ invalid-credentials diff --git a/test/libgit2/valid b/test/libgit2/valid new file mode 100644 index 0000000000000..bc6cf6a402ae8 --- /dev/null +++ b/test/libgit2/valid @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA3oA311O95RTuncUrnHxLu7CUMN9Mh5ZolXct33RoEoAkar7E +oyW2C0AQ+yA7C3PjlC2SmgYbebUC+N/0bFOoibDwFCUmZbOlRTWITYbq7t0f6nk7 +cl4l8BvyMPbRN25ozPyn1SrGc/vHj11LnGXGNZWqCfz5EJ6LfLIlA5P/0814QrRk +ePg/J3RZ3HJj7QlvHvG9uAQQ//WXeFdmRQDKNhZbMc0bibxZGgMQeKL7hoGwGBHT +a5DSWsCWermUC5IYsrC5PAzGcN/KwTbQ0S9U7xVTOuKDSEMv8AzEmkpCtkulVses +x7P47wUKhOOm9OFikVEC2s0uoRvNfUDUSzjFLQIDAQABAoIBACgKhwIXLetV8wMt +goWs0Kw8QM7ywID+DmAnjHfUKQ57RRJ4tkZt+O7ZXrTyve13s5LtCJ7zTmp+qsRC ++WetPn1Y/DuD2/8dbzafRaI+D3VhEedOXeZdDxLA1Lr/D/ObyPxq04FHK2OgAe/I +6FyC7EKkZm9ZuTDGd2+/V23nanihAXJ8ILk02cKxNjpq4gxt0i0Zm3CjVEYlrywm +fBov7sbLWwu2ovISdRBziJAs8wsGPKTcbOCREtHe2BP7bfiAcf9UEatdH7C1kO8U +gdCluvH2RMwy00NunthrKEy7CmdOh/lfV7p6VYbH3/ikq+XkhntE8SEJzafLuIUl +mbglGsECgYEA+SflaLun2WqqsUlrVH3mN7lUbYpHhs72zOa7NcvRzBqEeYhhFyoQ +WvQzJlzfIZ+I/JqgHjsGeJx8cHpxI7zwd7n45nc9ycU9yc5UHsAGcyQwrTpVMIEn +gHL6Ybe2vN7CIazPGnmiwaCD25/Kp5eBXmc8Syckt12GMDIwMuozwbkCgYEA5Jzh +O8lZMGTmoz4iGIWr0g21ySmNX+toKq+zto7jhZSZcEpFGxARP4GdiN9CafGJRRz1 +r4ZHruWazLpxozsWRAgVnWJrgee7JZ4oWi0geAw+wUxQYny+/rx0AQl81lHwPhOm +f8WfTZifKhu9QaR5fa3DAfrgc8Gfay1TP7UtaRUCgYEAqFeEcRcZeZTQb6ijlBrc +iZn2qWxcl4EOz7K1mstznOvtxg+XSgdptYp5ZNorCJS6AbKXrNVEsglJKtYPy43F +C5/jxBOc888IyGlX/M7RjMpO+TwIgxVAk1EcSxnNph78G7Se2cyFYz4I2UNFsaZ0 +CkzGOCDideIC4F2Io14KSkECgYBhRzw0Q45XIGhfyD2b761YWYskCTFmQnZ1y4QO +R6kbG0tbU88qH+EnA9Fz/4R0Llo59YO558qy6nK/cQqP6cGku0fvS4TxpxikfjMv +hApP6u39NKrz7Z8cKDa4hj//tqgi0hvEPOR3kV0Q/vK+bu1fhEP5oLZHq3lZktTc +xAyJrQKBgClH3j759U82D2bUXVanDqs7S0Zef91u11kYDFrnX4kUWlG0DZCB7e5P +yBUWd/ZoXULBkoCfbwO8PGGxFYRmXzYQvVnSGiAP09dHFodvMm7hP8VueoRbAbEL +/UKMQOhYhEpcENylUjaaLUzxaYzwqeT/dcu+Edd7sCKk0J/1beA/ +-----END RSA PRIVATE KEY----- diff --git a/test/libgit2/valid-passphrase b/test/libgit2/valid-passphrase new file mode 100644 index 0000000000000..169fef6df16d0 --- /dev/null +++ b/test/libgit2/valid-passphrase @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,EC9D334AA2ECD3E7A9DCBD7BDFB5CABC + +J9EWrlaxABoCAeow7k8oteiGorPKpZMVQBmIomx91kpfncVBC/WDbRa2HbmVkkYh +kgFbpDmzD6cMuu1sAbOROcCBFLqtKsXwHj4nynW8PTJRNSG9GazY9tmQ/OzAV9Ng +ZELXVncyexKEG7WDlXuArqd2KwbopNe6SetW4OYZy6t7QBNp6c4wwyRwuCYwGjMg +3rAugZvMPv7Ytr4DCluKp+/S5P81LKPpGDYdGSD3wMQtEzB/81DPsp+BP2r0rqPA +9Wa49c5aOcLdDaG8tVlogAQEwn4Mt/GI3U5b2Q14/YUy41AD4ZuYLl+3iv+18PsM +6Kqs0QmD6FM6hDg82+FYh5fbvJqoHjAFqieljWFdm0oLpuz7p3q3MMfAkhl5Xrvq +bCLQenFww8IxgyIMUGYRtq5kGKm+rXbKg/vQV/jxyLCdQZoZXO6F07gwUKX2YM1K +H1j9DbexbtJXoAtXP7nP0qo2WJjhGnDvcqxuBRrJFW7Z/T8r0pqetYSUFuIiS7Nt +ftT60QPziRB1G9nwR7TiWhOfl1VQlrRxXompfK+We3+njp96Yuqdw9mOZ3slEz40 +LlTlPqmC8Fp1rm+M5qtjotlYGJLUcFOpbwKzt0knl+gYPg7qCSVcjoQ0E6WrciyH +3t8+pRtguICjV2Jfq+MFnzfJujNSBIxYro/oQ+dl3b2s9eeks6SVloCmuaGj45hN +Y3wrfMe+4dv+wy9JuYzHualBpPk8GCxDNHqXkd7nTjDPY9uCW2xqjP8HBq+GNj0Q +XLSyZ95iPJwxcAigXa8g3+jMg/IUddIF7tNQbHLZR1h+4nP2HB7Fo0GQM9dOGNzU +r+LGBeIJTmFite0jSFDjRHDo/GunV7/A21ZSykt8aMAQYlEQg2eKAdfzDYK5JTXw +dFn5YURKGpmlyRWc91Dvg6uF28KAwcMBogRK/3EiEyZushndHqtE1Rqtq+Afi1Ld +GBgIk2dgD+TCqxtmTcPO+ZQN4Tm1rRM5j+b4VehCTQTxeqC+YMfiwZZvW8xQj5Ja +DMG/A+0M8PZtjr+QygGXGmtWxx0dn/ez0sLO6om11U7OmYXJGEp6KqNKnlZZiv3t +wY7wJ/IS5DhWhZ+ttsq3z+0XKCiP/06IhNmT6Aq0CUaYuXmbtx7hdnhGA92Jbn8X +pwF8GkDdPA/nqmRiwwJSI2jAMs5EGtYCbcn4QlpCZu9x+m3/YTCGqByQN3VLRtrd +u3UlYH1dT+EAsnG6fxXzj1MdSmT5RRadbEOW6Z3Ykfw47Ft6FKEC+WX0ZC5bBrpC +8a9f/knEbAd5NHEgEuhHHZ+8yv0cz/SHdrDS+cygKIEB237Zwx1TIJ0x9wVtns7g +jk7EV9PBFdmw2aD0WBoK16gQYVhGb8sDQ88ps6llxm8iLV4iQSIAeBDURvjx90aA +vIjqC2RzqXsbtueIo1zk41gElDpDYdI9hSMqhyJaShBOcAPPSso3V0VcAylRTjRt +B9NBbLVjajgWjqse6jcNacbhMnFvbqFEWeHLAI3jOog3aXTmkgIDtuSHGjD/r0WA +FpOQrzn5IDkMEamtqaemFxp14MKxRAPMJ8nOPyZzXM1Pz6dARH1d3EKeC1tFMh3j +-----END RSA PRIVATE KEY----- diff --git a/test/libgit2/valid-passphrase.pub b/test/libgit2/valid-passphrase.pub new file mode 100644 index 0000000000000..2ed2cd3eb54d9 --- /dev/null +++ b/test/libgit2/valid-passphrase.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLsjzj6P2eGJEASR+MqajdDkVzCK5rTnc/BXDzgCA3Zff8uGgHQlXTX++OspluSnPKvaHFuYUWbCDEH2mwHeLWiwXk68T5+3/GqOX9/m2dxEy6TOmLQ4iOPVC5mrBrSFbHTc5J2PPh3q9hwp/p35BwWo5nCUMc+pqq7WEVdgNNziRuFyAtva2vCVDXEZ5+6FJDvysoMndoIQQlVJXHoPp8w+Eb8gOg5kxTYtpNjb7Vjx/Ux70nl26B5JYtpWUTxLHfnzPRcjGrINgtbsbVDC6BJTnSnBttxaZRcON4/pZDpcvLBhOhqBKjkRkxoMvTGeiA7M5cm/G9LWROeCc88I/D valid-credentials-with-passphrase diff --git a/test/libgit2/valid.pub b/test/libgit2/valid.pub new file mode 100644 index 0000000000000..1e53e6c51b7b0 --- /dev/null +++ b/test/libgit2/valid.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDegDfXU73lFO6dxSucfEu7sJQw30yHlmiVdy3fdGgSgCRqvsSjJbYLQBD7IDsLc+OULZKaBht5tQL43/RsU6iJsPAUJSZls6VFNYhNhuru3R/qeTtyXiXwG/Iw9tE3bmjM/KfVKsZz+8ePXUucZcY1laoJ/PkQnot8siUDk//TzXhCtGR4+D8ndFnccmPtCW8e8b24BBD/9Zd4V2ZFAMo2FlsxzRuJvFkaAxB4ovuGgbAYEdNrkNJawJZ6uZQLkhiysLk8DMZw38rBNtDRL1TvFVM64oNIQy/wDMSaSkK2S6VWx6zHs/jvBQqE46b04WKRUQLazS6hG819QNRLOMUt valid-credentials