Coverage Report

Created: 2023-11-19 07:08

/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
  if (credential_read(c, fh) < 0)
119
0
    return -1;
120
0
  return 0;
121
0
}
122
123
static void serve_one_client(FILE *in, FILE *out)
124
0
{
125
0
  struct credential c = CREDENTIAL_INIT;
126
0
  struct strbuf action = STRBUF_INIT;
127
0
  int timeout = -1;
128
129
0
  if (read_request(in, &c, &action, &timeout) < 0)
130
0
    /* ignore error */ ;
131
0
  else if (!strcmp(action.buf, "get")) {
132
0
    struct credential_cache_entry *e = lookup_credential(&c);
133
0
    if (e) {
134
0
      fprintf(out, "username=%s\n", e->item.username);
135
0
      fprintf(out, "password=%s\n", e->item.password);
136
0
      if (e->item.password_expiry_utc != TIME_MAX)
137
0
        fprintf(out, "password_expiry_utc=%"PRItime"\n",
138
0
          e->item.password_expiry_utc);
139
0
      if (e->item.oauth_refresh_token)
140
0
        fprintf(out, "oauth_refresh_token=%s\n",
141
0
          e->item.oauth_refresh_token);
142
0
    }
143
0
  }
144
0
  else if (!strcmp(action.buf, "exit")) {
145
    /*
146
     * It's important that we clean up our socket first, and then
147
     * signal the client only once we have finished the cleanup.
148
     * Calling exit() directly does this, because we clean up in
149
     * our atexit() handler, and then signal the client when our
150
     * process actually ends, which closes the socket and gives
151
     * them EOF.
152
     */
153
0
    exit(0);
154
0
  }
155
0
  else if (!strcmp(action.buf, "erase"))
156
0
    remove_credential(&c, 1);
157
0
  else if (!strcmp(action.buf, "store")) {
158
0
    if (timeout < 0)
159
0
      warning("cache client didn't specify a timeout");
160
0
    else if (!c.username || !c.password)
161
0
      warning("cache client gave us a partial credential");
162
0
    else {
163
0
      remove_credential(&c, 0);
164
0
      cache_credential(&c, timeout);
165
0
    }
166
0
  }
167
0
  else
168
0
    warning("cache client sent unknown action: %s", action.buf);
169
170
0
  credential_clear(&c);
171
0
  strbuf_release(&action);
172
0
}
173
174
static int serve_cache_loop(int fd)
175
0
{
176
0
  struct pollfd pfd;
177
0
  timestamp_t wakeup;
178
179
0
  wakeup = check_expirations();
180
0
  if (!wakeup)
181
0
    return 0;
182
183
0
  pfd.fd = fd;
184
0
  pfd.events = POLLIN;
185
0
  if (poll(&pfd, 1, 1000 * wakeup) < 0) {
186
0
    if (errno != EINTR)
187
0
      die_errno("poll failed");
188
0
    return 1;
189
0
  }
190
191
0
  if (pfd.revents & POLLIN) {
192
0
    int client, client2;
193
0
    FILE *in, *out;
194
195
0
    client = accept(fd, NULL, NULL);
196
0
    if (client < 0) {
197
0
      warning_errno("accept failed");
198
0
      return 1;
199
0
    }
200
0
    client2 = dup(client);
201
0
    if (client2 < 0) {
202
0
      warning_errno("dup failed");
203
0
      close(client);
204
0
      return 1;
205
0
    }
206
207
0
    in = xfdopen(client, "r");
208
0
    out = xfdopen(client2, "w");
209
0
    serve_one_client(in, out);
210
0
    fclose(in);
211
0
    fclose(out);
212
0
  }
213
0
  return 1;
214
0
}
215
216
static void serve_cache(const char *socket_path, int debug)
217
0
{
218
0
  struct unix_stream_listen_opts opts = UNIX_STREAM_LISTEN_OPTS_INIT;
219
0
  int fd;
220
221
0
  fd = unix_stream_listen(socket_path, &opts);
222
0
  if (fd < 0)
223
0
    die_errno("unable to bind to '%s'", socket_path);
224
225
0
  printf("ok\n");
226
0
  fclose(stdout);
227
0
  if (!debug) {
228
0
    if (!freopen("/dev/null", "w", stderr))
229
0
      die_errno("unable to point stderr to /dev/null");
230
0
  }
231
232
0
  while (serve_cache_loop(fd))
233
0
    ; /* nothing */
234
235
0
  close(fd);
236
0
}
237
238
static const char permissions_advice[] = N_(
239
"The permissions on your socket directory are too loose; other\n"
240
"users may be able to read your cached credentials. Consider running:\n"
241
"\n"
242
" chmod 0700 %s");
243
static void init_socket_directory(const char *path)
244
0
{
245
0
  struct stat st;
246
0
  char *path_copy = xstrdup(path);
247
0
  char *dir = dirname(path_copy);
248
249
0
  if (!stat(dir, &st)) {
250
0
    if (st.st_mode & 077)
251
0
      die(_(permissions_advice), dir);
252
0
  } else {
253
    /*
254
     * We must be sure to create the directory with the correct mode,
255
     * not just chmod it after the fact; otherwise, there is a race
256
     * condition in which somebody can chdir to it, sleep, then try to open
257
     * our protected socket.
258
     */
259
0
    if (safe_create_leading_directories_const(dir) < 0)
260
0
      die_errno("unable to create directories for '%s'", dir);
261
0
    if (mkdir(dir, 0700) < 0)
262
0
      die_errno("unable to mkdir '%s'", dir);
263
0
  }
264
265
0
  if (chdir(dir))
266
    /*
267
     * We don't actually care what our cwd is; we chdir here just to
268
     * be a friendly daemon and avoid tying up our original cwd.
269
     * If this fails, it's OK to just continue without that benefit.
270
     */
271
0
    ;
272
273
0
  free(path_copy);
274
0
}
275
276
int cmd_credential_cache_daemon(int argc, const char **argv, const char *prefix)
277
0
{
278
0
  struct tempfile *socket_file;
279
0
  const char *socket_path;
280
0
  int ignore_sighup = 0;
281
0
  static const char *usage[] = {
282
0
    "git credential-cache--daemon [--debug] <socket-path>",
283
0
    NULL
284
0
  };
285
0
  int debug = 0;
286
0
  const struct option options[] = {
287
0
    OPT_BOOL(0, "debug", &debug,
288
0
       N_("print debugging messages to stderr")),
289
0
    OPT_END()
290
0
  };
291
292
0
  git_config_get_bool("credentialcache.ignoresighup", &ignore_sighup);
293
294
0
  argc = parse_options(argc, argv, prefix, options, usage, 0);
295
0
  socket_path = argv[0];
296
297
0
  if (!socket_path)
298
0
    usage_with_options(usage, options);
299
300
0
  if (!is_absolute_path(socket_path))
301
0
    die("socket directory must be an absolute path");
302
303
0
  init_socket_directory(socket_path);
304
0
  socket_file = register_tempfile(socket_path);
305
306
0
  if (ignore_sighup)
307
0
    signal(SIGHUP, SIG_IGN);
308
309
0
  serve_cache(socket_path, debug);
310
0
  delete_tempfile(&socket_file);
311
312
0
  return 0;
313
0
}
314
315
#else
316
317
int cmd_credential_cache_daemon(int argc, const char **argv, const char *prefix)
318
{
319
  const char * const usage[] = {
320
    "git credential-cache--daemon [--debug] <socket-path>",
321
    "",
322
    "credential-cache--daemon is disabled in this build of Git",
323
    NULL
324
  };
325
  struct option options[] = { OPT_END() };
326
327
  argc = parse_options(argc, argv, prefix, options, usage, 0);
328
  die(_("credential-cache--daemon unavailable; no unix socket support"));
329
}
330
331
#endif /* NO_UNIX_SOCKET */