make got-read-gitconfig clear its imsgbuf before exit in an error case
[got-portable.git] / gotd / notify.c
blobc0dadca0240a3dc4613cbd0e8294100b0efbdf69
1 /*
2 * Copyright (c) 2024 Stefan Sperling <stsp@openbsd.org>
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 #include "got_compat.h"
19 #include <sys/types.h>
20 #include <sys/queue.h>
21 #include <sys/socket.h>
22 #include <sys/wait.h>
24 #include <errno.h>
25 #include <event.h>
26 #include <limits.h>
27 #include <signal.h>
28 #include <stdio.h>
29 #include <stdlib.h>
30 #include <string.h>
31 #include <imsg.h>
32 #include <unistd.h>
34 #include "got_error.h"
35 #include "got_object.h"
36 #include "got_path.h"
38 #include "gotd.h"
39 #include "log.h"
40 #include "notify.h"
41 #include "secrets.h"
43 #ifndef nitems
44 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
45 #endif
47 static struct gotd_secrets secrets;
49 static struct gotd_notify {
50 pid_t pid;
51 const char *title;
52 struct gotd_imsgev parent_iev;
53 struct gotd_repolist *repos;
54 const char *default_sender;
55 } gotd_notify;
57 struct gotd_notify_session {
58 STAILQ_ENTRY(gotd_notify_session) entry;
59 uint32_t id;
60 struct gotd_imsgev iev;
62 STAILQ_HEAD(gotd_notify_sessions, gotd_notify_session);
64 static struct gotd_notify_sessions gotd_notify_sessions[GOTD_CLIENT_TABLE_SIZE];
65 static SIPHASH_KEY sessions_hash_key;
67 static void gotd_notify_shutdown(void);
69 static uint64_t
70 session_hash(uint32_t session_id)
72 return SipHash24(&sessions_hash_key, &session_id, sizeof(session_id));
75 static void
76 add_session(struct gotd_notify_session *session)
78 uint64_t slot;
80 slot = session_hash(session->id) % nitems(gotd_notify_sessions);
81 STAILQ_INSERT_HEAD(&gotd_notify_sessions[slot], session, entry);
84 static struct gotd_notify_session *
85 find_session(uint32_t session_id)
87 uint64_t slot;
88 struct gotd_notify_session *s;
90 slot = session_hash(session_id) % nitems(gotd_notify_sessions);
91 STAILQ_FOREACH(s, &gotd_notify_sessions[slot], entry) {
92 if (s->id == session_id)
93 return s;
96 return NULL;
99 static struct gotd_notify_session *
100 find_session_by_fd(int fd)
102 uint64_t slot;
103 struct gotd_notify_session *s;
105 for (slot = 0; slot < nitems(gotd_notify_sessions); slot++) {
106 STAILQ_FOREACH(s, &gotd_notify_sessions[slot], entry) {
107 if (s->iev.ibuf.fd == fd)
108 return s;
112 return NULL;
115 static void
116 remove_session(struct gotd_notify_session *session)
118 uint64_t slot;
120 slot = session_hash(session->id) % nitems(gotd_notify_sessions);
121 STAILQ_REMOVE(&gotd_notify_sessions[slot], session,
122 gotd_notify_session, entry);
123 close(session->iev.ibuf.fd);
124 free(session);
127 static uint32_t
128 get_session_id(void)
130 int duplicate = 0;
131 uint32_t id;
133 do {
134 id = arc4random();
135 duplicate = (find_session(id) != NULL);
136 } while (duplicate || id == 0);
138 return id;
141 static void
142 gotd_notify_sighdlr(int sig, short event, void *arg)
145 * Normal signal handler rules don't apply because libevent
146 * decouples for us.
149 switch (sig) {
150 case SIGHUP:
151 log_info("%s: ignoring SIGHUP", __func__);
152 break;
153 case SIGUSR1:
154 log_info("%s: ignoring SIGUSR1", __func__);
155 break;
156 case SIGTERM:
157 case SIGINT:
158 gotd_notify_shutdown();
159 /* NOTREACHED */
160 break;
161 default:
162 fatalx("unexpected signal");
166 static void
167 run_notification_helper(const char *prog, const char **argv, int fd,
168 const char *user, const char *pass, const char *hmac_secret)
170 const struct got_error *err = NULL;
171 pid_t pid;
172 int child_status;
174 pid = fork();
175 if (pid == -1) {
176 err = got_error_from_errno("fork");
177 log_warn("%s", err->msg);
178 return;
179 } else if (pid == 0) {
180 signal(SIGQUIT, SIG_DFL);
181 signal(SIGINT, SIG_DFL);
182 signal(SIGCHLD, SIG_DFL);
184 if (dup2(fd, STDIN_FILENO) == -1) {
185 fprintf(stderr, "%s: dup2: %s\n", getprogname(),
186 strerror(errno));
187 _exit(1);
190 closefrom(STDERR_FILENO + 1);
192 if (user != NULL && pass != NULL) {
193 setenv("GOT_NOTIFY_HTTP_USER", user, 1);
194 setenv("GOT_NOTIFY_HTTP_PASS", pass, 1);
195 } else {
196 unsetenv("GOTD_NOTIFY_HTTP_USER");
197 unsetenv("GOTD_NOTIFY_HTTP_PASS");
200 if (hmac_secret)
201 setenv("GOT_NOTIFY_HTTP_HMAC_SECRET", hmac_secret, 1);
202 else
203 unsetenv("GOT_NOTIFY_HTTP_HMAC_SECRET");
205 if (execv(prog, (char *const *)argv) == -1) {
206 fprintf(stderr, "%s: exec %s: %s\n", getprogname(),
207 prog, strerror(errno));
208 _exit(1);
211 /* not reached */
214 if (waitpid(pid, &child_status, 0) == -1) {
215 err = got_error_from_errno("waitpid");
216 goto done;
219 if (!WIFEXITED(child_status)) {
220 err = got_error(GOT_ERR_PRIVSEP_DIED);
221 goto done;
224 if (WEXITSTATUS(child_status) != 0)
225 err = got_error(GOT_ERR_PRIVSEP_EXIT);
226 done:
227 if (err)
228 log_warnx("%s: child %s pid %d: %s", gotd_notify.title,
229 prog, pid, err->msg);
232 static void
233 notify_email(struct gotd_notification_target *target, const char *subject_line,
234 int fd)
236 const char *argv[13];
237 int i = 0;
239 argv[i++] = GOTD_PATH_PROG_NOTIFY_EMAIL;
241 argv[i++] = "-f";
242 if (target->conf.email.sender)
243 argv[i++] = target->conf.email.sender;
244 else
245 argv[i++] = gotd_notify.default_sender;
247 if (target->conf.email.responder) {
248 argv[i++] = "-r";
249 argv[i++] = target->conf.email.responder;
252 if (target->conf.email.hostname) {
253 argv[i++] = "-h";
254 argv[i++] = target->conf.email.hostname;
257 if (target->conf.email.port) {
258 argv[i++] = "-p";
259 argv[i++] = target->conf.email.port;
262 argv[i++] = "-s";
263 argv[i++] = subject_line;
265 argv[i++] = target->conf.email.recipient;
267 argv[i] = NULL;
269 run_notification_helper(GOTD_PATH_PROG_NOTIFY_EMAIL, argv, fd,
270 NULL, NULL, NULL);
273 static void
274 notify_http(struct gotd_notification_target *target, const char *repo,
275 const char *username, int fd)
277 struct gotd_secret *secret;
278 const char *http_user = NULL, *http_pass = NULL, *hmac = NULL;
279 const char *argv[12];
280 int argc = 0;
282 argv[argc++] = GOTD_PATH_PROG_NOTIFY_HTTP;
283 if (target->conf.http.tls)
284 argv[argc++] = "-c";
286 argv[argc++] = "-r";
287 argv[argc++] = repo;
288 argv[argc++] = "-h";
289 argv[argc++] = target->conf.http.hostname;
290 argv[argc++] = "-p";
291 argv[argc++] = target->conf.http.port;
292 argv[argc++] = "-u";
293 argv[argc++] = username;
295 argv[argc++] = target->conf.http.path;
297 argv[argc] = NULL;
299 if (target->conf.http.auth) {
300 secret = gotd_secrets_get(&secrets, GOTD_SECRET_AUTH,
301 target->conf.http.auth);
302 http_user = secret->user;
303 http_pass = secret->pass;
305 if (target->conf.http.hmac) {
306 secret = gotd_secrets_get(&secrets, GOTD_SECRET_HMAC,
307 target->conf.http.hmac);
308 hmac = secret->hmac;
311 run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP, argv, fd,
312 http_user, http_pass, hmac);
315 static const struct got_error *
316 send_notification(struct imsg *imsg, struct gotd_imsgev *iev)
318 const struct got_error *err = NULL;
319 struct gotd_imsg_notify inotify;
320 size_t datalen;
321 struct gotd_repo *repo;
322 struct gotd_notification_target *target;
323 int fd;
324 char *username = NULL;
326 datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
327 if (datalen < sizeof(inotify))
328 return got_error(GOT_ERR_PRIVSEP_LEN);
330 memcpy(&inotify, imsg->data, sizeof(inotify));
331 if (datalen != sizeof(inotify) + inotify.username_len)
332 return got_error(GOT_ERR_PRIVSEP_LEN);
334 repo = gotd_find_repo_by_name(inotify.repo_name, gotd_notify.repos);
335 if (repo == NULL)
336 return got_error(GOT_ERR_PRIVSEP_MSG);
338 fd = imsg_get_fd(imsg);
339 if (fd == -1)
340 return got_error(GOT_ERR_PRIVSEP_NO_FD);
342 username = strndup(imsg->data + sizeof(inotify), inotify.username_len);
343 if (username == NULL)
344 return got_error_from_errno("strndup");
346 STAILQ_FOREACH(target, &repo->notification_targets, entry) {
347 if (lseek(fd, 0, SEEK_SET) == -1) {
348 err = got_error_from_errno("lseek");
349 goto done;
351 switch (target->type) {
352 case GOTD_NOTIFICATION_VIA_EMAIL:
353 notify_email(target, inotify.subject_line, fd);
354 break;
355 case GOTD_NOTIFICATION_VIA_HTTP:
356 notify_http(target, repo->name, username, fd);
357 break;
361 if (gotd_imsg_compose_event(iev, GOTD_IMSG_NOTIFICATION_SENT,
362 PROC_NOTIFY, -1, NULL, 0) == -1) {
363 err = got_error_from_errno("imsg compose NOTIFY");
364 goto done;
366 done:
367 close(fd);
368 free(username);
369 return err;
372 static void
373 notify_dispatch_session(int fd, short event, void *arg)
375 const struct got_error *err = NULL;
376 struct gotd_imsgev *iev = arg;
377 struct imsgbuf *ibuf = &iev->ibuf;
378 ssize_t n;
379 int shut = 0;
380 struct imsg imsg;
382 if (event & EV_READ) {
383 if ((n = imsgbuf_read(ibuf)) == -1)
384 fatal("imsgbuf_read error");
385 if (n == 0) {
386 /* Connection closed. */
387 shut = 1;
388 goto done;
392 if (event & EV_WRITE) {
393 err = gotd_imsg_flush(ibuf);
394 if (err) {
395 if (err->code != GOT_ERR_ERRNO || errno != EPIPE)
396 fatalx("%s", err->msg);
397 shut = 1;
398 goto done;
402 for (;;) {
403 const struct got_error *err = NULL;
405 if ((n = imsg_get(ibuf, &imsg)) == -1)
406 fatal("%s: imsg_get error", __func__);
407 if (n == 0) /* No more messages. */
408 break;
410 switch (imsg.hdr.type) {
411 case GOTD_IMSG_NOTIFY:
412 err = send_notification(&imsg, iev);
413 break;
414 default:
415 log_debug("unexpected imsg %d", imsg.hdr.type);
416 break;
418 imsg_free(&imsg);
420 if (err)
421 log_warnx("%s: %s", __func__, err->msg);
423 done:
424 if (!shut) {
425 gotd_imsg_event_add(iev);
426 } else {
427 struct gotd_notify_session *session;
429 /* This pipe is dead. Remove its event handler */
430 event_del(&iev->ev);
431 imsgbuf_clear(&iev->ibuf);
433 session = find_session_by_fd(fd);
434 if (session)
435 remove_session(session);
439 static const struct got_error *
440 recv_session(struct imsg *imsg)
442 struct gotd_notify_session *session;
443 size_t datalen;
444 int fd;
446 datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
447 if (datalen != 0)
448 return got_error(GOT_ERR_PRIVSEP_LEN);
450 fd = imsg_get_fd(imsg);
451 if (fd == -1)
452 return got_error(GOT_ERR_PRIVSEP_NO_FD);
454 session = calloc(1, sizeof(*session));
455 if (session == NULL) {
456 close(fd);
457 return got_error_from_errno("calloc");
460 session->id = get_session_id();
461 if (imsgbuf_init(&session->iev.ibuf, fd) == -1) {
462 close(fd);
463 return got_error_from_errno("imsgbuf_init");
465 imsgbuf_allow_fdpass(&session->iev.ibuf);
466 session->iev.handler = notify_dispatch_session;
467 session->iev.events = EV_READ;
468 session->iev.handler_arg = NULL;
469 event_set(&session->iev.ev, session->iev.ibuf.fd, EV_READ,
470 notify_dispatch_session, &session->iev);
471 gotd_imsg_event_add(&session->iev);
472 add_session(session);
474 return NULL;
477 static const struct got_error *
478 notify_ibuf_get_str(char **ret, struct ibuf *ibuf)
480 const char *str, *end;
481 size_t len;
483 *ret = NULL;
485 str = ibuf_data(ibuf);
486 len = ibuf_size(ibuf);
488 end = memchr(str, '\0', len);
489 if (end == NULL)
490 return got_error(GOT_ERR_PRIVSEP_LEN);
491 *ret = strdup(str);
492 if (*ret == NULL)
493 return got_error_from_errno("strdup");
495 if (ibuf_skip(ibuf, end - str + 1) == -1) {
496 free(*ret);
497 *ret = NULL;
498 return got_error(GOT_ERR_PRIVSEP_LEN);
501 return NULL;
504 static void
505 notify_dispatch(int fd, short event, void *arg)
507 const struct got_error *err = NULL;
508 struct gotd_imsgev *iev = arg;
509 struct imsgbuf *imsgbuf = &iev->ibuf;
510 ssize_t n;
511 int shut = 0;
512 struct imsg imsg;
513 struct ibuf ibuf;
514 struct gotd_secret *s;
516 if (event & EV_READ) {
517 if ((n = imsgbuf_read(imsgbuf)) == -1)
518 fatal("imsgbuf_read error");
519 if (n == 0) {
520 /* Connection closed. */
521 shut = 1;
522 goto done;
526 if (event & EV_WRITE) {
527 err = gotd_imsg_flush(imsgbuf);
528 if (err)
529 fatalx("%s", err->msg);
532 for (;;) {
533 const struct got_error *err = NULL;
535 if ((n = imsg_get(imsgbuf, &imsg)) == -1)
536 fatal("%s: imsg_get error", __func__);
537 if (n == 0) /* No more messages. */
538 break;
540 switch (imsg.hdr.type) {
541 case GOTD_IMSG_CONNECT_SESSION:
542 err = recv_session(&imsg);
543 break;
544 case GOTD_IMSG_SECRETS:
545 if (secrets.cap != 0)
546 fatal("unexpected GOTD_IMSG_SECRETS");
547 if (imsg_get_data(&imsg, &secrets.cap,
548 sizeof(secrets.cap)) == -1)
549 fatalx("corrupted GOTD_IMSG_SECRETS");
550 if (secrets.cap == 0)
551 break;
552 secrets.secrets = calloc(secrets.cap,
553 sizeof(*secrets.secrets));
554 if (secrets.secrets == NULL)
555 fatal("calloc");
556 break;
557 case GOTD_IMSG_SECRET:
558 if (secrets.len == secrets.cap)
559 fatalx("unexpected GOTD_SECRET_AUTH");
560 s = &secrets.secrets[secrets.len++];
561 if (imsg_get_ibuf(&imsg, &ibuf) == -1)
562 fatal("imsg_get_ibuf");
563 if (ibuf_get(&ibuf, &s->type, sizeof(s->type)) == -1)
564 fatalx("corrupted GOTD_IMSG_SECRET");
565 err = notify_ibuf_get_str(&s->label, &ibuf);
566 if (err)
567 break;
568 if (s->type == GOTD_SECRET_AUTH) {
569 err = notify_ibuf_get_str(&s->user, &ibuf);
570 if (err)
571 break;
572 err = notify_ibuf_get_str(&s->pass, &ibuf);
573 if (err)
574 break;
575 } else {
576 err = notify_ibuf_get_str(&s->hmac, &ibuf);
577 if (err)
578 break;
580 if (ibuf_size(&ibuf) != 0)
581 fatalx("unexpected extra data in "
582 "GOTD_IMSG_SECRET");
583 break;
584 default:
585 log_debug("unexpected imsg %d", imsg.hdr.type);
586 break;
588 imsg_free(&imsg);
590 if (err)
591 log_warnx("%s: %s", __func__, err->msg);
593 done:
594 if (!shut) {
595 gotd_imsg_event_add(iev);
596 } else {
597 /* This pipe is dead. Remove its event handler */
598 event_del(&iev->ev);
599 event_loopexit(NULL);
604 void
605 notify_main(const char *title, struct gotd_repolist *repos,
606 const char *default_sender)
608 const struct got_error *err = NULL;
609 struct event evsigint, evsigterm, evsighup, evsigusr1;
611 arc4random_buf(&sessions_hash_key, sizeof(sessions_hash_key));
613 gotd_notify.title = title;
614 gotd_notify.repos = repos;
615 gotd_notify.default_sender = default_sender;
616 gotd_notify.pid = getpid();
618 signal_set(&evsigint, SIGINT, gotd_notify_sighdlr, NULL);
619 signal_set(&evsigterm, SIGTERM, gotd_notify_sighdlr, NULL);
620 signal_set(&evsighup, SIGHUP, gotd_notify_sighdlr, NULL);
621 signal_set(&evsigusr1, SIGUSR1, gotd_notify_sighdlr, NULL);
622 signal(SIGPIPE, SIG_IGN);
624 signal_add(&evsigint, NULL);
625 signal_add(&evsigterm, NULL);
626 signal_add(&evsighup, NULL);
627 signal_add(&evsigusr1, NULL);
629 if (imsgbuf_init(&gotd_notify.parent_iev.ibuf, GOTD_FILENO_MSG_PIPE)
630 == -1)
631 fatal("imsgbuf_init");
632 imsgbuf_allow_fdpass(&gotd_notify.parent_iev.ibuf);
633 gotd_notify.parent_iev.handler = notify_dispatch;
634 gotd_notify.parent_iev.events = EV_READ;
635 gotd_notify.parent_iev.handler_arg = NULL;
636 event_set(&gotd_notify.parent_iev.ev, gotd_notify.parent_iev.ibuf.fd,
637 EV_READ, notify_dispatch, &gotd_notify.parent_iev);
638 gotd_imsg_event_add(&gotd_notify.parent_iev);
640 event_dispatch();
642 if (err)
643 log_warnx("%s: %s", title, err->msg);
644 gotd_notify_shutdown();
647 void
648 gotd_notify_shutdown(void)
650 log_debug("%s: shutting down", gotd_notify.title);
651 exit(0);