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 <sys/types.h>
18 #include <sys/queue.h>
20 #include <sys/socket.h>
36 #include "got_error.h"
37 #include "got_object.h"
46 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
49 static struct gotd_secrets secrets;
51 static struct gotd_notify {
54 struct gotd_imsgev parent_iev;
55 struct gotd_repolist *repos;
56 const char *default_sender;
59 struct gotd_notify_session {
60 STAILQ_ENTRY(gotd_notify_session) entry;
62 struct gotd_imsgev iev;
64 STAILQ_HEAD(gotd_notify_sessions, gotd_notify_session);
66 static struct gotd_notify_sessions gotd_notify_sessions[GOTD_CLIENT_TABLE_SIZE];
67 static SIPHASH_KEY sessions_hash_key;
69 static void gotd_notify_shutdown(void);
72 session_hash(uint32_t session_id)
74 return SipHash24(&sessions_hash_key, &session_id, sizeof(session_id));
78 add_session(struct gotd_notify_session *session)
82 slot = session_hash(session->id) % nitems(gotd_notify_sessions);
83 STAILQ_INSERT_HEAD(&gotd_notify_sessions[slot], session, entry);
86 static struct gotd_notify_session *
87 find_session(uint32_t session_id)
90 struct gotd_notify_session *s;
92 slot = session_hash(session_id) % nitems(gotd_notify_sessions);
93 STAILQ_FOREACH(s, &gotd_notify_sessions[slot], entry) {
94 if (s->id == session_id)
101 static struct gotd_notify_session *
102 find_session_by_fd(int fd)
105 struct gotd_notify_session *s;
107 for (slot = 0; slot < nitems(gotd_notify_sessions); slot++) {
108 STAILQ_FOREACH(s, &gotd_notify_sessions[slot], entry) {
109 if (s->iev.ibuf.fd == fd)
118 remove_session(struct gotd_notify_session *session)
122 slot = session_hash(session->id) % nitems(gotd_notify_sessions);
123 STAILQ_REMOVE(&gotd_notify_sessions[slot], session,
124 gotd_notify_session, entry);
125 close(session->iev.ibuf.fd);
137 duplicate = (find_session(id) != NULL);
138 } while (duplicate || id == 0);
144 gotd_notify_sighdlr(int sig, short event, void *arg)
147 * Normal signal handler rules don't apply because libevent
153 log_info("%s: ignoring SIGHUP", __func__);
156 log_info("%s: ignoring SIGUSR1", __func__);
160 gotd_notify_shutdown();
164 fatalx("unexpected signal");
169 run_notification_helper(const char *prog, const char **argv, int fd,
170 const char *user, const char *pass, const char *hmac_secret)
172 const struct got_error *err = NULL;
178 err = got_error_from_errno("fork");
179 log_warn("%s", err->msg);
181 } else if (pid == 0) {
182 signal(SIGQUIT, SIG_DFL);
183 signal(SIGINT, SIG_DFL);
184 signal(SIGCHLD, SIG_DFL);
186 if (dup2(fd, STDIN_FILENO) == -1) {
187 fprintf(stderr, "%s: dup2: %s\n", getprogname(),
192 closefrom(STDERR_FILENO + 1);
194 if (user != NULL && pass != NULL) {
195 setenv("GOT_NOTIFY_HTTP_USER", user, 1);
196 setenv("GOT_NOTIFY_HTTP_PASS", pass, 1);
198 unsetenv("GOTD_NOTIFY_HTTP_USER");
199 unsetenv("GOTD_NOTIFY_HTTP_PASS");
203 setenv("GOT_NOTIFY_HTTP_HMAC_SECRET", hmac_secret, 1);
205 unsetenv("GOT_NOTIFY_HTTP_HMAC_SECRET");
207 if (execv(prog, (char *const *)argv) == -1) {
208 fprintf(stderr, "%s: exec %s: %s\n", getprogname(),
209 prog, strerror(errno));
216 if (waitpid(pid, &child_status, 0) == -1) {
217 err = got_error_from_errno("waitpid");
221 if (!WIFEXITED(child_status)) {
222 err = got_error(GOT_ERR_PRIVSEP_DIED);
226 if (WEXITSTATUS(child_status) != 0)
227 err = got_error(GOT_ERR_PRIVSEP_EXIT);
230 log_warnx("%s: child %s pid %d: %s", gotd_notify.title,
231 prog, pid, err->msg);
235 notify_email(struct gotd_notification_target *target, const char *subject_line,
238 const char *argv[13];
241 argv[i++] = GOTD_PATH_PROG_NOTIFY_EMAIL;
244 if (target->conf.email.sender)
245 argv[i++] = target->conf.email.sender;
247 argv[i++] = gotd_notify.default_sender;
249 if (target->conf.email.responder) {
251 argv[i++] = target->conf.email.responder;
254 if (target->conf.email.hostname) {
256 argv[i++] = target->conf.email.hostname;
259 if (target->conf.email.port) {
261 argv[i++] = target->conf.email.port;
265 argv[i++] = subject_line;
267 argv[i++] = target->conf.email.recipient;
271 run_notification_helper(GOTD_PATH_PROG_NOTIFY_EMAIL, argv, fd,
276 notify_http(struct gotd_notification_target *target, const char *repo,
277 const char *username, int fd)
279 struct gotd_secret *secret;
280 const char *http_user = NULL, *http_pass = NULL, *hmac = NULL;
281 const char *argv[12];
284 argv[argc++] = GOTD_PATH_PROG_NOTIFY_HTTP;
285 if (target->conf.http.tls)
291 argv[argc++] = target->conf.http.hostname;
293 argv[argc++] = target->conf.http.port;
295 argv[argc++] = username;
297 argv[argc++] = target->conf.http.path;
301 if (target->conf.http.auth) {
302 secret = gotd_secrets_get(&secrets, GOTD_SECRET_AUTH,
303 target->conf.http.auth);
304 http_user = secret->user;
305 http_pass = secret->pass;
307 if (target->conf.http.hmac) {
308 secret = gotd_secrets_get(&secrets, GOTD_SECRET_HMAC,
309 target->conf.http.hmac);
313 run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP, argv, fd,
314 http_user, http_pass, hmac);
317 static const struct got_error *
318 send_notification(struct imsg *imsg, struct gotd_imsgev *iev)
320 const struct got_error *err = NULL;
321 struct gotd_imsg_notify inotify;
323 struct gotd_repo *repo;
324 struct gotd_notification_target *target;
326 char *username = NULL;
328 datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
329 if (datalen < sizeof(inotify))
330 return got_error(GOT_ERR_PRIVSEP_LEN);
332 memcpy(&inotify, imsg->data, sizeof(inotify));
333 if (datalen != sizeof(inotify) + inotify.username_len)
334 return got_error(GOT_ERR_PRIVSEP_LEN);
336 repo = gotd_find_repo_by_name(inotify.repo_name, gotd_notify.repos);
338 return got_error(GOT_ERR_PRIVSEP_MSG);
340 fd = imsg_get_fd(imsg);
342 return got_error(GOT_ERR_PRIVSEP_NO_FD);
344 username = strndup(imsg->data + sizeof(inotify), inotify.username_len);
345 if (username == NULL)
346 return got_error_from_errno("strndup");
348 STAILQ_FOREACH(target, &repo->notification_targets, entry) {
349 if (lseek(fd, 0, SEEK_SET) == -1) {
350 err = got_error_from_errno("lseek");
353 switch (target->type) {
354 case GOTD_NOTIFICATION_VIA_EMAIL:
355 notify_email(target, inotify.subject_line, fd);
357 case GOTD_NOTIFICATION_VIA_HTTP:
358 notify_http(target, repo->name, username, fd);
363 if (gotd_imsg_compose_event(iev, GOTD_IMSG_NOTIFICATION_SENT,
364 PROC_NOTIFY, -1, NULL, 0) == -1) {
365 err = got_error_from_errno("imsg compose NOTIFY");
375 notify_dispatch_session(int fd, short event, void *arg)
377 struct gotd_imsgev *iev = arg;
378 struct imsgbuf *ibuf = &iev->ibuf;
383 if (event & EV_READ) {
384 if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
385 fatal("imsg_read error");
387 /* Connection closed. */
393 if (event & EV_WRITE) {
394 n = msgbuf_write(&ibuf->w);
395 if (n == -1 && errno != EAGAIN && errno != EPIPE)
396 fatal("msgbuf_write");
397 if (n == 0 || (n == -1 && errno == EPIPE)) {
398 /* Connection closed. */
405 const struct got_error *err = NULL;
407 if ((n = imsg_get(ibuf, &imsg)) == -1)
408 fatal("%s: imsg_get error", __func__);
409 if (n == 0) /* No more messages. */
412 switch (imsg.hdr.type) {
413 case GOTD_IMSG_NOTIFY:
414 err = send_notification(&imsg, iev);
417 log_debug("unexpected imsg %d", imsg.hdr.type);
423 log_warnx("%s: %s", __func__, err->msg);
427 gotd_imsg_event_add(iev);
429 struct gotd_notify_session *session;
431 /* This pipe is dead. Remove its event handler */
433 imsg_clear(&iev->ibuf);
435 session = find_session_by_fd(fd);
437 remove_session(session);
441 static const struct got_error *
442 recv_session(struct imsg *imsg)
444 struct gotd_notify_session *session;
448 datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
450 return got_error(GOT_ERR_PRIVSEP_LEN);
452 fd = imsg_get_fd(imsg);
454 return got_error(GOT_ERR_PRIVSEP_NO_FD);
456 session = calloc(1, sizeof(*session));
458 return got_error_from_errno("calloc");
460 session->id = get_session_id();
461 imsg_init(&session->iev.ibuf, fd);
462 session->iev.handler = notify_dispatch_session;
463 session->iev.events = EV_READ;
464 session->iev.handler_arg = NULL;
465 event_set(&session->iev.ev, session->iev.ibuf.fd, EV_READ,
466 notify_dispatch_session, &session->iev);
467 gotd_imsg_event_add(&session->iev);
468 add_session(session);
473 static const struct got_error *
474 notify_ibuf_get_str(char **ret, struct ibuf *ibuf)
476 const char *str, *end;
481 str = ibuf_data(ibuf);
482 len = ibuf_size(ibuf);
484 end = memchr(str, '\0', len);
486 return got_error(GOT_ERR_PRIVSEP_LEN);
489 return got_error_from_errno("strdup");
491 if (ibuf_skip(ibuf, end - str + 1) == -1) {
494 return got_error(GOT_ERR_PRIVSEP_LEN);
501 notify_dispatch(int fd, short event, void *arg)
503 struct gotd_imsgev *iev = arg;
504 struct imsgbuf *imsgbuf = &iev->ibuf;
509 struct gotd_secret *s;
511 if (event & EV_READ) {
512 if ((n = imsg_read(imsgbuf)) == -1 && errno != EAGAIN)
513 fatal("imsg_read error");
515 /* Connection closed. */
521 if (event & EV_WRITE) {
522 n = msgbuf_write(&imsgbuf->w);
523 if (n == -1 && errno != EAGAIN)
524 fatal("msgbuf_write");
526 /* Connection closed. */
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. */
540 switch (imsg.hdr.type) {
541 case GOTD_IMSG_CONNECT_SESSION:
542 err = recv_session(&imsg);
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)
552 secrets.secrets = calloc(secrets.cap,
553 sizeof(*secrets.secrets));
554 if (secrets.secrets == NULL)
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);
568 if (s->type == GOTD_SECRET_AUTH) {
569 err = notify_ibuf_get_str(&s->user, &ibuf);
572 err = notify_ibuf_get_str(&s->pass, &ibuf);
576 err = notify_ibuf_get_str(&s->hmac, &ibuf);
580 if (ibuf_size(&ibuf) != 0)
581 fatalx("unexpected extra data in "
585 log_debug("unexpected imsg %d", imsg.hdr.type);
591 log_warnx("%s: %s", __func__, err->msg);
595 gotd_imsg_event_add(iev);
597 /* This pipe is dead. Remove its event handler */
599 event_loopexit(NULL);
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 imsg_init(&gotd_notify.parent_iev.ibuf, GOTD_FILENO_MSG_PIPE);
630 gotd_notify.parent_iev.handler = notify_dispatch;
631 gotd_notify.parent_iev.events = EV_READ;
632 gotd_notify.parent_iev.handler_arg = NULL;
633 event_set(&gotd_notify.parent_iev.ev, gotd_notify.parent_iev.ibuf.fd,
634 EV_READ, notify_dispatch, &gotd_notify.parent_iev);
635 gotd_imsg_event_add(&gotd_notify.parent_iev);
640 log_warnx("%s: %s", title, err->msg);
641 gotd_notify_shutdown();
645 gotd_notify_shutdown(void)
647 log_debug("%s: shutting down", gotd_notify.title);