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 <sys/types.h>
18 #include <sys/queue.h>
19 #include <sys/tree.h>
20 #include <sys/socket.h>
21 #include <sys/wait.h>
23 #include <errno.h>
24 #include <event.h>
25 #include <siphash.h>
26 #include <limits.h>
27 #include <sha1.h>
28 #include <sha2.h>
29 #include <signal.h>
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <imsg.h>
34 #include <unistd.h>
36 #include "got_error.h"
37 #include "got_path.h"
39 #include "gotd.h"
40 #include "log.h"
41 #include "notify.h"
43 #ifndef nitems
44 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
45 #endif
47 static struct gotd_notify {
48 pid_t pid;
49 const char *title;
50 struct gotd_imsgev parent_iev;
51 struct gotd_repolist *repos;
52 const char *default_sender;
53 } gotd_notify;
55 struct gotd_notify_session {
56 STAILQ_ENTRY(gotd_notify_session) entry;
57 uint32_t id;
58 struct gotd_imsgev iev;
59 };
60 STAILQ_HEAD(gotd_notify_sessions, gotd_notify_session);
62 static struct gotd_notify_sessions gotd_notify_sessions[GOTD_CLIENT_TABLE_SIZE];
63 static SIPHASH_KEY sessions_hash_key;
65 static void gotd_notify_shutdown(void);
67 static uint64_t
68 session_hash(uint32_t session_id)
69 {
70 return SipHash24(&sessions_hash_key, &session_id, sizeof(session_id));
71 }
73 static void
74 add_session(struct gotd_notify_session *session)
75 {
76 uint64_t slot;
78 slot = session_hash(session->id) % nitems(gotd_notify_sessions);
79 STAILQ_INSERT_HEAD(&gotd_notify_sessions[slot], session, entry);
80 }
82 static struct gotd_notify_session *
83 find_session(uint32_t session_id)
84 {
85 uint64_t slot;
86 struct gotd_notify_session *s;
88 slot = session_hash(session_id) % nitems(gotd_notify_sessions);
89 STAILQ_FOREACH(s, &gotd_notify_sessions[slot], entry) {
90 if (s->id == session_id)
91 return s;
92 }
94 return NULL;
95 }
97 static struct gotd_notify_session *
98 find_session_by_fd(int fd)
99 {
100 uint64_t slot;
101 struct gotd_notify_session *s;
103 for (slot = 0; slot < nitems(gotd_notify_sessions); slot++) {
104 STAILQ_FOREACH(s, &gotd_notify_sessions[slot], entry) {
105 if (s->iev.ibuf.fd == fd)
106 return s;
110 return NULL;
113 static void
114 remove_session(struct gotd_notify_session *session)
116 uint64_t slot;
118 slot = session_hash(session->id) % nitems(gotd_notify_sessions);
119 STAILQ_REMOVE(&gotd_notify_sessions[slot], session,
120 gotd_notify_session, entry);
121 free(session);
124 static uint32_t
125 get_session_id(void)
127 int duplicate = 0;
128 uint32_t id;
130 do {
131 id = arc4random();
132 duplicate = (find_session(id) != NULL);
133 } while (duplicate || id == 0);
135 return id;
138 static void
139 gotd_notify_sighdlr(int sig, short event, void *arg)
141 /*
142 * Normal signal handler rules don't apply because libevent
143 * decouples for us.
144 */
146 switch (sig) {
147 case SIGHUP:
148 log_info("%s: ignoring SIGHUP", __func__);
149 break;
150 case SIGUSR1:
151 log_info("%s: ignoring SIGUSR1", __func__);
152 break;
153 case SIGTERM:
154 case SIGINT:
155 gotd_notify_shutdown();
156 /* NOTREACHED */
157 break;
158 default:
159 fatalx("unexpected signal");
163 static void
164 run_notification_helper(const char *prog, const char **argv, int fd,
165 const char *user, const char *pass)
167 const struct got_error *err = NULL;
168 pid_t pid;
169 int child_status;
171 pid = fork();
172 if (pid == -1) {
173 err = got_error_from_errno("fork");
174 log_warn("%s", err->msg);
175 return;
176 } else if (pid == 0) {
177 signal(SIGQUIT, SIG_DFL);
178 signal(SIGINT, SIG_DFL);
179 signal(SIGCHLD, SIG_DFL);
181 if (dup2(fd, STDIN_FILENO) == -1) {
182 fprintf(stderr, "%s: dup2: %s\n", getprogname(),
183 strerror(errno));
184 _exit(1);
187 closefrom(STDERR_FILENO + 1);
189 if (user != NULL && pass != NULL) {
190 setenv("GOT_NOTIFY_HTTP_USER", user, 1);
191 setenv("GOT_NOTIFY_HTTP_PASS", pass, 1);
194 if (execv(prog, (char *const *)argv) == -1) {
195 fprintf(stderr, "%s: exec %s: %s\n", getprogname(),
196 prog, strerror(errno));
197 _exit(1);
200 /* not reached */
203 if (waitpid(pid, &child_status, 0) == -1) {
204 err = got_error_from_errno("waitpid");
205 goto done;
208 if (!WIFEXITED(child_status)) {
209 err = got_error(GOT_ERR_PRIVSEP_DIED);
210 goto done;
213 if (WEXITSTATUS(child_status) != 0)
214 err = got_error(GOT_ERR_PRIVSEP_EXIT);
215 done:
216 if (err)
217 log_warnx("%s: child %s pid %d: %s", gotd_notify.title,
218 prog, pid, err->msg);
221 static void
222 notify_email(struct gotd_notification_target *target, const char *subject_line,
223 int fd)
225 const char *argv[13];
226 int i = 0;
228 argv[i++] = GOTD_PATH_PROG_NOTIFY_EMAIL;
230 argv[i++] = "-f";
231 if (target->conf.email.sender)
232 argv[i++] = target->conf.email.sender;
233 else
234 argv[i++] = gotd_notify.default_sender;
236 if (target->conf.email.responder) {
237 argv[i++] = "-r";
238 argv[i++] = target->conf.email.responder;
241 if (target->conf.email.hostname) {
242 argv[i++] = "-h";
243 argv[i++] = target->conf.email.hostname;
246 if (target->conf.email.port) {
247 argv[i++] = "-p";
248 argv[i++] = target->conf.email.port;
251 argv[i++] = "-s";
252 argv[i++] = subject_line;
254 argv[i++] = target->conf.email.recipient;
256 argv[i] = NULL;
258 run_notification_helper(GOTD_PATH_PROG_NOTIFY_EMAIL, argv, fd,
259 NULL, NULL);
262 static void
263 notify_http(struct gotd_notification_target *target, const char *repo, int fd)
265 const char *argv[10];
266 int argc = 0;
268 argv[argc++] = GOTD_PATH_PROG_NOTIFY_HTTP;
269 if (target->conf.http.tls)
270 argv[argc++] = "-c";
272 argv[argc++] = "-r";
273 argv[argc++] = repo;
274 argv[argc++] = "-h";
275 argv[argc++] = target->conf.http.hostname;
276 argv[argc++] = "-p";
277 argv[argc++] = target->conf.http.port;
279 argv[argc++] = target->conf.http.path;
281 argv[argc] = NULL;
283 run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP, argv, fd,
284 target->conf.http.user, target->conf.http.password);
287 static const struct got_error *
288 send_notification(struct imsg *imsg, struct gotd_imsgev *iev)
290 const struct got_error *err = NULL;
291 struct gotd_imsg_notify inotify;
292 size_t datalen;
293 struct gotd_repo *repo;
294 struct gotd_notification_target *target;
295 int fd;
297 datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
298 if (datalen != sizeof(inotify))
299 return got_error(GOT_ERR_PRIVSEP_LEN);
301 memcpy(&inotify, imsg->data, datalen);
303 repo = gotd_find_repo_by_name(inotify.repo_name, gotd_notify.repos);
304 if (repo == NULL)
305 return got_error(GOT_ERR_PRIVSEP_MSG);
307 fd = imsg_get_fd(imsg);
308 if (fd == -1)
309 return got_error(GOT_ERR_PRIVSEP_NO_FD);
311 if (lseek(fd, 0, SEEK_SET) == -1) {
312 err = got_error_from_errno("lseek");
313 goto done;
316 STAILQ_FOREACH(target, &repo->notification_targets, entry) {
317 switch (target->type) {
318 case GOTD_NOTIFICATION_VIA_EMAIL:
319 notify_email(target, inotify.subject_line, fd);
320 break;
321 case GOTD_NOTIFICATION_VIA_HTTP:
322 notify_http(target, repo->name, fd);
323 break;
327 if (gotd_imsg_compose_event(iev, GOTD_IMSG_NOTIFICATION_SENT,
328 PROC_NOTIFY, -1, NULL, 0) == -1) {
329 err = got_error_from_errno("imsg compose NOTIFY");
330 goto done;
332 done:
333 close(fd);
334 return err;
337 static void
338 notify_dispatch_session(int fd, short event, void *arg)
340 struct gotd_imsgev *iev = arg;
341 struct imsgbuf *ibuf = &iev->ibuf;
342 ssize_t n;
343 int shut = 0;
344 struct imsg imsg;
346 if (event & EV_READ) {
347 if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
348 fatal("imsg_read error");
349 if (n == 0) {
350 /* Connection closed. */
351 shut = 1;
352 goto done;
356 if (event & EV_WRITE) {
357 n = msgbuf_write(&ibuf->w);
358 if (n == -1 && errno != EAGAIN)
359 fatal("msgbuf_write");
360 if (n == 0) {
361 /* Connection closed. */
362 shut = 1;
363 goto done;
367 for (;;) {
368 const struct got_error *err = NULL;
370 if ((n = imsg_get(ibuf, &imsg)) == -1)
371 fatal("%s: imsg_get error", __func__);
372 if (n == 0) /* No more messages. */
373 break;
375 switch (imsg.hdr.type) {
376 case GOTD_IMSG_NOTIFY:
377 err = send_notification(&imsg, iev);
378 break;
379 default:
380 log_debug("unexpected imsg %d", imsg.hdr.type);
381 break;
383 imsg_free(&imsg);
385 if (err)
386 log_warnx("%s: %s", __func__, err->msg);
388 done:
389 if (!shut) {
390 gotd_imsg_event_add(iev);
391 } else {
392 struct gotd_notify_session *session;
394 /* This pipe is dead. Remove its event handler */
395 event_del(&iev->ev);
396 imsg_clear(&iev->ibuf);
398 session = find_session_by_fd(fd);
399 if (session)
400 remove_session(session);
404 static const struct got_error *
405 recv_session(struct imsg *imsg)
407 struct gotd_notify_session *session;
408 size_t datalen;
409 int fd;
411 datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
412 if (datalen != 0)
413 return got_error(GOT_ERR_PRIVSEP_LEN);
415 fd = imsg_get_fd(imsg);
416 if (fd == -1)
417 return got_error(GOT_ERR_PRIVSEP_NO_FD);
419 session = calloc(1, sizeof(*session));
420 if (session == NULL)
421 return got_error_from_errno("calloc");
423 session->id = get_session_id();
424 imsg_init(&session->iev.ibuf, fd);
425 session->iev.handler = notify_dispatch_session;
426 session->iev.events = EV_READ;
427 session->iev.handler_arg = NULL;
428 event_set(&session->iev.ev, session->iev.ibuf.fd, EV_READ,
429 notify_dispatch_session, &session->iev);
430 gotd_imsg_event_add(&session->iev);
431 add_session(session);
433 return NULL;
436 static void
437 notify_dispatch(int fd, short event, void *arg)
439 struct gotd_imsgev *iev = arg;
440 struct imsgbuf *ibuf = &iev->ibuf;
441 ssize_t n;
442 int shut = 0;
443 struct imsg imsg;
445 if (event & EV_READ) {
446 if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
447 fatal("imsg_read error");
448 if (n == 0) {
449 /* Connection closed. */
450 shut = 1;
451 goto done;
455 if (event & EV_WRITE) {
456 n = msgbuf_write(&ibuf->w);
457 if (n == -1 && errno != EAGAIN)
458 fatal("msgbuf_write");
459 if (n == 0) {
460 /* Connection closed. */
461 shut = 1;
462 goto done;
466 for (;;) {
467 const struct got_error *err = NULL;
469 if ((n = imsg_get(ibuf, &imsg)) == -1)
470 fatal("%s: imsg_get error", __func__);
471 if (n == 0) /* No more messages. */
472 break;
474 switch (imsg.hdr.type) {
475 case GOTD_IMSG_CONNECT_SESSION:
476 err = recv_session(&imsg);
477 break;
478 default:
479 log_debug("unexpected imsg %d", imsg.hdr.type);
480 break;
482 imsg_free(&imsg);
484 if (err)
485 log_warnx("%s: %s", __func__, err->msg);
487 done:
488 if (!shut) {
489 gotd_imsg_event_add(iev);
490 } else {
491 /* This pipe is dead. Remove its event handler */
492 event_del(&iev->ev);
493 event_loopexit(NULL);
498 void
499 notify_main(const char *title, struct gotd_repolist *repos,
500 const char *default_sender)
502 const struct got_error *err = NULL;
503 struct event evsigint, evsigterm, evsighup, evsigusr1;
505 arc4random_buf(&sessions_hash_key, sizeof(sessions_hash_key));
507 gotd_notify.title = title;
508 gotd_notify.repos = repos;
509 gotd_notify.default_sender = default_sender;
510 gotd_notify.pid = getpid();
512 signal_set(&evsigint, SIGINT, gotd_notify_sighdlr, NULL);
513 signal_set(&evsigterm, SIGTERM, gotd_notify_sighdlr, NULL);
514 signal_set(&evsighup, SIGHUP, gotd_notify_sighdlr, NULL);
515 signal_set(&evsigusr1, SIGUSR1, gotd_notify_sighdlr, NULL);
516 signal(SIGPIPE, SIG_IGN);
518 signal_add(&evsigint, NULL);
519 signal_add(&evsigterm, NULL);
520 signal_add(&evsighup, NULL);
521 signal_add(&evsigusr1, NULL);
523 imsg_init(&gotd_notify.parent_iev.ibuf, GOTD_FILENO_MSG_PIPE);
524 gotd_notify.parent_iev.handler = notify_dispatch;
525 gotd_notify.parent_iev.events = EV_READ;
526 gotd_notify.parent_iev.handler_arg = NULL;
527 event_set(&gotd_notify.parent_iev.ev, gotd_notify.parent_iev.ibuf.fd,
528 EV_READ, notify_dispatch, &gotd_notify.parent_iev);
529 gotd_imsg_event_add(&gotd_notify.parent_iev);
531 event_dispatch();
533 if (err)
534 log_warnx("%s: %s", title, err->msg);
535 gotd_notify_shutdown();
538 void
539 gotd_notify_shutdown(void)
541 log_debug("%s: shutting down", gotd_notify.title);
542 exit(0);