From a24a8479a9bdbb9412b80b5e17d8dd9f21768d8f Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Thu, 12 Dec 2024 14:14:20 +0000 Subject: [PATCH] cli: add ssh key handling to clone --- src/cli/cmd_clone.c | 151 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 140 insertions(+), 11 deletions(-) diff --git a/src/cli/cmd_clone.c b/src/cli/cmd_clone.c index c18cb28d4..473c723b6 100644 --- a/src/cli/cmd_clone.c +++ b/src/cli/cmd_clone.c @@ -12,6 +12,8 @@ #include "error.h" #include "sighandler.h" #include "progress.h" +#include "console.h" +#include "system.h" #include "fs_path.h" #include "futils.h" @@ -21,7 +23,6 @@ static char *branch, *remote_path, *local_path, *depth; static int quiet, checkout = 1, bare; static bool local_path_exists; -static cli_progress progress = CLI_PROGRESS_INIT; static const cli_opt_spec opts[] = { CLI_COMMON_OPT, @@ -44,6 +45,16 @@ static const cli_opt_spec opts[] = { { 0 } }; +#define CREDENTIAL_RETRY_MAX 3 + +struct clone_callback_data { + cli_progress progress; + size_t credential_retries; + git_str password; +}; + +static struct clone_callback_data callback_data = { CLI_PROGRESS_INIT }; + static void print_help(void) { cli_opt_usage_fprint(stdout, PROGRAM_NAME, COMMAND_NAME, opts, 0); @@ -105,7 +116,7 @@ static void cleanup(void) { int rmdir_flags = GIT_RMDIR_REMOVE_FILES; - cli_progress_abort(&progress); + cli_progress_abort(&callback_data.progress); if (local_path_exists) rmdir_flags |= GIT_RMDIR_SKIP_ROOT; @@ -122,6 +133,119 @@ static void interrupt_cleanup(void) exit(130); } +static int find_keys(git_str *pub, git_str *priv) +{ + git_str path = GIT_STR_INIT; + static const char *key_paths[6] = { + "id_dsa", "id_ecdsa", "id_ecdsa_sk", + "id_ed25519", "id_ed25519_sk", "id_rsa" + }; + size_t i, path_len; + int error = -1; + + if (git_system_homedir(&path) < 0) + goto done; + + path_len = git_str_len(&path); + + for (i = 0; i < ARRAY_SIZE(key_paths); i++) { + git_str_truncate(&path, path_len); + + if (git_str_puts(&path, "/.ssh/") < 0 || + git_str_puts(&path, key_paths[i]) < 0) + goto done; + + if (git_fs_path_exists(path.ptr)) { + if (git_str_puts(priv, path.ptr) < 0 || + git_str_puts(pub, path.ptr) < 0 || + git_str_puts(pub, ".pub") < 0) + goto done; + + error = 0; + goto done; + } + } + + error = GIT_ENOTFOUND; + +done: + git_str_dispose(&path); + return error; +} + +static int clone_credentials( + git_credential **out, + const char *url, + const char *username_from_url, + unsigned int allowed_types, + void *payload) +{ + struct clone_callback_data *data = (struct clone_callback_data *)payload; + git_str pubkey = GIT_STR_INIT, privkey = GIT_STR_INIT, + prompt = GIT_STR_INIT; + int error = GIT_PASSTHROUGH; + + GIT_UNUSED(url); + + if (++data->credential_retries > CREDENTIAL_RETRY_MAX) { + cli_error("authentication failed"); + error = GIT_EUSER; + goto done; + } + + if ((allowed_types & GIT_CREDENTIAL_SSH_KEY)) { + if ((error = find_keys(&pubkey, &privkey)) < 0) { + if (error == GIT_ENOTFOUND) { + cli_error("could not find ssh keys for authentication"); + error = GIT_EUSER; + } + + goto done; + } + + if ((error = git_str_printf(&prompt, "Enter passphrase for key '%s': ", pubkey.ptr)) < 0 || + (error = cli_console_getpass(&data->password, prompt.ptr)) < 0) + goto done; + + error = git_credential_ssh_key_new(out, + username_from_url, + pubkey.ptr, + privkey.ptr, + data->password.ptr); + } + +done: + git_str_zero(&data->password); + git_str_dispose(&prompt); + git_str_dispose(&pubkey); + git_str_dispose(&privkey); + return error; +} + +static int clone_progress_sideband(const char *str, int len, void *payload) +{ + struct clone_callback_data *data = (struct clone_callback_data *)payload; + return cli_progress_fetch_sideband(str, len, &data->progress); +} + +static int clone_progress_transfer( + const git_indexer_progress *stats, + void *payload) +{ + struct clone_callback_data *data = (struct clone_callback_data *)payload; + return cli_progress_fetch_transfer(stats, &data->progress); +} + +static void clone_progress_checkout( + const char *path, + size_t completed_steps, + size_t total_steps, + void *payload) +{ + struct clone_callback_data *data = (struct clone_callback_data *)payload; + return cli_progress_checkout(path, completed_steps, total_steps, &data->progress); +} + int cmd_clone(int argc, char **argv) { git_clone_options clone_opts = GIT_CLONE_OPTIONS_INIT; @@ -164,26 +288,31 @@ int cmd_clone(int argc, char **argv) } if (!quiet) { - clone_opts.fetch_opts.callbacks.sideband_progress = cli_progress_fetch_sideband; - clone_opts.fetch_opts.callbacks.transfer_progress = cli_progress_fetch_transfer; - clone_opts.fetch_opts.callbacks.payload = &progress; + clone_opts.fetch_opts.callbacks.credentials = clone_credentials; + clone_opts.fetch_opts.callbacks.sideband_progress = clone_progress_sideband; + clone_opts.fetch_opts.callbacks.transfer_progress = clone_progress_transfer; + clone_opts.fetch_opts.callbacks.payload = &callback_data; - clone_opts.checkout_opts.progress_cb = cli_progress_checkout; - clone_opts.checkout_opts.progress_payload = &progress; + clone_opts.checkout_opts.progress_cb = clone_progress_checkout; + clone_opts.checkout_opts.progress_payload = &callback_data; printf("Cloning into '%s'...\n", local_path); } - if (git_clone(&repo, remote_path, local_path, &clone_opts) < 0) { + if ((ret = git_clone(&repo, remote_path, local_path, &clone_opts)) < 0) { cleanup(); - ret = cli_error_git(); + + if (ret != GIT_EUSER) + ret = cli_error_git(); + goto done; } - cli_progress_finish(&progress); + cli_progress_finish(&callback_data.progress); done: - cli_progress_dispose(&progress); + cli_progress_dispose(&callback_data.progress); + git_str_zero(&callback_data.password); git__free(computed_path); git_repository_free(repo); return ret;