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 struct gotd_imsgev
*iev
= arg
;
376 struct imsgbuf
*ibuf
= &iev
->ibuf
;
381 if (event
& EV_READ
) {
382 if ((n
= imsg_read(ibuf
)) == -1 && errno
!= EAGAIN
)
383 fatal("imsg_read error");
385 /* Connection closed. */
391 if (event
& EV_WRITE
) {
392 n
= msgbuf_write(&ibuf
->w
);
393 if (n
== -1 && errno
!= EAGAIN
&& errno
!= EPIPE
)
394 fatal("msgbuf_write");
395 if (n
== 0 || (n
== -1 && errno
== EPIPE
)) {
396 /* Connection closed. */
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 imsg_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
));
456 return got_error_from_errno("calloc");
458 session
->id
= get_session_id();
459 imsg_init(&session
->iev
.ibuf
, fd
);
460 session
->iev
.handler
= notify_dispatch_session
;
461 session
->iev
.events
= EV_READ
;
462 session
->iev
.handler_arg
= NULL
;
463 event_set(&session
->iev
.ev
, session
->iev
.ibuf
.fd
, EV_READ
,
464 notify_dispatch_session
, &session
->iev
);
465 gotd_imsg_event_add(&session
->iev
);
466 add_session(session
);
471 static const struct got_error
*
472 notify_ibuf_get_str(char **ret
, struct ibuf
*ibuf
)
474 const char *str
, *end
;
479 str
= ibuf_data(ibuf
);
480 len
= ibuf_size(ibuf
);
482 end
= memchr(str
, '\0', len
);
484 return got_error(GOT_ERR_PRIVSEP_LEN
);
487 return got_error_from_errno("strdup");
489 if (ibuf_skip(ibuf
, end
- str
+ 1) == -1) {
492 return got_error(GOT_ERR_PRIVSEP_LEN
);
499 notify_dispatch(int fd
, short event
, void *arg
)
501 struct gotd_imsgev
*iev
= arg
;
502 struct imsgbuf
*imsgbuf
= &iev
->ibuf
;
507 struct gotd_secret
*s
;
509 if (event
& EV_READ
) {
510 if ((n
= imsg_read(imsgbuf
)) == -1 && errno
!= EAGAIN
)
511 fatal("imsg_read error");
513 /* Connection closed. */
519 if (event
& EV_WRITE
) {
520 n
= msgbuf_write(&imsgbuf
->w
);
521 if (n
== -1 && errno
!= EAGAIN
)
522 fatal("msgbuf_write");
524 /* Connection closed. */
531 const struct got_error
*err
= NULL
;
533 if ((n
= imsg_get(imsgbuf
, &imsg
)) == -1)
534 fatal("%s: imsg_get error", __func__
);
535 if (n
== 0) /* No more messages. */
538 switch (imsg
.hdr
.type
) {
539 case GOTD_IMSG_CONNECT_SESSION
:
540 err
= recv_session(&imsg
);
542 case GOTD_IMSG_SECRETS
:
543 if (secrets
.cap
!= 0)
544 fatal("unexpected GOTD_IMSG_SECRETS");
545 if (imsg_get_data(&imsg
, &secrets
.cap
,
546 sizeof(secrets
.cap
)) == -1)
547 fatalx("corrupted GOTD_IMSG_SECRETS");
548 if (secrets
.cap
== 0)
550 secrets
.secrets
= calloc(secrets
.cap
,
551 sizeof(*secrets
.secrets
));
552 if (secrets
.secrets
== NULL
)
555 case GOTD_IMSG_SECRET
:
556 if (secrets
.len
== secrets
.cap
)
557 fatalx("unexpected GOTD_SECRET_AUTH");
558 s
= &secrets
.secrets
[secrets
.len
++];
559 if (imsg_get_ibuf(&imsg
, &ibuf
) == -1)
560 fatal("imsg_get_ibuf");
561 if (ibuf_get(&ibuf
, &s
->type
, sizeof(s
->type
)) == -1)
562 fatalx("corrupted GOTD_IMSG_SECRET");
563 err
= notify_ibuf_get_str(&s
->label
, &ibuf
);
566 if (s
->type
== GOTD_SECRET_AUTH
) {
567 err
= notify_ibuf_get_str(&s
->user
, &ibuf
);
570 err
= notify_ibuf_get_str(&s
->pass
, &ibuf
);
574 err
= notify_ibuf_get_str(&s
->hmac
, &ibuf
);
578 if (ibuf_size(&ibuf
) != 0)
579 fatalx("unexpected extra data in "
583 log_debug("unexpected imsg %d", imsg
.hdr
.type
);
589 log_warnx("%s: %s", __func__
, err
->msg
);
593 gotd_imsg_event_add(iev
);
595 /* This pipe is dead. Remove its event handler */
597 event_loopexit(NULL
);
603 notify_main(const char *title
, struct gotd_repolist
*repos
,
604 const char *default_sender
)
606 const struct got_error
*err
= NULL
;
607 struct event evsigint
, evsigterm
, evsighup
, evsigusr1
;
609 arc4random_buf(&sessions_hash_key
, sizeof(sessions_hash_key
));
611 gotd_notify
.title
= title
;
612 gotd_notify
.repos
= repos
;
613 gotd_notify
.default_sender
= default_sender
;
614 gotd_notify
.pid
= getpid();
616 signal_set(&evsigint
, SIGINT
, gotd_notify_sighdlr
, NULL
);
617 signal_set(&evsigterm
, SIGTERM
, gotd_notify_sighdlr
, NULL
);
618 signal_set(&evsighup
, SIGHUP
, gotd_notify_sighdlr
, NULL
);
619 signal_set(&evsigusr1
, SIGUSR1
, gotd_notify_sighdlr
, NULL
);
620 signal(SIGPIPE
, SIG_IGN
);
622 signal_add(&evsigint
, NULL
);
623 signal_add(&evsigterm
, NULL
);
624 signal_add(&evsighup
, NULL
);
625 signal_add(&evsigusr1
, NULL
);
627 imsg_init(&gotd_notify
.parent_iev
.ibuf
, GOTD_FILENO_MSG_PIPE
);
628 gotd_notify
.parent_iev
.handler
= notify_dispatch
;
629 gotd_notify
.parent_iev
.events
= EV_READ
;
630 gotd_notify
.parent_iev
.handler_arg
= NULL
;
631 event_set(&gotd_notify
.parent_iev
.ev
, gotd_notify
.parent_iev
.ibuf
.fd
,
632 EV_READ
, notify_dispatch
, &gotd_notify
.parent_iev
);
633 gotd_imsg_event_add(&gotd_notify
.parent_iev
);
638 log_warnx("%s: %s", title
, err
->msg
);
639 gotd_notify_shutdown();
643 gotd_notify_shutdown(void)
645 log_debug("%s: shutting down", gotd_notify
.title
);