portable: remove SA_LEN/SS_LEN
[got-portable.git] / gotd / notify.c
blob1bafd105316d87c5c2e31cbd9217e6f18ce72997
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_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;
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)
68 return SipHash24(&sessions_hash_key, &session_id, sizeof(session_id));
71 static void
72 add_session(struct gotd_notify_session *session)
74 uint64_t slot;
76 slot = session_hash(session->id) % nitems(gotd_notify_sessions);
77 STAILQ_INSERT_HEAD(&gotd_notify_sessions[slot], session, entry);
80 static struct gotd_notify_session *
81 find_session(uint32_t session_id)
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;
92 return NULL;
95 static struct gotd_notify_session *
96 find_session_by_fd(int fd)
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 close(session->iev.ibuf.fd);
120 free(session);
123 static uint32_t
124 get_session_id(void)
126 int duplicate = 0;
127 uint32_t id;
129 do {
130 id = arc4random();
131 duplicate = (find_session(id) != NULL);
132 } while (duplicate || id == 0);
134 return id;
137 static void
138 gotd_notify_sighdlr(int sig, short event, void *arg)
141 * Normal signal handler rules don't apply because libevent
142 * decouples for us.
145 switch (sig) {
146 case SIGHUP:
147 log_info("%s: ignoring SIGHUP", __func__);
148 break;
149 case SIGUSR1:
150 log_info("%s: ignoring SIGUSR1", __func__);
151 break;
152 case SIGTERM:
153 case SIGINT:
154 gotd_notify_shutdown();
155 /* NOTREACHED */
156 break;
157 default:
158 fatalx("unexpected signal");
162 static void
163 run_notification_helper(const char *prog, const char **argv, int fd,
164 const char *user, const char *pass)
166 const struct got_error *err = NULL;
167 pid_t pid;
168 int child_status;
170 pid = fork();
171 if (pid == -1) {
172 err = got_error_from_errno("fork");
173 log_warn("%s", err->msg);
174 return;
175 } else if (pid == 0) {
176 signal(SIGQUIT, SIG_DFL);
177 signal(SIGINT, SIG_DFL);
178 signal(SIGCHLD, SIG_DFL);
180 if (dup2(fd, STDIN_FILENO) == -1) {
181 fprintf(stderr, "%s: dup2: %s\n", getprogname(),
182 strerror(errno));
183 _exit(1);
186 closefrom(STDERR_FILENO + 1);
188 if (user != NULL && pass != NULL) {
189 setenv("GOT_NOTIFY_HTTP_USER", user, 1);
190 setenv("GOT_NOTIFY_HTTP_PASS", pass, 1);
193 if (execv(prog, (char *const *)argv) == -1) {
194 fprintf(stderr, "%s: exec %s: %s\n", getprogname(),
195 prog, strerror(errno));
196 _exit(1);
199 /* not reached */
202 if (waitpid(pid, &child_status, 0) == -1) {
203 err = got_error_from_errno("waitpid");
204 goto done;
207 if (!WIFEXITED(child_status)) {
208 err = got_error(GOT_ERR_PRIVSEP_DIED);
209 goto done;
212 if (WEXITSTATUS(child_status) != 0)
213 err = got_error(GOT_ERR_PRIVSEP_EXIT);
214 done:
215 if (err)
216 log_warnx("%s: child %s pid %d: %s", gotd_notify.title,
217 prog, pid, err->msg);
220 static void
221 notify_email(struct gotd_notification_target *target, const char *subject_line,
222 int fd)
224 const char *argv[13];
225 int i = 0;
227 argv[i++] = GOTD_PATH_PROG_NOTIFY_EMAIL;
229 argv[i++] = "-f";
230 if (target->conf.email.sender)
231 argv[i++] = target->conf.email.sender;
232 else
233 argv[i++] = gotd_notify.default_sender;
235 if (target->conf.email.responder) {
236 argv[i++] = "-r";
237 argv[i++] = target->conf.email.responder;
240 if (target->conf.email.hostname) {
241 argv[i++] = "-h";
242 argv[i++] = target->conf.email.hostname;
245 if (target->conf.email.port) {
246 argv[i++] = "-p";
247 argv[i++] = target->conf.email.port;
250 argv[i++] = "-s";
251 argv[i++] = subject_line;
253 argv[i++] = target->conf.email.recipient;
255 argv[i] = NULL;
257 run_notification_helper(GOTD_PATH_PROG_NOTIFY_EMAIL, argv, fd,
258 NULL, NULL);
261 static void
262 notify_http(struct gotd_notification_target *target, const char *repo,
263 const char *username, int fd)
265 const char *argv[12];
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;
278 argv[argc++] = "-u";
279 argv[argc++] = username;
281 argv[argc++] = target->conf.http.path;
283 argv[argc] = NULL;
285 run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP, argv, fd,
286 target->conf.http.user, target->conf.http.password);
289 static const struct got_error *
290 send_notification(struct imsg *imsg, struct gotd_imsgev *iev)
292 const struct got_error *err = NULL;
293 struct gotd_imsg_notify inotify;
294 size_t datalen;
295 struct gotd_repo *repo;
296 struct gotd_notification_target *target;
297 int fd;
298 char *username = NULL;
300 datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
301 if (datalen < sizeof(inotify))
302 return got_error(GOT_ERR_PRIVSEP_LEN);
304 memcpy(&inotify, imsg->data, sizeof(inotify));
305 if (datalen != sizeof(inotify) + inotify.username_len)
306 return got_error(GOT_ERR_PRIVSEP_LEN);
308 repo = gotd_find_repo_by_name(inotify.repo_name, gotd_notify.repos);
309 if (repo == NULL)
310 return got_error(GOT_ERR_PRIVSEP_MSG);
312 fd = imsg_get_fd(imsg);
313 if (fd == -1)
314 return got_error(GOT_ERR_PRIVSEP_NO_FD);
316 username = strndup(imsg->data + sizeof(inotify), inotify.username_len);
317 if (username == NULL)
318 return got_error_from_errno("strndup");
320 STAILQ_FOREACH(target, &repo->notification_targets, entry) {
321 if (lseek(fd, 0, SEEK_SET) == -1) {
322 err = got_error_from_errno("lseek");
323 goto done;
325 switch (target->type) {
326 case GOTD_NOTIFICATION_VIA_EMAIL:
327 notify_email(target, inotify.subject_line, fd);
328 break;
329 case GOTD_NOTIFICATION_VIA_HTTP:
330 notify_http(target, repo->name, username, fd);
331 break;
335 if (gotd_imsg_compose_event(iev, GOTD_IMSG_NOTIFICATION_SENT,
336 PROC_NOTIFY, -1, NULL, 0) == -1) {
337 err = got_error_from_errno("imsg compose NOTIFY");
338 goto done;
340 done:
341 close(fd);
342 free(username);
343 return err;
346 static void
347 notify_dispatch_session(int fd, short event, void *arg)
349 struct gotd_imsgev *iev = arg;
350 struct imsgbuf *ibuf = &iev->ibuf;
351 ssize_t n;
352 int shut = 0;
353 struct imsg imsg;
355 if (event & EV_READ) {
356 if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
357 fatal("imsg_read error");
358 if (n == 0) {
359 /* Connection closed. */
360 shut = 1;
361 goto done;
365 if (event & EV_WRITE) {
366 n = msgbuf_write(&ibuf->w);
367 if (n == -1 && errno != EAGAIN && errno != EPIPE)
368 fatal("msgbuf_write");
369 if (n == 0 || (n == -1 && errno == EPIPE)) {
370 /* Connection closed. */
371 shut = 1;
372 goto done;
376 for (;;) {
377 const struct got_error *err = NULL;
379 if ((n = imsg_get(ibuf, &imsg)) == -1)
380 fatal("%s: imsg_get error", __func__);
381 if (n == 0) /* No more messages. */
382 break;
384 switch (imsg.hdr.type) {
385 case GOTD_IMSG_NOTIFY:
386 err = send_notification(&imsg, iev);
387 break;
388 default:
389 log_debug("unexpected imsg %d", imsg.hdr.type);
390 break;
392 imsg_free(&imsg);
394 if (err)
395 log_warnx("%s: %s", __func__, err->msg);
397 done:
398 if (!shut) {
399 gotd_imsg_event_add(iev);
400 } else {
401 struct gotd_notify_session *session;
403 /* This pipe is dead. Remove its event handler */
404 event_del(&iev->ev);
405 imsg_clear(&iev->ibuf);
407 session = find_session_by_fd(fd);
408 if (session)
409 remove_session(session);
413 static const struct got_error *
414 recv_session(struct imsg *imsg)
416 struct gotd_notify_session *session;
417 size_t datalen;
418 int fd;
420 datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
421 if (datalen != 0)
422 return got_error(GOT_ERR_PRIVSEP_LEN);
424 fd = imsg_get_fd(imsg);
425 if (fd == -1)
426 return got_error(GOT_ERR_PRIVSEP_NO_FD);
428 session = calloc(1, sizeof(*session));
429 if (session == NULL)
430 return got_error_from_errno("calloc");
432 session->id = get_session_id();
433 imsg_init(&session->iev.ibuf, fd);
434 session->iev.handler = notify_dispatch_session;
435 session->iev.events = EV_READ;
436 session->iev.handler_arg = NULL;
437 event_set(&session->iev.ev, session->iev.ibuf.fd, EV_READ,
438 notify_dispatch_session, &session->iev);
439 gotd_imsg_event_add(&session->iev);
440 add_session(session);
442 return NULL;
445 static void
446 notify_dispatch(int fd, short event, void *arg)
448 struct gotd_imsgev *iev = arg;
449 struct imsgbuf *ibuf = &iev->ibuf;
450 ssize_t n;
451 int shut = 0;
452 struct imsg imsg;
454 if (event & EV_READ) {
455 if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
456 fatal("imsg_read error");
457 if (n == 0) {
458 /* Connection closed. */
459 shut = 1;
460 goto done;
464 if (event & EV_WRITE) {
465 n = msgbuf_write(&ibuf->w);
466 if (n == -1 && errno != EAGAIN)
467 fatal("msgbuf_write");
468 if (n == 0) {
469 /* Connection closed. */
470 shut = 1;
471 goto done;
475 for (;;) {
476 const struct got_error *err = NULL;
478 if ((n = imsg_get(ibuf, &imsg)) == -1)
479 fatal("%s: imsg_get error", __func__);
480 if (n == 0) /* No more messages. */
481 break;
483 switch (imsg.hdr.type) {
484 case GOTD_IMSG_CONNECT_SESSION:
485 err = recv_session(&imsg);
486 break;
487 default:
488 log_debug("unexpected imsg %d", imsg.hdr.type);
489 break;
491 imsg_free(&imsg);
493 if (err)
494 log_warnx("%s: %s", __func__, err->msg);
496 done:
497 if (!shut) {
498 gotd_imsg_event_add(iev);
499 } else {
500 /* This pipe is dead. Remove its event handler */
501 event_del(&iev->ev);
502 event_loopexit(NULL);
507 void
508 notify_main(const char *title, struct gotd_repolist *repos,
509 const char *default_sender)
511 const struct got_error *err = NULL;
512 struct event evsigint, evsigterm, evsighup, evsigusr1;
514 arc4random_buf(&sessions_hash_key, sizeof(sessions_hash_key));
516 gotd_notify.title = title;
517 gotd_notify.repos = repos;
518 gotd_notify.default_sender = default_sender;
519 gotd_notify.pid = getpid();
521 signal_set(&evsigint, SIGINT, gotd_notify_sighdlr, NULL);
522 signal_set(&evsigterm, SIGTERM, gotd_notify_sighdlr, NULL);
523 signal_set(&evsighup, SIGHUP, gotd_notify_sighdlr, NULL);
524 signal_set(&evsigusr1, SIGUSR1, gotd_notify_sighdlr, NULL);
525 signal(SIGPIPE, SIG_IGN);
527 signal_add(&evsigint, NULL);
528 signal_add(&evsigterm, NULL);
529 signal_add(&evsighup, NULL);
530 signal_add(&evsigusr1, NULL);
532 imsg_init(&gotd_notify.parent_iev.ibuf, GOTD_FILENO_MSG_PIPE);
533 gotd_notify.parent_iev.handler = notify_dispatch;
534 gotd_notify.parent_iev.events = EV_READ;
535 gotd_notify.parent_iev.handler_arg = NULL;
536 event_set(&gotd_notify.parent_iev.ev, gotd_notify.parent_iev.ibuf.fd,
537 EV_READ, notify_dispatch, &gotd_notify.parent_iev);
538 gotd_imsg_event_add(&gotd_notify.parent_iev);
540 event_dispatch();
542 if (err)
543 log_warnx("%s: %s", title, err->msg);
544 gotd_notify_shutdown();
547 void
548 gotd_notify_shutdown(void)
550 log_debug("%s: shutting down", gotd_notify.title);
551 exit(0);