Coverage Report

Created: 2024-09-08 06:23

/src/git/builtin/credential-cache--daemon.c
Line
Count
Source (jump to first uncovered line)
1
#include "builtin.h"
2
#include "abspath.h"
3
#include "gettext.h"
4
#include "object-file.h"
5
#include "parse-options.h"
6
7
#ifndef NO_UNIX_SOCKETS
8
9
#include "config.h"
10
#include "tempfile.h"
11
#include "credential.h"
12
#include "unix-socket.h"
13
14
struct credential_cache_entry {
15
  struct credential item;
16
  timestamp_t expiration;
17
};
18
static struct credential_cache_entry *entries;
19
static int entries_nr;
20
static int entries_alloc;
21
22
static void cache_credential(struct credential *c, int timeout)
23
0
{
24
0
  struct credential_cache_entry *e;
25
26
0
  ALLOC_GROW(entries, entries_nr + 1, entries_alloc);
27
0
  e = &entries[entries_nr++];
28
29
  /* take ownership of pointers */
30
0
  memcpy(&e->item, c, sizeof(*c));
31
0
  memset(c, 0, sizeof(*c));
32
0
  e->expiration = time(NULL) + timeout;
33
0
}
34
35
static struct credential_cache_entry *lookup_credential(const struct credential *c)
36
0
{
37
0
  int i;
38
0
  for (i = 0; i < entries_nr; i++) {
39
0
    struct credential *e = &entries[i].item;
40
0
    if (credential_match(c, e, 0))
41
0
      return &entries[i];
42
0
  }
43
0
  return NULL;
44
0
}
45
46
static void remove_credential(const struct credential *c, int match_password)
47
0
{
48
0
  struct credential_cache_entry *e;
49
50
0
  int i;
51
0
  for (i = 0; i < entries_nr; i++) {
52
0
    e = &entries[i];
53
0
    if (credential_match(c, &e->item, match_password))
54
0
      e->expiration = 0;
55
0
  }
56
0
}
57
58
static timestamp_t check_expirations(void)
59
0
{
60
0
  static timestamp_t wait_for_entry_until;
61
0
  int i = 0;
62
0
  timestamp_t now = time(NULL);
63
0
  timestamp_t next = TIME_MAX;
64
65
  /*
66
   * Initially give the client 30 seconds to actually contact us
67
   * and store a credential before we decide there's no point in
68
   * keeping the daemon around.
69
   */
70
0
  if (!wait_for_entry_until)
71
0
    wait_for_entry_until = now + 30;
72
73
0
  while (i < entries_nr) {
74
0
    if (entries[i].expiration <= now) {
75
0
      entries_nr--;
76
0
      credential_clear(&entries[i].item);
77
0
      if (i != entries_nr)
78
0
        memcpy(&entries[i], &entries[entries_nr], sizeof(*entries));
79
      /*
80
       * Stick around 30 seconds in case a new credential
81
       * shows up (e.g., because we just removed a failed
82
       * one, and we will soon get the correct one).
83
       */
84
0
      wait_for_entry_until = now + 30;
85
0
    }
86
0
    else {
87
0
      if (entries[i].expiration < next)
88
0
        next = entries[i].expiration;
89
0
      i++;
90
0
    }
91
0
  }
92
93
0
  if (!entries_nr) {
94
0
    if (wait_for_entry_until <= now)
95
0
      return 0;
96
0
    next = wait_for_entry_until;
97
0
  }
98
99
0
  return next - now;
100
0
}
101
102
static int read_request(FILE *fh, struct credential *c,
103
      struct strbuf *action, int *timeout)
104
0
{
105
0
  static struct strbuf item = STRBUF_INIT;
106
0
  const char *p;
107
108
0
  strbuf_getline_lf(&item, fh);
109
0
  if (!skip_prefix(item.buf, "action=", &p))
110
0
    return error("client sent bogus action line: %s", item.buf);
111
0
  strbuf_addstr(action, p);
112
113
0
  strbuf_getline_lf(&item, fh);
114
0
  if (!skip_prefix(item.buf, "timeout=", &p))
115
0
    return error("client sent bogus timeout line: %s", item.buf);
116
0
  *timeout = atoi(p);
117
118
0
  credential_set_all_capabilities(c, CREDENTIAL_OP_INITIAL);
119
120
0
  if (credential_read(c, fh, CREDENTIAL_OP_HELPER) < 0)
121
0
    return -1;
122
0
  return 0;
123
0
}
124
125
static void serve_one_client(FILE *in, FILE *out)
126
0
{
127
0
  struct credential c = CREDENTIAL_INIT;
128
0
  struct strbuf action = STRBUF_INIT;
129
0
  int timeout = -1;
130
131
0
  if (read_request(in, &c, &action, &timeout) < 0)
132
0
    /* ignore error */ ;
133
0
  else if (!strcmp(action.buf, "get")) {
134
0
    struct credential_cache_entry *e = lookup_credential(&c);
135
0
    if (e) {
136
0
      e->item.capa_authtype.request_initial = 1;
137
0
      e->item.capa_authtype.request_helper = 1;
138
139
0
      fprintf(out, "capability[]=authtype\n");
140
0
      if (e->item.username)
141
0
        fprintf(out, "username=%s\n", e->item.username);
142
0
      if (e->item.password)
143
0
        fprintf(out, "password=%s\n", e->item.password);
144
0
      if (credential_has_capability(&c.capa_authtype, CREDENTIAL_OP_HELPER) && e->item.authtype)
145
0
        fprintf(out, "authtype=%s\n", e->item.authtype);
146
0
      if (credential_has_capability(&c.capa_authtype, CREDENTIAL_OP_HELPER) && e->item.credential)
147
0
        fprintf(out, "credential=%s\n", e->item.credential);
148
0
      if (e->item.password_expiry_utc != TIME_MAX)
149
0
        fprintf(out, "password_expiry_utc=%"PRItime"\n",
150
0
          e->item.password_expiry_utc);
151
0
      if (e->item.oauth_refresh_token)
152
0
        fprintf(out, "oauth_refresh_token=%s\n",
153
0
          e->item.oauth_refresh_token);
154
0
    }
155
0
  }
156
0
  else if (!strcmp(action.buf, "exit")) {
157
    /*
158
     * It's important that we clean up our socket first, and then
159
     * signal the client only once we have finished the cleanup.
160
     * Calling exit() directly does this, because we clean up in
161
     * our atexit() handler, and then signal the client when our
162
     * process actually ends, which closes the socket and gives
163
     * them EOF.
164
     */
165
0
    exit(0);
166
0
  }
167
0
  else if (!strcmp(action.buf, "erase"))
168
0
    remove_credential(&c, 1);
169
0
  else if (!strcmp(action.buf, "store")) {
170
0
    if (timeout < 0)
171
0
      warning("cache client didn't specify a timeout");
172
0
    else if ((!c.username || !c.password) && (!c.authtype && !c.credential))
173
0
      warning("cache client gave us a partial credential");
174
0
    else if (c.ephemeral)
175
0
      warning("not storing ephemeral credential");
176
0
    else {
177
0
      remove_credential(&c, 0);
178
0
      cache_credential(&c, timeout);
179
0
    }
180
0
  }
181
0
  else
182
0
    warning("cache client sent unknown action: %s", action.buf);
183
184
0
  credential_clear(&c);
185
0
  strbuf_release(&action);
186
0
}
187
188
static int serve_cache_loop(int fd)
189
0
{
190
0
  struct pollfd pfd;
191
0
  timestamp_t wakeup;
192
193
0
  wakeup = check_expirations();
194
0
  if (!wakeup)
195
0
    return 0;
196
197
0
  pfd.fd = fd;
198
0
  pfd.events = POLLIN;
199
0
  if (poll(&pfd, 1, 1000 * wakeup) < 0) {
200
0
    if (errno != EINTR)
201
0
      die_errno("poll failed");
202
0
    return 1;
203
0
  }
204
205
0
  if (pfd.revents & POLLIN) {
206
0
    int client, client2;
207
0
    FILE *in, *out;
208
209
0
    client = accept(fd, NULL, NULL);
210
0
    if (client < 0) {
211
0
      warning_errno("accept failed");
212
0
      return 1;
213
0
    }
214
0
    client2 = dup(client);
215
0
    if (client2 < 0) {
216
0
      warning_errno("dup failed");
217
0
      close(client);
218
0
      return 1;
219
0
    }
220
221
0
    in = xfdopen(client, "r");
222
0
    out = xfdopen(client2, "w");
223
0
    serve_one_client(in, out);
224
0
    fclose(in);
225
0
    fclose(out);
226
0
  }
227
0
  return 1;
228
0
}
229
230
static void serve_cache(const char *socket_path, int debug)
231
0
{
232
0
  struct unix_stream_listen_opts opts = UNIX_STREAM_LISTEN_OPTS_INIT;
233
0
  int fd;
234
235
0
  fd = unix_stream_listen(socket_path, &opts);
236
0
  if (fd < 0)
237
0
    die_errno("unable to bind to '%s'", socket_path);
238
239
0
  printf("ok\n");
240
0
  fclose(stdout);
241
0
  if (!debug) {
242
0
    if (!freopen("/dev/null", "w", stderr))
243
0
      die_errno("unable to point stderr to /dev/null");
244
0
  }
245
246
0
  while (serve_cache_loop(fd))
247
0
    ; /* nothing */
248
249
0
  close(fd);
250
0
}
251
252
static const char permissions_advice[] = N_(
253
"The permissions on your socket directory are too loose; other\n"
254
"users may be able to read your cached credentials. Consider running:\n"
255
"\n"
256
" chmod 0700 %s");
257
static void init_socket_directory(const char *path)
258
0
{
259
0
  struct stat st;
260
0
  char *path_copy = xstrdup(path);
261
0
  char *dir = dirname(path_copy);
262
263
0
  if (!stat(dir, &st)) {
264
0
    if (st.st_mode & 077)
265
0
      die(_(permissions_advice), dir);
266
0
  } else {
267
    /*
268
     * We must be sure to create the directory with the correct mode,
269
     * not just chmod it after the fact; otherwise, there is a race
270
     * condition in which somebody can chdir to it, sleep, then try to open
271
     * our protected socket.
272
     */
273
0
    if (safe_create_leading_directories_const(dir) < 0)
274
0
      die_errno("unable to create directories for '%s'", dir);
275
0
    if (mkdir(dir, 0700) < 0)
276
0
      die_errno("unable to mkdir '%s'", dir);
277
0
  }
278
279
0
  if (chdir(dir))
280
    /*
281
     * We don't actually care what our cwd is; we chdir here just to
282
     * be a friendly daemon and avoid tying up our original cwd.
283
     * If this fails, it's OK to just continue without that benefit.
284
     */
285
0
    ;
286
287
0
  free(path_copy);
288
0
}
289
290
int cmd_credential_cache_daemon(int argc, const char **argv, const char *prefix)
291
0
{
292
0
  struct tempfile *socket_file;
293
0
  const char *socket_path;
294
0
  int ignore_sighup = 0;
295
0
  static const char *usage[] = {
296
0
    "git credential-cache--daemon [--debug] <socket-path>",
297
0
    NULL
298
0
  };
299
0
  int debug = 0;
300
0
  const struct option options[] = {
301
0
    OPT_BOOL(0, "debug", &debug,
302
0
       N_("print debugging messages to stderr")),
303
0
    OPT_END()
304
0
  };
305
306
0
  git_config_get_bool("credentialcache.ignoresighup", &ignore_sighup);
307
308
0
  argc = parse_options(argc, argv, prefix, options, usage, 0);
309
0
  socket_path = argv[0];
310
311
0
  if (!have_unix_sockets())
312
0
    die(_("credential-cache--daemon unavailable; no unix socket support"));
313
0
  if (!socket_path)
314
0
    usage_with_options(usage, options);
315
316
0
  if (!is_absolute_path(socket_path))
317
0
    die("socket directory must be an absolute path");
318
319
0
  init_socket_directory(socket_path);
320
0
  socket_file = register_tempfile(socket_path);
321
322
0
  if (ignore_sighup)
323
0
    signal(SIGHUP, SIG_IGN);
324
325
0
  serve_cache(socket_path, debug);
326
0
  delete_tempfile(&socket_file);
327
328
0
  return 0;
329
0
}
330
331
#else
332
333
int cmd_credential_cache_daemon(int argc, const char **argv, const char *prefix)
334
{
335
  const char * const usage[] = {
336
    "git credential-cache--daemon [--debug] <socket-path>",
337
    "",
338
    "credential-cache--daemon is disabled in this build of Git",
339
    NULL
340
  };
341
  struct option options[] = { OPT_END() };
342
343
  argc = parse_options(argc, argv, prefix, options, usage, 0);
344
  die(_("credential-cache--daemon unavailable; no unix socket support"));
345
}
346
347
#endif /* NO_UNIX_SOCKET */