diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-09 13:34:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-09 13:34:27 +0000 |
commit | 4dbdc42d9e7c3968ff7f690d00680419c9b8cb0f (patch) | |
tree | 47c1d492e9c956c1cd2b74dbd3b9d8b0db44dc4e /credential.c | |
parent | Initial commit. (diff) | |
download | git-4dbdc42d9e7c3968ff7f690d00680419c9b8cb0f.tar.xz git-4dbdc42d9e7c3968ff7f690d00680419c9b8cb0f.zip |
Adding upstream version 1:2.43.0.upstream/1%2.43.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'credential.c')
-rw-r--r-- | credential.c | 548 |
1 files changed, 548 insertions, 0 deletions
diff --git a/credential.c b/credential.c new file mode 100644 index 0000000..18098bd --- /dev/null +++ b/credential.c @@ -0,0 +1,548 @@ +#include "git-compat-util.h" +#include "abspath.h" +#include "config.h" +#include "credential.h" +#include "gettext.h" +#include "string-list.h" +#include "run-command.h" +#include "url.h" +#include "prompt.h" +#include "sigchain.h" +#include "strbuf.h" +#include "urlmatch.h" +#include "git-compat-util.h" + +void credential_init(struct credential *c) +{ + struct credential blank = CREDENTIAL_INIT; + memcpy(c, &blank, sizeof(*c)); +} + +void credential_clear(struct credential *c) +{ + free(c->protocol); + free(c->host); + free(c->path); + free(c->username); + free(c->password); + free(c->oauth_refresh_token); + string_list_clear(&c->helpers, 0); + strvec_clear(&c->wwwauth_headers); + + credential_init(c); +} + +int credential_match(const struct credential *want, + const struct credential *have, int match_password) +{ +#define CHECK(x) (!want->x || (have->x && !strcmp(want->x, have->x))) + return CHECK(protocol) && + CHECK(host) && + CHECK(path) && + CHECK(username) && + (!match_password || CHECK(password)); +#undef CHECK +} + + +static int credential_from_potentially_partial_url(struct credential *c, + const char *url); + +static int credential_config_callback(const char *var, const char *value, + const struct config_context *ctx UNUSED, + void *data) +{ + struct credential *c = data; + const char *key; + + if (!skip_prefix(var, "credential.", &key)) + return 0; + + if (!value) + return config_error_nonbool(var); + + if (!strcmp(key, "helper")) { + if (*value) + string_list_append(&c->helpers, value); + else + string_list_clear(&c->helpers, 0); + } else if (!strcmp(key, "username")) { + if (!c->username_from_proto) { + free(c->username); + c->username = xstrdup(value); + } + } + else if (!strcmp(key, "usehttppath")) + c->use_http_path = git_config_bool(var, value); + + return 0; +} + +static int proto_is_http(const char *s) +{ + if (!s) + return 0; + return !strcmp(s, "https") || !strcmp(s, "http"); +} + +static void credential_describe(struct credential *c, struct strbuf *out); +static void credential_format(struct credential *c, struct strbuf *out); + +static int select_all(const struct urlmatch_item *a UNUSED, + const struct urlmatch_item *b UNUSED) +{ + return 0; +} + +static int match_partial_url(const char *url, void *cb) +{ + struct credential *c = cb; + struct credential want = CREDENTIAL_INIT; + int matches = 0; + + if (credential_from_potentially_partial_url(&want, url) < 0) + warning(_("skipping credential lookup for key: credential.%s"), + url); + else + matches = credential_match(&want, c, 0); + credential_clear(&want); + + return matches; +} + +static void credential_apply_config(struct credential *c) +{ + char *normalized_url; + struct urlmatch_config config = URLMATCH_CONFIG_INIT; + struct strbuf url = STRBUF_INIT; + + if (!c->host) + die(_("refusing to work with credential missing host field")); + if (!c->protocol) + die(_("refusing to work with credential missing protocol field")); + + if (c->configured) + return; + + config.section = "credential"; + config.key = NULL; + config.collect_fn = credential_config_callback; + config.cascade_fn = NULL; + config.select_fn = select_all; + config.fallback_match_fn = match_partial_url; + config.cb = c; + + credential_format(c, &url); + normalized_url = url_normalize(url.buf, &config.url); + + git_config(urlmatch_config_entry, &config); + string_list_clear(&config.vars, 1); + free(normalized_url); + urlmatch_config_release(&config); + strbuf_release(&url); + + c->configured = 1; + + if (!c->use_http_path && proto_is_http(c->protocol)) { + FREE_AND_NULL(c->path); + } +} + +static void credential_describe(struct credential *c, struct strbuf *out) +{ + if (!c->protocol) + return; + strbuf_addf(out, "%s://", c->protocol); + if (c->username && *c->username) + strbuf_addf(out, "%s@", c->username); + if (c->host) + strbuf_addstr(out, c->host); + if (c->path) + strbuf_addf(out, "/%s", c->path); +} + +static void credential_format(struct credential *c, struct strbuf *out) +{ + if (!c->protocol) + return; + strbuf_addf(out, "%s://", c->protocol); + if (c->username && *c->username) { + strbuf_add_percentencode(out, c->username, STRBUF_ENCODE_SLASH); + strbuf_addch(out, '@'); + } + if (c->host) + strbuf_addstr(out, c->host); + if (c->path) { + strbuf_addch(out, '/'); + strbuf_add_percentencode(out, c->path, 0); + } +} + +static char *credential_ask_one(const char *what, struct credential *c, + int flags) +{ + struct strbuf desc = STRBUF_INIT; + struct strbuf prompt = STRBUF_INIT; + char *r; + + credential_describe(c, &desc); + if (desc.len) + strbuf_addf(&prompt, "%s for '%s': ", what, desc.buf); + else + strbuf_addf(&prompt, "%s: ", what); + + r = git_prompt(prompt.buf, flags); + + strbuf_release(&desc); + strbuf_release(&prompt); + return xstrdup(r); +} + +static void credential_getpass(struct credential *c) +{ + if (!c->username) + c->username = credential_ask_one("Username", c, + PROMPT_ASKPASS|PROMPT_ECHO); + if (!c->password) + c->password = credential_ask_one("Password", c, + PROMPT_ASKPASS); +} + +int credential_read(struct credential *c, FILE *fp) +{ + struct strbuf line = STRBUF_INIT; + + while (strbuf_getline(&line, fp) != EOF) { + char *key = line.buf; + char *value = strchr(key, '='); + + if (!line.len) + break; + + if (!value) { + warning("invalid credential line: %s", key); + strbuf_release(&line); + return -1; + } + *value++ = '\0'; + + if (!strcmp(key, "username")) { + free(c->username); + c->username = xstrdup(value); + c->username_from_proto = 1; + } else if (!strcmp(key, "password")) { + free(c->password); + c->password = xstrdup(value); + } else if (!strcmp(key, "protocol")) { + free(c->protocol); + c->protocol = xstrdup(value); + } else if (!strcmp(key, "host")) { + free(c->host); + c->host = xstrdup(value); + } else if (!strcmp(key, "path")) { + free(c->path); + c->path = xstrdup(value); + } else if (!strcmp(key, "wwwauth[]")) { + strvec_push(&c->wwwauth_headers, value); + } else if (!strcmp(key, "password_expiry_utc")) { + errno = 0; + c->password_expiry_utc = parse_timestamp(value, NULL, 10); + if (c->password_expiry_utc == 0 || errno == ERANGE) + c->password_expiry_utc = TIME_MAX; + } else if (!strcmp(key, "oauth_refresh_token")) { + free(c->oauth_refresh_token); + c->oauth_refresh_token = xstrdup(value); + } else if (!strcmp(key, "url")) { + credential_from_url(c, value); + } else if (!strcmp(key, "quit")) { + c->quit = !!git_config_bool("quit", value); + } + /* + * Ignore other lines; we don't know what they mean, but + * this future-proofs us when later versions of git do + * learn new lines, and the helpers are updated to match. + */ + } + + strbuf_release(&line); + return 0; +} + +static void credential_write_item(FILE *fp, const char *key, const char *value, + int required) +{ + if (!value && required) + BUG("credential value for %s is missing", key); + if (!value) + return; + if (strchr(value, '\n')) + die("credential value for %s contains newline", key); + fprintf(fp, "%s=%s\n", key, value); +} + +void credential_write(const struct credential *c, FILE *fp) +{ + credential_write_item(fp, "protocol", c->protocol, 1); + credential_write_item(fp, "host", c->host, 1); + credential_write_item(fp, "path", c->path, 0); + credential_write_item(fp, "username", c->username, 0); + credential_write_item(fp, "password", c->password, 0); + credential_write_item(fp, "oauth_refresh_token", c->oauth_refresh_token, 0); + if (c->password_expiry_utc != TIME_MAX) { + char *s = xstrfmt("%"PRItime, c->password_expiry_utc); + credential_write_item(fp, "password_expiry_utc", s, 0); + free(s); + } + for (size_t i = 0; i < c->wwwauth_headers.nr; i++) + credential_write_item(fp, "wwwauth[]", c->wwwauth_headers.v[i], 0); +} + +static int run_credential_helper(struct credential *c, + const char *cmd, + int want_output) +{ + struct child_process helper = CHILD_PROCESS_INIT; + FILE *fp; + + strvec_push(&helper.args, cmd); + helper.use_shell = 1; + helper.in = -1; + if (want_output) + helper.out = -1; + else + helper.no_stdout = 1; + + if (start_command(&helper) < 0) + return -1; + + fp = xfdopen(helper.in, "w"); + sigchain_push(SIGPIPE, SIG_IGN); + credential_write(c, fp); + fclose(fp); + sigchain_pop(SIGPIPE); + + if (want_output) { + int r; + fp = xfdopen(helper.out, "r"); + r = credential_read(c, fp); + fclose(fp); + if (r < 0) { + finish_command(&helper); + return -1; + } + } + + if (finish_command(&helper)) + return -1; + return 0; +} + +static int credential_do(struct credential *c, const char *helper, + const char *operation) +{ + struct strbuf cmd = STRBUF_INIT; + int r; + + if (helper[0] == '!') + strbuf_addstr(&cmd, helper + 1); + else if (is_absolute_path(helper)) + strbuf_addstr(&cmd, helper); + else + strbuf_addf(&cmd, "git credential-%s", helper); + + strbuf_addf(&cmd, " %s", operation); + r = run_credential_helper(c, cmd.buf, !strcmp(operation, "get")); + + strbuf_release(&cmd); + return r; +} + +void credential_fill(struct credential *c) +{ + int i; + + if (c->username && c->password) + return; + + credential_apply_config(c); + + for (i = 0; i < c->helpers.nr; i++) { + credential_do(c, c->helpers.items[i].string, "get"); + if (c->password_expiry_utc < time(NULL)) { + /* Discard expired password */ + FREE_AND_NULL(c->password); + /* Reset expiry to maintain consistency */ + c->password_expiry_utc = TIME_MAX; + } + if (c->username && c->password) + return; + if (c->quit) + die("credential helper '%s' told us to quit", + c->helpers.items[i].string); + } + + credential_getpass(c); + if (!c->username && !c->password) + die("unable to get password from user"); +} + +void credential_approve(struct credential *c) +{ + int i; + + if (c->approved) + return; + if (!c->username || !c->password || c->password_expiry_utc < time(NULL)) + return; + + credential_apply_config(c); + + for (i = 0; i < c->helpers.nr; i++) + credential_do(c, c->helpers.items[i].string, "store"); + c->approved = 1; +} + +void credential_reject(struct credential *c) +{ + int i; + + credential_apply_config(c); + + for (i = 0; i < c->helpers.nr; i++) + credential_do(c, c->helpers.items[i].string, "erase"); + + FREE_AND_NULL(c->username); + FREE_AND_NULL(c->password); + FREE_AND_NULL(c->oauth_refresh_token); + c->password_expiry_utc = TIME_MAX; + c->approved = 0; +} + +static int check_url_component(const char *url, int quiet, + const char *name, const char *value) +{ + if (!value) + return 0; + if (!strchr(value, '\n')) + return 0; + + if (!quiet) + warning(_("url contains a newline in its %s component: %s"), + name, url); + return -1; +} + +/* + * Potentially-partial URLs can, but do not have to, contain + * + * - a protocol (or scheme) of the form "<protocol>://" + * + * - a host name (the part after the protocol and before the first slash after + * that, if any) + * + * - a user name and potentially a password (as "<user>[:<password>]@" part of + * the host name) + * + * - a path (the part after the host name, if any, starting with the slash) + * + * Missing parts will be left unset in `struct credential`. Thus, `https://` + * will have only the `protocol` set, `example.com` only the host name, and + * `/git` only the path. + * + * Note that an empty host name in an otherwise fully-qualified URL (e.g. + * `cert:///path/to/cert.pem`) will be treated as unset if we expect the URL to + * be potentially partial, and only then (otherwise, the empty string is used). + * + * The credential_from_url() function does not allow partial URLs. + */ +static int credential_from_url_1(struct credential *c, const char *url, + int allow_partial_url, int quiet) +{ + const char *at, *colon, *cp, *slash, *host, *proto_end; + + credential_clear(c); + + /* + * Match one of: + * (1) proto://<host>/... + * (2) proto://<user>@<host>/... + * (3) proto://<user>:<pass>@<host>/... + */ + proto_end = strstr(url, "://"); + if (!allow_partial_url && (!proto_end || proto_end == url)) { + if (!quiet) + warning(_("url has no scheme: %s"), url); + return -1; + } + cp = proto_end ? proto_end + 3 : url; + at = strchr(cp, '@'); + colon = strchr(cp, ':'); + + /* + * A query or fragment marker before the slash ends the host portion. + * We'll just continue to call this "slash" for simplicity. Notably our + * "trim leading slashes" part won't skip over this part of the path, + * but that's what we'd want. + */ + slash = cp + strcspn(cp, "/?#"); + + if (!at || slash <= at) { + /* Case (1) */ + host = cp; + } + else if (!colon || at <= colon) { + /* Case (2) */ + c->username = url_decode_mem(cp, at - cp); + if (c->username && *c->username) + c->username_from_proto = 1; + host = at + 1; + } else { + /* Case (3) */ + c->username = url_decode_mem(cp, colon - cp); + if (c->username && *c->username) + c->username_from_proto = 1; + c->password = url_decode_mem(colon + 1, at - (colon + 1)); + host = at + 1; + } + + if (proto_end && proto_end - url > 0) + c->protocol = xmemdupz(url, proto_end - url); + if (!allow_partial_url || slash - host > 0) + c->host = url_decode_mem(host, slash - host); + /* Trim leading and trailing slashes from path */ + while (*slash == '/') + slash++; + if (*slash) { + char *p; + c->path = url_decode(slash); + p = c->path + strlen(c->path) - 1; + while (p > c->path && *p == '/') + *p-- = '\0'; + } + + if (check_url_component(url, quiet, "username", c->username) < 0 || + check_url_component(url, quiet, "password", c->password) < 0 || + check_url_component(url, quiet, "protocol", c->protocol) < 0 || + check_url_component(url, quiet, "host", c->host) < 0 || + check_url_component(url, quiet, "path", c->path) < 0) + return -1; + + return 0; +} + +static int credential_from_potentially_partial_url(struct credential *c, + const char *url) +{ + return credential_from_url_1(c, url, 1, 0); +} + +int credential_from_url_gently(struct credential *c, const char *url, int quiet) +{ + return credential_from_url_1(c, url, 0, quiet); +} + +void credential_from_url(struct credential *c, const char *url) +{ + if (credential_from_url_gently(c, url, 0) < 0) + die(_("credential url cannot be parsed: %s"), url); +} |