]> git.ipfire.org Git - thirdparty/git.git/blob - credential.c
credential: add a field called "ephemeral"
[thirdparty/git.git] / credential.c
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"
14
15 void credential_init(struct credential *c)
16 {
17 struct credential blank = CREDENTIAL_INIT;
18 memcpy(c, &blank, sizeof(*c));
19 }
20
21 void credential_clear(struct credential *c)
22 {
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
34 credential_init(c);
35 }
36
37 static void credential_set_capability(struct credential_capability *capa,
38 enum credential_op_type op_type)
39 {
40 switch (op_type) {
41 case CREDENTIAL_OP_INITIAL:
42 capa->request_initial = 1;
43 break;
44 case CREDENTIAL_OP_HELPER:
45 capa->request_helper = 1;
46 break;
47 case CREDENTIAL_OP_RESPONSE:
48 capa->response = 1;
49 break;
50 }
51 }
52
53
54 void credential_set_all_capabilities(struct credential *c,
55 enum credential_op_type op_type)
56 {
57 credential_set_capability(&c->capa_authtype, op_type);
58 }
59
60 int credential_match(const struct credential *want,
61 const struct credential *have, int match_password)
62 {
63 #define CHECK(x) (!want->x || (have->x && !strcmp(want->x, have->x)))
64 return CHECK(protocol) &&
65 CHECK(host) &&
66 CHECK(path) &&
67 CHECK(username) &&
68 (!match_password || CHECK(password));
69 #undef CHECK
70 }
71
72
73 static int credential_from_potentially_partial_url(struct credential *c,
74 const char *url);
75
76 static int credential_config_callback(const char *var, const char *value,
77 const struct config_context *ctx UNUSED,
78 void *data)
79 {
80 struct credential *c = data;
81 const char *key;
82
83 if (!skip_prefix(var, "credential.", &key))
84 return 0;
85
86 if (!value)
87 return config_error_nonbool(var);
88
89 if (!strcmp(key, "helper")) {
90 if (*value)
91 string_list_append(&c->helpers, value);
92 else
93 string_list_clear(&c->helpers, 0);
94 } else if (!strcmp(key, "username")) {
95 if (!c->username_from_proto) {
96 free(c->username);
97 c->username = xstrdup(value);
98 }
99 }
100 else if (!strcmp(key, "usehttppath"))
101 c->use_http_path = git_config_bool(var, value);
102
103 return 0;
104 }
105
106 static int proto_is_http(const char *s)
107 {
108 if (!s)
109 return 0;
110 return !strcmp(s, "https") || !strcmp(s, "http");
111 }
112
113 static void credential_describe(struct credential *c, struct strbuf *out);
114 static void credential_format(struct credential *c, struct strbuf *out);
115
116 static int select_all(const struct urlmatch_item *a UNUSED,
117 const struct urlmatch_item *b UNUSED)
118 {
119 return 0;
120 }
121
122 static int match_partial_url(const char *url, void *cb)
123 {
124 struct credential *c = cb;
125 struct credential want = CREDENTIAL_INIT;
126 int matches = 0;
127
128 if (credential_from_potentially_partial_url(&want, url) < 0)
129 warning(_("skipping credential lookup for key: credential.%s"),
130 url);
131 else
132 matches = credential_match(&want, c, 0);
133 credential_clear(&want);
134
135 return matches;
136 }
137
138 static void credential_apply_config(struct credential *c)
139 {
140 char *normalized_url;
141 struct urlmatch_config config = URLMATCH_CONFIG_INIT;
142 struct strbuf url = STRBUF_INIT;
143
144 if (!c->host)
145 die(_("refusing to work with credential missing host field"));
146 if (!c->protocol)
147 die(_("refusing to work with credential missing protocol field"));
148
149 if (c->configured)
150 return;
151
152 config.section = "credential";
153 config.key = NULL;
154 config.collect_fn = credential_config_callback;
155 config.cascade_fn = NULL;
156 config.select_fn = select_all;
157 config.fallback_match_fn = match_partial_url;
158 config.cb = c;
159
160 credential_format(c, &url);
161 normalized_url = url_normalize(url.buf, &config.url);
162
163 git_config(urlmatch_config_entry, &config);
164 string_list_clear(&config.vars, 1);
165 free(normalized_url);
166 urlmatch_config_release(&config);
167 strbuf_release(&url);
168
169 c->configured = 1;
170
171 if (!c->use_http_path && proto_is_http(c->protocol)) {
172 FREE_AND_NULL(c->path);
173 }
174 }
175
176 static void credential_describe(struct credential *c, struct strbuf *out)
177 {
178 if (!c->protocol)
179 return;
180 strbuf_addf(out, "%s://", c->protocol);
181 if (c->username && *c->username)
182 strbuf_addf(out, "%s@", c->username);
183 if (c->host)
184 strbuf_addstr(out, c->host);
185 if (c->path)
186 strbuf_addf(out, "/%s", c->path);
187 }
188
189 static void credential_format(struct credential *c, struct strbuf *out)
190 {
191 if (!c->protocol)
192 return;
193 strbuf_addf(out, "%s://", c->protocol);
194 if (c->username && *c->username) {
195 strbuf_add_percentencode(out, c->username, STRBUF_ENCODE_SLASH);
196 strbuf_addch(out, '@');
197 }
198 if (c->host)
199 strbuf_addstr(out, c->host);
200 if (c->path) {
201 strbuf_addch(out, '/');
202 strbuf_add_percentencode(out, c->path, 0);
203 }
204 }
205
206 static char *credential_ask_one(const char *what, struct credential *c,
207 int flags)
208 {
209 struct strbuf desc = STRBUF_INIT;
210 struct strbuf prompt = STRBUF_INIT;
211 char *r;
212
213 credential_describe(c, &desc);
214 if (desc.len)
215 strbuf_addf(&prompt, "%s for '%s': ", what, desc.buf);
216 else
217 strbuf_addf(&prompt, "%s: ", what);
218
219 r = git_prompt(prompt.buf, flags);
220
221 strbuf_release(&desc);
222 strbuf_release(&prompt);
223 return xstrdup(r);
224 }
225
226 static void credential_getpass(struct credential *c)
227 {
228 if (!c->username)
229 c->username = credential_ask_one("Username", c,
230 PROMPT_ASKPASS|PROMPT_ECHO);
231 if (!c->password)
232 c->password = credential_ask_one("Password", c,
233 PROMPT_ASKPASS);
234 }
235
236 static int credential_has_capability(const struct credential_capability *capa,
237 enum credential_op_type op_type)
238 {
239 /*
240 * We're checking here if each previous step indicated that we had the
241 * capability. If it did, then we want to pass it along; conversely, if
242 * it did not, we don't want to report that to our caller.
243 */
244 switch (op_type) {
245 case CREDENTIAL_OP_HELPER:
246 return capa->request_initial;
247 case CREDENTIAL_OP_RESPONSE:
248 return capa->request_initial && capa->request_helper;
249 default:
250 return 0;
251 }
252 }
253
254 int credential_read(struct credential *c, FILE *fp,
255 enum credential_op_type op_type)
256 {
257 struct strbuf line = STRBUF_INIT;
258
259 while (strbuf_getline(&line, fp) != EOF) {
260 char *key = line.buf;
261 char *value = strchr(key, '=');
262
263 if (!line.len)
264 break;
265
266 if (!value) {
267 warning("invalid credential line: %s", key);
268 strbuf_release(&line);
269 return -1;
270 }
271 *value++ = '\0';
272
273 if (!strcmp(key, "username")) {
274 free(c->username);
275 c->username = xstrdup(value);
276 c->username_from_proto = 1;
277 } else if (!strcmp(key, "password")) {
278 free(c->password);
279 c->password = xstrdup(value);
280 } else if (!strcmp(key, "credential")) {
281 free(c->credential);
282 c->credential = xstrdup(value);
283 } else if (!strcmp(key, "protocol")) {
284 free(c->protocol);
285 c->protocol = xstrdup(value);
286 } else if (!strcmp(key, "host")) {
287 free(c->host);
288 c->host = xstrdup(value);
289 } else if (!strcmp(key, "path")) {
290 free(c->path);
291 c->path = xstrdup(value);
292 } else if (!strcmp(key, "ephemeral")) {
293 c->ephemeral = !!git_config_bool("ephemeral", value);
294 } else if (!strcmp(key, "wwwauth[]")) {
295 strvec_push(&c->wwwauth_headers, value);
296 } else if (!strcmp(key, "capability[]") && !strcmp(value, "authtype")) {
297 credential_set_capability(&c->capa_authtype, op_type);
298 } else if (!strcmp(key, "password_expiry_utc")) {
299 errno = 0;
300 c->password_expiry_utc = parse_timestamp(value, NULL, 10);
301 if (c->password_expiry_utc == 0 || errno == ERANGE)
302 c->password_expiry_utc = TIME_MAX;
303 } else if (!strcmp(key, "oauth_refresh_token")) {
304 free(c->oauth_refresh_token);
305 c->oauth_refresh_token = xstrdup(value);
306 } else if (!strcmp(key, "authtype")) {
307 free(c->authtype);
308 c->authtype = xstrdup(value);
309 } else if (!strcmp(key, "url")) {
310 credential_from_url(c, value);
311 } else if (!strcmp(key, "quit")) {
312 c->quit = !!git_config_bool("quit", value);
313 }
314 /*
315 * Ignore other lines; we don't know what they mean, but
316 * this future-proofs us when later versions of git do
317 * learn new lines, and the helpers are updated to match.
318 */
319 }
320
321 strbuf_release(&line);
322 return 0;
323 }
324
325 static void credential_write_item(FILE *fp, const char *key, const char *value,
326 int required)
327 {
328 if (!value && required)
329 BUG("credential value for %s is missing", key);
330 if (!value)
331 return;
332 if (strchr(value, '\n'))
333 die("credential value for %s contains newline", key);
334 fprintf(fp, "%s=%s\n", key, value);
335 }
336
337 void credential_write(const struct credential *c, FILE *fp,
338 enum credential_op_type op_type)
339 {
340 if (credential_has_capability(&c->capa_authtype, op_type)) {
341 credential_write_item(fp, "capability[]", "authtype", 0);
342 credential_write_item(fp, "authtype", c->authtype, 0);
343 credential_write_item(fp, "credential", c->credential, 0);
344 if (c->ephemeral)
345 credential_write_item(fp, "ephemeral", "1", 0);
346 }
347 credential_write_item(fp, "protocol", c->protocol, 1);
348 credential_write_item(fp, "host", c->host, 1);
349 credential_write_item(fp, "path", c->path, 0);
350 credential_write_item(fp, "username", c->username, 0);
351 credential_write_item(fp, "password", c->password, 0);
352 credential_write_item(fp, "oauth_refresh_token", c->oauth_refresh_token, 0);
353 if (c->password_expiry_utc != TIME_MAX) {
354 char *s = xstrfmt("%"PRItime, c->password_expiry_utc);
355 credential_write_item(fp, "password_expiry_utc", s, 0);
356 free(s);
357 }
358 for (size_t i = 0; i < c->wwwauth_headers.nr; i++)
359 credential_write_item(fp, "wwwauth[]", c->wwwauth_headers.v[i], 0);
360 }
361
362 static int run_credential_helper(struct credential *c,
363 const char *cmd,
364 int want_output)
365 {
366 struct child_process helper = CHILD_PROCESS_INIT;
367 FILE *fp;
368
369 strvec_push(&helper.args, cmd);
370 helper.use_shell = 1;
371 helper.in = -1;
372 if (want_output)
373 helper.out = -1;
374 else
375 helper.no_stdout = 1;
376
377 if (start_command(&helper) < 0)
378 return -1;
379
380 fp = xfdopen(helper.in, "w");
381 sigchain_push(SIGPIPE, SIG_IGN);
382 credential_write(c, fp, want_output ? CREDENTIAL_OP_HELPER : CREDENTIAL_OP_RESPONSE);
383 fclose(fp);
384 sigchain_pop(SIGPIPE);
385
386 if (want_output) {
387 int r;
388 fp = xfdopen(helper.out, "r");
389 r = credential_read(c, fp, CREDENTIAL_OP_HELPER);
390 fclose(fp);
391 if (r < 0) {
392 finish_command(&helper);
393 return -1;
394 }
395 }
396
397 if (finish_command(&helper))
398 return -1;
399 return 0;
400 }
401
402 static int credential_do(struct credential *c, const char *helper,
403 const char *operation)
404 {
405 struct strbuf cmd = STRBUF_INIT;
406 int r;
407
408 if (helper[0] == '!')
409 strbuf_addstr(&cmd, helper + 1);
410 else if (is_absolute_path(helper))
411 strbuf_addstr(&cmd, helper);
412 else
413 strbuf_addf(&cmd, "git credential-%s", helper);
414
415 strbuf_addf(&cmd, " %s", operation);
416 r = run_credential_helper(c, cmd.buf, !strcmp(operation, "get"));
417
418 strbuf_release(&cmd);
419 return r;
420 }
421
422 void credential_fill(struct credential *c, int all_capabilities)
423 {
424 int i;
425
426 if ((c->username && c->password) || c->credential)
427 return;
428
429 credential_apply_config(c);
430 if (all_capabilities)
431 credential_set_all_capabilities(c, CREDENTIAL_OP_INITIAL);
432
433 for (i = 0; i < c->helpers.nr; i++) {
434 credential_do(c, c->helpers.items[i].string, "get");
435 if (c->password_expiry_utc < time(NULL)) {
436 /* Discard expired password */
437 FREE_AND_NULL(c->password);
438 /* Reset expiry to maintain consistency */
439 c->password_expiry_utc = TIME_MAX;
440 }
441 if ((c->username && c->password) || c->credential)
442 return;
443 if (c->quit)
444 die("credential helper '%s' told us to quit",
445 c->helpers.items[i].string);
446 }
447
448 credential_getpass(c);
449 if (!c->username && !c->password && !c->credential)
450 die("unable to get password from user");
451 }
452
453 void credential_approve(struct credential *c)
454 {
455 int i;
456
457 if (c->approved)
458 return;
459 if (((!c->username || !c->password) && !c->credential) || c->password_expiry_utc < time(NULL))
460 return;
461
462 credential_apply_config(c);
463
464 for (i = 0; i < c->helpers.nr; i++)
465 credential_do(c, c->helpers.items[i].string, "store");
466 c->approved = 1;
467 }
468
469 void credential_reject(struct credential *c)
470 {
471 int i;
472
473 credential_apply_config(c);
474
475 for (i = 0; i < c->helpers.nr; i++)
476 credential_do(c, c->helpers.items[i].string, "erase");
477
478 FREE_AND_NULL(c->username);
479 FREE_AND_NULL(c->password);
480 FREE_AND_NULL(c->credential);
481 FREE_AND_NULL(c->oauth_refresh_token);
482 c->password_expiry_utc = TIME_MAX;
483 c->approved = 0;
484 }
485
486 static int check_url_component(const char *url, int quiet,
487 const char *name, const char *value)
488 {
489 if (!value)
490 return 0;
491 if (!strchr(value, '\n'))
492 return 0;
493
494 if (!quiet)
495 warning(_("url contains a newline in its %s component: %s"),
496 name, url);
497 return -1;
498 }
499
500 /*
501 * Potentially-partial URLs can, but do not have to, contain
502 *
503 * - a protocol (or scheme) of the form "<protocol>://"
504 *
505 * - a host name (the part after the protocol and before the first slash after
506 * that, if any)
507 *
508 * - a user name and potentially a password (as "<user>[:<password>]@" part of
509 * the host name)
510 *
511 * - a path (the part after the host name, if any, starting with the slash)
512 *
513 * Missing parts will be left unset in `struct credential`. Thus, `https://`
514 * will have only the `protocol` set, `example.com` only the host name, and
515 * `/git` only the path.
516 *
517 * Note that an empty host name in an otherwise fully-qualified URL (e.g.
518 * `cert:///path/to/cert.pem`) will be treated as unset if we expect the URL to
519 * be potentially partial, and only then (otherwise, the empty string is used).
520 *
521 * The credential_from_url() function does not allow partial URLs.
522 */
523 static int credential_from_url_1(struct credential *c, const char *url,
524 int allow_partial_url, int quiet)
525 {
526 const char *at, *colon, *cp, *slash, *host, *proto_end;
527
528 credential_clear(c);
529
530 /*
531 * Match one of:
532 * (1) proto://<host>/...
533 * (2) proto://<user>@<host>/...
534 * (3) proto://<user>:<pass>@<host>/...
535 */
536 proto_end = strstr(url, "://");
537 if (!allow_partial_url && (!proto_end || proto_end == url)) {
538 if (!quiet)
539 warning(_("url has no scheme: %s"), url);
540 return -1;
541 }
542 cp = proto_end ? proto_end + 3 : url;
543 at = strchr(cp, '@');
544 colon = strchr(cp, ':');
545
546 /*
547 * A query or fragment marker before the slash ends the host portion.
548 * We'll just continue to call this "slash" for simplicity. Notably our
549 * "trim leading slashes" part won't skip over this part of the path,
550 * but that's what we'd want.
551 */
552 slash = cp + strcspn(cp, "/?#");
553
554 if (!at || slash <= at) {
555 /* Case (1) */
556 host = cp;
557 }
558 else if (!colon || at <= colon) {
559 /* Case (2) */
560 c->username = url_decode_mem(cp, at - cp);
561 if (c->username && *c->username)
562 c->username_from_proto = 1;
563 host = at + 1;
564 } else {
565 /* Case (3) */
566 c->username = url_decode_mem(cp, colon - cp);
567 if (c->username && *c->username)
568 c->username_from_proto = 1;
569 c->password = url_decode_mem(colon + 1, at - (colon + 1));
570 host = at + 1;
571 }
572
573 if (proto_end && proto_end - url > 0)
574 c->protocol = xmemdupz(url, proto_end - url);
575 if (!allow_partial_url || slash - host > 0)
576 c->host = url_decode_mem(host, slash - host);
577 /* Trim leading and trailing slashes from path */
578 while (*slash == '/')
579 slash++;
580 if (*slash) {
581 char *p;
582 c->path = url_decode(slash);
583 p = c->path + strlen(c->path) - 1;
584 while (p > c->path && *p == '/')
585 *p-- = '\0';
586 }
587
588 if (check_url_component(url, quiet, "username", c->username) < 0 ||
589 check_url_component(url, quiet, "password", c->password) < 0 ||
590 check_url_component(url, quiet, "protocol", c->protocol) < 0 ||
591 check_url_component(url, quiet, "host", c->host) < 0 ||
592 check_url_component(url, quiet, "path", c->path) < 0)
593 return -1;
594
595 return 0;
596 }
597
598 static int credential_from_potentially_partial_url(struct credential *c,
599 const char *url)
600 {
601 return credential_from_url_1(c, url, 1, 0);
602 }
603
604 int credential_from_url_gently(struct credential *c, const char *url, int quiet)
605 {
606 return credential_from_url_1(c, url, 0, quiet);
607 }
608
609 void credential_from_url(struct credential *c, const char *url)
610 {
611 if (credential_from_url_gently(c, url, 0) < 0)
612 die(_("credential url cannot be parsed: %s"), url);
613 }