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"
44 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
47 static struct gotd_notify {
50 struct gotd_imsgev parent_iev;
51 struct gotd_repolist *repos;
52 const char *default_sender;
55 struct gotd_notify_session {
56 STAILQ_ENTRY(gotd_notify_session) entry;
58 struct gotd_imsgev iev;
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);
68 session_hash(uint32_t session_id)
70 return SipHash24(&sessions_hash_key, &session_id, sizeof(session_id));
74 add_session(struct gotd_notify_session *session)
78 slot = session_hash(session->id) % nitems(gotd_notify_sessions);
79 STAILQ_INSERT_HEAD(&gotd_notify_sessions[slot], session, entry);
82 static struct gotd_notify_session *
83 find_session(uint32_t session_id)
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)
97 static struct gotd_notify_session *
98 find_session_by_fd(int fd)
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)
114 remove_session(struct gotd_notify_session *session)
118 slot = session_hash(session->id) % nitems(gotd_notify_sessions);
119 STAILQ_REMOVE(&gotd_notify_sessions[slot], session,
120 gotd_notify_session, entry);
121 close(session->iev.ibuf.fd);
133 duplicate = (find_session(id) != NULL);
134 } while (duplicate || id == 0);
140 gotd_notify_sighdlr(int sig, short event, void *arg)
143 * Normal signal handler rules don't apply because libevent
149 log_info("%s: ignoring SIGHUP", __func__);
152 log_info("%s: ignoring SIGUSR1", __func__);
156 gotd_notify_shutdown();
160 fatalx("unexpected signal");
165 run_notification_helper(const char *prog, const char **argv, int fd,
166 const char *user, const char *pass)
168 const struct got_error *err = NULL;
174 err = got_error_from_errno("fork");
175 log_warn("%s", err->msg);
177 } else if (pid == 0) {
178 signal(SIGQUIT, SIG_DFL);
179 signal(SIGINT, SIG_DFL);
180 signal(SIGCHLD, SIG_DFL);
182 if (dup2(fd, STDIN_FILENO) == -1) {
183 fprintf(stderr, "%s: dup2: %s\n", getprogname(),
188 closefrom(STDERR_FILENO + 1);
190 if (user != NULL && pass != NULL) {
191 setenv("GOT_NOTIFY_HTTP_USER", user, 1);
192 setenv("GOT_NOTIFY_HTTP_PASS", pass, 1);
195 if (execv(prog, (char *const *)argv) == -1) {
196 fprintf(stderr, "%s: exec %s: %s\n", getprogname(),
197 prog, strerror(errno));
204 if (waitpid(pid, &child_status, 0) == -1) {
205 err = got_error_from_errno("waitpid");
209 if (!WIFEXITED(child_status)) {
210 err = got_error(GOT_ERR_PRIVSEP_DIED);
214 if (WEXITSTATUS(child_status) != 0)
215 err = got_error(GOT_ERR_PRIVSEP_EXIT);
218 log_warnx("%s: child %s pid %d: %s", gotd_notify.title,
219 prog, pid, err->msg);
223 notify_email(struct gotd_notification_target *target, const char *subject_line,
226 const char *argv[13];
229 argv[i++] = GOTD_PATH_PROG_NOTIFY_EMAIL;
232 if (target->conf.email.sender)
233 argv[i++] = target->conf.email.sender;
235 argv[i++] = gotd_notify.default_sender;
237 if (target->conf.email.responder) {
239 argv[i++] = target->conf.email.responder;
242 if (target->conf.email.hostname) {
244 argv[i++] = target->conf.email.hostname;
247 if (target->conf.email.port) {
249 argv[i++] = target->conf.email.port;
253 argv[i++] = subject_line;
255 argv[i++] = target->conf.email.recipient;
259 run_notification_helper(GOTD_PATH_PROG_NOTIFY_EMAIL, argv, fd,
264 notify_http(struct gotd_notification_target *target, const char *repo,
265 const char *username, int fd)
267 const char *argv[12];
270 argv[argc++] = GOTD_PATH_PROG_NOTIFY_HTTP;
271 if (target->conf.http.tls)
277 argv[argc++] = target->conf.http.hostname;
279 argv[argc++] = target->conf.http.port;
281 argv[argc++] = username;
283 argv[argc++] = target->conf.http.path;
287 run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP, argv, fd,
288 target->conf.http.user, target->conf.http.password);
291 static const struct got_error *
292 send_notification(struct imsg *imsg, struct gotd_imsgev *iev)
294 const struct got_error *err = NULL;
295 struct gotd_imsg_notify inotify;
297 struct gotd_repo *repo;
298 struct gotd_notification_target *target;
300 char *username = NULL;
302 datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
303 if (datalen < sizeof(inotify))
304 return got_error(GOT_ERR_PRIVSEP_LEN);
306 memcpy(&inotify, imsg->data, sizeof(inotify));
307 if (datalen != sizeof(inotify) + inotify.username_len)
308 return got_error(GOT_ERR_PRIVSEP_LEN);
310 repo = gotd_find_repo_by_name(inotify.repo_name, gotd_notify.repos);
312 return got_error(GOT_ERR_PRIVSEP_MSG);
314 fd = imsg_get_fd(imsg);
316 return got_error(GOT_ERR_PRIVSEP_NO_FD);
318 username = strndup(imsg->data + sizeof(inotify), inotify.username_len);
319 if (username == NULL)
320 return got_error_from_errno("strndup");
322 if (lseek(fd, 0, SEEK_SET) == -1) {
323 err = got_error_from_errno("lseek");
328 STAILQ_FOREACH(target, &repo->notification_targets, entry) {
329 switch (target->type) {
330 case GOTD_NOTIFICATION_VIA_EMAIL:
331 notify_email(target, inotify.subject_line, fd);
333 case GOTD_NOTIFICATION_VIA_HTTP:
334 notify_http(target, repo->name, username, fd);
339 if (gotd_imsg_compose_event(iev, GOTD_IMSG_NOTIFICATION_SENT,
340 PROC_NOTIFY, -1, NULL, 0) == -1) {
341 err = got_error_from_errno("imsg compose NOTIFY");
351 notify_dispatch_session(int fd, short event, void *arg)
353 struct gotd_imsgev *iev = arg;
354 struct imsgbuf *ibuf = &iev->ibuf;
359 if (event & EV_READ) {
360 if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
361 fatal("imsg_read error");
363 /* Connection closed. */
369 if (event & EV_WRITE) {
370 n = msgbuf_write(&ibuf->w);
371 if (n == -1 && errno != EAGAIN && errno != EPIPE)
372 fatal("msgbuf_write");
373 if (n == 0 || (n == -1 && errno == EPIPE)) {
374 /* Connection closed. */
381 const struct got_error *err = NULL;
383 if ((n = imsg_get(ibuf, &imsg)) == -1)
384 fatal("%s: imsg_get error", __func__);
385 if (n == 0) /* No more messages. */
388 switch (imsg.hdr.type) {
389 case GOTD_IMSG_NOTIFY:
390 err = send_notification(&imsg, iev);
393 log_debug("unexpected imsg %d", imsg.hdr.type);
399 log_warnx("%s: %s", __func__, err->msg);
403 gotd_imsg_event_add(iev);
405 struct gotd_notify_session *session;
407 /* This pipe is dead. Remove its event handler */
409 imsg_clear(&iev->ibuf);
411 session = find_session_by_fd(fd);
413 remove_session(session);
417 static const struct got_error *
418 recv_session(struct imsg *imsg)
420 struct gotd_notify_session *session;
424 datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
426 return got_error(GOT_ERR_PRIVSEP_LEN);
428 fd = imsg_get_fd(imsg);
430 return got_error(GOT_ERR_PRIVSEP_NO_FD);
432 session = calloc(1, sizeof(*session));
434 return got_error_from_errno("calloc");
436 session->id = get_session_id();
437 imsg_init(&session->iev.ibuf, fd);
438 session->iev.handler = notify_dispatch_session;
439 session->iev.events = EV_READ;
440 session->iev.handler_arg = NULL;
441 event_set(&session->iev.ev, session->iev.ibuf.fd, EV_READ,
442 notify_dispatch_session, &session->iev);
443 gotd_imsg_event_add(&session->iev);
444 add_session(session);
450 notify_dispatch(int fd, short event, void *arg)
452 struct gotd_imsgev *iev = arg;
453 struct imsgbuf *ibuf = &iev->ibuf;
458 if (event & EV_READ) {
459 if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
460 fatal("imsg_read error");
462 /* Connection closed. */
468 if (event & EV_WRITE) {
469 n = msgbuf_write(&ibuf->w);
470 if (n == -1 && errno != EAGAIN)
471 fatal("msgbuf_write");
473 /* Connection closed. */
480 const struct got_error *err = NULL;
482 if ((n = imsg_get(ibuf, &imsg)) == -1)
483 fatal("%s: imsg_get error", __func__);
484 if (n == 0) /* No more messages. */
487 switch (imsg.hdr.type) {
488 case GOTD_IMSG_CONNECT_SESSION:
489 err = recv_session(&imsg);
492 log_debug("unexpected imsg %d", imsg.hdr.type);
498 log_warnx("%s: %s", __func__, err->msg);
502 gotd_imsg_event_add(iev);
504 /* This pipe is dead. Remove its event handler */
506 event_loopexit(NULL);
512 notify_main(const char *title, struct gotd_repolist *repos,
513 const char *default_sender)
515 const struct got_error *err = NULL;
516 struct event evsigint, evsigterm, evsighup, evsigusr1;
518 arc4random_buf(&sessions_hash_key, sizeof(sessions_hash_key));
520 gotd_notify.title = title;
521 gotd_notify.repos = repos;
522 gotd_notify.default_sender = default_sender;
523 gotd_notify.pid = getpid();
525 signal_set(&evsigint, SIGINT, gotd_notify_sighdlr, NULL);
526 signal_set(&evsigterm, SIGTERM, gotd_notify_sighdlr, NULL);
527 signal_set(&evsighup, SIGHUP, gotd_notify_sighdlr, NULL);
528 signal_set(&evsigusr1, SIGUSR1, gotd_notify_sighdlr, NULL);
529 signal(SIGPIPE, SIG_IGN);
531 signal_add(&evsigint, NULL);
532 signal_add(&evsigterm, NULL);
533 signal_add(&evsighup, NULL);
534 signal_add(&evsigusr1, NULL);
536 imsg_init(&gotd_notify.parent_iev.ibuf, GOTD_FILENO_MSG_PIPE);
537 gotd_notify.parent_iev.handler = notify_dispatch;
538 gotd_notify.parent_iev.events = EV_READ;
539 gotd_notify.parent_iev.handler_arg = NULL;
540 event_set(&gotd_notify.parent_iev.ev, gotd_notify.parent_iev.ibuf.fd,
541 EV_READ, notify_dispatch, &gotd_notify.parent_iev);
542 gotd_imsg_event_add(&gotd_notify.parent_iev);
547 log_warnx("%s: %s", title, err->msg);
548 gotd_notify_shutdown();
552 gotd_notify_shutdown(void)
554 log_debug("%s: shutting down", gotd_notify.title);