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>
34 #include "got_error.h"
35 #include "got_object.h"
44 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
47 static struct gotd_secrets secrets
;
49 static struct gotd_notify
{
52 struct gotd_imsgev parent_iev
;
53 struct gotd_repolist
*repos
;
54 const char *default_sender
;
57 struct gotd_notify_session
{
58 STAILQ_ENTRY(gotd_notify_session
) entry
;
60 struct gotd_imsgev iev
;
62 STAILQ_HEAD(gotd_notify_sessions
, gotd_notify_session
);
64 static struct gotd_notify_sessions gotd_notify_sessions
[GOTD_CLIENT_TABLE_SIZE
];
65 static SIPHASH_KEY sessions_hash_key
;
67 static void gotd_notify_shutdown(void);
70 session_hash(uint32_t session_id
)
72 return SipHash24(&sessions_hash_key
, &session_id
, sizeof(session_id
));
76 add_session(struct gotd_notify_session
*session
)
80 slot
= session_hash(session
->id
) % nitems(gotd_notify_sessions
);
81 STAILQ_INSERT_HEAD(&gotd_notify_sessions
[slot
], session
, entry
);
84 static struct gotd_notify_session
*
85 find_session(uint32_t session_id
)
88 struct gotd_notify_session
*s
;
90 slot
= session_hash(session_id
) % nitems(gotd_notify_sessions
);
91 STAILQ_FOREACH(s
, &gotd_notify_sessions
[slot
], entry
) {
92 if (s
->id
== session_id
)
99 static struct gotd_notify_session
*
100 find_session_by_fd(int fd
)
103 struct gotd_notify_session
*s
;
105 for (slot
= 0; slot
< nitems(gotd_notify_sessions
); slot
++) {
106 STAILQ_FOREACH(s
, &gotd_notify_sessions
[slot
], entry
) {
107 if (s
->iev
.ibuf
.fd
== fd
)
116 remove_session(struct gotd_notify_session
*session
)
120 slot
= session_hash(session
->id
) % nitems(gotd_notify_sessions
);
121 STAILQ_REMOVE(&gotd_notify_sessions
[slot
], session
,
122 gotd_notify_session
, entry
);
123 close(session
->iev
.ibuf
.fd
);
135 duplicate
= (find_session(id
) != NULL
);
136 } while (duplicate
|| id
== 0);
142 gotd_notify_sighdlr(int sig
, short event
, void *arg
)
145 * Normal signal handler rules don't apply because libevent
151 log_info("%s: ignoring SIGHUP", __func__
);
154 log_info("%s: ignoring SIGUSR1", __func__
);
158 gotd_notify_shutdown();
162 fatalx("unexpected signal");
167 run_notification_helper(const char *prog
, const char **argv
, int fd
,
168 const char *user
, const char *pass
, const char *hmac_secret
)
170 const struct got_error
*err
= NULL
;
176 err
= got_error_from_errno("fork");
177 log_warn("%s", err
->msg
);
179 } else if (pid
== 0) {
180 signal(SIGQUIT
, SIG_DFL
);
181 signal(SIGINT
, SIG_DFL
);
182 signal(SIGCHLD
, SIG_DFL
);
184 if (dup2(fd
, STDIN_FILENO
) == -1) {
185 fprintf(stderr
, "%s: dup2: %s\n", getprogname(),
190 closefrom(STDERR_FILENO
+ 1);
192 if (user
!= NULL
&& pass
!= NULL
) {
193 setenv("GOT_NOTIFY_HTTP_USER", user
, 1);
194 setenv("GOT_NOTIFY_HTTP_PASS", pass
, 1);
196 unsetenv("GOTD_NOTIFY_HTTP_USER");
197 unsetenv("GOTD_NOTIFY_HTTP_PASS");
201 setenv("GOT_NOTIFY_HTTP_HMAC_SECRET", hmac_secret
, 1);
203 unsetenv("GOT_NOTIFY_HTTP_HMAC_SECRET");
205 if (execv(prog
, (char *const *)argv
) == -1) {
206 fprintf(stderr
, "%s: exec %s: %s\n", getprogname(),
207 prog
, strerror(errno
));
214 if (waitpid(pid
, &child_status
, 0) == -1) {
215 err
= got_error_from_errno("waitpid");
219 if (!WIFEXITED(child_status
)) {
220 err
= got_error(GOT_ERR_PRIVSEP_DIED
);
224 if (WEXITSTATUS(child_status
) != 0)
225 err
= got_error(GOT_ERR_PRIVSEP_EXIT
);
228 log_warnx("%s: child %s pid %d: %s", gotd_notify
.title
,
229 prog
, pid
, err
->msg
);
233 notify_email(struct gotd_notification_target
*target
, const char *subject_line
,
236 const char *argv
[13];
239 argv
[i
++] = GOTD_PATH_PROG_NOTIFY_EMAIL
;
242 if (target
->conf
.email
.sender
)
243 argv
[i
++] = target
->conf
.email
.sender
;
245 argv
[i
++] = gotd_notify
.default_sender
;
247 if (target
->conf
.email
.responder
) {
249 argv
[i
++] = target
->conf
.email
.responder
;
252 if (target
->conf
.email
.hostname
) {
254 argv
[i
++] = target
->conf
.email
.hostname
;
257 if (target
->conf
.email
.port
) {
259 argv
[i
++] = target
->conf
.email
.port
;
263 argv
[i
++] = subject_line
;
265 argv
[i
++] = target
->conf
.email
.recipient
;
269 run_notification_helper(GOTD_PATH_PROG_NOTIFY_EMAIL
, argv
, fd
,
274 notify_http(struct gotd_notification_target
*target
, const char *repo
,
275 const char *username
, int fd
)
277 struct gotd_secret
*secret
;
278 const char *http_user
= NULL
, *http_pass
= NULL
, *hmac
= NULL
;
279 const char *argv
[12];
282 argv
[argc
++] = GOTD_PATH_PROG_NOTIFY_HTTP
;
283 if (target
->conf
.http
.tls
)
289 argv
[argc
++] = target
->conf
.http
.hostname
;
291 argv
[argc
++] = target
->conf
.http
.port
;
293 argv
[argc
++] = username
;
295 argv
[argc
++] = target
->conf
.http
.path
;
299 if (target
->conf
.http
.auth
) {
300 secret
= gotd_secrets_get(&secrets
, GOTD_SECRET_AUTH
,
301 target
->conf
.http
.auth
);
302 http_user
= secret
->user
;
303 http_pass
= secret
->pass
;
305 if (target
->conf
.http
.hmac
) {
306 secret
= gotd_secrets_get(&secrets
, GOTD_SECRET_HMAC
,
307 target
->conf
.http
.hmac
);
311 run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP
, argv
, fd
,
312 http_user
, http_pass
, hmac
);
315 static const struct got_error
*
316 send_notification(struct imsg
*imsg
, struct gotd_imsgev
*iev
)
318 const struct got_error
*err
= NULL
;
319 struct gotd_imsg_notify inotify
;
321 struct gotd_repo
*repo
;
322 struct gotd_notification_target
*target
;
324 char *username
= NULL
;
326 datalen
= imsg
->hdr
.len
- IMSG_HEADER_SIZE
;
327 if (datalen
< sizeof(inotify
))
328 return got_error(GOT_ERR_PRIVSEP_LEN
);
330 memcpy(&inotify
, imsg
->data
, sizeof(inotify
));
331 if (datalen
!= sizeof(inotify
) + inotify
.username_len
)
332 return got_error(GOT_ERR_PRIVSEP_LEN
);
334 repo
= gotd_find_repo_by_name(inotify
.repo_name
, gotd_notify
.repos
);
336 return got_error(GOT_ERR_PRIVSEP_MSG
);
338 fd
= imsg_get_fd(imsg
);
340 return got_error(GOT_ERR_PRIVSEP_NO_FD
);
342 username
= strndup(imsg
->data
+ sizeof(inotify
), inotify
.username_len
);
343 if (username
== NULL
)
344 return got_error_from_errno("strndup");
346 STAILQ_FOREACH(target
, &repo
->notification_targets
, entry
) {
347 if (lseek(fd
, 0, SEEK_SET
) == -1) {
348 err
= got_error_from_errno("lseek");
351 switch (target
->type
) {
352 case GOTD_NOTIFICATION_VIA_EMAIL
:
353 notify_email(target
, inotify
.subject_line
, fd
);
355 case GOTD_NOTIFICATION_VIA_HTTP
:
356 notify_http(target
, repo
->name
, username
, fd
);
361 if (gotd_imsg_compose_event(iev
, GOTD_IMSG_NOTIFICATION_SENT
,
362 PROC_NOTIFY
, -1, NULL
, 0) == -1) {
363 err
= got_error_from_errno("imsg compose NOTIFY");
373 notify_dispatch_session(int fd
, short event
, void *arg
)
375 const struct got_error
*err
= NULL
;
376 struct gotd_imsgev
*iev
= arg
;
377 struct imsgbuf
*ibuf
= &iev
->ibuf
;
382 if (event
& EV_READ
) {
383 if ((n
= imsgbuf_read(ibuf
)) == -1)
384 fatal("imsgbuf_read error");
386 /* Connection closed. */
392 if (event
& EV_WRITE
) {
393 err
= gotd_imsg_flush(ibuf
);
395 if (err
->code
!= GOT_ERR_ERRNO
|| errno
!= EPIPE
)
396 fatalx("%s", err
->msg
);
403 const struct got_error
*err
= NULL
;
405 if ((n
= imsg_get(ibuf
, &imsg
)) == -1)
406 fatal("%s: imsg_get error", __func__
);
407 if (n
== 0) /* No more messages. */
410 switch (imsg
.hdr
.type
) {
411 case GOTD_IMSG_NOTIFY
:
412 err
= send_notification(&imsg
, iev
);
415 log_debug("unexpected imsg %d", imsg
.hdr
.type
);
421 log_warnx("%s: %s", __func__
, err
->msg
);
425 gotd_imsg_event_add(iev
);
427 struct gotd_notify_session
*session
;
429 /* This pipe is dead. Remove its event handler */
431 imsgbuf_clear(&iev
->ibuf
);
433 session
= find_session_by_fd(fd
);
435 remove_session(session
);
439 static const struct got_error
*
440 recv_session(struct imsg
*imsg
)
442 struct gotd_notify_session
*session
;
446 datalen
= imsg
->hdr
.len
- IMSG_HEADER_SIZE
;
448 return got_error(GOT_ERR_PRIVSEP_LEN
);
450 fd
= imsg_get_fd(imsg
);
452 return got_error(GOT_ERR_PRIVSEP_NO_FD
);
454 session
= calloc(1, sizeof(*session
));
455 if (session
== NULL
) {
457 return got_error_from_errno("calloc");
460 session
->id
= get_session_id();
461 if (imsgbuf_init(&session
->iev
.ibuf
, fd
) == -1) {
463 return got_error_from_errno("imsgbuf_init");
465 imsgbuf_allow_fdpass(&session
->iev
.ibuf
);
466 session
->iev
.handler
= notify_dispatch_session
;
467 session
->iev
.events
= EV_READ
;
468 session
->iev
.handler_arg
= NULL
;
469 event_set(&session
->iev
.ev
, session
->iev
.ibuf
.fd
, EV_READ
,
470 notify_dispatch_session
, &session
->iev
);
471 gotd_imsg_event_add(&session
->iev
);
472 add_session(session
);
477 static const struct got_error
*
478 notify_ibuf_get_str(char **ret
, struct ibuf
*ibuf
)
480 const char *str
, *end
;
485 str
= ibuf_data(ibuf
);
486 len
= ibuf_size(ibuf
);
488 end
= memchr(str
, '\0', len
);
490 return got_error(GOT_ERR_PRIVSEP_LEN
);
493 return got_error_from_errno("strdup");
495 if (ibuf_skip(ibuf
, end
- str
+ 1) == -1) {
498 return got_error(GOT_ERR_PRIVSEP_LEN
);
505 notify_dispatch(int fd
, short event
, void *arg
)
507 const struct got_error
*err
= NULL
;
508 struct gotd_imsgev
*iev
= arg
;
509 struct imsgbuf
*imsgbuf
= &iev
->ibuf
;
514 struct gotd_secret
*s
;
516 if (event
& EV_READ
) {
517 if ((n
= imsgbuf_read(imsgbuf
)) == -1)
518 fatal("imsgbuf_read error");
520 /* Connection closed. */
526 if (event
& EV_WRITE
) {
527 err
= gotd_imsg_flush(imsgbuf
);
529 fatalx("%s", err
->msg
);
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 if (imsgbuf_init(&gotd_notify
.parent_iev
.ibuf
, GOTD_FILENO_MSG_PIPE
)
631 fatal("imsgbuf_init");
632 imsgbuf_allow_fdpass(&gotd_notify
.parent_iev
.ibuf
);
633 gotd_notify
.parent_iev
.handler
= notify_dispatch
;
634 gotd_notify
.parent_iev
.events
= EV_READ
;
635 gotd_notify
.parent_iev
.handler_arg
= NULL
;
636 event_set(&gotd_notify
.parent_iev
.ev
, gotd_notify
.parent_iev
.ibuf
.fd
,
637 EV_READ
, notify_dispatch
, &gotd_notify
.parent_iev
);
638 gotd_imsg_event_add(&gotd_notify
.parent_iev
);
643 log_warnx("%s: %s", title
, err
->msg
);
644 gotd_notify_shutdown();
648 gotd_notify_shutdown(void)
650 log_debug("%s: shutting down", gotd_notify
.title
);