Adding upstream version 0.5.5.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
dde4be91ba
commit
d2d6608958
17 changed files with 2615 additions and 0 deletions
6
.cargo_vcs_info.json
Normal file
6
.cargo_vcs_info.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"git": {
|
||||
"sha1": "0f916a0b5a375b481a63e795875388f30add5390"
|
||||
},
|
||||
"path_in_vcs": ""
|
||||
}
|
22
.github/workflows/rust.yml
vendored
Normal file
22
.github/workflows/rust.yml
vendored
Normal 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
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/Cargo.lock
|
53
CHANGELOG
Normal file
53
CHANGELOG
Normal 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
785
Cargo.lock
generated
Normal 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
60
Cargo.toml
Normal 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
29
Cargo.toml.orig
generated
Normal 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
24
LICENSE.md
Normal 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
100
README.md
Normal 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
16
README.tpl
Normal 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
|
139
examples/custom-prompt-clone.rs
Normal file
139
examples/custom-prompt-clone.rs
Normal 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
146
examples/git.rs
Normal 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
110
src/base64_decode.rs
Normal 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
183
src/default_prompt.rs
Normal 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
719
src/lib.rs
Normal 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
65
src/prompter.rs
Normal 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
156
src/ssh_key.rs
Normal 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()));
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue