Blob


1 /*
2 * Copyright (c) 2023 Omar Polo <op@omarpolo.com>
3 * Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org>
4 *
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 */
18 #include "config.h"
20 #include <sys/socket.h>
21 #include <sys/types.h>
22 #include <sys/un.h>
24 #include <ctype.h>
25 #include <errno.h>
26 #include <fnmatch.h>
27 #include <limits.h>
28 #include <locale.h>
29 #include <netdb.h>
30 #include <poll.h>
31 #include <signal.h>
32 #include <stdio.h>
33 #include <stdlib.h>
34 #include <string.h>
35 #include <syslog.h>
36 #include <unistd.h>
38 #include "amused.h"
39 #include "bufio.h"
40 #include "ev.h"
41 #include "http.h"
42 #include "log.h"
43 #include "playlist.h"
44 #include "xmalloc.h"
46 #ifndef nitems
47 #define nitems(x) (sizeof(x)/sizeof(x[0]))
48 #endif
50 #define FORM_URLENCODED "application/x-www-form-urlencoded"
52 #define ICON_REPEAT_ALL "🔁"
53 #define ICON_REPEAT_ONE "🔂"
54 #define ICON_PREV "⏮"
55 #define ICON_NEXT "⏭"
56 #define ICON_STOP "⏹"
57 #define ICON_PAUSE "⏸"
58 #define ICON_TOGGLE "⏯"
59 #define ICON_PLAY "⏵"
61 static struct imsgbuf ibuf;
62 static struct playlist playlist_tmp;
63 static struct player_status player_status;
64 static uint64_t position, duration;
65 static const char *prefix = "";
66 static size_t prefixlen;
68 const char *head = "<!doctype html>"
69 "<html>"
70 "<head>"
71 "<meta name='viewport' content='width=device-width, initial-scale=1'/>"
72 "<title>Amused Web</title>"
73 "<link rel='stylesheet' href='/style.css?v=0'>"
74 "</style>"
75 "</head>"
76 "<body>";
78 const char *css = "*{box-sizing:border-box}"
79 "html,body{"
80 " padding: 0;"
81 " border: 0;"
82 " margin: 0;"
83 "}"
84 "main{"
85 " display: flex;"
86 " flex-direction: column;"
87 "}"
88 "button{cursor:pointer}"
89 ".searchbox{"
90 " position: sticky;"
91 " top: 0;"
92 "}"
93 ".searchbox input{"
94 " width: 100%;"
95 " padding: 9px;"
96 "}"
97 ".playlist-wrapper{min-height:80vh}"
98 ".playlist{"
99 " list-style: none;"
100 " padding: 0;"
101 " margin: 0;"
102 "}"
103 ".playlist button{"
104 " font-family: monospace;"
105 " text-align: left;"
106 " width: 100%;"
107 " padding: 5px;"
108 " border: 0;"
109 " background: transparent;"
110 " transition: background-color .25s ease-in-out;"
111 "}"
112 ".playlist button::before{"
113 " content: \"\";"
114 " width: 2ch;"
115 " display: inline-block;"
116 "}"
117 ".playlist button:hover{"
118 " background-color: #dfdddd;"
119 "}"
120 ".playlist #current button{"
121 " font-weight: bold;"
122 "}"
123 ".playlist #current button::before{"
124 " content: \"→ \";"
125 " font-weight: bold;"
126 "}"
127 ".controls{"
128 " position: sticky;"
129 " width: 100%;"
130 " max-width: 800px;"
131 " margin: 0 auto;"
132 " bottom: 0;"
133 " background-color: white;"
134 " background: #3d3d3d;"
135 " color: white;"
136 " border-radius: 10px 10px 0 0;"
137 " padding: 10px;"
138 " text-align: center;"
139 " order: 2;"
140 "}"
141 ".controls p{"
142 " margin: .4rem;"
143 "}"
144 ".controls a{"
145 " color: white;"
146 "}"
147 ".controls .status{"
148 " font-size: 0.9rem;"
149 "}"
150 ".controls button{"
151 " margin: 5px;"
152 " padding: 5px 20px;"
153 "}"
154 ".mode-active{"
155 " color: #0064ff;"
156 "}";
158 const char *js =
159 "function cur(e) {"
160 " if (e) {e.preventDefault()}"
161 " let cur = document.querySelector('#current');"
162 " if (cur) {cur.scrollIntoView(); window.scrollBy(0, -100);}"
163 "}"
164 "cur();"
165 "document.querySelector('.controls a').addEventListener('click',cur)";
167 const char *foot = "<script src='/app.js?v=0'></script></body></html>";
169 static int
170 dial(const char *sock)
172 struct sockaddr_un sa;
173 size_t len;
174 int s;
176 memset(&sa, 0, sizeof(sa));
177 sa.sun_family = AF_UNIX;
178 len = strlcpy(sa.sun_path, sock, sizeof(sa.sun_path));
179 if (len >= sizeof(sa.sun_path))
180 err(1, "path too long: %s", sock);
182 if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
183 err(1, "socket");
184 if (connect(s, (struct sockaddr *)&sa, sizeof(sa)) == -1)
185 err(1, "failed to connect to %s", sock);
187 return s;
190 /*
191 * Adapted from usr.sbin/httpd/httpd.c' url_decode.
192 */
193 static int
194 url_decode(char *url)
196 char*p, *q;
197 char hex[3] = {0};
198 unsigned long x;
200 p = q = url;
201 while (*p != '\0') {
202 switch (*p) {
203 case '%':
204 /* Encoding character is followed by two hex chars */
205 if (!isxdigit((unsigned char)p[1]) ||
206 !isxdigit((unsigned char)p[2]) ||
207 (p[1] == '0' && p[2] == '0'))
208 return (-1);
210 hex[0] = p[1];
211 hex[1] = p[2];
213 /*
214 * We don't have to validate "hex" because it is
215 * guaranteed to include two hex chars followed
216 * by NUL.
217 */
218 x = strtoul(hex, NULL, 16);
219 *q = (char)x;
220 p += 2;
221 break;
222 case '+':
223 *q = ' ';
224 break;
225 default:
226 *q = *p;
227 break;
229 p++;
230 q++;
232 *q = '\0';
234 return (0);
237 static void
238 imsg_dispatch(int fd, int ev, void *d)
240 static ssize_t off;
241 static int off_found;
242 struct imsg imsg;
243 struct player_status ps;
244 struct player_event event;
245 const char *msg;
246 ssize_t n;
247 size_t datalen;
249 if (ev & (POLLIN|POLLHUP)) {
250 if ((n = imsg_read(&ibuf)) == -1 && errno != EAGAIN)
251 fatal("imsg_read");
252 if (n == 0)
253 fatalx("pipe closed");
255 if (ev & POLLOUT) {
256 if ((n = msgbuf_write(&ibuf.w)) == -1 && errno != EAGAIN)
257 fatal("msgbuf_write");
258 if (n == 0)
259 fatalx("pipe closed");
262 for (;;) {
263 if ((n = imsg_get(&ibuf, &imsg)) == -1)
264 fatal("imsg_get");
265 if (n == 0)
266 break;
268 datalen = IMSG_DATA_SIZE(imsg);
270 switch (imsg.hdr.type) {
271 case IMSG_CTL_ERR:
272 msg = imsg.data;
273 if (datalen == 0 || msg[datalen - 1] != '\0')
274 fatalx("malformed error message");
275 log_warnx("error: %s", msg);
276 break;
278 case IMSG_CTL_ADD:
279 playlist_free(&playlist_tmp);
280 imsg_compose(&ibuf, IMSG_CTL_SHOW, 0, 0, -1, NULL, 0);
281 break;
283 case IMSG_CTL_MONITOR:
284 if (datalen != sizeof(event))
285 fatalx("corrupted IMSG_CTL_MONITOR");
286 memcpy(&event, imsg.data, sizeof(event));
287 switch (event.event) {
288 case IMSG_CTL_PLAY:
289 case IMSG_CTL_PAUSE:
290 case IMSG_CTL_STOP:
291 case IMSG_CTL_MODE:
292 imsg_compose(&ibuf, IMSG_CTL_STATUS, 0, 0, -1,
293 NULL, 0);
294 break;
296 case IMSG_CTL_NEXT:
297 case IMSG_CTL_PREV:
298 case IMSG_CTL_JUMP:
299 case IMSG_CTL_COMMIT:
300 imsg_compose(&ibuf, IMSG_CTL_SHOW, 0, 0, -1,
301 NULL, 0);
302 imsg_compose(&ibuf, IMSG_CTL_STATUS, 0, 0, -1,
303 NULL, 0);
304 break;
306 case IMSG_CTL_SEEK:
307 position = event.position;
308 duration = event.duration;
309 break;
311 default:
312 log_debug("ignoring event %d", event.event);
313 break;
315 break;
317 case IMSG_CTL_SHOW:
318 if (datalen == 0) {
319 playlist_swap(&playlist_tmp, off);
320 memset(&playlist_tmp, 0, sizeof(playlist_tmp));
321 off = 0;
322 off_found = 0;
323 break;
325 if (datalen != sizeof(ps))
326 fatalx("corrupted IMSG_CTL_SHOW");
327 memcpy(&ps, imsg.data, sizeof(ps));
328 if (ps.path[sizeof(ps.path) - 1] != '\0')
329 fatalx("corrupted IMSG_CTL_SHOW");
330 playlist_push(&playlist_tmp, ps.path);
331 if (ps.status == STATE_PLAYING)
332 off_found = 1;
333 if (!off_found)
334 off++;
335 break;
337 case IMSG_CTL_STATUS:
338 if (datalen != sizeof(player_status))
339 fatalx("corrupted IMSG_CTL_STATUS");
340 memcpy(&player_status, imsg.data, datalen);
341 if (player_status.path[sizeof(player_status.path) - 1]
342 != '\0')
343 fatalx("corrupted IMSG_CTL_STATUS");
344 break;
348 ev = POLLIN;
349 if (ibuf.w.queued)
350 ev |= POLLOUT;
351 ev_add(fd, ev, imsg_dispatch, NULL);
354 static void
355 route_notfound(struct client *clt)
357 if (http_reply(clt, 404, "Not Found", "text/plain") == -1 ||
358 http_writes(clt, "Page not found\n") == -1)
359 return;
362 static void
363 render_playlist(struct client *clt)
365 ssize_t i;
366 const char *path, *p;
367 int current;
369 http_writes(clt, "<section class='playlist-wrapper'>");
370 http_writes(clt, "<form action=jump method=post"
371 " enctype='"FORM_URLENCODED"'>");
372 http_writes(clt, "<ul class=playlist>");
374 for (i = 0; i < playlist.len; ++i) {
375 current = play_off == i;
377 p = path = playlist.songs[i];
378 if (!strncmp(p, prefix, prefixlen))
379 p += prefixlen;
381 http_fmt(clt, "<li%s>", current ? " id=current" : "");
382 http_writes(clt, "<button type=submit name=jump value=\"");
383 http_htmlescape(clt, path);
384 http_writes(clt, "\">");
385 http_htmlescape(clt, p);
386 http_writes(clt, "</button></li>");
389 http_writes(clt, "</ul>");
390 http_writes(clt, "</form>");
391 http_writes(clt, "</section>");
394 static void
395 render_controls(struct client *clt)
397 const char *oc, *ac, *p;
398 int playing;
400 ac = player_status.mode.repeat_all ? " class='mode-active'" : "";
401 oc = player_status.mode.repeat_one ? " class='mode-active'" : "";
402 playing = player_status.status == STATE_PLAYING;
404 if ((p = strrchr(player_status.path, '/')) != NULL)
405 p++;
406 else
407 p = player_status.path;
409 if (http_writes(clt, "<section class=controls>") == -1 ||
410 http_writes(clt, "<p><a href='#current'>") == -1 ||
411 http_htmlescape(clt, p) == -1 ||
412 http_writes(clt, "</a></p>") == -1 ||
413 http_writes(clt, "<form action=ctrls method=post"
414 " enctype='"FORM_URLENCODED"'>") == -1 ||
415 http_writes(clt, "<button type=submit name=ctl value=prev>"
416 ICON_PREV"</button>") == -1 ||
417 http_fmt(clt, "<button type=submit name=ctl value=%s>"
418 "%s</button>", playing ? "pause" : "play",
419 playing ? ICON_PAUSE : ICON_PLAY) == -1 ||
420 http_writes(clt, "<button type=submit name=ctl value=next>"
421 ICON_NEXT"</button>") == -1 ||
422 http_writes(clt, "</form>") == -1 ||
423 http_writes(clt, "<form action=mode method=post"
424 " enctype='"FORM_URLENCODED"'>") == -1 ||
425 http_fmt(clt, "<button%s type=submit name=mode value=all>"
426 ICON_REPEAT_ALL"</button>", ac) == -1 ||
427 http_fmt(clt, "<button%s type=submit name=mode value=one>"
428 ICON_REPEAT_ONE"</button>", oc) == -1 ||
429 http_writes(clt, "</form>") == -1 ||
430 http_writes(clt, "</section>") == -1)
431 return;
434 static void
435 route_home(struct client *clt)
437 if (http_reply(clt, 200, "OK", "text/html;charset=UTF-8") == -1)
438 return;
440 if (http_write(clt, head, strlen(head)) == -1)
441 return;
443 if (http_writes(clt, "<main>") == -1)
444 return;
446 if (http_writes(clt, "<section class=searchbox>"
447 "<input type=search name=filter aria-label='Filter playlist'"
448 " placeholder='Filter playlist' id=search />"
449 "</section>") == -1)
450 return;
452 render_controls(clt);
453 render_playlist(clt);
455 if (http_writes(clt, "</main>") == -1)
456 return;
458 http_write(clt, foot, strlen(foot));
461 static void
462 route_jump(struct client *clt)
464 char path[PATH_MAX];
465 char *form, *field;
466 int found = 0;
468 form = clt->buf;
469 while ((field = strsep(&form, "&")) != NULL) {
470 if (url_decode(field) == -1)
471 goto badreq;
473 if (strncmp(field, "jump=", 5) != 0)
474 continue;
475 field += 5;
476 found = 1;
478 memset(&path, 0, sizeof(path));
479 if (strlcpy(path, field, sizeof(path)) >= sizeof(path))
480 goto badreq;
482 log_warnx("path is %s", path);
483 imsg_compose(&ibuf, IMSG_CTL_JUMP, 0, 0, -1,
484 path, sizeof(path));
485 ev_add(ibuf.w.fd, POLLIN|POLLOUT, imsg_dispatch, NULL);
486 break;
489 if (!found)
490 goto badreq;
492 http_reply(clt, 302, "See Other", "/");
493 return;
495 badreq:
496 http_reply(clt, 400, "Bad Request", "text/plain");
497 http_writes(clt, "Bad Request.\n");
500 static void
501 route_controls(struct client *clt)
503 char *form, *field;
504 int cmd, found = 0;
506 form = clt->buf;
507 while ((field = strsep(&form, "&")) != NULL) {
508 if (url_decode(field) == -1)
509 goto badreq;
511 if (strncmp(field, "ctl=", 4) != 0)
512 continue;
513 field += 4;
514 found = 1;
516 if (!strcmp(field, "play"))
517 cmd = IMSG_CTL_PLAY;
518 else if (!strcmp(field, "pause"))
519 cmd = IMSG_CTL_PAUSE;
520 else if (!strcmp(field, "next"))
521 cmd = IMSG_CTL_NEXT;
522 else if (!strcmp(field, "prev"))
523 cmd = IMSG_CTL_PREV;
524 else
525 goto badreq;
527 imsg_compose(&ibuf, cmd, 0, 0, -1, NULL, 0);
528 imsg_flush(&ibuf);
529 break;
532 if (!found)
533 goto badreq;
535 http_reply(clt, 302, "See Other", "/");
536 return;
538 badreq:
539 http_reply(clt, 400, "Bad Request", "text/plain");
540 http_writes(clt, "Bad Request.\n");
543 static void
544 route_mode(struct client *clt)
546 char *form, *field;
547 int found = 0;
548 struct player_mode pm;
550 pm.repeat_one = pm.repeat_all = pm.consume = MODE_UNDEF;
552 form = clt->buf;
553 while ((field = strsep(&form, "&")) != NULL) {
554 if (url_decode(field) == -1)
555 goto badreq;
557 if (strncmp(field, "mode=", 5) != 0)
558 continue;
559 field += 5;
560 found = 1;
562 if (!strcmp(field, "all"))
563 pm.repeat_all = MODE_TOGGLE;
564 else if (!strcmp(field, "one"))
565 pm.repeat_one = MODE_TOGGLE;
566 else
567 goto badreq;
569 imsg_compose(&ibuf, IMSG_CTL_MODE, 0, 0, -1, &pm, sizeof(pm));
570 ev_add(ibuf.w.fd, POLLIN|POLLOUT, imsg_dispatch, NULL);
571 break;
574 if (!found)
575 goto badreq;
577 http_reply(clt, 302, "See Other", "/");
578 return;
580 badreq:
581 http_reply(clt, 400, "Bad Request", "text/plain");
582 http_writes(clt, "Bad Request.\n");
585 static void
586 route_assets(struct client *clt)
588 if (!strcmp(clt->req.path, "/style.css")) {
589 http_reply(clt, 200, "OK", "text/css");
590 http_write(clt, css, strlen(css));
591 return;
594 if (!strcmp(clt->req.path, "/app.js")) {
595 http_reply(clt, 200, "OK", "application/javascript");
596 http_write(clt, js, strlen(js));
597 return;
600 route_notfound(clt);
603 static void
604 route_dispatch(struct client *clt)
606 static const struct route {
607 int method;
608 const char *path;
609 route_fn route;
610 } routes[] = {
611 { METHOD_GET, "/", &route_home },
612 { METHOD_POST, "/jump", &route_jump },
613 { METHOD_POST, "/ctrls", &route_controls },
614 { METHOD_POST, "/mode", &route_mode },
616 { METHOD_GET, "/style.css", &route_assets },
617 { METHOD_GET, "/app.js", &route_assets },
619 { METHOD_GET, "*", &route_notfound },
620 { METHOD_POST, "*", &route_notfound },
621 };
622 struct request *req = &clt->req;
623 size_t i;
625 if ((req->method != METHOD_GET && req->method != METHOD_POST) ||
626 (req->ctype != NULL && strcmp(req->ctype, FORM_URLENCODED) != 0) ||
627 req->path == NULL) {
628 http_reply(clt, 400, "Bad Request", NULL);
629 return;
632 for (i = 0; i < nitems(routes); ++i) {
633 if (req->method != routes[i].method ||
634 fnmatch(routes[i].path, req->path, 0) != 0)
635 continue;
636 clt->done = 1; /* assume with one round is done */
637 clt->route = routes[i].route;
638 clt->route(clt);
639 if (clt->done)
640 http_close(clt);
641 return;
645 static void
646 client_ev(int fd, int ev, void *d)
648 struct client *clt = d;
650 if (ev & (POLLIN|POLLHUP)) {
651 if (bufio_read(&clt->bio) == -1 && errno != EAGAIN) {
652 log_warn("bufio_read");
653 goto err;
657 if (ev & POLLOUT) {
658 if (bufio_write(&clt->bio) == -1 && errno != EAGAIN) {
659 log_warn("bufio_read");
660 goto err;
664 if (clt->route == NULL) {
665 if (http_parse(clt) == -1) {
666 if (errno == EAGAIN)
667 goto again;
668 log_warnx("HTTP parse request failed");
669 goto err;
671 if (clt->req.method == METHOD_POST &&
672 http_read(clt) == -1) {
673 if (errno == EAGAIN)
674 goto again;
675 log_warnx("failed to read POST data");
676 goto err;
678 route_dispatch(clt);
679 goto again;
682 if (!clt->done)
683 clt->route(clt);
685 again:
686 ev = bufio_pollev(&clt->bio);
687 if (ev == POLLIN && clt->done) {
688 goto err; /* done with this client */
691 ev_add(fd, ev, client_ev, clt);
692 return;
694 err:
695 ev_del(fd);
696 http_free(clt);
699 static void
700 web_accept(int psock, int ev, void *d)
702 struct client *clt;
703 int sock;
705 if ((sock = accept(psock, NULL, NULL)) == -1) {
706 warn("accept");
707 return;
709 clt = xcalloc(1, sizeof(*clt));
710 if ((clt = calloc(1, sizeof(*clt))) == NULL ||
711 http_init(clt, sock) == -1) {
712 log_warn("failed to initialize client");
713 free(clt);
714 close(sock);
715 return;
718 client_ev(sock, POLLIN, clt);
719 return;
722 void __dead
723 usage(void)
725 fprintf(stderr, "usage: %s [-v] [-s sock] [-t prefix] [[host] port]\n",
726 getprogname());
727 exit(1);
730 int
731 main(int argc, char **argv)
733 struct addrinfo hints, *res, *res0;
734 const char *cause = NULL;
735 const char *host = NULL;
736 const char *port = "9090";
737 char *sock = NULL;
738 size_t nsock, error, save_errno;
739 int ch, v, amused_sock, fd;
740 int verbose = 0;
742 setlocale(LC_ALL, NULL);
744 log_init(1, LOG_DAEMON);
746 if (pledge("stdio rpath unix inet dns", NULL) == -1)
747 err(1, "pledge");
749 while ((ch = getopt(argc, argv, "s:t:v")) != -1) {
750 switch (ch) {
751 case 's':
752 sock = optarg;
753 break;
754 case 't':
755 prefix = optarg;
756 prefixlen = strlen(prefix);
757 break;
758 case 'v':
759 verbose = 1;
760 break;
761 default:
762 usage();
765 argc -= optind;
766 argv += optind;
768 if (argc == 1)
769 port = argv[0];
770 if (argc == 2) {
771 host = argv[0];
772 port = argv[1];
774 if (argc > 2)
775 usage();
777 log_setverbose(verbose);
779 if (sock == NULL)
780 xasprintf(&sock, "/tmp/amused-%d", getuid());
782 signal(SIGPIPE, SIG_IGN);
784 if (ev_init() == -1)
785 fatal("ev_init");
787 amused_sock = dial(sock);
788 imsg_init(&ibuf, amused_sock);
789 imsg_compose(&ibuf, IMSG_CTL_SHOW, 0, 0, -1, NULL, 0);
790 imsg_compose(&ibuf, IMSG_CTL_STATUS, 0, 0, -1, NULL, 0);
791 imsg_compose(&ibuf, IMSG_CTL_MONITOR, 0, 0, -1, NULL, 0);
792 ev_add(amused_sock, POLLIN|POLLOUT, imsg_dispatch, NULL);
794 memset(&hints, 0, sizeof(hints));
795 hints.ai_family = AF_UNSPEC;
796 hints.ai_socktype = SOCK_STREAM;
797 hints.ai_flags = AI_PASSIVE;
798 error = getaddrinfo(host, port, &hints, &res0);
799 if (error)
800 errx(1, "%s", gai_strerror(error));
802 nsock = 0;
803 for (res = res0; res; res = res->ai_next) {
804 fd = socket(res->ai_family, res->ai_socktype,
805 res->ai_protocol);
806 if (fd == -1) {
807 cause = "socket";
808 continue;
811 v = 1;
812 if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
813 &v, sizeof(v)) == -1)
814 fatal("setsockopt(SO_REUSEADDR)");
816 if (bind(fd, res->ai_addr, res->ai_addrlen) == -1) {
817 cause = "bind";
818 save_errno = errno;
819 close(fd);
820 errno = save_errno;
821 continue;
824 if (listen(fd, 5) == -1)
825 err(1, "listen");
827 if (ev_add(fd, POLLIN, web_accept, NULL) == -1)
828 fatal("ev_add");
829 nsock++;
831 if (nsock == 0)
832 err(1, "%s", cause);
833 freeaddrinfo(res0);
835 if (pledge("stdio inet", NULL) == -1)
836 err(1, "pledge");
838 log_info("starting");
839 ev_loop();
840 return (1);