credential: add support for multistage credential rounds
[alt-git.git] / credential.c
blob98b040cf11a0c569901265c197d07dc326d8b9d4
1 #include "git-compat-util.h"
2 #include "abspath.h"
3 #include "config.h"
4 #include "credential.h"
5 #include "gettext.h"
6 #include "string-list.h"
7 #include "run-command.h"
8 #include "url.h"
9 #include "prompt.h"
10 #include "sigchain.h"
11 #include "strbuf.h"
12 #include "urlmatch.h"
13 #include "git-compat-util.h"
15 void credential_init(struct credential *c)
17 struct credential blank = CREDENTIAL_INIT;
18 memcpy(c, &blank, sizeof(*c));
21 void credential_clear(struct credential *c)
23 free(c->protocol);
24 free(c->host);
25 free(c->path);
26 free(c->username);
27 free(c->password);
28 free(c->credential);
29 free(c->oauth_refresh_token);
30 free(c->authtype);
31 string_list_clear(&c->helpers, 0);
32 strvec_clear(&c->wwwauth_headers);
33 strvec_clear(&c->state_headers);
34 strvec_clear(&c->state_headers_to_send);
36 credential_init(c);
39 void credential_next_state(struct credential *c)
41 strvec_clear(&c->state_headers_to_send);
42 SWAP(c->state_headers, c->state_headers_to_send);
45 void credential_clear_secrets(struct credential *c)
47 FREE_AND_NULL(c->password);
48 FREE_AND_NULL(c->credential);
51 static void credential_set_capability(struct credential_capability *capa,
52 enum credential_op_type op_type)
54 switch (op_type) {
55 case CREDENTIAL_OP_INITIAL:
56 capa->request_initial = 1;
57 break;
58 case CREDENTIAL_OP_HELPER:
59 capa->request_helper = 1;
60 break;
61 case CREDENTIAL_OP_RESPONSE:
62 capa->response = 1;
63 break;
68 void credential_set_all_capabilities(struct credential *c,
69 enum credential_op_type op_type)
71 credential_set_capability(&c->capa_authtype, op_type);
72 credential_set_capability(&c->capa_state, op_type);
75 int credential_match(const struct credential *want,
76 const struct credential *have, int match_password)
78 #define CHECK(x) (!want->x || (have->x && !strcmp(want->x, have->x)))
79 return CHECK(protocol) &&
80 CHECK(host) &&
81 CHECK(path) &&
82 CHECK(username) &&
83 (!match_password || CHECK(password));
84 #undef CHECK
88 static int credential_from_potentially_partial_url(struct credential *c,
89 const char *url);
91 static int credential_config_callback(const char *var, const char *value,
92 const struct config_context *ctx UNUSED,
93 void *data)
95 struct credential *c = data;
96 const char *key;
98 if (!skip_prefix(var, "credential.", &key))
99 return 0;
101 if (!value)
102 return config_error_nonbool(var);
104 if (!strcmp(key, "helper")) {
105 if (*value)
106 string_list_append(&c->helpers, value);
107 else
108 string_list_clear(&c->helpers, 0);
109 } else if (!strcmp(key, "username")) {
110 if (!c->username_from_proto) {
111 free(c->username);
112 c->username = xstrdup(value);
115 else if (!strcmp(key, "usehttppath"))
116 c->use_http_path = git_config_bool(var, value);
118 return 0;
121 static int proto_is_http(const char *s)
123 if (!s)
124 return 0;
125 return !strcmp(s, "https") || !strcmp(s, "http");
128 static void credential_describe(struct credential *c, struct strbuf *out);
129 static void credential_format(struct credential *c, struct strbuf *out);
131 static int select_all(const struct urlmatch_item *a UNUSED,
132 const struct urlmatch_item *b UNUSED)
134 return 0;
137 static int match_partial_url(const char *url, void *cb)
139 struct credential *c = cb;
140 struct credential want = CREDENTIAL_INIT;
141 int matches = 0;
143 if (credential_from_potentially_partial_url(&want, url) < 0)
144 warning(_("skipping credential lookup for key: credential.%s"),
145 url);
146 else
147 matches = credential_match(&want, c, 0);
148 credential_clear(&want);
150 return matches;
153 static void credential_apply_config(struct credential *c)
155 char *normalized_url;
156 struct urlmatch_config config = URLMATCH_CONFIG_INIT;
157 struct strbuf url = STRBUF_INIT;
159 if (!c->host)
160 die(_("refusing to work with credential missing host field"));
161 if (!c->protocol)
162 die(_("refusing to work with credential missing protocol field"));
164 if (c->configured)
165 return;
167 config.section = "credential";
168 config.key = NULL;
169 config.collect_fn = credential_config_callback;
170 config.cascade_fn = NULL;
171 config.select_fn = select_all;
172 config.fallback_match_fn = match_partial_url;
173 config.cb = c;
175 credential_format(c, &url);
176 normalized_url = url_normalize(url.buf, &config.url);
178 git_config(urlmatch_config_entry, &config);
179 string_list_clear(&config.vars, 1);
180 free(normalized_url);
181 urlmatch_config_release(&config);
182 strbuf_release(&url);
184 c->configured = 1;
186 if (!c->use_http_path && proto_is_http(c->protocol)) {
187 FREE_AND_NULL(c->path);
191 static void credential_describe(struct credential *c, struct strbuf *out)
193 if (!c->protocol)
194 return;
195 strbuf_addf(out, "%s://", c->protocol);
196 if (c->username && *c->username)
197 strbuf_addf(out, "%s@", c->username);
198 if (c->host)
199 strbuf_addstr(out, c->host);
200 if (c->path)
201 strbuf_addf(out, "/%s", c->path);
204 static void credential_format(struct credential *c, struct strbuf *out)
206 if (!c->protocol)
207 return;
208 strbuf_addf(out, "%s://", c->protocol);
209 if (c->username && *c->username) {
210 strbuf_add_percentencode(out, c->username, STRBUF_ENCODE_SLASH);
211 strbuf_addch(out, '@');
213 if (c->host)
214 strbuf_addstr(out, c->host);
215 if (c->path) {
216 strbuf_addch(out, '/');
217 strbuf_add_percentencode(out, c->path, 0);
221 static char *credential_ask_one(const char *what, struct credential *c,
222 int flags)
224 struct strbuf desc = STRBUF_INIT;
225 struct strbuf prompt = STRBUF_INIT;
226 char *r;
228 credential_describe(c, &desc);
229 if (desc.len)
230 strbuf_addf(&prompt, "%s for '%s': ", what, desc.buf);
231 else
232 strbuf_addf(&prompt, "%s: ", what);
234 r = git_prompt(prompt.buf, flags);
236 strbuf_release(&desc);
237 strbuf_release(&prompt);
238 return xstrdup(r);
241 static void credential_getpass(struct credential *c)
243 if (!c->username)
244 c->username = credential_ask_one("Username", c,
245 PROMPT_ASKPASS|PROMPT_ECHO);
246 if (!c->password)
247 c->password = credential_ask_one("Password", c,
248 PROMPT_ASKPASS);
251 static int credential_has_capability(const struct credential_capability *capa,
252 enum credential_op_type op_type)
255 * We're checking here if each previous step indicated that we had the
256 * capability. If it did, then we want to pass it along; conversely, if
257 * it did not, we don't want to report that to our caller.
259 switch (op_type) {
260 case CREDENTIAL_OP_HELPER:
261 return capa->request_initial;
262 case CREDENTIAL_OP_RESPONSE:
263 return capa->request_initial && capa->request_helper;
264 default:
265 return 0;
269 int credential_read(struct credential *c, FILE *fp,
270 enum credential_op_type op_type)
272 struct strbuf line = STRBUF_INIT;
274 while (strbuf_getline(&line, fp) != EOF) {
275 char *key = line.buf;
276 char *value = strchr(key, '=');
278 if (!line.len)
279 break;
281 if (!value) {
282 warning("invalid credential line: %s", key);
283 strbuf_release(&line);
284 return -1;
286 *value++ = '\0';
288 if (!strcmp(key, "username")) {
289 free(c->username);
290 c->username = xstrdup(value);
291 c->username_from_proto = 1;
292 } else if (!strcmp(key, "password")) {
293 free(c->password);
294 c->password = xstrdup(value);
295 } else if (!strcmp(key, "credential")) {
296 free(c->credential);
297 c->credential = xstrdup(value);
298 } else if (!strcmp(key, "protocol")) {
299 free(c->protocol);
300 c->protocol = xstrdup(value);
301 } else if (!strcmp(key, "host")) {
302 free(c->host);
303 c->host = xstrdup(value);
304 } else if (!strcmp(key, "path")) {
305 free(c->path);
306 c->path = xstrdup(value);
307 } else if (!strcmp(key, "ephemeral")) {
308 c->ephemeral = !!git_config_bool("ephemeral", value);
309 } else if (!strcmp(key, "wwwauth[]")) {
310 strvec_push(&c->wwwauth_headers, value);
311 } else if (!strcmp(key, "state[]")) {
312 strvec_push(&c->state_headers, value);
313 } else if (!strcmp(key, "capability[]")) {
314 if (!strcmp(value, "authtype"))
315 credential_set_capability(&c->capa_authtype, op_type);
316 else if (!strcmp(value, "state"))
317 credential_set_capability(&c->capa_state, op_type);
318 } else if (!strcmp(key, "continue")) {
319 c->multistage = !!git_config_bool("continue", value);
320 } else if (!strcmp(key, "password_expiry_utc")) {
321 errno = 0;
322 c->password_expiry_utc = parse_timestamp(value, NULL, 10);
323 if (c->password_expiry_utc == 0 || errno == ERANGE)
324 c->password_expiry_utc = TIME_MAX;
325 } else if (!strcmp(key, "oauth_refresh_token")) {
326 free(c->oauth_refresh_token);
327 c->oauth_refresh_token = xstrdup(value);
328 } else if (!strcmp(key, "authtype")) {
329 free(c->authtype);
330 c->authtype = xstrdup(value);
331 } else if (!strcmp(key, "url")) {
332 credential_from_url(c, value);
333 } else if (!strcmp(key, "quit")) {
334 c->quit = !!git_config_bool("quit", value);
337 * Ignore other lines; we don't know what they mean, but
338 * this future-proofs us when later versions of git do
339 * learn new lines, and the helpers are updated to match.
343 strbuf_release(&line);
344 return 0;
347 static void credential_write_item(FILE *fp, const char *key, const char *value,
348 int required)
350 if (!value && required)
351 BUG("credential value for %s is missing", key);
352 if (!value)
353 return;
354 if (strchr(value, '\n'))
355 die("credential value for %s contains newline", key);
356 fprintf(fp, "%s=%s\n", key, value);
359 void credential_write(const struct credential *c, FILE *fp,
360 enum credential_op_type op_type)
362 if (credential_has_capability(&c->capa_authtype, op_type))
363 credential_write_item(fp, "capability[]", "authtype", 0);
364 if (credential_has_capability(&c->capa_state, op_type))
365 credential_write_item(fp, "capability[]", "state", 0);
367 if (credential_has_capability(&c->capa_authtype, op_type)) {
368 credential_write_item(fp, "authtype", c->authtype, 0);
369 credential_write_item(fp, "credential", c->credential, 0);
370 if (c->ephemeral)
371 credential_write_item(fp, "ephemeral", "1", 0);
373 credential_write_item(fp, "protocol", c->protocol, 1);
374 credential_write_item(fp, "host", c->host, 1);
375 credential_write_item(fp, "path", c->path, 0);
376 credential_write_item(fp, "username", c->username, 0);
377 credential_write_item(fp, "password", c->password, 0);
378 credential_write_item(fp, "oauth_refresh_token", c->oauth_refresh_token, 0);
379 if (c->password_expiry_utc != TIME_MAX) {
380 char *s = xstrfmt("%"PRItime, c->password_expiry_utc);
381 credential_write_item(fp, "password_expiry_utc", s, 0);
382 free(s);
384 for (size_t i = 0; i < c->wwwauth_headers.nr; i++)
385 credential_write_item(fp, "wwwauth[]", c->wwwauth_headers.v[i], 0);
386 if (credential_has_capability(&c->capa_state, op_type)) {
387 if (c->multistage)
388 credential_write_item(fp, "continue", "1", 0);
389 for (size_t i = 0; i < c->state_headers_to_send.nr; i++)
390 credential_write_item(fp, "state[]", c->state_headers_to_send.v[i], 0);
394 static int run_credential_helper(struct credential *c,
395 const char *cmd,
396 int want_output)
398 struct child_process helper = CHILD_PROCESS_INIT;
399 FILE *fp;
401 strvec_push(&helper.args, cmd);
402 helper.use_shell = 1;
403 helper.in = -1;
404 if (want_output)
405 helper.out = -1;
406 else
407 helper.no_stdout = 1;
409 if (start_command(&helper) < 0)
410 return -1;
412 fp = xfdopen(helper.in, "w");
413 sigchain_push(SIGPIPE, SIG_IGN);
414 credential_write(c, fp, want_output ? CREDENTIAL_OP_HELPER : CREDENTIAL_OP_RESPONSE);
415 fclose(fp);
416 sigchain_pop(SIGPIPE);
418 if (want_output) {
419 int r;
420 fp = xfdopen(helper.out, "r");
421 r = credential_read(c, fp, CREDENTIAL_OP_HELPER);
422 fclose(fp);
423 if (r < 0) {
424 finish_command(&helper);
425 return -1;
429 if (finish_command(&helper))
430 return -1;
431 return 0;
434 static int credential_do(struct credential *c, const char *helper,
435 const char *operation)
437 struct strbuf cmd = STRBUF_INIT;
438 int r;
440 if (helper[0] == '!')
441 strbuf_addstr(&cmd, helper + 1);
442 else if (is_absolute_path(helper))
443 strbuf_addstr(&cmd, helper);
444 else
445 strbuf_addf(&cmd, "git credential-%s", helper);
447 strbuf_addf(&cmd, " %s", operation);
448 r = run_credential_helper(c, cmd.buf, !strcmp(operation, "get"));
450 strbuf_release(&cmd);
451 return r;
454 void credential_fill(struct credential *c, int all_capabilities)
456 int i;
458 if ((c->username && c->password) || c->credential)
459 return;
461 credential_next_state(c);
462 c->multistage = 0;
464 credential_apply_config(c);
465 if (all_capabilities)
466 credential_set_all_capabilities(c, CREDENTIAL_OP_INITIAL);
468 for (i = 0; i < c->helpers.nr; i++) {
469 credential_do(c, c->helpers.items[i].string, "get");
470 if (c->password_expiry_utc < time(NULL)) {
471 /* Discard expired password */
472 FREE_AND_NULL(c->password);
473 /* Reset expiry to maintain consistency */
474 c->password_expiry_utc = TIME_MAX;
476 if ((c->username && c->password) || c->credential) {
477 strvec_clear(&c->wwwauth_headers);
478 return;
480 if (c->quit)
481 die("credential helper '%s' told us to quit",
482 c->helpers.items[i].string);
485 credential_getpass(c);
486 if (!c->username && !c->password && !c->credential)
487 die("unable to get password from user");
490 void credential_approve(struct credential *c)
492 int i;
494 if (c->approved)
495 return;
496 if (((!c->username || !c->password) && !c->credential) || c->password_expiry_utc < time(NULL))
497 return;
499 credential_next_state(c);
501 credential_apply_config(c);
503 for (i = 0; i < c->helpers.nr; i++)
504 credential_do(c, c->helpers.items[i].string, "store");
505 c->approved = 1;
508 void credential_reject(struct credential *c)
510 int i;
512 credential_next_state(c);
514 credential_apply_config(c);
516 for (i = 0; i < c->helpers.nr; i++)
517 credential_do(c, c->helpers.items[i].string, "erase");
519 FREE_AND_NULL(c->username);
520 FREE_AND_NULL(c->password);
521 FREE_AND_NULL(c->credential);
522 FREE_AND_NULL(c->oauth_refresh_token);
523 c->password_expiry_utc = TIME_MAX;
524 c->approved = 0;
527 static int check_url_component(const char *url, int quiet,
528 const char *name, const char *value)
530 if (!value)
531 return 0;
532 if (!strchr(value, '\n'))
533 return 0;
535 if (!quiet)
536 warning(_("url contains a newline in its %s component: %s"),
537 name, url);
538 return -1;
542 * Potentially-partial URLs can, but do not have to, contain
544 * - a protocol (or scheme) of the form "<protocol>://"
546 * - a host name (the part after the protocol and before the first slash after
547 * that, if any)
549 * - a user name and potentially a password (as "<user>[:<password>]@" part of
550 * the host name)
552 * - a path (the part after the host name, if any, starting with the slash)
554 * Missing parts will be left unset in `struct credential`. Thus, `https://`
555 * will have only the `protocol` set, `example.com` only the host name, and
556 * `/git` only the path.
558 * Note that an empty host name in an otherwise fully-qualified URL (e.g.
559 * `cert:///path/to/cert.pem`) will be treated as unset if we expect the URL to
560 * be potentially partial, and only then (otherwise, the empty string is used).
562 * The credential_from_url() function does not allow partial URLs.
564 static int credential_from_url_1(struct credential *c, const char *url,
565 int allow_partial_url, int quiet)
567 const char *at, *colon, *cp, *slash, *host, *proto_end;
569 credential_clear(c);
572 * Match one of:
573 * (1) proto://<host>/...
574 * (2) proto://<user>@<host>/...
575 * (3) proto://<user>:<pass>@<host>/...
577 proto_end = strstr(url, "://");
578 if (!allow_partial_url && (!proto_end || proto_end == url)) {
579 if (!quiet)
580 warning(_("url has no scheme: %s"), url);
581 return -1;
583 cp = proto_end ? proto_end + 3 : url;
584 at = strchr(cp, '@');
585 colon = strchr(cp, ':');
588 * A query or fragment marker before the slash ends the host portion.
589 * We'll just continue to call this "slash" for simplicity. Notably our
590 * "trim leading slashes" part won't skip over this part of the path,
591 * but that's what we'd want.
593 slash = cp + strcspn(cp, "/?#");
595 if (!at || slash <= at) {
596 /* Case (1) */
597 host = cp;
599 else if (!colon || at <= colon) {
600 /* Case (2) */
601 c->username = url_decode_mem(cp, at - cp);
602 if (c->username && *c->username)
603 c->username_from_proto = 1;
604 host = at + 1;
605 } else {
606 /* Case (3) */
607 c->username = url_decode_mem(cp, colon - cp);
608 if (c->username && *c->username)
609 c->username_from_proto = 1;
610 c->password = url_decode_mem(colon + 1, at - (colon + 1));
611 host = at + 1;
614 if (proto_end && proto_end - url > 0)
615 c->protocol = xmemdupz(url, proto_end - url);
616 if (!allow_partial_url || slash - host > 0)
617 c->host = url_decode_mem(host, slash - host);
618 /* Trim leading and trailing slashes from path */
619 while (*slash == '/')
620 slash++;
621 if (*slash) {
622 char *p;
623 c->path = url_decode(slash);
624 p = c->path + strlen(c->path) - 1;
625 while (p > c->path && *p == '/')
626 *p-- = '\0';
629 if (check_url_component(url, quiet, "username", c->username) < 0 ||
630 check_url_component(url, quiet, "password", c->password) < 0 ||
631 check_url_component(url, quiet, "protocol", c->protocol) < 0 ||
632 check_url_component(url, quiet, "host", c->host) < 0 ||
633 check_url_component(url, quiet, "path", c->path) < 0)
634 return -1;
636 return 0;
639 static int credential_from_potentially_partial_url(struct credential *c,
640 const char *url)
642 return credential_from_url_1(c, url, 1, 0);
645 int credential_from_url_gently(struct credential *c, const char *url, int quiet)
647 return credential_from_url_1(c, url, 0, quiet);
650 void credential_from_url(struct credential *c, const char *url)
652 if (credential_from_url_gently(c, url, 0) < 0)
653 die(_("credential url cannot be parsed: %s"), url);