Blob


1 /*
2 * Copyright (c) 2024 Stefan Sperling <stsp@openbsd.org>
3 *
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.
7 *
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.
15 */
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_path.h"
37 #include "gotd.h"
38 #include "log.h"
39 #include "notify.h"
41 #ifndef nitems
42 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
43 #endif
45 static struct gotd_notify {
46 pid_t pid;
47 const char *title;
48 struct gotd_imsgev parent_iev;
49 struct gotd_repolist *repos;
50 const char *default_sender;
51 } gotd_notify;
53 struct gotd_notify_session {
54 STAILQ_ENTRY(gotd_notify_session) entry;
55 uint32_t id;
56 struct gotd_imsgev iev;
57 };
58 STAILQ_HEAD(gotd_notify_sessions, gotd_notify_session);
60 static struct gotd_notify_sessions gotd_notify_sessions[GOTD_CLIENT_TABLE_SIZE];
61 static SIPHASH_KEY sessions_hash_key;
63 static void gotd_notify_shutdown(void);
65 static uint64_t
66 session_hash(uint32_t session_id)
67 {
68 return SipHash24(&sessions_hash_key, &session_id, sizeof(session_id));
69 }
71 static void
72 add_session(struct gotd_notify_session *session)
73 {
74 uint64_t slot;
76 slot = session_hash(session->id) % nitems(gotd_notify_sessions);
77 STAILQ_INSERT_HEAD(&gotd_notify_sessions[slot], session, entry);
78 }
80 static struct gotd_notify_session *
81 find_session(uint32_t session_id)
82 {
83 uint64_t slot;
84 struct gotd_notify_session *s;
86 slot = session_hash(session_id) % nitems(gotd_notify_sessions);
87 STAILQ_FOREACH(s, &gotd_notify_sessions[slot], entry) {
88 if (s->id == session_id)
89 return s;
90 }
92 return NULL;
93 }
95 static struct gotd_notify_session *
96 find_session_by_fd(int fd)
97 {
98 uint64_t slot;
99 struct gotd_notify_session *s;
101 for (slot = 0; slot < nitems(gotd_notify_sessions); slot++) {
102 STAILQ_FOREACH(s, &gotd_notify_sessions[slot], entry) {
103 if (s->iev.ibuf.fd == fd)
104 return s;
108 return NULL;
111 static void
112 remove_session(struct gotd_notify_session *session)
114 uint64_t slot;
116 slot = session_hash(session->id) % nitems(gotd_notify_sessions);
117 STAILQ_REMOVE(&gotd_notify_sessions[slot], session,
118 gotd_notify_session, entry);
119 free(session);
122 static uint32_t
123 get_session_id(void)
125 int duplicate = 0;
126 uint32_t id;
128 do {
129 id = arc4random();
130 duplicate = (find_session(id) != NULL);
131 } while (duplicate || id == 0);
133 return id;
136 static void
137 gotd_notify_sighdlr(int sig, short event, void *arg)
139 /*
140 * Normal signal handler rules don't apply because libevent
141 * decouples for us.
142 */
144 switch (sig) {
145 case SIGHUP:
146 log_info("%s: ignoring SIGHUP", __func__);
147 break;
148 case SIGUSR1:
149 log_info("%s: ignoring SIGUSR1", __func__);
150 break;
151 case SIGTERM:
152 case SIGINT:
153 gotd_notify_shutdown();
154 /* NOTREACHED */
155 break;
156 default:
157 fatalx("unexpected signal");
161 static void
162 run_notification_helper(const char *prog, const char **argv, int fd)
164 const struct got_error *err = NULL;
165 pid_t pid;
166 int child_status;
168 pid = fork();
169 if (pid == -1) {
170 err = got_error_from_errno("fork");
171 log_warn("%s", err->msg);
172 return;
173 } else if (pid == 0) {
174 signal(SIGQUIT, SIG_DFL);
175 signal(SIGINT, SIG_DFL);
176 signal(SIGCHLD, SIG_DFL);
178 if (dup2(fd, STDIN_FILENO) == -1) {
179 fprintf(stderr, "%s: dup2: %s\n", getprogname(),
180 strerror(errno));
181 _exit(1);
184 closefrom(STDERR_FILENO + 1);
186 if (execv(prog, (char *const *)argv) == -1) {
187 fprintf(stderr, "%s: exec %s: %s\n", getprogname(),
188 prog, strerror(errno));
189 _exit(1);
192 /* not reached */
195 if (waitpid(pid, &child_status, 0) == -1) {
196 err = got_error_from_errno("waitpid");
197 goto done;
200 if (!WIFEXITED(child_status)) {
201 err = got_error(GOT_ERR_PRIVSEP_DIED);
202 goto done;
205 if (WEXITSTATUS(child_status) != 0)
206 err = got_error(GOT_ERR_PRIVSEP_EXIT);
207 done:
208 if (err)
209 log_warnx("%s: child %s pid %d: %s", gotd_notify.title,
210 prog, pid, err->msg);
213 static void
214 notify_email(struct gotd_notification_target *target, const char *subject_line,
215 int fd)
217 const char *argv[13];
218 int i = 0;
220 argv[i++] = GOTD_PATH_PROG_NOTIFY_EMAIL;
222 argv[i++] = "-f";
223 if (target->conf.email.sender)
224 argv[i++] = target->conf.email.sender;
225 else
226 argv[i++] = gotd_notify.default_sender;
228 if (target->conf.email.responder) {
229 argv[i++] = "-r";
230 argv[i++] = target->conf.email.responder;
233 if (target->conf.email.hostname) {
234 argv[i++] = "-h";
235 argv[i++] = target->conf.email.hostname;
238 if (target->conf.email.port) {
239 argv[i++] = "-p";
240 argv[i++] = target->conf.email.port;
243 argv[i++] = "-s";
244 argv[i++] = subject_line;
246 argv[i++] = target->conf.email.recipient;
248 argv[i] = NULL;
250 run_notification_helper(GOTD_PATH_PROG_NOTIFY_EMAIL, argv, fd);
253 static void
254 notify_http(struct gotd_notification_target *target, int fd)
256 const char *argv[8];
257 int argc = 0;
259 argv[argc++] = GOTD_PATH_PROG_NOTIFY_HTTP;
260 if (target->conf.http.tls)
261 argv[argc++] = "-c";
263 argv[argc++] = "-h";
264 argv[argc++] = target->conf.http.hostname;
265 argv[argc++] = "-p";
266 argv[argc++] = target->conf.http.port;
268 argv[argc++] = target->conf.http.path;
270 argv[argc] = NULL;
272 run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP, argv, fd);
275 static const struct got_error *
276 send_notification(struct imsg *imsg, struct gotd_imsgev *iev)
278 const struct got_error *err = NULL;
279 struct gotd_imsg_notify inotify;
280 size_t datalen;
281 struct gotd_repo *repo;
282 struct gotd_notification_target *target;
283 int fd;
285 datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
286 if (datalen != sizeof(inotify))
287 return got_error(GOT_ERR_PRIVSEP_LEN);
289 memcpy(&inotify, imsg->data, datalen);
291 repo = gotd_find_repo_by_name(inotify.repo_name, gotd_notify.repos);
292 if (repo == NULL)
293 return got_error(GOT_ERR_PRIVSEP_MSG);
295 fd = imsg_get_fd(imsg);
296 if (fd == -1)
297 return got_error(GOT_ERR_PRIVSEP_NO_FD);
299 if (lseek(fd, 0, SEEK_SET) == -1) {
300 err = got_error_from_errno("lseek");
301 goto done;
304 STAILQ_FOREACH(target, &repo->notification_targets, entry) {
305 switch (target->type) {
306 case GOTD_NOTIFICATION_VIA_EMAIL:
307 notify_email(target, inotify.subject_line, fd);
308 break;
309 case GOTD_NOTIFICATION_VIA_HTTP:
310 notify_http(target, fd);
311 break;
315 if (gotd_imsg_compose_event(iev, GOTD_IMSG_NOTIFICATION_SENT,
316 PROC_NOTIFY, -1, NULL, 0) == -1) {
317 err = got_error_from_errno("imsg compose NOTIFY");
318 goto done;
320 done:
321 close(fd);
322 return err;
325 static void
326 notify_dispatch_session(int fd, short event, void *arg)
328 struct gotd_imsgev *iev = arg;
329 struct imsgbuf *ibuf = &iev->ibuf;
330 ssize_t n;
331 int shut = 0;
332 struct imsg imsg;
334 if (event & EV_READ) {
335 if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
336 fatal("imsg_read error");
337 if (n == 0) {
338 /* Connection closed. */
339 shut = 1;
340 goto done;
344 if (event & EV_WRITE) {
345 n = msgbuf_write(&ibuf->w);
346 if (n == -1 && errno != EAGAIN)
347 fatal("msgbuf_write");
348 if (n == 0) {
349 /* Connection closed. */
350 shut = 1;
351 goto done;
355 for (;;) {
356 const struct got_error *err = NULL;
358 if ((n = imsg_get(ibuf, &imsg)) == -1)
359 fatal("%s: imsg_get error", __func__);
360 if (n == 0) /* No more messages. */
361 break;
363 switch (imsg.hdr.type) {
364 case GOTD_IMSG_NOTIFY:
365 err = send_notification(&imsg, iev);
366 break;
367 default:
368 log_debug("unexpected imsg %d", imsg.hdr.type);
369 break;
371 imsg_free(&imsg);
373 if (err)
374 log_warnx("%s: %s", __func__, err->msg);
376 done:
377 if (!shut) {
378 gotd_imsg_event_add(iev);
379 } else {
380 struct gotd_notify_session *session;
382 /* This pipe is dead. Remove its event handler */
383 event_del(&iev->ev);
384 imsg_clear(&iev->ibuf);
386 session = find_session_by_fd(fd);
387 if (session)
388 remove_session(session);
392 static const struct got_error *
393 recv_session(struct imsg *imsg)
395 struct gotd_notify_session *session;
396 size_t datalen;
397 int fd;
399 datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
400 if (datalen != 0)
401 return got_error(GOT_ERR_PRIVSEP_LEN);
403 fd = imsg_get_fd(imsg);
404 if (fd == -1)
405 return got_error(GOT_ERR_PRIVSEP_NO_FD);
407 session = calloc(1, sizeof(*session));
408 if (session == NULL)
409 return got_error_from_errno("calloc");
411 session->id = get_session_id();
412 imsg_init(&session->iev.ibuf, fd);
413 session->iev.handler = notify_dispatch_session;
414 session->iev.events = EV_READ;
415 session->iev.handler_arg = NULL;
416 event_set(&session->iev.ev, session->iev.ibuf.fd, EV_READ,
417 notify_dispatch_session, &session->iev);
418 gotd_imsg_event_add(&session->iev);
419 add_session(session);
421 return NULL;
424 static void
425 notify_dispatch(int fd, short event, void *arg)
427 struct gotd_imsgev *iev = arg;
428 struct imsgbuf *ibuf = &iev->ibuf;
429 ssize_t n;
430 int shut = 0;
431 struct imsg imsg;
433 if (event & EV_READ) {
434 if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
435 fatal("imsg_read error");
436 if (n == 0) {
437 /* Connection closed. */
438 shut = 1;
439 goto done;
443 if (event & EV_WRITE) {
444 n = msgbuf_write(&ibuf->w);
445 if (n == -1 && errno != EAGAIN)
446 fatal("msgbuf_write");
447 if (n == 0) {
448 /* Connection closed. */
449 shut = 1;
450 goto done;
454 for (;;) {
455 const struct got_error *err = NULL;
457 if ((n = imsg_get(ibuf, &imsg)) == -1)
458 fatal("%s: imsg_get error", __func__);
459 if (n == 0) /* No more messages. */
460 break;
462 switch (imsg.hdr.type) {
463 case GOTD_IMSG_CONNECT_SESSION:
464 err = recv_session(&imsg);
465 break;
466 default:
467 log_debug("unexpected imsg %d", imsg.hdr.type);
468 break;
470 imsg_free(&imsg);
472 if (err)
473 log_warnx("%s: %s", __func__, err->msg);
475 done:
476 if (!shut) {
477 gotd_imsg_event_add(iev);
478 } else {
479 /* This pipe is dead. Remove its event handler */
480 event_del(&iev->ev);
481 event_loopexit(NULL);
486 void
487 notify_main(const char *title, struct gotd_repolist *repos,
488 const char *default_sender)
490 const struct got_error *err = NULL;
491 struct event evsigint, evsigterm, evsighup, evsigusr1;
493 arc4random_buf(&sessions_hash_key, sizeof(sessions_hash_key));
495 gotd_notify.title = title;
496 gotd_notify.repos = repos;
497 gotd_notify.default_sender = default_sender;
498 gotd_notify.pid = getpid();
500 signal_set(&evsigint, SIGINT, gotd_notify_sighdlr, NULL);
501 signal_set(&evsigterm, SIGTERM, gotd_notify_sighdlr, NULL);
502 signal_set(&evsighup, SIGHUP, gotd_notify_sighdlr, NULL);
503 signal_set(&evsigusr1, SIGUSR1, gotd_notify_sighdlr, NULL);
504 signal(SIGPIPE, SIG_IGN);
506 signal_add(&evsigint, NULL);
507 signal_add(&evsigterm, NULL);
508 signal_add(&evsighup, NULL);
509 signal_add(&evsigusr1, NULL);
511 imsg_init(&gotd_notify.parent_iev.ibuf, GOTD_FILENO_MSG_PIPE);
512 gotd_notify.parent_iev.handler = notify_dispatch;
513 gotd_notify.parent_iev.events = EV_READ;
514 gotd_notify.parent_iev.handler_arg = NULL;
515 event_set(&gotd_notify.parent_iev.ev, gotd_notify.parent_iev.ibuf.fd,
516 EV_READ, notify_dispatch, &gotd_notify.parent_iev);
517 gotd_imsg_event_add(&gotd_notify.parent_iev);
519 event_dispatch();
521 if (err)
522 log_warnx("%s: %s", title, err->msg);
523 gotd_notify_shutdown();
526 void
527 gotd_notify_shutdown(void)
529 log_debug("%s: shutting down", gotd_notify.title);
530 exit(0);