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 <signal.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 #include <syslog.h>
35 #include <unistd.h>
37 #include "amused.h"
38 #include "bufio.h"
39 #include "ev.h"
40 #include "http.h"
41 #include "log.h"
42 #include "playlist.h"
43 #include "ws.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 clthead clients;
62 static struct imsgbuf imsgbuf;
63 static struct playlist playlist_tmp;
64 static struct player_status player_status;
65 static uint64_t position, duration;
67 static void client_ev(int, int, void *);
69 const char *head = "<!doctype html>"
70 "<html>"
71 "<head>"
72 "<meta name='viewport' content='width=device-width, initial-scale=1'/>"
73 "<title>Amused Web</title>"
74 "<link rel='stylesheet' href='/style.css?v=0'>"
75 "</style>"
76 "</head>"
77 "<body>";
79 const char *css = "*{box-sizing:border-box}"
80 "html,body{"
81 " padding: 0;"
82 " border: 0;"
83 " margin: 0;"
84 "}"
85 "main{"
86 " display: flex;"
87 " flex-direction: column;"
88 "}"
89 "button{cursor:pointer}"
90 ".searchbox{"
91 " position: sticky;"
92 " top: 0;"
93 "}"
94 ".searchbox input{"
95 " width: 100%;"
96 " padding: 9px;"
97 "}"
98 ".playlist-wrapper{min-height:80vh}"
99 ".playlist{"
100 " list-style: none;"
101 " padding: 0;"
102 " margin: 0;"
103 "}"
104 ".playlist button{"
105 " font-family: monospace;"
106 " text-align: left;"
107 " width: 100%;"
108 " padding: 5px;"
109 " border: 0;"
110 " background: transparent;"
111 " transition: background-color .25s ease-in-out;"
112 "}"
113 ".playlist button::before{"
114 " content: \"\";"
115 " width: 2ch;"
116 " display: inline-block;"
117 "}"
118 ".playlist button:hover{"
119 " background-color: #dfdddd;"
120 "}"
121 ".playlist #current button{"
122 " font-weight: bold;"
123 "}"
124 ".playlist #current button::before{"
125 " content: \"→ \";"
126 " font-weight: bold;"
127 "}"
128 ".controls{"
129 " position: sticky;"
130 " width: 100%;"
131 " max-width: 800px;"
132 " margin: 0 auto;"
133 " bottom: 0;"
134 " background-color: white;"
135 " background: #3d3d3d;"
136 " color: white;"
137 " border-radius: 10px 10px 0 0;"
138 " padding: 10px;"
139 " text-align: center;"
140 " order: 2;"
141 "}"
142 ".controls p{"
143 " margin: .4rem;"
144 "}"
145 ".controls a{"
146 " color: white;"
147 "}"
148 ".controls .status{"
149 " font-size: 0.9rem;"
150 "}"
151 ".controls button{"
152 " margin: 5px;"
153 " padding: 5px 20px;"
154 "}"
155 ".mode-active{"
156 " color: #0064ff;"
157 "}";
159 const char *js =
160 "var ws;"
161 "let pos=0, dur=0;"
162 "const playlist=document.querySelector('.playlist');"
163 "function cur(e) {"
164 " if (e) {e.preventDefault()}"
165 " let cur = document.querySelector('#current');"
166 " if (cur) {cur.scrollIntoView(); window.scrollBy(0, -100);}"
167 "};"
168 "function b(x){return x=='on'};"
169 "function c(p, c){"
170 " const l=document.createElement('li');"
171 " if(c){l.id='current'};"
172 " const b=document.createElement('button');"
173 " b.type='submit'; b.name='jump'; b.value=p;"
174 " b.innerText=p;"
175 " l.appendChild(b);"
176 " playlist.appendChild(l);"
177 "}"
178 "function d(t){"
179 " const [, type, payload] = t.split(/^(.):(.*)$/);"
180 " if (type=='s'){"
181 " let s=payload.split(' ');"
182 " pos=s[0], dur=s[1];"
183 " } else if (type=='S') {"
184 " const btn=document.querySelector('#toggle');"
185 " if (payload=='playing') {"
186 " btn.innerHTML='"ICON_PAUSE"';"
187 " btn.value='pause';"
188 " } else {"
189 " btn.innerHTML='"ICON_PLAY"';"
190 " btn.value='play';"
191 " }"
192 " } else if (type=='r') {"
193 " const btn=document.querySelector('#rone');"
194 " btn.className=b(payload)?'mode-active':'';"
195 " } else if (type=='R') {"
196 " const btn=document.querySelector('#rall');"
197 " btn.className=b(payload)?'mode-active':'';"
198 " } else if (type=='c') {"
199 /* consume */
200 " } else if (type=='x') {"
201 " playlist.innerHTML='';"
202 " } else if (type=='X') {"
203 " dofilt();" /* done with the list */
204 " } else if (type=='A') {"
205 " c(payload, true);"
206 " } else if (type=='a') {"
207 " c(payload, false);"
208 " } else if (type=='C') {"
209 " const t=document.querySelector('.controls>p>a');"
210 " t.innerText = payload.replace(/.*\\//, '');"
211 " cur();"
212 " } else {"
213 " console.log('unknown:',t);"
214 " }"
215 "};"
216 "function w(){"
217 " ws = new WebSocket((location.protocol=='http:'?'ws://':'wss://')"
218 " + location.host + '/ws');"
219 " ws.addEventListener('open', () => console.log('ws: connected'));"
220 " ws.addEventListener('close', () => {"
221 " alert('Websocket closed. The interface won\\'t update itself.'"
222 " + ' Please refresh the page');"
223 " });"
224 " ws.addEventListener('message', e => d(e.data))"
225 "};"
226 "w();"
227 "cur();"
228 "document.querySelector('.controls a').addEventListener('click',cur);"
229 "document.querySelectorAll('form').forEach(f => {"
230 " f.action='/a/'+f.getAttribute('action');"
231 " f.addEventListener('submit', e => {"
232 " e.preventDefault();"
233 " const fd = new FormData(f);"
234 " if (e.submitter && e.submitter.value && e.submitter.value != '')"
235 " fd.append(e.submitter.name, e.submitter.value);"
236 " fetch(f.action, {"
237 " method:'POST',"
238 " body: new URLSearchParams(fd)"
239 " })"
240 " .catch(x => console.log('failed to submit form:', x));"
241 " });"
242 "});"
243 "const sb = document.createElement('section');"
244 "sb.className = 'searchbox';"
245 "const filter = document.createElement('input');"
246 "filter.type = 'search';"
247 "filter.setAttribute('aria-label', 'Filter Playlist');"
248 "filter.placeholder = 'Filter Playlist';"
249 "sb.append(filter);"
250 "document.querySelector('main').prepend(sb);"
251 "function dofilt() {"
252 " let t = filter.value.toLowerCase();"
253 " document.querySelectorAll('.playlist li').forEach(e => {"
254 " if (e.querySelector('button').value.toLowerCase().indexOf(t) == -1)"
255 " e.setAttribute('hidden', 'true');"
256 " else"
257 " e.removeAttribute('hidden');"
258 " });"
259 "};"
260 "function dbc(fn, wait) {"
261 " let tout;"
262 " return function() {"
263 " let later = () => {tout = null; fn()};"
264 " clearTimeout(tout);"
265 " if (!tout) fn();"
266 " tout = setTimeout(later, wait);"
267 " };"
268 "};"
269 "filter.addEventListener('input', dbc(dofilt, 400));"
272 const char *foot = "<script src='/app.js?v=0'></script></body></html>";
274 static inline int
275 bio_ev(struct bufio *bio)
277 int ret, ev;
279 ret = 0;
280 ev = bufio_ev(bio);
281 if (ev & BUFIO_WANT_READ)
282 ret |= EV_READ;
283 if (ev & BUFIO_WANT_WRITE)
284 ret |= EV_WRITE;
285 return ret;
288 static int
289 dial(const char *sock)
291 struct sockaddr_un sa;
292 size_t len;
293 int s;
295 memset(&sa, 0, sizeof(sa));
296 sa.sun_family = AF_UNIX;
297 len = strlcpy(sa.sun_path, sock, sizeof(sa.sun_path));
298 if (len >= sizeof(sa.sun_path))
299 err(1, "path too long: %s", sock);
301 if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
302 err(1, "socket");
303 if (connect(s, (struct sockaddr *)&sa, sizeof(sa)) == -1)
304 err(1, "failed to connect to %s", sock);
306 return s;
309 /*
310 * Adapted from usr.sbin/httpd/httpd.c' url_decode.
311 */
312 static int
313 url_decode(char *url)
315 char*p, *q;
316 char hex[3] = {0};
317 unsigned long x;
319 p = q = url;
320 while (*p != '\0') {
321 switch (*p) {
322 case '%':
323 /* Encoding character is followed by two hex chars */
324 if (!isxdigit((unsigned char)p[1]) ||
325 !isxdigit((unsigned char)p[2]) ||
326 (p[1] == '0' && p[2] == '0'))
327 return (-1);
329 hex[0] = p[1];
330 hex[1] = p[2];
332 /*
333 * We don't have to validate "hex" because it is
334 * guaranteed to include two hex chars followed
335 * by NUL.
336 */
337 x = strtoul(hex, NULL, 16);
338 *q = (char)x;
339 p += 2;
340 break;
341 case '+':
342 *q = ' ';
343 break;
344 default:
345 *q = *p;
346 break;
348 p++;
349 q++;
351 *q = '\0';
353 return (0);
356 static int
357 dispatch_event(const char *msg)
359 struct client *clt;
360 size_t len;
361 int ret = 0;
363 len = strlen(msg);
364 TAILQ_FOREACH(clt, &clients, clients) {
365 if (!clt->ws || clt->done || clt->err)
366 continue;
368 if (ws_compose(clt, WST_TEXT, msg, len) == -1)
369 ret = -1;
371 ev_add(clt->bio.fd, EV_READ|EV_WRITE, client_ev, clt);
374 return (ret);
377 static int
378 dispatch_event_status(void)
380 const char *status;
381 char buf[PATH_MAX + 2];
382 int r;
384 switch (player_status.status) {
385 case STATE_STOPPED: status = "stopped"; break;
386 case STATE_PLAYING: status = "playing"; break;
387 case STATE_PAUSED: status = "paused"; break;
388 default: status = "unknown";
391 r = snprintf(buf, sizeof(buf), "S:%s", status);
392 if (r < 0 || (size_t)r >= sizeof(buf)) {
393 log_warn("snprintf");
394 return -1;
396 dispatch_event(buf);
398 r = snprintf(buf, sizeof(buf), "r:%s",
399 player_status.mode.repeat_one == MODE_ON ? "on" : "off");
400 if (r < 0 || (size_t)r >= sizeof(buf)) {
401 log_warn("snprintf");
402 return -1;
404 dispatch_event(buf);
406 r = snprintf(buf, sizeof(buf), "R:%s",
407 player_status.mode.repeat_all == MODE_ON ? "on" : "off");
408 if (r < 0 || (size_t)r >= sizeof(buf)) {
409 log_warn("snprintf");
410 return -1;
412 dispatch_event(buf);
414 r = snprintf(buf, sizeof(buf), "c:%s",
415 player_status.mode.consume == MODE_ON ? "on" : "off");
416 if (r < 0 || (size_t)r >= sizeof(buf)) {
417 log_warn("snprintf");
418 return -1;
420 dispatch_event(buf);
422 r = snprintf(buf, sizeof(buf), "C:%s", player_status.path);
423 if (r < 0 || (size_t)r >= sizeof(buf)) {
424 log_warn("snprintf");
425 return -1;
427 dispatch_event(buf);
429 return 0;
432 static int
433 dispatch_event_track(struct player_status *ps)
435 char p[PATH_MAX + 2];
436 int r;
438 r = snprintf(p, sizeof(p), "%c:%s",
439 ps->status == STATE_PLAYING ? 'A' : 'a', ps->path);
440 if (r < 0 || (size_t)r >= sizeof(p))
441 return (-1);
443 return dispatch_event(p);
446 static void
447 imsg_dispatch(int fd, int ev, void *d)
449 static ssize_t off;
450 static int off_found;
451 char seekmsg[128];
452 struct imsg imsg;
453 struct ibuf ibuf;
454 struct player_status ps;
455 struct player_event event;
456 const char *msg;
457 ssize_t n;
458 size_t datalen;
459 int r;
461 if (ev & EV_READ) {
462 if ((n = imsg_read(&imsgbuf)) == -1 && errno != EAGAIN)
463 fatal("imsg_read");
464 if (n == 0)
465 fatalx("pipe closed");
467 if (ev & EV_WRITE) {
468 if ((n = msgbuf_write(&imsgbuf.w)) == -1 && errno != EAGAIN)
469 fatal("msgbuf_write");
470 if (n == 0)
471 fatalx("pipe closed");
474 for (;;) {
475 if ((n = imsg_get(&imsgbuf, &imsg)) == -1)
476 fatal("imsg_get");
477 if (n == 0)
478 break;
480 datalen = IMSG_DATA_SIZE(imsg);
482 switch (imsg_get_type(&imsg)) {
483 case IMSG_CTL_ERR:
484 if (imsg_get_ibuf(&imsg, &ibuf) == -1 ||
485 (datalen = ibuf_size(&ibuf)) == 0 ||
486 (msg = ibuf_data(&ibuf)) == NULL ||
487 msg[datalen - 1] != '\0')
488 fatalx("malformed error message");
489 log_warnx("error: %s", msg);
490 break;
492 case IMSG_CTL_ADD:
493 playlist_free(&playlist_tmp);
494 imsg_compose(&imsgbuf, IMSG_CTL_SHOW, 0, 0, -1,
495 NULL, 0);
496 break;
498 case IMSG_CTL_MONITOR:
499 if (imsg_get_data(&imsg, &event, sizeof(event)) == -1)
500 fatalx("corrupted IMSG_CTL_MONITOR");
501 switch (event.event) {
502 case IMSG_CTL_PLAY:
503 case IMSG_CTL_PAUSE:
504 case IMSG_CTL_STOP:
505 case IMSG_CTL_MODE:
506 imsg_compose(&imsgbuf, IMSG_CTL_STATUS, 0, 0,
507 -1, NULL, 0);
508 break;
510 case IMSG_CTL_NEXT:
511 case IMSG_CTL_PREV:
512 case IMSG_CTL_JUMP:
513 case IMSG_CTL_COMMIT:
514 imsg_compose(&imsgbuf, IMSG_CTL_SHOW, 0, 0, -1,
515 NULL, 0);
516 imsg_compose(&imsgbuf, IMSG_CTL_STATUS, 0, 0,
517 -1, NULL, 0);
518 break;
520 case IMSG_CTL_SEEK:
521 position = event.position;
522 duration = event.duration;
523 r = snprintf(seekmsg, sizeof(seekmsg),
524 "s:%lld %lld", (long long)position,
525 (long long)duration);
526 if (r < 0 || (size_t)r >= sizeof(seekmsg)) {
527 log_warn("snprintf failed");
528 break;
530 dispatch_event(seekmsg);
531 break;
533 default:
534 log_debug("ignoring event %d", event.event);
535 break;
537 break;
539 case IMSG_CTL_SHOW:
540 if (imsg_get_len(&imsg) == 0) {
541 if (playlist_tmp.len == 0) {
542 dispatch_event("x:");
543 off = -1;
544 } else if (playlist_tmp.len == off)
545 off = -1;
546 dispatch_event("X:");
547 playlist_swap(&playlist_tmp, off);
548 memset(&playlist_tmp, 0, sizeof(playlist_tmp));
549 off = 0;
550 off_found = 0;
551 break;
553 if (imsg_get_data(&imsg, &ps, sizeof(ps)) == -1)
554 fatalx("corrupted IMSG_CTL_SHOW");
555 if (ps.path[sizeof(ps.path) - 1] != '\0')
556 fatalx("corrupted IMSG_CTL_SHOW");
557 if (playlist_tmp.len == 0)
558 dispatch_event("x:");
559 dispatch_event_track(&ps);
560 playlist_push(&playlist_tmp, ps.path);
561 if (ps.status == STATE_PLAYING)
562 off_found = 1;
563 if (!off_found)
564 off++;
565 break;
567 case IMSG_CTL_STATUS:
568 if (imsg_get_data(&imsg, &player_status,
569 sizeof(player_status)) == -1)
570 fatalx("corrupted IMSG_CTL_STATUS");
571 if (player_status.path[sizeof(player_status.path) - 1]
572 != '\0')
573 fatalx("corrupted IMSG_CTL_STATUS");
574 dispatch_event_status();
575 break;
579 ev = EV_READ;
580 if (imsgbuf.w.queued)
581 ev |= EV_WRITE;
582 ev_add(fd, ev, imsg_dispatch, NULL);
585 static void
586 route_notfound(struct client *clt)
588 if (http_reply(clt, 404, "Not Found", "text/plain") == -1 ||
589 http_writes(clt, "Page not found\n") == -1)
590 return;
593 static void
594 render_playlist(struct client *clt)
596 ssize_t i;
597 const char *path;
598 int current;
600 http_writes(clt, "<section class='playlist-wrapper'>");
601 http_writes(clt, "<form action=jump method=post"
602 " enctype='"FORM_URLENCODED"'>");
603 http_writes(clt, "<ul class=playlist>");
605 for (i = 0; i < playlist.len; ++i) {
606 current = play_off == i;
608 path = playlist.songs[i];
610 http_fmt(clt, "<li%s>", current ? " id=current" : "");
611 http_writes(clt, "<button type=submit name=jump value=\"");
612 http_htmlescape(clt, path);
613 http_writes(clt, "\">");
614 http_htmlescape(clt, path);
615 http_writes(clt, "</button></li>");
618 http_writes(clt, "</ul>");
619 http_writes(clt, "</form>");
620 http_writes(clt, "</section>");
623 static void
624 render_controls(struct client *clt)
626 const char *oc, *ac, *p;
627 int playing;
629 ac = player_status.mode.repeat_all ? " class='mode-active'" : "";
630 oc = player_status.mode.repeat_one ? " class='mode-active'" : "";
631 playing = player_status.status == STATE_PLAYING;
633 if ((p = strrchr(player_status.path, '/')) != NULL)
634 p++;
635 else
636 p = player_status.path;
638 if (http_writes(clt, "<section class=controls>") == -1 ||
639 http_writes(clt, "<p><a href='#current'>") == -1 ||
640 http_htmlescape(clt, p) == -1 ||
641 http_writes(clt, "</a></p>") == -1 ||
642 http_writes(clt, "<form action=ctrls method=post"
643 " enctype='"FORM_URLENCODED"'>") == -1 ||
644 http_writes(clt, "<button type=submit name=ctl value=prev>"
645 ICON_PREV"</button>") == -1 ||
646 http_fmt(clt, "<button id='toggle' type=submit name=ctl value=%s>"
647 "%s</button>", playing ? "pause" : "play",
648 playing ? ICON_PAUSE : ICON_PLAY) == -1 ||
649 http_writes(clt, "<button type=submit name=ctl value=next>"
650 ICON_NEXT"</button>") == -1 ||
651 http_writes(clt, "</form>") == -1 ||
652 http_writes(clt, "<form action=mode method=post"
653 " enctype='"FORM_URLENCODED"'>") == -1 ||
654 http_fmt(clt, "<button%s id=rall type=submit name=mode value=all>"
655 ICON_REPEAT_ALL"</button>", ac) == -1 ||
656 http_fmt(clt, "<button%s id=rone type=submit name=mode value=one>"
657 ICON_REPEAT_ONE"</button>", oc) == -1 ||
658 http_writes(clt, "</form>") == -1 ||
659 http_writes(clt, "</section>") == -1)
660 return;
663 static void
664 route_home(struct client *clt)
666 if (http_reply(clt, 200, "OK", "text/html;charset=UTF-8") == -1)
667 return;
669 if (http_write(clt, head, strlen(head)) == -1)
670 return;
672 if (http_writes(clt, "<main>") == -1)
673 return;
675 render_controls(clt);
676 render_playlist(clt);
678 if (http_writes(clt, "</main>") == -1)
679 return;
681 http_write(clt, foot, strlen(foot));
684 static void
685 route_jump(struct client *clt)
687 char path[PATH_MAX];
688 char *form, *field;
689 int found = 0;
691 http_postdata(clt, &form, NULL);
692 while ((field = strsep(&form, "&")) != NULL) {
693 if (url_decode(field) == -1)
694 goto badreq;
696 if (strncmp(field, "jump=", 5) != 0)
697 continue;
698 field += 5;
699 found = 1;
701 memset(&path, 0, sizeof(path));
702 if (strlcpy(path, field, sizeof(path)) >= sizeof(path))
703 goto badreq;
705 imsg_compose(&imsgbuf, IMSG_CTL_JUMP, 0, 0, -1,
706 path, sizeof(path));
707 ev_add(imsgbuf.w.fd, EV_READ|EV_WRITE, imsg_dispatch, NULL);
708 break;
711 if (!found)
712 goto badreq;
714 if (!strncmp(clt->req.path, "/a/", 2))
715 http_reply(clt, 200, "OK", "text/plain");
716 else
717 http_reply(clt, 302, "See Other", "/");
718 return;
720 badreq:
721 http_reply(clt, 400, "Bad Request", "text/plain");
722 http_writes(clt, "Bad Request.\n");
725 static void
726 route_controls(struct client *clt)
728 char *form, *field;
729 int cmd, found = 0;
731 http_postdata(clt, &form, NULL);
732 while ((field = strsep(&form, "&")) != NULL) {
733 if (url_decode(field) == -1)
734 goto badreq;
736 if (strncmp(field, "ctl=", 4) != 0)
737 continue;
738 field += 4;
739 found = 1;
741 if (!strcmp(field, "play"))
742 cmd = IMSG_CTL_PLAY;
743 else if (!strcmp(field, "pause"))
744 cmd = IMSG_CTL_PAUSE;
745 else if (!strcmp(field, "next"))
746 cmd = IMSG_CTL_NEXT;
747 else if (!strcmp(field, "prev"))
748 cmd = IMSG_CTL_PREV;
749 else
750 goto badreq;
752 imsg_compose(&imsgbuf, cmd, 0, 0, -1, NULL, 0);
753 imsg_flush(&imsgbuf);
754 break;
757 if (!found)
758 goto badreq;
760 if (!strncmp(clt->req.path, "/a/", 2))
761 http_reply(clt, 200, "OK", "text/plain");
762 else
763 http_reply(clt, 302, "See Other", "/");
764 return;
766 badreq:
767 http_reply(clt, 400, "Bad Request", "text/plain");
768 http_writes(clt, "Bad Request.\n");
771 static void
772 route_mode(struct client *clt)
774 char *form, *field;
775 int found = 0;
776 struct player_mode pm;
778 pm.repeat_one = pm.repeat_all = pm.consume = MODE_UNDEF;
780 http_postdata(clt, &form, NULL);
781 while ((field = strsep(&form, "&")) != NULL) {
782 if (url_decode(field) == -1)
783 goto badreq;
785 if (strncmp(field, "mode=", 5) != 0)
786 continue;
787 field += 5;
788 found = 1;
790 if (!strcmp(field, "all"))
791 pm.repeat_all = MODE_TOGGLE;
792 else if (!strcmp(field, "one"))
793 pm.repeat_one = MODE_TOGGLE;
794 else
795 goto badreq;
797 imsg_compose(&imsgbuf, IMSG_CTL_MODE, 0, 0, -1,
798 &pm, sizeof(pm));
799 ev_add(imsgbuf.w.fd, EV_READ|EV_WRITE, imsg_dispatch, NULL);
800 break;
803 if (!found)
804 goto badreq;
806 if (!strncmp(clt->req.path, "/a/", 2))
807 http_reply(clt, 200, "OK", "text/plain");
808 else
809 http_reply(clt, 302, "See Other", "/");
810 return;
812 badreq:
813 http_reply(clt, 400, "Bad Request", "text/plain");
814 http_writes(clt, "Bad Request.\n");
817 static void
818 route_handle_ws(struct client *clt)
820 struct buf *rbuf = &clt->bio.rbuf;
821 int type;
822 size_t len;
824 if (ws_read(clt, &type, &len) == -1) {
825 if (errno != EAGAIN) {
826 log_warn("ws_read");
827 clt->done = 1;
829 return;
832 switch (type) {
833 case WST_PING:
834 ws_compose(clt, WST_PONG, rbuf->buf, len);
835 break;
836 case WST_TEXT:
837 /* log_info("<<< %.*s", (int)len, rbuf->buf); */
838 break;
839 case WST_CLOSE:
840 /* TODO send a close too (ack) */
841 clt->done = 1;
842 break;
843 default:
844 log_info("got unexpected ws frame type 0x%02x", type);
845 break;
848 buf_drain(rbuf, len);
851 static void
852 route_init_ws(struct client *clt)
854 if (!(clt->req.flags & (R_CONNUPGR|R_UPGRADEWS|R_WSVERSION)) ||
855 clt->req.secret == NULL) {
856 http_reply(clt, 400, "Bad Request", "text/plain");
857 http_writes(clt, "Invalid websocket handshake.\r\n");
858 return;
861 clt->ws = 1;
862 clt->done = 0;
863 clt->route = route_handle_ws;
864 http_reply(clt, 101, "Switching Protocols", NULL);
867 static void
868 route_assets(struct client *clt)
870 if (!strcmp(clt->req.path, "/style.css")) {
871 http_reply(clt, 200, "OK", "text/css");
872 http_write(clt, css, strlen(css));
873 return;
876 if (!strcmp(clt->req.path, "/app.js")) {
877 http_reply(clt, 200, "OK", "application/javascript");
878 http_write(clt, js, strlen(js));
879 return;
882 route_notfound(clt);
885 static void
886 route_dispatch(struct client *clt)
888 static const struct route {
889 int method;
890 const char *path;
891 route_fn route;
892 } routes[] = {
893 { METHOD_GET, "/", &route_home },
895 { METHOD_POST, "/jump", &route_jump },
896 { METHOD_POST, "/ctrls", &route_controls },
897 { METHOD_POST, "/mode", &route_mode },
899 { METHOD_POST, "/a/jump", &route_jump },
900 { METHOD_POST, "/a/ctrls", &route_controls },
901 { METHOD_POST, "/a/mode", &route_mode },
903 { METHOD_GET, "/ws", &route_init_ws },
905 { METHOD_GET, "/style.css", &route_assets },
906 { METHOD_GET, "/app.js", &route_assets },
908 { METHOD_GET, "*", &route_notfound },
909 { METHOD_POST, "*", &route_notfound },
910 };
911 struct request *req = &clt->req;
912 size_t i;
914 if ((req->method != METHOD_GET && req->method != METHOD_POST) ||
915 (req->ctype != NULL && strcmp(req->ctype, FORM_URLENCODED) != 0) ||
916 req->path == NULL) {
917 http_reply(clt, 400, "Bad Request", NULL);
918 return;
921 for (i = 0; i < nitems(routes); ++i) {
922 if (req->method != routes[i].method ||
923 fnmatch(routes[i].path, req->path, 0) != 0)
924 continue;
925 clt->done = 1; /* assume with one round is done */
926 clt->route = routes[i].route;
927 clt->route(clt);
928 if (clt->done)
929 http_close(clt);
930 return;
934 static void
935 client_ev(int fd, int ev, void *d)
937 struct client *clt = d;
938 ssize_t ret;
940 if (ev & EV_READ) {
941 if ((ret = bufio_read(&clt->bio)) == -1 && errno != EAGAIN) {
942 log_warn("bufio_read");
943 goto err;
945 if (ret == 0)
946 goto err;
949 if (ev & EV_WRITE) {
950 if ((ret = bufio_write(&clt->bio)) == -1 && errno != EAGAIN) {
951 log_warn("bufio_write");
952 goto err;
954 if (ret == 0)
955 goto err;
958 if (clt->route == NULL) {
959 if (http_parse(clt) == -1) {
960 if (errno == EAGAIN)
961 goto again;
962 log_warnx("HTTP parse request failed");
963 goto err;
965 if (clt->req.method == METHOD_POST &&
966 http_read(clt) == -1) {
967 if (errno == EAGAIN)
968 goto again;
969 log_warnx("failed to read POST data");
970 goto err;
972 route_dispatch(clt);
973 goto again;
976 if (!clt->done && !clt->err)
977 clt->route(clt);
979 again:
980 ev = bio_ev(&clt->bio);
981 if (ev == EV_READ && (clt->done || clt->err)) {
982 goto err; /* done with this client */
985 ev_add(fd, ev, client_ev, clt);
986 return;
988 err:
989 ev_del(fd);
990 TAILQ_REMOVE(&clients, clt, clients);
991 http_free(clt);
994 static void
995 web_accept(int psock, int ev, void *d)
997 struct client *clt;
998 int sock;
1000 if ((sock = accept(psock, NULL, NULL)) == -1) {
1001 warn("accept");
1002 return;
1004 if ((clt = calloc(1, sizeof(*clt))) == NULL ||
1005 http_init(clt, sock) == -1) {
1006 log_warn("failed to initialize client");
1007 free(clt);
1008 close(sock);
1009 return;
1012 TAILQ_INSERT_TAIL(&clients, clt, clients);
1014 client_ev(sock, EV_READ, clt);
1015 return;
1018 void __dead
1019 usage(void)
1021 fprintf(stderr, "usage: %s [-v] [-s sock] [[host] port]\n",
1022 getprogname());
1023 exit(1);
1026 int
1027 main(int argc, char **argv)
1029 struct addrinfo hints, *res, *res0;
1030 const char *cause = NULL;
1031 const char *host = NULL;
1032 const char *port = "9090";
1033 char *sock = NULL;
1034 size_t nsock, error, save_errno;
1035 int ch, v, amused_sock, fd;
1036 int verbose = 0;
1038 TAILQ_INIT(&clients);
1039 setlocale(LC_ALL, NULL);
1041 log_init(1, LOG_DAEMON);
1043 if (pledge("stdio rpath unix inet dns", NULL) == -1)
1044 err(1, "pledge");
1046 while ((ch = getopt(argc, argv, "s:v")) != -1) {
1047 switch (ch) {
1048 case 's':
1049 sock = optarg;
1050 break;
1051 case 'v':
1052 verbose = 1;
1053 break;
1054 default:
1055 usage();
1058 argc -= optind;
1059 argv += optind;
1061 if (argc == 1)
1062 port = argv[0];
1063 if (argc == 2) {
1064 host = argv[0];
1065 port = argv[1];
1067 if (argc > 2)
1068 usage();
1070 log_setverbose(verbose);
1072 if (sock == NULL) {
1073 const char *tmpdir;
1075 if ((tmpdir = getenv("TMPDIR")) == NULL)
1076 tmpdir = "/tmp";
1078 xasprintf(&sock, "%s/amused-%d", tmpdir, getuid());
1081 signal(SIGPIPE, SIG_IGN);
1083 if (ev_init() == -1)
1084 fatal("ev_init");
1086 amused_sock = dial(sock);
1087 imsg_init(&imsgbuf, amused_sock);
1088 imsg_compose(&imsgbuf, IMSG_CTL_SHOW, 0, 0, -1, NULL, 0);
1089 imsg_compose(&imsgbuf, IMSG_CTL_STATUS, 0, 0, -1, NULL, 0);
1090 imsg_compose(&imsgbuf, IMSG_CTL_MONITOR, 0, 0, -1, NULL, 0);
1091 ev_add(amused_sock, EV_READ|EV_WRITE, imsg_dispatch, NULL);
1093 memset(&hints, 0, sizeof(hints));
1094 hints.ai_family = AF_UNSPEC;
1095 hints.ai_socktype = SOCK_STREAM;
1096 hints.ai_flags = AI_PASSIVE;
1097 error = getaddrinfo(host, port, &hints, &res0);
1098 if (error)
1099 errx(1, "%s", gai_strerror(error));
1101 nsock = 0;
1102 for (res = res0; res; res = res->ai_next) {
1103 fd = socket(res->ai_family, res->ai_socktype,
1104 res->ai_protocol);
1105 if (fd == -1) {
1106 cause = "socket";
1107 continue;
1110 v = 1;
1111 if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
1112 &v, sizeof(v)) == -1)
1113 fatal("setsockopt(SO_REUSEADDR)");
1115 if (bind(fd, res->ai_addr, res->ai_addrlen) == -1) {
1116 cause = "bind";
1117 save_errno = errno;
1118 close(fd);
1119 errno = save_errno;
1120 continue;
1123 if (listen(fd, 5) == -1)
1124 err(1, "listen");
1126 if (ev_add(fd, EV_READ, web_accept, NULL) == -1)
1127 fatal("ev_add");
1128 nsock++;
1130 if (nsock == 0)
1131 err(1, "%s", cause);
1132 freeaddrinfo(res0);
1134 if (pledge("stdio inet", NULL) == -1)
1135 err(1, "pledge");
1137 log_info("listening on port %s", port);
1138 ev_loop();
1139 return (1);