1
0
Fork 0

Adding upstream version 0.5.5.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-05 05:16:34 +01:00
parent dde4be91ba
commit d2d6608958
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
17 changed files with 2615 additions and 0 deletions

6
.cargo_vcs_info.json Normal file
View file

@ -0,0 +1,6 @@
{
"git": {
"sha1": "0f916a0b5a375b481a63e795875388f30add5390"
},
"path_in_vcs": ""
}

22
.github/workflows/rust.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Rust
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/Cargo.lock

53
CHANGELOG Normal file
View file

@ -0,0 +1,53 @@
# Version 0.5.5 - 2024-09-15
- [add][minor] Add support for `git2` version `0.19`.
# Version 0.5.4 - 2024-03-15
- [add][minor] Add the `GitAuthenticator::download()` convenience function.
# Version 0.5.3 - 2023-10-08
- [add][minor] Add support for customizing user prompts with `GitAuthenticator::set_prompter()`.
# Version 0.5.2 - 2023-09-09
- [change][patch] Fix typo and formatting of nested list in documentation.
# Version 0.5.1 - 2023-09-09
- [change][patch] Improve library level documentation and README.
# Version 0.5.0 - 2023-09-06
- [change][major] Rename `GitAuthenticator::clone()` to `clone_repo()` to avoid conflict with the `Clone` trait.
# Version 0.4.1 - 2023-09-06
- [add][minor] Add support for `git2` version `0.18`.
# Version 0.4.0 - 2023-08-09
- [change][major] Accept any `impl Into<PathBuf>` in `GitAuthenticator::add_ssh_key_from_file()`.
# Version 0.3.3 - 2023-08-09
- [add][minor] Support `git2` versions `0.14`, `0.15`, `0.16` and `0.17`.
# Version 0.3.2 - 2023-08-09
- [change][patch] Document that the `askpass` helper will be used for prompts, if available.
# Version 0.3.1 - 2023-08-09
- [add][minor] Add support for `askpass` helpers.
# Version 0.3.0 - 2023-08-09
- [change][major] Add optional password parameter to `GitAuthenticator::add_ssh_key_file()`.
- [add][minor] Add option to prompt for the password of encrypted SSH key files.
# Version 0.2.0 - 2023-08-08
- [remove][major] Remove `GitAuthenticator::run_operation()`.
- [change][major] Support only one username per domain name.
- [change][major] Support only one set of plaintext credentials per domain name.
- [add][minor] Add `GitAuthenticator::credentials()` to get the credentials callback.
- [add][minor] Add `GitAuthenticator::clone()`.
- [add][minor] Add `GitAuthenticator::fetch()`.
- [add][minor] Add `GitAuthenticator::push()`.
- [add][minor] Add optional support for the `log` crate.
- [fix][patch] Bump minimum `terminal-prompt` version to `0.2.2`.
# Version 0.1.1 - 2023-08-07
- [patch][change] Fix examples and `README.md` for updated crate name.
# Version 0.1.0 - 2023-08-07
- [minor][add] Add `GitAuthenticator` struct for authentication with `git2`.

785
Cargo.lock generated Normal file
View file

@ -0,0 +1,785 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]]
name = "assert2"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d31fea2b6e18dfe892863c3a0a68f9e005b0195565f3d55b8612946ebca789cc"
dependencies = [
"assert2-macros",
"diff",
"is-terminal",
"yansi",
]
[[package]]
name = "assert2-macros"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c1ac052c642f6d94e4be0b33028b346b7ab809ea5432b584eb8859f12f7ad2c"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"syn",
]
[[package]]
name = "auth-git2"
version = "0.5.5"
dependencies = [
"assert2",
"clap",
"dirs",
"env_logger",
"git2",
"log",
"terminal-prompt",
]
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "cc"
version = "1.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476"
dependencies = [
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
[[package]]
name = "colorchoice"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "dirs"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]]
name = "env_logger"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
dependencies = [
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
]
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "git2"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
dependencies = [
"bitflags",
"libc",
"libgit2-sys",
"log",
"openssl-probe",
"openssl-sys",
"url",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "idna"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "is-terminal"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "libgit2-sys"
version = "0.17.0+1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
dependencies = [
"cc",
"libc",
"libssh2-sys",
"libz-sys",
"openssl-sys",
"pkg-config",
]
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags",
"libc",
]
[[package]]
name = "libssh2-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
]
[[package]]
name = "libz-sys"
version = "1.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pkg-config"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox",
"thiserror",
]
[[package]]
name = "regex"
version = "1.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "semver"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "terminal-prompt"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572818b3472910acbd5dff46a3413715c18e934b071ab2ba464a7b2c2af16376"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "thiserror"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinyvec"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "unicode-normalization"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
dependencies = [
"tinyvec",
]
[[package]]
name = "url"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"

60
Cargo.toml Normal file
View file

@ -0,0 +1,60 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "auth-git2"
version = "0.5.5"
authors = ["Maarten de Vries <maarten@de-vri.es>"]
publish = ["crates-io"]
description = "Authentication for `git2`"
documentation = "https://docs.rs/auth-git2"
readme = "README.md"
keywords = [
"git",
"auth",
"credentials",
"git2",
"authentication",
]
categories = ["authentication"]
license = "BSD-2-Clause"
repository = "https://github.com/de-vri-es/auth-git2-rs"
[dependencies.dirs]
version = "5.0.1"
[dependencies.git2]
version = ">0.14, <20.0"
default-features = false
[dependencies.log]
version = "0.4.19"
optional = true
[dependencies.terminal-prompt]
version = "0.2.2"
[dev-dependencies.assert2]
version = "0.3.11"
[dev-dependencies.clap]
version = "4.3.21"
features = ["derive"]
[dev-dependencies.env_logger]
version = "0.10.0"
[dev-dependencies.git2]
version = ">=0.14, <18.0"
[features]
log = ["dep:log"]

29
Cargo.toml.orig generated Normal file
View file

@ -0,0 +1,29 @@
[package]
name = "auth-git2"
version = "0.5.5"
description = "Authentication for `git2`"
license = "BSD-2-Clause"
authors = ["Maarten de Vries <maarten@de-vri.es>"]
repository = "https://github.com/de-vri-es/auth-git2-rs"
documentation = "https://docs.rs/auth-git2"
keywords = ["git", "auth", "credentials", "git2", "authentication"]
categories = ["authentication"]
edition = "2021"
publish = ["crates-io"]
[features]
log = ["dep:log"]
[dependencies]
dirs = "5.0.1"
git2 = { version = ">0.14, <20.0", default-features = false }
log = { version = "0.4.19", optional = true }
terminal-prompt = "0.2.2"
[dev-dependencies]
assert2 = "0.3.11"
auth-git2 = { path = ".", features = ["log"] }
clap = { version = "4.3.21", features = ["derive"] }
env_logger = "0.10.0"
git2 = ">=0.14, <18.0"

24
LICENSE.md Normal file
View file

@ -0,0 +1,24 @@
BSD 2-Clause License
Copyright (c) 2023, Maarten de Vries <maarten@de-vri.es>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

100
README.md Normal file
View file

@ -0,0 +1,100 @@
# auth-git2
Easy authentication for [`git2`].
Authentication with [`git2`] can be quite difficult to implement correctly.
This crate aims to make it easy.
## Features
* Has a small dependency tree.
* Can query the SSH agent for private key authentication.
* Can get SSH keys from files.
* Can prompt the user for passwords for encrypted SSH keys.
* Only supported for OpenSSH private keys.
* Can query the git credential helper for usernames and passwords.
* Can use pre-provided plain usernames and passwords.
* Can prompt the user for credentials as a last resort.
* Allows you to fully customize all user prompts.
The default user prompts will:
* Use the git `askpass` helper if it is configured.
* Fall back to prompting the user on the terminal if there is no `askpass` program configured.
* Skip the prompt if there is also no terminal available for the process.
## Creating an authenticator and enabling authentication mechanisms
You can create use [`GitAuthenticator::new()`] (or [`default()`][`GitAuthenticator::default()`]) to create a ready-to-use authenticator.
Using one of these constructors will enable all supported authentication mechanisms.
You can still add more private key files from non-default locations to try if desired.
You can also use [`GitAuthenticator::new_empty()`] to create an authenticator without any authentication mechanism enabled.
Then you can selectively enable authentication mechanisms and add custom private key files.
## Using the authenticator
For the most flexibility, you can get a [`git2::Credentials`] callback using the [`GitAuthenticator::credentials()`] function.
You can use it with any git operation that requires authentication.
Doing this gives you full control to set other options and callbacks for the git operation.
If you don't need to set other options or callbacks, you can also use the convenience functions on [`GitAuthenticator`].
They wrap git operations with the credentials callback set:
* [`GitAuthenticator::clone_repo()`]
* [`GitAuthenticator::fetch()`]
* [`GitAuthenticator::download()`]
* [`GitAuthenticator::push()`]
## Customizing user prompts
All user prompts can be fully customized by calling [`GitAuthenticator::set_prompter()`].
This allows you to override the way that the user is prompted for credentials or passphrases.
If you have a fancy user interface, you can use a custom prompter to integrate the prompts with your user interface.
## Example: Clone a repository
```rust
use auth_git2::GitAuthenticator;
use std::path::Path;
let url = "https://github.com/de-vri-es/auth-git2-rs";
let into = Path::new("/tmp/dyfhxoaj/auth-git2-rs");
let auth = GitAuthenticator::default();
let mut repo = auth.clone_repo(url, into);
```
## Example: Clone a repository with full control over fetch options
```rust
use auth_git2::GitAuthenticator;
use std::path::Path;
let auth = GitAuthenticator::default();
let git_config = git2::Config::open_default()?;
let mut repo_builder = git2::build::RepoBuilder::new();
let mut fetch_options = git2::FetchOptions::new();
let mut remote_callbacks = git2::RemoteCallbacks::new();
remote_callbacks.credentials(auth.credentials(&git_config));
fetch_options.remote_callbacks(remote_callbacks);
repo_builder.fetch_options(fetch_options);
let url = "https://github.com/de-vri-es/auth-git2-rs";
let into = Path::new("/tmp/dyfhxoaj/auth-git2-rs");
let mut repo = repo_builder.clone(url, into);
```
[`git2`]: https://docs.rs/git2
[`GitAuthenticator`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html
[`GitAuthenticator::new()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.new
[`GitAuthenticator::default()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.default
[`GitAuthenticator::new_empty()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.new_empty
[`git2::Credentials`]: https://docs.rs/git2/latest/git2/type.Credentials.html
[`GitAuthenticator::credentials()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.credentials
[`GitAuthenticator::clone_repo()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.clone_repo
[`GitAuthenticator::fetch()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.fetch
[`GitAuthenticator::push()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.push
[`GitAuthenticator::download()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.download
[`GitAuthenticator::set_prompter()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.set_prompter

16
README.tpl Normal file
View file

@ -0,0 +1,16 @@
# {{crate}}
{{readme}}
[`git2`]: https://docs.rs/git2
[`GitAuthenticator`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html
[`GitAuthenticator::new()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.new
[`GitAuthenticator::default()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.default
[`GitAuthenticator::new_empty()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.new_empty
[`git2::Credentials`]: https://docs.rs/git2/latest/git2/type.Credentials.html
[`GitAuthenticator::credentials()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.credentials
[`GitAuthenticator::clone_repo()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.clone_repo
[`GitAuthenticator::fetch()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.fetch
[`GitAuthenticator::push()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.push
[`GitAuthenticator::download()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.download
[`GitAuthenticator::set_prompter()`]: https://docs.rs/auth-git2/latest/auth_git2/struct.GitAuthenticator.html#method.set_prompter

View file

@ -0,0 +1,139 @@
use std::path::{Path, PathBuf};
#[derive(Copy, Clone)]
struct YadPrompter;
impl auth_git2::Prompter for YadPrompter {
fn prompt_username_password(&mut self, url: &str, _git_config: &git2::Config) -> Option<(String, String)> {
let mut items = yad_prompt(
"Git authentication",
&format!("Authentication required for {url}"),
&["Username", "Password:H"],
).ok()?.into_iter();
let username = items.next()?;
let password = items.next()?;
Some((username, password))
}
fn prompt_password(&mut self, username: &str, url: &str, _git_config: &git2::Config) -> Option<String> {
let mut items = yad_prompt(
"Git authentication",
&format!("Authentication required for {url}"),
&[&format!("Username: {username}:LBL"), "Password:H"],
).ok()?.into_iter();
let password = items.next()?;
Some(password)
}
fn prompt_ssh_key_passphrase(&mut self, private_key_path: &std::path::Path, _git_config: &git2::Config) -> Option<String> {
let mut items = yad_prompt(
"Git authentication",
&format!("Passphrase required for {}", private_key_path.display()),
&["Passphrase:H"],
).ok()?.into_iter();
let passphrase = items.next()?;
Some(passphrase)
}
}
fn yad_prompt(title: &str, text: &str, fields: &[&str]) -> Result<Vec<String>, ()> {
let mut command = std::process::Command::new("yad");
command
.arg("--title")
.arg(title)
.arg("--text")
.arg(text)
.arg("--form")
.arg("--separator=\n");
for field in fields {
command.arg("--field");
command.arg(field);
}
let output = command
.stderr(std::process::Stdio::inherit())
.output()
.map_err(|e| log::error!("Failed to run `yad`: {e}"))?;
if !output.status.success() {
log::debug!("yad exited with {}", output.status);
return Err(());
}
let output = String::from_utf8(output.stdout)
.map_err(|_| log::warn!("Invalid UTF-8 in response from yad"))?;
let mut items: Vec<_> = output.splitn(fields.len() + 1, '\n')
.take(fields.len())
.map(|x| x.to_owned())
.collect();
if let Some(last) = items.pop() {
if !last.is_empty() {
items.push(last)
}
}
if items.len() != fields.len() {
log::error!("asked yad for {} values but got only {}", fields.len(), items.len());
Err(())
} else {
Ok(items)
}
}
#[derive(clap::Parser)]
struct Options {
/// Show more verbose statement.
#[clap(long, short)]
#[clap(global = true)]
#[clap(action = clap::ArgAction::Count)]
verbose: u8,
/// The URL of the repository to clone.
#[clap(value_name = "URL")]
repo: String,
/// The path where to clone the repository.
#[clap(value_name = "PATH")]
local_path: Option<PathBuf>,
}
fn main() {
if let Err(()) = do_main(clap::Parser::parse()) {
std::process::exit(1);
}
}
fn log_level(verbose: u8) -> log::LevelFilter {
match verbose {
0 => log::LevelFilter::Info,
1 => log::LevelFilter::Debug,
2.. => log::LevelFilter::Trace,
}
}
fn do_main(options: Options) -> Result<(), ()> {
let log_level = log_level(options.verbose);
env_logger::builder()
.parse_default_env()
.filter_module(module_path!(), log_level)
.filter_module("auth_git2", log_level)
.init();
let local_path = options.local_path.as_deref()
.unwrap_or_else(|| Path::new(repo_name_from_url(&options.repo)));
log::info!("Cloning {} into {}", options.repo, local_path.display());
let auth = auth_git2::GitAuthenticator::default()
.set_prompter(YadPrompter);
auth.clone_repo(&options.repo, local_path)
.map_err(|e| log::error!("Failed to clone {}: {}", options.repo, e))?;
Ok(())
}
fn repo_name_from_url(url: &str) -> &str {
url.rsplit_once('/')
.map(|(_head, tail)| tail)
.unwrap_or(url)
}

146
examples/git.rs Normal file
View file

@ -0,0 +1,146 @@
use std::path::{Path, PathBuf};
#[derive(clap::Parser)]
struct Options {
/// Show more verbose statement.
#[clap(long, short)]
#[clap(global = true)]
#[clap(action = clap::ArgAction::Count)]
verbose: u8,
/// The subcommand.
#[clap(subcommand)]
command: Command,
}
#[derive(clap::Subcommand)]
enum Command {
Clone(CloneCommand),
Fetch(FetchCommand),
Push(PushCommand),
}
/// Clone a repository.
#[derive(clap::Parser)]
struct CloneCommand {
/// The URL of the repository to clone.
#[clap(value_name = "URL")]
repo: String,
/// The path where to clone the repository.
#[clap(value_name = "PATH")]
local_path: Option<PathBuf>,
}
/// Fetch from a remote.
#[derive(clap::Parser)]
struct FetchCommand {
/// The repository to operate on.
#[clap(value_name = "PATH")]
#[clap(short = 'C', long)]
repo: PathBuf,
/// The repository to operate on.
#[clap(value_name = "REMOTE")]
remote: String,
/// The refs to fetch.
#[clap(trailing_var_arg = true)]
#[clap(required = true)]
refspec: Vec<String>,
}
/// Push to a remote.
#[derive(clap::Parser)]
struct PushCommand {
/// The repository to operate on.
#[clap(value_name = "PATH")]
#[clap(short = 'C', long)]
#[clap(default_value = ".")]
repo: PathBuf,
/// The repository to operate on.
#[clap(value_name = "REMOTE")]
remote: String,
/// The refs to fetch.
#[clap(trailing_var_arg = true)]
#[clap(required = true)]
refspec: Vec<String>,
}
fn main() {
if let Err(()) = do_main(clap::Parser::parse()) {
std::process::exit(1);
}
}
fn log_level(verbose: u8) -> log::LevelFilter {
match verbose {
0 => log::LevelFilter::Info,
1 => log::LevelFilter::Debug,
2.. => log::LevelFilter::Trace,
}
}
fn do_main(options: Options) -> Result<(), ()> {
let log_level = log_level(options.verbose);
env_logger::builder()
.parse_default_env()
.filter_module(module_path!(), log_level)
.filter_module("auth_git2", log_level)
.init();
match options.command {
Command::Clone(command) => clone(command),
Command::Fetch(command) => fetch(command),
Command::Push(command) => push(command),
}
}
fn clone(command: CloneCommand) -> Result<(), ()> {
let local_path = command.local_path.as_deref()
.unwrap_or_else(|| Path::new(repo_name_from_url(&command.repo)));
log::info!("Cloning {} into {}", command.repo, local_path.display());
let auth = auth_git2::GitAuthenticator::default();
auth.clone_repo(&command.repo, local_path)
.map_err(|e| log::error!("Failed to clone {}: {}", command.repo, e))?;
Ok(())
}
fn fetch(command: FetchCommand) -> Result<(), ()> {
let repo = git2::Repository::open(&command.repo)
.map_err(|e| log::error!("Failed to open git repo at {}: {e}", command.repo.display()))?;
let refspecs: Vec<_> = command.refspec.iter().map(|x| x.as_str()).collect();
let auth = auth_git2::GitAuthenticator::default();
let mut remote = repo.find_remote(&command.remote)
.map_err(|e| log::error!("Failed to find remote {:?}: {e}", command.remote))?;
auth.fetch(&repo, &mut remote, &refspecs, None)
.map_err(|e| log::error!("Failed to fetch from remote {:?}: {e}", command.remote))?;
Ok(())
}
fn push(command: PushCommand) -> Result<(), ()> {
let repo = git2::Repository::open(&command.repo)
.map_err(|e| log::error!("Failed to open git repo at {}: {e}", command.repo.display()))?;
log::info!("Fetching {:?} from remote {:?}", command.refspec, command.remote);
let refspecs: Vec<_> = command.refspec.iter().map(|x| x.as_str()).collect();
let auth = auth_git2::GitAuthenticator::default();
let mut remote = repo.find_remote(&command.remote)
.map_err(|e| log::error!("Failed to find remote {:?}: {e}", command.remote))?;
auth.push(&repo, &mut remote, &refspecs,)
.map_err(|e| log::error!("Failed to push to remote {:?}: {e}", command.remote))?;
Ok(())
}
fn repo_name_from_url(url: &str) -> &str {
url.rsplit_once('/')
.map(|(_head, tail)| tail)
.unwrap_or(url)
}

110
src/base64_decode.rs Normal file
View file

@ -0,0 +1,110 @@
/// An error that can occur during base64 decoding.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Error {
InvalidBase64Char(u8),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidBase64Char(value) => write!(f, "Invalid base64 character: {:?}", char::from_u32(*value as u32).unwrap()),
}
}
}
/// Decode a base64 string.
///
/// Padding in the input is optional.
pub fn base64_decode(input: &[u8]) -> Result<Vec<u8>, Error> {
let input = match input.iter().rposition(|&byte| byte != b'=' && !byte.is_ascii_whitespace()) {
Some(x) => &input[..=x],
None => return Ok(Vec::new()),
};
let mut output = Vec::with_capacity((input.len() + 3) / 4 * 3);
let mut decoder = Base64Decoder::new();
for &byte in input {
if byte.is_ascii_whitespace() {
continue;
}
if let Some(byte) = decoder.feed(byte)? {
output.push(byte);
}
}
Ok(output)
}
/// Get the 6 bit value for a base64 character.
fn base64_value(byte: u8) -> Result<u8, Error> {
match byte {
b'A'..=b'Z' => Ok(byte - b'A'),
b'a'..=b'z' => Ok(byte - b'a' + 26),
b'0'..=b'9' => Ok(byte - b'0' + 52),
b'+' => Ok(62),
b'/' => Ok(63),
byte => Err(Error::InvalidBase64Char(byte)),
}
}
/// Decoder for base64 data.
struct Base64Decoder {
/// The current buffer.
buffer: u16,
/// The number of valid bits in the buffer.
valid_bits: u8,
}
impl Base64Decoder {
/// Create a new base64 decoder.
fn new() -> Self {
Self {
buffer: 0,
valid_bits: 0,
}
}
/// Feed a base64 character to the decoder.
///
/// Returns `Ok(Some(u8))` if a new character is fully decoded.
/// Returns `Ok(None)` if there is no new character available yet.
fn feed(&mut self, byte: u8) -> Result<Option<u8>, Error> {
debug_assert!(self.valid_bits < 8);
// Paste the new 6 bit value at the least significant position in the buffer.
self.buffer |= (base64_value(byte)? as u16) << (10 - self.valid_bits);
// Bump the number of valid bits.
self.valid_bits += 6;
// Consume the most significant byte if it is complete.
Ok(self.consume_buffer_front())
}
/// Consume the first character in the buffer.
fn consume_buffer_front(&mut self) -> Option<u8> {
if self.valid_bits >= 8 {
let value = self.buffer >> 8 & 0xFF;
self.buffer <<= 8;
self.valid_bits -= 8;
Some(value as u8)
} else {
None
}
}
}
#[cfg(test)]
mod test {
use super::*;
use assert2::assert;
#[test]
fn test_decode_base64() {
assert!(let Ok(b"0") = base64_decode(b"MA").as_deref());
assert!(let Ok(b"0") = base64_decode(b"MA=").as_deref());
assert!(let Ok(b"0") = base64_decode(b"MA==").as_deref());
assert!(let Ok(b"aap noot mies") = base64_decode(b"YWFwIG5vb3QgbWllcw").as_deref());
assert!(let Ok(b"aap noot mies") = base64_decode(b"YWFwIG5vb3QgbWllcw=").as_deref());
assert!(let Ok(b"aap noot mies") = base64_decode(b"YWFwIG5vb3QgbWllcw==").as_deref());
}
}

183
src/default_prompt.rs Normal file
View file

@ -0,0 +1,183 @@
use std::io::Write;
use std::path::{Path, PathBuf};
#[cfg(feature = "log")]
use crate::log::*;
#[derive(Copy, Clone)]
pub(crate) struct DefaultPrompter;
impl crate::Prompter for DefaultPrompter {
fn prompt_username_password(&mut self, url: &str, git_config: &git2::Config) -> Option<(String, String)> {
prompt_username_password(url, git_config)
.map_err(|e| log_error("username and password", &e))
.ok()
}
fn prompt_password(&mut self, username: &str, url: &str, git_config: &git2::Config) -> Option<String> {
prompt_password(username, url, git_config)
.map_err(|e| log_error("password", &e))
.ok()
}
fn prompt_ssh_key_passphrase(&mut self, private_key_path: &Path, git_config: &git2::Config) -> Option<String> {
prompt_ssh_key_passphrase(private_key_path, git_config)
.map_err(|e| log_error("SSH key passphrase", &e))
.ok()
}
}
fn log_error(kind: &str, error: &Error) {
warn!("Failed to prompt the user for {kind}: {error}");
if let Error::AskpassExitStatus(error) = error {
if let Some(extra_message) = error.extra_message() {
for line in extra_message.lines() {
warn!("askpass: {line}");
}
}
}
}
/// Error that can occur when prompting for a password.
pub enum Error {
/// Failed to run the askpass command.
AskpassCommand(std::io::Error),
/// Askpass command exitted with a non-zero error code.
AskpassExitStatus(AskpassExitStatusError),
/// Password contains invalid UTF-8.
InvalidUtf8(std::string::FromUtf8Error),
/// Failed to open a handle to the main terminal of the process.
OpenTerminal(std::io::Error),
/// Failed to read/write to the terminal.
ReadWriteTerminal(std::io::Error),
}
/// The askpass process exited with a non-zero exit code.
pub struct AskpassExitStatusError {
/// The exit status of the askpass process.
pub status: std::process::ExitStatus,
/// The standard error of the askpass process.
pub stderr: Result<String, std::string::FromUtf8Error>,
}
impl AskpassExitStatusError {
/// Get the extra error message, if any.
///
/// This will give the standard error of the askpass process if it exited with an error.
pub fn extra_message(&self) -> Option<&str> {
self.stderr.as_deref().ok()
}
}
/// Prompt the user for a username and password for a particular URL.
///
/// This uses the askpass helper if configured,
/// and falls back to prompting on the terminal otherwise.
fn prompt_username_password(url: &str, git_config: &git2::Config) -> Result<(String, String), Error> {
if let Some(askpass) = askpass_command(git_config) {
let username = askpass_prompt(&askpass, &format!("Username for {url}"))?;
let password = askpass_prompt(&askpass, &format!("Password for {url}"))?;
Ok((username, password))
} else {
let mut terminal = terminal_prompt::Terminal::open()
.map_err(Error::OpenTerminal)?;
writeln!(terminal, "Authentication needed for {url}")
.map_err(Error::ReadWriteTerminal)?;
let username = terminal.prompt("Username: ")
.map_err(Error::ReadWriteTerminal)?;
let password = terminal.prompt_sensitive("Password: ")
.map_err(Error::ReadWriteTerminal)?;
Ok((username, password))
}
}
/// Prompt the user for a password for a particular URL and username.
///
/// This uses the askpass helper if configured,
/// and falls back to prompting on the terminal otherwise.
fn prompt_password(_username: &str, url: &str, git_config: &git2::Config) -> Result<String, Error> {
if let Some(askpass) = askpass_command(git_config) {
let password = askpass_prompt(&askpass, &format!("Password for {url}"))?;
Ok(password)
} else {
let mut terminal = terminal_prompt::Terminal::open()
.map_err(Error::OpenTerminal)?;
writeln!(terminal, "Authentication needed for {url}")
.map_err(Error::ReadWriteTerminal)?;
let password = terminal.prompt_sensitive("Password: ")
.map_err(Error::ReadWriteTerminal)?;
Ok(password)
}
}
/// Prompt the user for the password of an encrypted SSH key.
///
/// This uses the askpass helper if configured,
/// and falls back to prompting on the terminal otherwise.
fn prompt_ssh_key_passphrase(private_key_path: &Path, git_config: &git2::Config) -> Result<String, Error> {
if let Some(askpass) = askpass_command(git_config) {
askpass_prompt(&askpass, &format!("Password for {}", private_key_path.display()))
} else {
let mut terminal = terminal_prompt::Terminal::open()
.map_err(Error::OpenTerminal)?;
writeln!(terminal, "Password needed for {}", private_key_path.display())
.map_err(Error::ReadWriteTerminal)?;
terminal.prompt_sensitive("Password: ")
.map_err(Error::ReadWriteTerminal)
}
}
/// Get the configured askpass program, if any.
fn askpass_command(git_config: &git2::Config) -> Option<PathBuf> {
if let Some(command) = std::env::var_os("GIT_ASKPASS") {
Some(command.into())
} else if let Ok(command) = git_config.get_path("core.askPass") {
return Some(command)
} else if let Some(command) = std::env::var_os("SSH_ASKPASS") {
return Some(command.into());
} else {
None
}
}
/// Prompt the user using the given askpass program.
fn askpass_prompt(program: &Path, prompt: &str) -> Result<String, Error> {
let output = std::process::Command::new(program)
.arg(prompt)
.output()
.map_err(Error::AskpassCommand)?;
if output.status.success() {
let password = String::from_utf8(output.stdout)
.map_err(Error::InvalidUtf8)?;
Ok(password)
} else {
// Do not keep stdout, it could contain a password D:
Err(Error::AskpassExitStatus(AskpassExitStatusError {
status: output.status,
stderr: String::from_utf8(output.stderr),
}))
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AskpassCommand(e) => write!(f, "Failed to run askpass command: {e}"),
Self::AskpassExitStatus(e) => write!(f, "{e}"),
Self::InvalidUtf8(_) => write!(f, "User response contains invalid UTF-8"),
Self::OpenTerminal(e) => write!(f, "Failed to open terminal: {e}"),
Self::ReadWriteTerminal(e) => write!(f, "Failed to read/write to terminal: {e}"),
}
}
}
impl std::fmt::Display for AskpassExitStatusError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Program exitted with {}", self.status)
}
}

719
src/lib.rs Normal file
View file

@ -0,0 +1,719 @@
//! Easy authentication for [`git2`].
//!
//! Authentication with [`git2`] can be quite difficult to implement correctly.
//! This crate aims to make it easy.
//!
//! # Features
//!
//! * Has a small dependency tree.
//! * Can query the SSH agent for private key authentication.
//! * Can get SSH keys from files.
//! * Can prompt the user for passwords for encrypted SSH keys.
//! * Only supported for OpenSSH private keys.
//! * Can query the git credential helper for usernames and passwords.
//! * Can use pre-provided plain usernames and passwords.
//! * Can prompt the user for credentials as a last resort.
//! * Allows you to fully customize all user prompts.
//!
//! The default user prompts will:
//! * Use the git `askpass` helper if it is configured.
//! * Fall back to prompting the user on the terminal if there is no `askpass` program configured.
//! * Skip the prompt if there is also no terminal available for the process.
//!
//! # Creating an authenticator and enabling authentication mechanisms
//!
//! You can create use [`GitAuthenticator::new()`] (or [`default()`][`GitAuthenticator::default()`]) to create a ready-to-use authenticator.
//! Using one of these constructors will enable all supported authentication mechanisms.
//! You can still add more private key files from non-default locations to try if desired.
//!
//! You can also use [`GitAuthenticator::new_empty()`] to create an authenticator without any authentication mechanism enabled.
//! Then you can selectively enable authentication mechanisms and add custom private key files.
//!
//! # Using the authenticator
//!
//! For the most flexibility, you can get a [`git2::Credentials`] callback using the [`GitAuthenticator::credentials()`] function.
//! You can use it with any git operation that requires authentication.
//! Doing this gives you full control to set other options and callbacks for the git operation.
//!
//! If you don't need to set other options or callbacks, you can also use the convenience functions on [`GitAuthenticator`].
//! They wrap git operations with the credentials callback set:
//!
//! * [`GitAuthenticator::clone_repo()`]
//! * [`GitAuthenticator::fetch()`]
//! * [`GitAuthenticator::download()`]
//! * [`GitAuthenticator::push()`]
//!
//! # Customizing user prompts
//!
//! All user prompts can be fully customized by calling [`GitAuthenticator::set_prompter()`].
//! This allows you to override the way that the user is prompted for credentials or passphrases.
//!
//! If you have a fancy user interface, you can use a custom prompter to integrate the prompts with your user interface.
//!
//! # Example: Clone a repository
//!
//! ```no_run
//! # fn main() -> Result<(), git2::Error> {
//! use auth_git2::GitAuthenticator;
//! use std::path::Path;
//!
//! let url = "https://github.com/de-vri-es/auth-git2-rs";
//! let into = Path::new("/tmp/dyfhxoaj/auth-git2-rs");
//!
//! let auth = GitAuthenticator::default();
//! let mut repo = auth.clone_repo(url, into);
//! # let _ = repo;
//! # Ok(())
//! # }
//! ```
//!
//! # Example: Clone a repository with full control over fetch options
//!
//! ```no_run
//! # fn main() -> Result<(), git2::Error> {
//! use auth_git2::GitAuthenticator;
//! use std::path::Path;
//!
//! let auth = GitAuthenticator::default();
//! let git_config = git2::Config::open_default()?;
//! let mut repo_builder = git2::build::RepoBuilder::new();
//! let mut fetch_options = git2::FetchOptions::new();
//! let mut remote_callbacks = git2::RemoteCallbacks::new();
//!
//! remote_callbacks.credentials(auth.credentials(&git_config));
//! fetch_options.remote_callbacks(remote_callbacks);
//! repo_builder.fetch_options(fetch_options);
//!
//! let url = "https://github.com/de-vri-es/auth-git2-rs";
//! let into = Path::new("/tmp/dyfhxoaj/auth-git2-rs");
//! let mut repo = repo_builder.clone(url, into);
//! # let _ = repo;
//! # Ok(())
//! # }
//! ```
#![warn(missing_docs)]
use std::collections::BTreeMap;
use std::path::{PathBuf, Path};
#[cfg(feature = "log")]
mod log {
pub use ::log::warn;
pub use ::log::debug;
pub use ::log::trace;
}
#[cfg(feature = "log")]
use crate::log::*;
#[cfg(not(feature = "log"))]
#[macro_use]
mod log {
macro_rules! warn {
($($tokens:tt)*) => { { let _ = format_args!($($tokens)*); } };
}
macro_rules! debug {
($($tokens:tt)*) => { { let _ = format_args!($($tokens)*); } };
}
macro_rules! trace {
($($tokens:tt)*) => { { let _ = format_args!($($tokens)*); } };
}
}
mod base64_decode;
mod default_prompt;
mod prompter;
mod ssh_key;
pub use prompter::Prompter;
/// Configurable authenticator to use with [`git2`].
#[derive(Clone)]
pub struct GitAuthenticator {
/// Map of domain names to plaintext credentials.
plaintext_credentials: BTreeMap<String, PlaintextCredentials>,
/// Try getting username/password from the git credential helper.
try_cred_helper: bool,
/// Number of times to ask the user for a username/password on the terminal.
try_password_prompt: u32,
/// Map of domain names to usernames to try for SSH connections if no username was specified.
usernames: BTreeMap<String, String>,
/// Try to use the SSH agent to get a working SSH key.
try_ssh_agent: bool,
/// SSH keys to use from file.
ssh_keys: Vec<PrivateKeyFile>,
/// Prompt for passwords for encrypted SSH keys.
prompt_ssh_key_password: bool,
/// Custom prompter to use.
prompter: Box<dyn prompter::ClonePrompter>,
}
impl std::fmt::Debug for GitAuthenticator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GitAuthenticator")
.field("plaintext_credentials", &self.plaintext_credentials)
.field("try_cred_helper", &self.try_cred_helper)
.field("try_password_prompt", &self.try_password_prompt)
.field("usernames", &self.usernames)
.field("try_ssh_agent", &self.try_ssh_agent)
.field("ssh_keys", &self.ssh_keys)
.field("prompt_ssh_key_password", &self.prompt_ssh_key_password)
.finish()
}
}
impl Default for GitAuthenticator {
/// Create a new authenticator with all supported options enabled.
///
/// This is the same as [`GitAuthenticator::new()`].
fn default() -> Self {
Self::new()
}
}
impl GitAuthenticator {
/// Create a new authenticator with all supported options enabled.
///
/// This is equivalent to:
/// ```
/// # use auth_git2::GitAuthenticator;
/// GitAuthenticator::new_empty()
/// .try_cred_helper(true)
/// .try_password_prompt(3)
/// .add_default_username()
/// .try_ssh_agent(true)
/// .add_default_ssh_keys()
/// .prompt_ssh_key_password(true)
/// # ;
/// ```
pub fn new() -> Self {
Self::new_empty()
.try_cred_helper(true)
.try_password_prompt(3)
.add_default_username()
.try_ssh_agent(true)
.add_default_ssh_keys()
.prompt_ssh_key_password(true)
}
/// Create a new authenticator with all authentication options disabled.
pub fn new_empty() -> Self {
Self {
try_ssh_agent: false,
try_cred_helper: false,
plaintext_credentials: BTreeMap::new(),
try_password_prompt: 0,
usernames: BTreeMap::new(),
ssh_keys: Vec::new(),
prompt_ssh_key_password: false,
prompter: prompter::wrap_prompter(default_prompt::DefaultPrompter),
}
}
/// Set the username + password to use for a specific domain.
///
/// Use the special value "*" for the domain name to add fallback credentials when there is no exact match for the domain.
pub fn add_plaintext_credentials(mut self, domain: impl Into<String>, username: impl Into<String>, password: impl Into<String>) -> Self {
let domain = domain.into();
let username = username.into();
let password = password.into();
self.plaintext_credentials.insert(domain, PlaintextCredentials {
username,
password,
});
self
}
/// Configure if the git credentials helper should be used.
///
/// See the git documentation of the `credential.helper` configuration options for more details.
pub fn try_cred_helper(mut self, enable: bool) -> Self {
self.try_cred_helper = enable;
self
}
/// Configure the number of times we should prompt the user for a username/password.
///
/// Setting this value to `0` disables password prompts.
///
/// By default, if an `askpass` helper is configured, it will be used for the prompts.
/// Otherwise, the user will be prompted directly on the terminal of the current process.
/// If there is also no terminal available, the prompt is skipped.
///
/// An `askpass` helper can be configured in the `GIT_ASKPASS` environment variable,
/// the `core.askPass` configuration value or the `SSH_ASKPASS` environment variable.
///
/// You can override the prompt behaviour by calling [`Self::set_prompter()`].
pub fn try_password_prompt(mut self, max_count: u32) -> Self {
self.try_password_prompt = max_count;
self
}
/// Use a custom [`Prompter`] to prompt the user for credentials and passphrases.
///
/// If you set a custom prompter,
/// the authenticator will no longer try to use the `askpass` helper or prompt the user on the terminal.
/// Instead, the provided prompter will be called.
///
/// Note that prompts must still be enabled with [`Self::try_password_prompt()`] and [`Self::prompt_ssh_key_password()`].
/// If prompts are disabled, your custom prompter will not be called.
///
/// You can use this function to integrate the prompts with your own user interface
/// or simply to tweak the way the user is prompted on the terminal.
///
/// A unique clone of the prompter will be used for each [`git2::Credentials`] callback returned by [`Self::credentials()`].
pub fn set_prompter<P: Prompter + Clone + Send + 'static>(mut self, prompter: P) -> Self {
self.prompter = prompter::wrap_prompter(prompter);
self
}
/// Add a username to try for authentication for a specific domain.
///
/// Some authentication mechanisms need a username, but not all valid git URLs specify one.
/// You can add one or more usernames to try in that situation.
///
/// You can use the special domain name "*" to set a fallback username for domains that do not have a specific username set.
pub fn add_username(mut self, domain: impl Into<String>, username: impl Into<String>) -> Self {
let domain = domain.into();
let username = username.into();
self.usernames.insert(domain, username);
self
}
/// Add the default username to try.
///
/// The default username if read from the `USER` or `USERNAME` environment variable.
pub fn add_default_username(self) -> Self {
if let Ok(username) = std::env::var("USER").or_else(|_| std::env::var("USERNAME")) {
self.add_username("*", username)
} else {
self
}
}
/// Configure if the SSH agent should be used for public key authentication.
pub fn try_ssh_agent(mut self, enable: bool) -> Self {
self.try_ssh_agent = enable;
self
}
/// Add a private key to use for public key authentication.
///
/// The key will be read from disk by `git2`, so it must still exist when the authentication is performed.
///
/// You can provide a password for decryption of the private key.
/// If no password is provided and the `Self::prompt_ssh_key_password()` is enabled,
/// the user will be prompted for the passphrase of encrypted keys.
/// Note that currently only the `OpenSSH` private key format is supported for detecting that a key is encrypted.
///
/// A matching `.pub` file will also be read if it exists.
/// For example, if you add the private key `"foo/my_ssh_id"`,
/// then `"foo/my_ssh_id.pub"` will be used too, if it exists.
pub fn add_ssh_key_from_file(mut self, private_key: impl Into<PathBuf>, password: impl Into<Option<String>>) -> Self {
let private_key = private_key.into();
let public_key = get_pub_key_path(&private_key);
let password = password.into();
self.ssh_keys.push(PrivateKeyFile {
private_key,
public_key,
password,
});
self
}
/// Add all default SSH keys for public key authentication.
///
/// This will add all of the following files, if they exist:
///
/// * `"$HOME/.ssh/id_rsa"`
/// * `"$HOME/.ssh/id_ecdsa"`
/// * `"$HOME/.ssh/id_ecdsa_sk"`
/// * `"$HOME/.ssh/id_ed25519"`
/// * `"$HOME/.ssh/id_ed25519_sk"`
/// * `"$HOME/.ssh/id_dsa"`
pub fn add_default_ssh_keys(mut self) -> Self {
let ssh_dir = match dirs::home_dir() {
Some(x) => x.join(".ssh"),
None => return self,
};
let candidates = [
"id_rsa",
"id_ecdsa,",
"id_ecdsa_sk",
"id_ed25519",
"id_ed25519_sk",
"id_dsa",
];
for candidate in candidates {
let private_key = ssh_dir.join(candidate);
if !private_key.is_file() {
continue;
}
self = self.add_ssh_key_from_file(private_key, None);
}
self
}
/// Prompt for passwords for encrypted SSH keys if needed.
///
/// By default, if an `askpass` helper is configured, it will be used for the prompts.
/// Otherwise, the user will be prompted directly on the terminal of the current process.
/// If there is also no terminal available, the prompt is skipped.
///
/// An `askpass` helper can be configured in the `GIT_ASKPASS` environment variable,
/// the `core.askPass` configuration value or the `SSH_ASKPASS` environment variable.
///
/// You can override the prompt behaviour by calling [`Self::set_prompter()`].
pub fn prompt_ssh_key_password(mut self, enable: bool) -> Self {
self.prompt_ssh_key_password = enable;
self
}
/// Get the credentials callback to use for [`git2::Credentials`].
///
/// # Example: Fetch from a remote with authentication
/// ```no_run
/// # fn foo(repo: &mut git2::Repository) -> Result<(), git2::Error> {
/// use auth_git2::GitAuthenticator;
///
/// let auth = GitAuthenticator::default();
/// let git_config = repo.config()?;
/// let mut fetch_options = git2::FetchOptions::new();
/// let mut remote_callbacks = git2::RemoteCallbacks::new();
///
/// remote_callbacks.credentials(auth.credentials(&git_config));
/// fetch_options.remote_callbacks(remote_callbacks);
///
/// repo.find_remote("origin")?
/// .fetch(&["main"], Some(&mut fetch_options), None)?;
/// # Ok(())
/// # }
/// ```
pub fn credentials<'a>(
&'a self,
git_config: &'a git2::Config,
) -> impl 'a + FnMut(&str, Option<&str>, git2::CredentialType) -> Result<git2::Cred, git2::Error> {
make_credentials_callback(self, git_config)
}
/// Clone a repository using the git authenticator.
///
/// If you need more control over the clone options,
/// use [`Self::credentials()`] with a [`git2::build::RepoBuilder`].
pub fn clone_repo(&self, url: impl AsRef<str>, into: impl AsRef<Path>) -> Result<git2::Repository, git2::Error> {
let url = url.as_ref();
let into = into.as_ref();
let git_config = git2::Config::open_default()?;
let mut repo_builder = git2::build::RepoBuilder::new();
let mut fetch_options = git2::FetchOptions::new();
let mut remote_callbacks = git2::RemoteCallbacks::new();
remote_callbacks.credentials(self.credentials(&git_config));
fetch_options.remote_callbacks(remote_callbacks);
repo_builder.fetch_options(fetch_options);
repo_builder.clone(url, into)
}
/// Fetch from a remote using the git authenticator.
///
/// If you need more control over the fetch options,
/// use [`Self::credentials()`] with [`git2::Remote::fetch()`].
pub fn fetch(&self, repo: &git2::Repository, remote: &mut git2::Remote, refspecs: &[&str], reflog_msg: Option<&str>) -> Result<(), git2::Error> {
let git_config = repo.config()?;
let mut fetch_options = git2::FetchOptions::new();
let mut remote_callbacks = git2::RemoteCallbacks::new();
remote_callbacks.credentials(self.credentials(&git_config));
fetch_options.remote_callbacks(remote_callbacks);
remote.fetch(refspecs, Some(&mut fetch_options), reflog_msg)
}
/// Download and index the packfile from a remote using the git authenticator.
///
/// If you need more control over the download options,
/// use [`Self::credentials()`] with [`git2::Remote::download()`].
///
/// This function does not update the remote tracking branches.
/// Consider using [`Self::fetch()`] if that is what you want.
pub fn download(&self, repo: &git2::Repository, remote: &mut git2::Remote, refspecs: &[&str]) -> Result<(), git2::Error> {
let git_config = repo.config()?;
let mut fetch_options = git2::FetchOptions::new();
let mut remote_callbacks = git2::RemoteCallbacks::new();
remote_callbacks.credentials(self.credentials(&git_config));
fetch_options.remote_callbacks(remote_callbacks);
remote.download(refspecs, Some(&mut fetch_options))
}
/// Push to a remote using the git authenticator.
///
/// If you need more control over the push options,
/// use [`Self::credentials()`] with [`git2::Remote::push()`].
pub fn push(&self, repo: &git2::Repository, remote: &mut git2::Remote, refspecs: &[&str]) -> Result<(), git2::Error> {
let git_config = repo.config()?;
let mut push_options = git2::PushOptions::new();
let mut remote_callbacks = git2::RemoteCallbacks::new();
remote_callbacks.credentials(self.credentials(&git_config));
push_options.remote_callbacks(remote_callbacks);
remote.push(refspecs, Some(&mut push_options))
}
/// Get the configured username for a URL.
fn get_username(&self, url: &str) -> Option<&str> {
if let Some(domain) = domain_from_url(url) {
if let Some(username) = self.usernames.get(domain) {
return Some(username);
}
}
self.usernames.get("*").map(|x| x.as_str())
}
/// Get the configured plaintext credentials for a URL.
fn get_plaintext_credentials(&self, url: &str) -> Option<&PlaintextCredentials> {
if let Some(domain) = domain_from_url(url) {
if let Some(credentials) = self.plaintext_credentials.get(domain) {
return Some(credentials);
}
}
self.plaintext_credentials.get("*")
}
}
fn make_credentials_callback<'a>(
authenticator: &'a GitAuthenticator,
git_config: &'a git2::Config,
) -> impl 'a + FnMut(&str, Option<&str>, git2::CredentialType) -> Result<git2::Cred, git2::Error> {
let mut try_cred_helper = authenticator.try_cred_helper;
let mut try_password_prompt = authenticator.try_password_prompt;
let mut try_ssh_agent = authenticator.try_ssh_agent;
let mut ssh_keys = authenticator.ssh_keys.iter();
let mut prompter = authenticator.prompter.clone();
move |url: &str, username: Option<&str>, allowed: git2::CredentialType| {
trace!("credentials callback called with url: {url:?}, username: {username:?}, allowed_credentials: {allowed:?}");
// If git2 is asking for a username, we got an SSH url without username specified.
// After we supply a username, it will ask for the real credentials.
//
// Sadly, we can not switch usernames during an authentication session,
// so to try different usernames, we need to retry the git operation multiple times.
// If this happens, we'll bail and go into stage 2.
if allowed.contains(git2::CredentialType::USERNAME) {
if let Some(username) = authenticator.get_username(url) {
debug!("credentials_callback: returning username: {username:?}");
match git2::Cred::username(username) {
Ok(x) => return Ok(x),
Err(e) => {
debug!("credentials_callback: failed to wrap username: {e}");
return Err(e);
},
}
}
}
// Try public key authentication.
if allowed.contains(git2::CredentialType::SSH_KEY) {
if let Some(username) = username {
if try_ssh_agent {
try_ssh_agent = false;
debug!("credentials_callback: trying ssh_key_from_agent with username: {username:?}");
match git2::Cred::ssh_key_from_agent(username) {
Ok(x) => return Ok(x),
Err(e) => debug!("credentials_callback: failed to use SSH agent: {e}"),
}
}
#[allow(clippy::while_let_on_iterator)] // Incorrect lint: we're not consuming the iterator.
while let Some(key) = ssh_keys.next() {
debug!("credentials_callback: trying ssh key, username: {username:?}, private key: {:?}", key.private_key);
let prompter = Some(prompter.as_prompter_mut())
.filter(|_| authenticator.prompt_ssh_key_password);
match key.to_credentials(username, prompter, git_config) {
Ok(x) => return Ok(x),
Err(e) => debug!("credentials_callback: failed to use SSH key from file {:?}: {e}", key.private_key),
}
}
}
}
// Sometimes libgit2 will ask for a username/password in plaintext.
if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
// Try provided plaintext credentials first.
if let Some(credentials) = authenticator.get_plaintext_credentials(url) {
debug!("credentials_callback: trying plain text credentials with username: {:?}", credentials.username);
match credentials.to_credentials() {
Ok(x) => return Ok(x),
Err(e) => {
debug!("credentials_callback: failed to wrap plain text credentials: {e}");
return Err(e);
},
}
}
// Try the git credential helper.
if try_cred_helper {
try_cred_helper = false;
debug!("credentials_callback: trying credential_helper");
match git2::Cred::credential_helper(git_config, url, username) {
Ok(x) => return Ok(x),
Err(e) => debug!("credentials_callback: failed to use credential helper: {e}"),
}
}
// Prompt the user on the terminal.
if try_password_prompt > 0 {
try_password_prompt -= 1;
let credentials = PlaintextCredentials::prompt(
prompter.as_prompter_mut(),
username,
url,
git_config
);
if let Some(credentials) = credentials {
return credentials.to_credentials();
}
}
}
Err(git2::Error::from_str("all authentication attempts failed"))
}
}
#[derive(Debug, Clone)]
struct PrivateKeyFile {
private_key: PathBuf,
public_key: Option<PathBuf>,
password: Option<String>,
}
impl PrivateKeyFile {
fn to_credentials(&self, username: &str, prompter: Option<&mut dyn Prompter>, git_config: &git2::Config) -> Result<git2::Cred, git2::Error> {
if let Some(password) = &self.password {
git2::Cred::ssh_key(username, self.public_key.as_deref(), &self.private_key, Some(password))
} else if let Some(prompter) = prompter {
let password = match ssh_key::analyze_ssh_key_file(&self.private_key) {
Err(e) => {
warn!("Failed to analyze SSH key: {}: {}", self.private_key.display(), e);
None
},
Ok(key_info) => {
if key_info.encrypted {
prompter.prompt_ssh_key_passphrase(&self.private_key, git_config)
} else {
None
}
},
};
git2::Cred::ssh_key(username, self.public_key.as_deref(), &self.private_key, password.as_deref())
} else {
git2::Cred::ssh_key(username, self.public_key.as_deref(), &self.private_key, None)
}
}
}
#[derive(Debug, Clone)]
struct PlaintextCredentials {
username: String,
password: String,
}
impl PlaintextCredentials {
fn prompt(prompter: &mut dyn Prompter, username: Option<&str>, url: &str, git_config: &git2::Config) -> Option<Self> {
if let Some(username) = username {
let password = prompter.prompt_password(username, url, git_config)?;
Some(Self {
username: username.into(),
password,
})
} else {
let (username, password) = prompter.prompt_username_password(url, git_config)?;
Some(Self {
username,
password,
})
}
}
fn to_credentials(&self) -> Result<git2::Cred, git2::Error> {
git2::Cred::userpass_plaintext(&self.username, &self.password)
}
}
fn get_pub_key_path(priv_key_path: &Path) -> Option<PathBuf> {
let name = priv_key_path.file_name()?;
let name = name.to_str()?;
let pub_key_path = priv_key_path.with_file_name(format!("{name}.pub"));
if pub_key_path.is_file() {
Some(pub_key_path)
} else {
None
}
}
fn domain_from_url(url: &str) -> Option<&str> {
// We support:
// Relative paths
// Real URLs: scheme://[user[:pass]@]host/path
// SSH URLs: [user@]host:path.
// If there is no colon: URL is a relative path and there is no domain (or need for credentials).
let (head, tail) = url.split_once(':')?;
// Real URL
if let Some(tail) = tail.strip_prefix("//") {
let (_credentials, tail) = tail.split_once('@').unwrap_or(("", tail));
let (host, _path) = tail.split_once('/').unwrap_or((tail, ""));
Some(host)
// SSH "URL"
} else {
let (_credentials, host) = head.split_once('@').unwrap_or(("", head));
Some(host)
}
}
#[cfg(test)]
mod test {
use super::*;
use assert2::assert;
#[test]
fn test_domain_from_url() {
assert!(let Some("host") = domain_from_url("user@host:path"));
assert!(let Some("host") = domain_from_url("host:path"));
assert!(let Some("host") = domain_from_url("host:path@with:stuff"));
assert!(let Some("host") = domain_from_url("ssh://user:pass@host/path"));
assert!(let Some("host") = domain_from_url("ssh://user@host/path"));
assert!(let Some("host") = domain_from_url("ssh://host/path"));
assert!(let None = domain_from_url("some/relative/path"));
assert!(let None = domain_from_url("some/relative/path@with-at-sign"));
}
#[test]
fn test_that_authenticator_is_send() {
let authenticator = GitAuthenticator::new();
let thread = std::thread::spawn(move || {
drop(authenticator);
});
thread.join().unwrap();
}
}

65
src/prompter.rs Normal file
View file

@ -0,0 +1,65 @@
use std::path::Path;
/// Trait for customizing user prompts.
///
/// You can provide an implementor of this trait to customize the way a user is prompted for credentials and passphrases.
pub trait Prompter: Send {
/// Promp the user for a username and password.
///
/// If the prompt fails or the user fails to provide the requested information, this function should return `None`.
fn prompt_username_password(&mut self, url: &str, git_config: &git2::Config) -> Option<(String, String)>;
/// Promp the user for a password when the username is already known.
///
/// If the prompt fails or the user fails to provide the requested information, this function should return `None`.
fn prompt_password(&mut self, username: &str, url: &str, git_config: &git2::Config) -> Option<String>;
/// Promp the user for the passphrase of an encrypted SSH key.
///
/// If the prompt fails or the user fails to provide the requested information, this function should return `None`.
fn prompt_ssh_key_passphrase(&mut self, private_key_path: &Path, git_config: &git2::Config) -> Option<String>;
}
/// Wrap a clonable [`Prompter`] in a `Box<dyn MakePrompter>`.
pub(crate) fn wrap_prompter<P>(prompter: P) -> Box<dyn ClonePrompter>
where
P: Prompter + Clone + 'static,
{
Box::new(prompter)
}
/// Trait to allow making clones of a `Box<dyn Prompter + Send>`.
pub(crate) trait ClonePrompter: Prompter {
/// Clone the `Box<dyn ClonePrompter>`.
fn dyn_clone(&self) -> Box<dyn ClonePrompter>;
/// Get `self` as plain `Prompter`.
fn as_prompter(&self) -> &dyn Prompter;
/// Get `self` as plain `Prompter`.
fn as_prompter_mut(&mut self) -> &mut dyn Prompter;
}
/// Implement `ClonePrompter` for clonable Prompters.
impl<P> ClonePrompter for P
where
P: Prompter + Clone + 'static,
{
fn dyn_clone(&self) -> Box<dyn ClonePrompter> {
Box::new(self.clone())
}
fn as_prompter(&self) -> &dyn Prompter {
self
}
fn as_prompter_mut(&mut self) -> &mut dyn Prompter {
self
}
}
impl Clone for Box<dyn ClonePrompter> {
fn clone(&self) -> Self {
self.dyn_clone()
}
}

156
src/ssh_key.rs Normal file
View file

@ -0,0 +1,156 @@
use std::path::Path;
use crate::base64_decode;
/// An error that can occur when analyzing SSH keys.
#[derive(Debug)]
pub enum Error {
/// Failed to open the key file.
OpenFile(std::io::Error),
/// Failed to read from the key file.
ReadFile(std::io::Error),
/// Missing PEM trailer in the file (there was a PEM header).
MissingPemTrailer,
/// The key is not valid somehow.
MalformedKey,
/// There was an invalid base64 blob in the key.
Base64(base64_decode::Error),
}
/// The format of a key file.
pub enum KeyFormat {
/// We don't know what format it is.
Unknown,
/// It's an openssh-key-v1 file.
///
/// See https://coolaj86.com/articles/the-openssh-private-key-format/ for a description of the format.
OpensshKeyV1,
}
/// Information about a key file.
pub struct KeyInfo {
/// The format of the key file.
pub format: KeyFormat,
/// Is the key encrypted?
pub encrypted: bool,
}
/// Analyze an SSH key file.
pub fn analyze_ssh_key_file(priv_key_path: &Path) -> Result<KeyInfo, Error> {
use std::io::Read;
let mut buffer = Vec::new();
let mut file = std::fs::File::open(priv_key_path)
.map_err(Error::OpenFile)?;
file.read_to_end(&mut buffer)
.map_err(Error::ReadFile)?;
analyze_pem_openssh_key(&buffer)
}
/// Analyze a PEM encoded openssh-key-v1 file.
fn analyze_pem_openssh_key(data: &[u8]) -> Result<KeyInfo, Error> {
let data = trim_bytes(data);
let data = match data.strip_prefix(b"-----BEGIN OPENSSH PRIVATE KEY-----") {
Some(x) => x,
None => return Ok(KeyInfo { format: KeyFormat::Unknown, encrypted: false }),
};
let data = match data.strip_suffix(b"-----END OPENSSH PRIVATE KEY-----") {
Some(x) => x,
None => return Err(Error::MissingPemTrailer),
};
let data = base64_decode::base64_decode(data).map_err(Error::Base64)?;
analyze_binary_openssh_key(&data)
}
/// Analyze a binary openss-key-v1 blob.
fn analyze_binary_openssh_key(data: &[u8]) -> Result<KeyInfo, Error> {
let tail = data.strip_prefix(b"openssh-key-v1\0")
.ok_or(Error::MalformedKey)?;
if tail.len() <= 4 {
return Err(Error::MalformedKey);
}
let (cipher_len, tail) = tail.split_at(4);
let cipher_len = u32::from_be_bytes(cipher_len.try_into().unwrap()) as usize;
if tail.len() < cipher_len {
return Err(Error::MalformedKey);
}
let cipher = &tail[..cipher_len];
let encrypted = cipher != b"none";
Ok(KeyInfo { format: KeyFormat::OpensshKeyV1, encrypted })
}
/// Trim whitespace from the start and end of a byte slice.
fn trim_bytes(data: &[u8]) -> &[u8] {
let data = match data.iter().position(|b| !b.is_ascii_whitespace()) {
Some(x) => &data[x..],
None => return b"",
};
let data = match data.iter().rposition(|b| !b.is_ascii_whitespace()) {
Some(x) => &data[..=x],
None => return b"",
};
data
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OpenFile(e) => write!(f, "Failed to open file: {e}"),
Self::ReadFile(e) => write!(f, "Failed to read from file: {e}"),
Self::MissingPemTrailer => write!(f, "Missing PEM trailer in key file"),
Self::MalformedKey => write!(f, "Invalid or malformed key file"),
Self::Base64(e) => write!(f, "Invalid base64 in key file: {e}"),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use assert2::assert;
#[test]
fn test_is_encrypted_pem_openssh_key() {
// Encrypted OpenSSH key.
assert!(let Ok(KeyInfo { format: KeyFormat::OpensshKeyV1, encrypted: true }) = analyze_pem_openssh_key(concat!(
"-----BEGIN OPENSSH PRIVATE KEY-----\n",
"b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBddrJWnj\n",
"6eysG+DqTberHEAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIARNG0xAyCq6/OFQ\n",
"8eQFG1zKYlhtLLz2GC3Sou+C9PTmAAAAoGPGz6ZQhBk8FL4MRDaGsaZuVkPAn/+curIR7r\n",
"rDoXPAf0/7S2dVWY0gUjolhwlqGFnps4NgukXtKNs4qlAJiVAY/kKPr0fN+ZScuNuKP/Im\n",
"JbFoNPRaakzgbBwj9/UTpwNgUJa+3fu25l1RMLlrx7OjkQKAHBb6VMsGqH8k9rAEsCCBUK\n",
"XVJQOMAfa214eo9wgHD06ZnIlk3jS++3hzyUs=\n",
"-----END OPENSSH PRIVATE KEY-----\n",
).as_bytes()));
// Encrypted OpenSSH key with extra random whitespace.
assert!(let Ok(KeyInfo { format: KeyFormat::OpensshKeyV1, encrypted: true }) = analyze_pem_openssh_key(concat!(
" \n\t\r-----BEGIN OPENSSH PRIVATE KEY-----\n",
"b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBddrJWnj\n",
"6eysG+DqTberHEAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIARNG0xAyCq6/OFQ\n \r",
"8eQFG1zKYlhtLLz2GC3Sou+ C9PTmAAAAoGPGz6ZQhBk8FL4MRDaGsaZuVkPAn/+curIR7r\n",
"rDoXPAf0/7S2dVWY0gUjolhwlqGFnps4NgukXtKNs4qlAJiVAY/kKPr0fN+ZScuNuKP/Im\n",
"JbFoNPRaakzgbBwj9/UTpwNgUJa+3fu25l1RMLlrx7OjkQKAHBb6VMsGqH8k9rAEsCCBUK\n",
"XVJQOMAfa214eo9wgHD06ZnIlk3jS++3hzyUs=\n",
"-----END OPENSSH PRIVATE KEY-----",
).as_bytes()));
// Unencrypted OpenSSH key.
assert!(let Ok(KeyInfo { format: KeyFormat::OpensshKeyV1, encrypted: false }) = analyze_pem_openssh_key(concat!(
"-----BEGIN OPENSSH PRIVATE KEY-----\n",
"b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n",
"QyNTUxOQAAACDTKM0+RYzELoLewv5n5UoEPhmCpwkrtXM4GpWUVF+w3AAAAJhSNRa9UjUW\n",
"vQAAAAtzc2gtZWQyNTUxOQAAACDTKM0+RYzELoLewv5n5UoEPhmCpwkrtXM4GpWUVF+w3A\n",
"AAAECZObXz1xTSvl4vpLsMVTuhjroyDteKlW+Uun0yIMl7edMozT5FjMQugt7C/mflSgQ+\n",
"GYKnCSu1czgalZRUX7DcAAAAEW1hYXJ0ZW5AbWFnbmV0cm9uAQIDBA==\n",
"-----END OPENSSH PRIVATE KEY-----\n",
).as_bytes()));
}
}