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"
42 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
45 static struct gotd_notify
{
48 struct gotd_imsgev parent_iev
;
49 struct gotd_repolist
*repos
;
50 const char *default_sender
;
53 struct gotd_notify_session
{
54 STAILQ_ENTRY(gotd_notify_session
) entry
;
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);
66 session_hash(uint32_t session_id
)
68 return SipHash24(&sessions_hash_key
, &session_id
, sizeof(session_id
));
72 add_session(struct gotd_notify_session
*session
)
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
)
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
)
95 static struct gotd_notify_session
*
96 find_session_by_fd(int fd
)
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
)
112 remove_session(struct gotd_notify_session
*session
)
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
);
131 duplicate
= (find_session(id
) != NULL
);
132 } while (duplicate
|| id
== 0);
138 gotd_notify_sighdlr(int sig
, short event
, void *arg
)
141 * Normal signal handler rules don't apply because libevent
147 log_info("%s: ignoring SIGHUP", __func__
);
150 log_info("%s: ignoring SIGUSR1", __func__
);
154 gotd_notify_shutdown();
158 fatalx("unexpected signal");
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
;
172 err
= got_error_from_errno("fork");
173 log_warn("%s", err
->msg
);
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(),
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
));
202 if (waitpid(pid
, &child_status
, 0) == -1) {
203 err
= got_error_from_errno("waitpid");
207 if (!WIFEXITED(child_status
)) {
208 err
= got_error(GOT_ERR_PRIVSEP_DIED
);
212 if (WEXITSTATUS(child_status
) != 0)
213 err
= got_error(GOT_ERR_PRIVSEP_EXIT
);
216 log_warnx("%s: child %s pid %d: %s", gotd_notify
.title
,
217 prog
, pid
, err
->msg
);
221 notify_email(struct gotd_notification_target
*target
, const char *subject_line
,
224 const char *argv
[13];
227 argv
[i
++] = GOTD_PATH_PROG_NOTIFY_EMAIL
;
230 if (target
->conf
.email
.sender
)
231 argv
[i
++] = target
->conf
.email
.sender
;
233 argv
[i
++] = gotd_notify
.default_sender
;
235 if (target
->conf
.email
.responder
) {
237 argv
[i
++] = target
->conf
.email
.responder
;
240 if (target
->conf
.email
.hostname
) {
242 argv
[i
++] = target
->conf
.email
.hostname
;
245 if (target
->conf
.email
.port
) {
247 argv
[i
++] = target
->conf
.email
.port
;
251 argv
[i
++] = subject_line
;
253 argv
[i
++] = target
->conf
.email
.recipient
;
257 run_notification_helper(GOTD_PATH_PROG_NOTIFY_EMAIL
, argv
, fd
,
262 notify_http(struct gotd_notification_target
*target
, const char *repo
,
263 const char *username
, int fd
)
265 const char *argv
[12];
268 argv
[argc
++] = GOTD_PATH_PROG_NOTIFY_HTTP
;
269 if (target
->conf
.http
.tls
)
275 argv
[argc
++] = target
->conf
.http
.hostname
;
277 argv
[argc
++] = target
->conf
.http
.port
;
279 argv
[argc
++] = username
;
281 argv
[argc
++] = target
->conf
.http
.path
;
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
;
295 struct gotd_repo
*repo
;
296 struct gotd_notification_target
*target
;
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
);
310 return got_error(GOT_ERR_PRIVSEP_MSG
);
312 fd
= imsg_get_fd(imsg
);
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");
325 switch (target
->type
) {
326 case GOTD_NOTIFICATION_VIA_EMAIL
:
327 notify_email(target
, inotify
.subject_line
, fd
);
329 case GOTD_NOTIFICATION_VIA_HTTP
:
330 notify_http(target
, repo
->name
, username
, fd
);
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");
347 notify_dispatch_session(int fd
, short event
, void *arg
)
349 struct gotd_imsgev
*iev
= arg
;
350 struct imsgbuf
*ibuf
= &iev
->ibuf
;
355 if (event
& EV_READ
) {
356 if ((n
= imsg_read(ibuf
)) == -1 && errno
!= EAGAIN
)
357 fatal("imsg_read error");
359 /* Connection closed. */
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. */
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. */
384 switch (imsg
.hdr
.type
) {
385 case GOTD_IMSG_NOTIFY
:
386 err
= send_notification(&imsg
, iev
);
389 log_debug("unexpected imsg %d", imsg
.hdr
.type
);
395 log_warnx("%s: %s", __func__
, err
->msg
);
399 gotd_imsg_event_add(iev
);
401 struct gotd_notify_session
*session
;
403 /* This pipe is dead. Remove its event handler */
405 imsg_clear(&iev
->ibuf
);
407 session
= find_session_by_fd(fd
);
409 remove_session(session
);
413 static const struct got_error
*
414 recv_session(struct imsg
*imsg
)
416 struct gotd_notify_session
*session
;
420 datalen
= imsg
->hdr
.len
- IMSG_HEADER_SIZE
;
422 return got_error(GOT_ERR_PRIVSEP_LEN
);
424 fd
= imsg_get_fd(imsg
);
426 return got_error(GOT_ERR_PRIVSEP_NO_FD
);
428 session
= calloc(1, sizeof(*session
));
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
);
446 notify_dispatch(int fd
, short event
, void *arg
)
448 struct gotd_imsgev
*iev
= arg
;
449 struct imsgbuf
*ibuf
= &iev
->ibuf
;
454 if (event
& EV_READ
) {
455 if ((n
= imsg_read(ibuf
)) == -1 && errno
!= EAGAIN
)
456 fatal("imsg_read error");
458 /* Connection closed. */
464 if (event
& EV_WRITE
) {
465 n
= msgbuf_write(&ibuf
->w
);
466 if (n
== -1 && errno
!= EAGAIN
)
467 fatal("msgbuf_write");
469 /* Connection closed. */
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. */
483 switch (imsg
.hdr
.type
) {
484 case GOTD_IMSG_CONNECT_SESSION
:
485 err
= recv_session(&imsg
);
488 log_debug("unexpected imsg %d", imsg
.hdr
.type
);
494 log_warnx("%s: %s", __func__
, err
->msg
);
498 gotd_imsg_event_add(iev
);
500 /* This pipe is dead. Remove its event handler */
502 event_loopexit(NULL
);
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
);
543 log_warnx("%s: %s", title
, err
->msg
);
544 gotd_notify_shutdown();
548 gotd_notify_shutdown(void)
550 log_debug("%s: shutting down", gotd_notify
.title
);