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 "ws.h"
45 #include "xmalloc.h"
47 #ifndef nitems
48 #define nitems(x) (sizeof(x)/sizeof(x[0]))
49 #endif
51 #define FORM_URLENCODED "application/x-www-form-urlencoded"
53 #define ICON_REPEAT_ALL "🔁"
54 #define ICON_REPEAT_ONE "🔂"
55 #define ICON_PREV "⏮"
56 #define ICON_NEXT "⏭"
57 #define ICON_STOP "⏹"
58 #define ICON_PAUSE "⏸"
59 #define ICON_TOGGLE "⏯"
60 #define ICON_PLAY "⏵"
62 static struct clthead clients;
63 static struct imsgbuf imsgbuf;
64 static struct playlist playlist_tmp;
65 static struct player_status player_status;
66 static uint64_t position, duration;
68 static void client_ev(int, int, void *);
70 const char *head = "<!doctype html>"
71 "<html>"
72 "<head>"
73 "<meta name='viewport' content='width=device-width, initial-scale=1'/>"
74 "<title>Amused Web</title>"
75 "<link rel='stylesheet' href='/style.css?v=0'>"
76 "</style>"
77 "</head>"
78 "<body>";
80 const char *css = "*{box-sizing:border-box}"
81 "html,body{"
82 " padding: 0;"
83 " border: 0;"
84 " margin: 0;"
85 "}"
86 "main{"
87 " display: flex;"
88 " flex-direction: column;"
89 "}"
90 "button{cursor:pointer}"
91 ".searchbox{"
92 " position: sticky;"
93 " top: 0;"
94 "}"
95 ".searchbox input{"
96 " width: 100%;"
97 " padding: 9px;"
98 "}"
99 ".playlist-wrapper{min-height:80vh}"
100 ".playlist{"
101 " list-style: none;"
102 " padding: 0;"
103 " margin: 0;"
104 "}"
105 ".playlist button{"
106 " font-family: monospace;"
107 " text-align: left;"
108 " width: 100%;"
109 " padding: 5px;"
110 " border: 0;"
111 " background: transparent;"
112 " transition: background-color .25s ease-in-out;"
113 "}"
114 ".playlist button::before{"
115 " content: \"\";"
116 " width: 2ch;"
117 " display: inline-block;"
118 "}"
119 ".playlist button:hover{"
120 " background-color: #dfdddd;"
121 "}"
122 ".playlist #current button{"
123 " font-weight: bold;"
124 "}"
125 ".playlist #current button::before{"
126 " content: \"→ \";"
127 " font-weight: bold;"
128 "}"
129 ".controls{"
130 " position: sticky;"
131 " width: 100%;"
132 " max-width: 800px;"
133 " margin: 0 auto;"
134 " bottom: 0;"
135 " background-color: white;"
136 " background: #3d3d3d;"
137 " color: white;"
138 " border-radius: 10px 10px 0 0;"
139 " padding: 10px;"
140 " text-align: center;"
141 " order: 2;"
142 "}"
143 ".controls p{"
144 " margin: .4rem;"
145 "}"
146 ".controls a{"
147 " color: white;"
148 "}"
149 ".controls .status{"
150 " font-size: 0.9rem;"
151 "}"
152 ".controls button{"
153 " margin: 5px;"
154 " padding: 5px 20px;"
155 "}"
156 ".mode-active{"
157 " color: #0064ff;"
158 "}";
160 const char *js =
161 "var ws;"
162 "let pos=0, dur=0;"
163 "const playlist=document.querySelector('.playlist');"
164 "function cur(e) {"
165 " if (e) {e.preventDefault()}"
166 " let cur = document.querySelector('#current');"
167 " if (cur) {cur.scrollIntoView(); window.scrollBy(0, -100);}"
168 "};"
169 "function b(x){return x=='on'};"
170 "function c(p, c){"
171 " const l=document.createElement('li');"
172 " if(c){l.id='current'};"
173 " const b=document.createElement('button');"
174 " b.type='submit'; b.name='jump'; b.value=p;"
175 " b.innerText=p;"
176 " l.appendChild(b);"
177 " playlist.appendChild(l);"
178 "}"
179 "function d(t){"
180 " const [, type, payload] = t.split(/^(.):(.*)$/);"
181 " if (type=='s'){"
182 " let s=payload.split(' ');"
183 " pos=s[0], dur=s[1];"
184 " } else if (type=='S') {"
185 " const btn=document.querySelector('#toggle');"
186 " if (payload=='playing') {"
187 " btn.innerHTML='"ICON_PAUSE"';"
188 " btn.value='pause';"
189 " } else {"
190 " btn.innerHTML='"ICON_PLAY"';"
191 " btn.value='play';"
192 " }"
193 " } else if (type=='r') {"
194 " const btn=document.querySelector('#rone');"
195 " btn.className=b(payload)?'mode-active':'';"
196 " } else if (type=='R') {"
197 " const btn=document.querySelector('#rall');"
198 " btn.className=b(payload)?'mode-active':'';"
199 " } else if (type=='c') {"
200 /* consume */
201 " } else if (type=='x') {"
202 " playlist.innerHTML='';"
203 " } else if (type=='X') {"
204 " dofilt();" /* done with the list */
205 " } else if (type=='A') {"
206 " c(payload, true);"
207 " } else if (type=='a') {"
208 " c(payload, false);"
209 " } else if (type=='C') {"
210 " const t=document.querySelector('.controls>p>a');"
211 " t.innerText = payload.replace(/.*\\//, '');"
212 " cur();"
213 " } else {"
214 " console.log('unknown:',t);"
215 " }"
216 "};"
217 "function w(){"
218 " ws = new WebSocket((location.protocol=='http:'?'ws://':'wss://')"
219 " + location.host + '/ws');"
220 " ws.addEventListener('open', () => console.log('ws: connected'));"
221 " ws.addEventListener('close', () => {"
222 " alert('Websocket closed. The interface won\\'t update itself.'"
223 " + ' Please refresh the page');"
224 " });"
225 " ws.addEventListener('message', e => d(e.data))"
226 "};"
227 "w();"
228 "cur();"
229 "document.querySelector('.controls a').addEventListener('click',cur);"
230 "document.querySelectorAll('form').forEach(f => {"
231 " f.action='/a/'+f.getAttribute('action');"
232 " f.addEventListener('submit', e => {"
233 " e.preventDefault();"
234 " const fd = new FormData(f);"
235 " if (e.submitter && e.submitter.value && e.submitter.value != '')"
236 " fd.append(e.submitter.name, e.submitter.value);"
237 " fetch(f.action, {"
238 " method:'POST',"
239 " body: new URLSearchParams(fd)"
240 " })"
241 " .catch(x => console.log('failed to submit form:', x));"
242 " });"
243 "});"
244 "const sb = document.createElement('section');"
245 "sb.className = 'searchbox';"
246 "const filter = document.createElement('input');"
247 "filter.type = 'search';"
248 "filter.setAttribute('aria-label', 'Filter Playlist');"
249 "filter.placeholder = 'Filter Playlist';"
250 "sb.append(filter);"
251 "document.querySelector('main').prepend(sb);"
252 "function dofilt() {"
253 " let t = filter.value.toLowerCase();"
254 " document.querySelectorAll('.playlist li').forEach(e => {"
255 " if (e.querySelector('button').value.toLowerCase().indexOf(t) == -1)"
256 " e.setAttribute('hidden', 'true');"
257 " else"
258 " e.removeAttribute('hidden');"
259 " });"
260 "};"
261 "function dbc(fn, wait) {"
262 " let tout;"
263 " return function() {"
264 " let later = () => {tout = null; fn()};"
265 " clearTimeout(tout);"
266 " if (!tout) fn();"
267 " tout = setTimeout(later, wait);"
268 " };"
269 "};"
270 "filter.addEventListener('input', dbc(dofilt, 400));"
273 const char *foot = "<script src='/app.js?v=0'></script></body></html>";
275 static int
276 dial(const char *sock)
278 struct sockaddr_un sa;
279 size_t len;
280 int s;
282 memset(&sa, 0, sizeof(sa));
283 sa.sun_family = AF_UNIX;
284 len = strlcpy(sa.sun_path, sock, sizeof(sa.sun_path));
285 if (len >= sizeof(sa.sun_path))
286 err(1, "path too long: %s", sock);
288 if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
289 err(1, "socket");
290 if (connect(s, (struct sockaddr *)&sa, sizeof(sa)) == -1)
291 err(1, "failed to connect to %s", sock);
293 return s;
296 /*
297 * Adapted from usr.sbin/httpd/httpd.c' url_decode.
298 */
299 static int
300 url_decode(char *url)
302 char*p, *q;
303 char hex[3] = {0};
304 unsigned long x;
306 p = q = url;
307 while (*p != '\0') {
308 switch (*p) {
309 case '%':
310 /* Encoding character is followed by two hex chars */
311 if (!isxdigit((unsigned char)p[1]) ||
312 !isxdigit((unsigned char)p[2]) ||
313 (p[1] == '0' && p[2] == '0'))
314 return (-1);
316 hex[0] = p[1];
317 hex[1] = p[2];
319 /*
320 * We don't have to validate "hex" because it is
321 * guaranteed to include two hex chars followed
322 * by NUL.
323 */
324 x = strtoul(hex, NULL, 16);
325 *q = (char)x;
326 p += 2;
327 break;
328 case '+':
329 *q = ' ';
330 break;
331 default:
332 *q = *p;
333 break;
335 p++;
336 q++;
338 *q = '\0';
340 return (0);
343 static int
344 dispatch_event(const char *msg)
346 struct client *clt;
347 size_t len;
348 int ret = 0;
350 len = strlen(msg);
351 TAILQ_FOREACH(clt, &clients, clients) {
352 if (!clt->ws || clt->done || clt->err)
353 continue;
355 if (ws_compose(clt, WST_TEXT, msg, len) == -1)
356 ret = -1;
358 ev_add(clt->bio.fd, POLLIN|POLLOUT, client_ev, clt);
361 return (ret);
364 static int
365 dispatch_event_status(void)
367 const char *status;
368 char buf[PATH_MAX + 2];
369 int r;
371 switch (player_status.status) {
372 case STATE_STOPPED: status = "stopped"; break;
373 case STATE_PLAYING: status = "playing"; break;
374 case STATE_PAUSED: status = "paused"; break;
375 default: status = "unknown";
378 r = snprintf(buf, sizeof(buf), "S:%s", status);
379 if (r < 0 || (size_t)r >= sizeof(buf)) {
380 log_warn("snprintf");
381 return -1;
383 dispatch_event(buf);
385 r = snprintf(buf, sizeof(buf), "r:%s",
386 player_status.mode.repeat_one == MODE_ON ? "on" : "off");
387 if (r < 0 || (size_t)r >= sizeof(buf)) {
388 log_warn("snprintf");
389 return -1;
391 dispatch_event(buf);
393 r = snprintf(buf, sizeof(buf), "R:%s",
394 player_status.mode.repeat_all == MODE_ON ? "on" : "off");
395 if (r < 0 || (size_t)r >= sizeof(buf)) {
396 log_warn("snprintf");
397 return -1;
399 dispatch_event(buf);
401 r = snprintf(buf, sizeof(buf), "c:%s",
402 player_status.mode.consume == MODE_ON ? "on" : "off");
403 if (r < 0 || (size_t)r >= sizeof(buf)) {
404 log_warn("snprintf");
405 return -1;
407 dispatch_event(buf);
409 r = snprintf(buf, sizeof(buf), "C:%s", player_status.path);
410 if (r < 0 || (size_t)r >= sizeof(buf)) {
411 log_warn("snprintf");
412 return -1;
414 dispatch_event(buf);
416 return 0;
419 static int
420 dispatch_event_track(struct player_status *ps)
422 char p[PATH_MAX + 2];
423 int r;
425 r = snprintf(p, sizeof(p), "%c:%s",
426 ps->status == STATE_PLAYING ? 'A' : 'a', ps->path);
427 if (r < 0 || (size_t)r >= sizeof(p))
428 return (-1);
430 return dispatch_event(p);
433 static void
434 imsg_dispatch(int fd, int ev, void *d)
436 static ssize_t off;
437 static int off_found;
438 char seekmsg[128];
439 struct imsg imsg;
440 struct ibuf ibuf;
441 struct player_status ps;
442 struct player_event event;
443 const char *msg;
444 ssize_t n;
445 size_t datalen;
446 int r;
448 if (ev & (POLLIN|POLLHUP)) {
449 if ((n = imsg_read(&imsgbuf)) == -1 && errno != EAGAIN)
450 fatal("imsg_read");
451 if (n == 0)
452 fatalx("pipe closed");
454 if (ev & POLLOUT) {
455 if ((n = msgbuf_write(&imsgbuf.w)) == -1 && errno != EAGAIN)
456 fatal("msgbuf_write");
457 if (n == 0)
458 fatalx("pipe closed");
461 for (;;) {
462 if ((n = imsg_get(&imsgbuf, &imsg)) == -1)
463 fatal("imsg_get");
464 if (n == 0)
465 break;
467 datalen = IMSG_DATA_SIZE(imsg);
469 switch (imsg_get_type(&imsg)) {
470 case IMSG_CTL_ERR:
471 if (imsg_get_ibuf(&imsg, &ibuf) == -1 ||
472 (datalen = ibuf_size(&ibuf)) == 0 ||
473 (msg = ibuf_data(&ibuf)) == NULL ||
474 msg[datalen - 1] != '\0')
475 fatalx("malformed error message");
476 log_warnx("error: %s", msg);
477 break;
479 case IMSG_CTL_ADD:
480 playlist_free(&playlist_tmp);
481 imsg_compose(&imsgbuf, IMSG_CTL_SHOW, 0, 0, -1,
482 NULL, 0);
483 break;
485 case IMSG_CTL_MONITOR:
486 if (imsg_get_data(&imsg, &event, sizeof(event)) == -1)
487 fatalx("corrupted IMSG_CTL_MONITOR");
488 switch (event.event) {
489 case IMSG_CTL_PLAY:
490 case IMSG_CTL_PAUSE:
491 case IMSG_CTL_STOP:
492 case IMSG_CTL_MODE:
493 imsg_compose(&imsgbuf, IMSG_CTL_STATUS, 0, 0,
494 -1, NULL, 0);
495 break;
497 case IMSG_CTL_NEXT:
498 case IMSG_CTL_PREV:
499 case IMSG_CTL_JUMP:
500 case IMSG_CTL_COMMIT:
501 imsg_compose(&imsgbuf, IMSG_CTL_SHOW, 0, 0, -1,
502 NULL, 0);
503 imsg_compose(&imsgbuf, IMSG_CTL_STATUS, 0, 0,
504 -1, NULL, 0);
505 break;
507 case IMSG_CTL_SEEK:
508 position = event.position;
509 duration = event.duration;
510 r = snprintf(seekmsg, sizeof(seekmsg),
511 "s:%lld %lld", (long long)position,
512 (long long)duration);
513 if (r < 0 || (size_t)r >= sizeof(seekmsg)) {
514 log_warn("snprintf failed");
515 break;
517 dispatch_event(seekmsg);
518 break;
520 default:
521 log_debug("ignoring event %d", event.event);
522 break;
524 break;
526 case IMSG_CTL_SHOW:
527 if (imsg_get_len(&imsg) == 0) {
528 if (playlist_tmp.len == 0) {
529 dispatch_event("x:");
530 off = -1;
531 } else if (playlist_tmp.len == off)
532 off = -1;
533 dispatch_event("X:");
534 playlist_swap(&playlist_tmp, off);
535 memset(&playlist_tmp, 0, sizeof(playlist_tmp));
536 off = 0;
537 off_found = 0;
538 break;
540 if (imsg_get_data(&imsg, &ps, sizeof(ps)) == -1)
541 fatalx("corrupted IMSG_CTL_SHOW");
542 if (ps.path[sizeof(ps.path) - 1] != '\0')
543 fatalx("corrupted IMSG_CTL_SHOW");
544 if (playlist_tmp.len == 0)
545 dispatch_event("x:");
546 dispatch_event_track(&ps);
547 playlist_push(&playlist_tmp, ps.path);
548 if (ps.status == STATE_PLAYING)
549 off_found = 1;
550 if (!off_found)
551 off++;
552 break;
554 case IMSG_CTL_STATUS:
555 if (imsg_get_data(&imsg, &player_status,
556 sizeof(player_status)) == -1)
557 fatalx("corrupted IMSG_CTL_STATUS");
558 if (player_status.path[sizeof(player_status.path) - 1]
559 != '\0')
560 fatalx("corrupted IMSG_CTL_STATUS");
561 dispatch_event_status();
562 break;
566 ev = POLLIN;
567 if (imsgbuf.w.queued)
568 ev |= POLLOUT;
569 ev_add(fd, ev, imsg_dispatch, NULL);
572 static void
573 route_notfound(struct client *clt)
575 if (http_reply(clt, 404, "Not Found", "text/plain") == -1 ||
576 http_writes(clt, "Page not found\n") == -1)
577 return;
580 static void
581 render_playlist(struct client *clt)
583 ssize_t i;
584 const char *path;
585 int current;
587 http_writes(clt, "<section class='playlist-wrapper'>");
588 http_writes(clt, "<form action=jump method=post"
589 " enctype='"FORM_URLENCODED"'>");
590 http_writes(clt, "<ul class=playlist>");
592 for (i = 0; i < playlist.len; ++i) {
593 current = play_off == i;
595 path = playlist.songs[i];
597 http_fmt(clt, "<li%s>", current ? " id=current" : "");
598 http_writes(clt, "<button type=submit name=jump value=\"");
599 http_htmlescape(clt, path);
600 http_writes(clt, "\">");
601 http_htmlescape(clt, path);
602 http_writes(clt, "</button></li>");
605 http_writes(clt, "</ul>");
606 http_writes(clt, "</form>");
607 http_writes(clt, "</section>");
610 static void
611 render_controls(struct client *clt)
613 const char *oc, *ac, *p;
614 int playing;
616 ac = player_status.mode.repeat_all ? " class='mode-active'" : "";
617 oc = player_status.mode.repeat_one ? " class='mode-active'" : "";
618 playing = player_status.status == STATE_PLAYING;
620 if ((p = strrchr(player_status.path, '/')) != NULL)
621 p++;
622 else
623 p = player_status.path;
625 if (http_writes(clt, "<section class=controls>") == -1 ||
626 http_writes(clt, "<p><a href='#current'>") == -1 ||
627 http_htmlescape(clt, p) == -1 ||
628 http_writes(clt, "</a></p>") == -1 ||
629 http_writes(clt, "<form action=ctrls method=post"
630 " enctype='"FORM_URLENCODED"'>") == -1 ||
631 http_writes(clt, "<button type=submit name=ctl value=prev>"
632 ICON_PREV"</button>") == -1 ||
633 http_fmt(clt, "<button id='toggle' type=submit name=ctl value=%s>"
634 "%s</button>", playing ? "pause" : "play",
635 playing ? ICON_PAUSE : ICON_PLAY) == -1 ||
636 http_writes(clt, "<button type=submit name=ctl value=next>"
637 ICON_NEXT"</button>") == -1 ||
638 http_writes(clt, "</form>") == -1 ||
639 http_writes(clt, "<form action=mode method=post"
640 " enctype='"FORM_URLENCODED"'>") == -1 ||
641 http_fmt(clt, "<button%s id=rall type=submit name=mode value=all>"
642 ICON_REPEAT_ALL"</button>", ac) == -1 ||
643 http_fmt(clt, "<button%s id=rone type=submit name=mode value=one>"
644 ICON_REPEAT_ONE"</button>", oc) == -1 ||
645 http_writes(clt, "</form>") == -1 ||
646 http_writes(clt, "</section>") == -1)
647 return;
650 static void
651 route_home(struct client *clt)
653 if (http_reply(clt, 200, "OK", "text/html;charset=UTF-8") == -1)
654 return;
656 if (http_write(clt, head, strlen(head)) == -1)
657 return;
659 if (http_writes(clt, "<main>") == -1)
660 return;
662 render_controls(clt);
663 render_playlist(clt);
665 if (http_writes(clt, "</main>") == -1)
666 return;
668 http_write(clt, foot, strlen(foot));
671 static void
672 route_jump(struct client *clt)
674 char path[PATH_MAX];
675 char *form, *field;
676 int found = 0;
678 http_postdata(clt, &form, NULL);
679 while ((field = strsep(&form, "&")) != NULL) {
680 if (url_decode(field) == -1)
681 goto badreq;
683 if (strncmp(field, "jump=", 5) != 0)
684 continue;
685 field += 5;
686 found = 1;
688 memset(&path, 0, sizeof(path));
689 if (strlcpy(path, field, sizeof(path)) >= sizeof(path))
690 goto badreq;
692 imsg_compose(&imsgbuf, IMSG_CTL_JUMP, 0, 0, -1,
693 path, sizeof(path));
694 ev_add(imsgbuf.w.fd, POLLIN|POLLOUT, imsg_dispatch, NULL);
695 break;
698 if (!found)
699 goto badreq;
701 if (!strncmp(clt->req.path, "/a/", 2))
702 http_reply(clt, 200, "OK", "text/plain");
703 else
704 http_reply(clt, 302, "See Other", "/");
705 return;
707 badreq:
708 http_reply(clt, 400, "Bad Request", "text/plain");
709 http_writes(clt, "Bad Request.\n");
712 static void
713 route_controls(struct client *clt)
715 char *form, *field;
716 int cmd, found = 0;
718 http_postdata(clt, &form, NULL);
719 while ((field = strsep(&form, "&")) != NULL) {
720 if (url_decode(field) == -1)
721 goto badreq;
723 if (strncmp(field, "ctl=", 4) != 0)
724 continue;
725 field += 4;
726 found = 1;
728 if (!strcmp(field, "play"))
729 cmd = IMSG_CTL_PLAY;
730 else if (!strcmp(field, "pause"))
731 cmd = IMSG_CTL_PAUSE;
732 else if (!strcmp(field, "next"))
733 cmd = IMSG_CTL_NEXT;
734 else if (!strcmp(field, "prev"))
735 cmd = IMSG_CTL_PREV;
736 else
737 goto badreq;
739 imsg_compose(&imsgbuf, cmd, 0, 0, -1, NULL, 0);
740 imsg_flush(&imsgbuf);
741 break;
744 if (!found)
745 goto badreq;
747 if (!strncmp(clt->req.path, "/a/", 2))
748 http_reply(clt, 200, "OK", "text/plain");
749 else
750 http_reply(clt, 302, "See Other", "/");
751 return;
753 badreq:
754 http_reply(clt, 400, "Bad Request", "text/plain");
755 http_writes(clt, "Bad Request.\n");
758 static void
759 route_mode(struct client *clt)
761 char *form, *field;
762 int found = 0;
763 struct player_mode pm;
765 pm.repeat_one = pm.repeat_all = pm.consume = MODE_UNDEF;
767 http_postdata(clt, &form, NULL);
768 while ((field = strsep(&form, "&")) != NULL) {
769 if (url_decode(field) == -1)
770 goto badreq;
772 if (strncmp(field, "mode=", 5) != 0)
773 continue;
774 field += 5;
775 found = 1;
777 if (!strcmp(field, "all"))
778 pm.repeat_all = MODE_TOGGLE;
779 else if (!strcmp(field, "one"))
780 pm.repeat_one = MODE_TOGGLE;
781 else
782 goto badreq;
784 imsg_compose(&imsgbuf, IMSG_CTL_MODE, 0, 0, -1,
785 &pm, sizeof(pm));
786 ev_add(imsgbuf.w.fd, POLLIN|POLLOUT, imsg_dispatch, NULL);
787 break;
790 if (!found)
791 goto badreq;
793 if (!strncmp(clt->req.path, "/a/", 2))
794 http_reply(clt, 200, "OK", "text/plain");
795 else
796 http_reply(clt, 302, "See Other", "/");
797 return;
799 badreq:
800 http_reply(clt, 400, "Bad Request", "text/plain");
801 http_writes(clt, "Bad Request.\n");
804 static void
805 route_handle_ws(struct client *clt)
807 struct buffer *rbuf = &clt->bio.rbuf;
808 int type;
809 size_t len;
811 if (ws_read(clt, &type, &len) == -1) {
812 if (errno != EAGAIN) {
813 log_warn("ws_read");
814 clt->done = 1;
816 return;
819 switch (type) {
820 case WST_PING:
821 ws_compose(clt, WST_PONG, rbuf->buf, len);
822 break;
823 case WST_TEXT:
824 /* log_info("<<< %.*s", (int)len, rbuf->buf); */
825 break;
826 case WST_CLOSE:
827 /* TODO send a close too (ack) */
828 clt->done = 1;
829 break;
830 default:
831 log_info("got unexpected ws frame type 0x%02x", type);
832 break;
835 buf_drain(rbuf, len);
838 static void
839 route_init_ws(struct client *clt)
841 if (!(clt->req.flags & (R_CONNUPGR|R_UPGRADEWS|R_WSVERSION)) ||
842 clt->req.secret == NULL) {
843 http_reply(clt, 400, "Bad Request", "text/plain");
844 http_writes(clt, "Invalid websocket handshake.\r\n");
845 return;
848 clt->ws = 1;
849 clt->done = 0;
850 clt->route = route_handle_ws;
851 http_reply(clt, 101, "Switching Protocols", NULL);
854 static void
855 route_assets(struct client *clt)
857 if (!strcmp(clt->req.path, "/style.css")) {
858 http_reply(clt, 200, "OK", "text/css");
859 http_write(clt, css, strlen(css));
860 return;
863 if (!strcmp(clt->req.path, "/app.js")) {
864 http_reply(clt, 200, "OK", "application/javascript");
865 http_write(clt, js, strlen(js));
866 return;
869 route_notfound(clt);
872 static void
873 route_dispatch(struct client *clt)
875 static const struct route {
876 int method;
877 const char *path;
878 route_fn route;
879 } routes[] = {
880 { METHOD_GET, "/", &route_home },
882 { METHOD_POST, "/jump", &route_jump },
883 { METHOD_POST, "/ctrls", &route_controls },
884 { METHOD_POST, "/mode", &route_mode },
886 { METHOD_POST, "/a/jump", &route_jump },
887 { METHOD_POST, "/a/ctrls", &route_controls },
888 { METHOD_POST, "/a/mode", &route_mode },
890 { METHOD_GET, "/ws", &route_init_ws },
892 { METHOD_GET, "/style.css", &route_assets },
893 { METHOD_GET, "/app.js", &route_assets },
895 { METHOD_GET, "*", &route_notfound },
896 { METHOD_POST, "*", &route_notfound },
897 };
898 struct request *req = &clt->req;
899 size_t i;
901 if ((req->method != METHOD_GET && req->method != METHOD_POST) ||
902 (req->ctype != NULL && strcmp(req->ctype, FORM_URLENCODED) != 0) ||
903 req->path == NULL) {
904 http_reply(clt, 400, "Bad Request", NULL);
905 return;
908 for (i = 0; i < nitems(routes); ++i) {
909 if (req->method != routes[i].method ||
910 fnmatch(routes[i].path, req->path, 0) != 0)
911 continue;
912 clt->done = 1; /* assume with one round is done */
913 clt->route = routes[i].route;
914 clt->route(clt);
915 if (clt->done)
916 http_close(clt);
917 return;
921 static void
922 client_ev(int fd, int ev, void *d)
924 struct client *clt = d;
926 if (ev & (POLLIN|POLLHUP)) {
927 if (bufio_read(&clt->bio) == -1 && errno != EAGAIN) {
928 log_warn("bufio_read");
929 goto err;
933 if (ev & POLLOUT) {
934 if (bufio_write(&clt->bio) == -1 && errno != EAGAIN) {
935 log_warn("bufio_write");
936 goto err;
940 if (clt->route == NULL) {
941 if (http_parse(clt) == -1) {
942 if (errno == EAGAIN)
943 goto again;
944 log_warnx("HTTP parse request failed");
945 goto err;
947 if (clt->req.method == METHOD_POST &&
948 http_read(clt) == -1) {
949 if (errno == EAGAIN)
950 goto again;
951 log_warnx("failed to read POST data");
952 goto err;
954 route_dispatch(clt);
955 goto again;
958 if (!clt->done && !clt->err)
959 clt->route(clt);
961 again:
962 ev = bufio_pollev(&clt->bio);
963 if (ev == POLLIN && (clt->done || clt->err)) {
964 goto err; /* done with this client */
967 ev_add(fd, ev, client_ev, clt);
968 return;
970 err:
971 ev_del(fd);
972 TAILQ_REMOVE(&clients, clt, clients);
973 http_free(clt);
976 static void
977 web_accept(int psock, int ev, void *d)
979 struct client *clt;
980 int sock;
982 if ((sock = accept(psock, NULL, NULL)) == -1) {
983 warn("accept");
984 return;
986 if ((clt = calloc(1, sizeof(*clt))) == NULL ||
987 http_init(clt, sock) == -1) {
988 log_warn("failed to initialize client");
989 free(clt);
990 close(sock);
991 return;
994 TAILQ_INSERT_TAIL(&clients, clt, clients);
996 client_ev(sock, POLLIN, clt);
997 return;
1000 void __dead
1001 usage(void)
1003 fprintf(stderr, "usage: %s [-v] [-s sock] [[host] port]\n",
1004 getprogname());
1005 exit(1);
1008 int
1009 main(int argc, char **argv)
1011 struct addrinfo hints, *res, *res0;
1012 const char *cause = NULL;
1013 const char *host = NULL;
1014 const char *port = "9090";
1015 char *sock = NULL;
1016 size_t nsock, error, save_errno;
1017 int ch, v, amused_sock, fd;
1018 int verbose = 0;
1020 TAILQ_INIT(&clients);
1021 setlocale(LC_ALL, NULL);
1023 log_init(1, LOG_DAEMON);
1025 if (pledge("stdio rpath unix inet dns", NULL) == -1)
1026 err(1, "pledge");
1028 while ((ch = getopt(argc, argv, "s:v")) != -1) {
1029 switch (ch) {
1030 case 's':
1031 sock = optarg;
1032 break;
1033 case 'v':
1034 verbose = 1;
1035 break;
1036 default:
1037 usage();
1040 argc -= optind;
1041 argv += optind;
1043 if (argc == 1)
1044 port = argv[0];
1045 if (argc == 2) {
1046 host = argv[0];
1047 port = argv[1];
1049 if (argc > 2)
1050 usage();
1052 log_setverbose(verbose);
1054 if (sock == NULL) {
1055 const char *tmpdir;
1057 if ((tmpdir = getenv("TMPDIR")) == NULL)
1058 tmpdir = "/tmp";
1060 xasprintf(&sock, "%s/amused-%d", tmpdir, getuid());
1063 signal(SIGPIPE, SIG_IGN);
1065 if (ev_init() == -1)
1066 fatal("ev_init");
1068 amused_sock = dial(sock);
1069 imsg_init(&imsgbuf, amused_sock);
1070 imsg_compose(&imsgbuf, IMSG_CTL_SHOW, 0, 0, -1, NULL, 0);
1071 imsg_compose(&imsgbuf, IMSG_CTL_STATUS, 0, 0, -1, NULL, 0);
1072 imsg_compose(&imsgbuf, IMSG_CTL_MONITOR, 0, 0, -1, NULL, 0);
1073 ev_add(amused_sock, POLLIN|POLLOUT, imsg_dispatch, NULL);
1075 memset(&hints, 0, sizeof(hints));
1076 hints.ai_family = AF_UNSPEC;
1077 hints.ai_socktype = SOCK_STREAM;
1078 hints.ai_flags = AI_PASSIVE;
1079 error = getaddrinfo(host, port, &hints, &res0);
1080 if (error)
1081 errx(1, "%s", gai_strerror(error));
1083 nsock = 0;
1084 for (res = res0; res; res = res->ai_next) {
1085 fd = socket(res->ai_family, res->ai_socktype,
1086 res->ai_protocol);
1087 if (fd == -1) {
1088 cause = "socket";
1089 continue;
1092 v = 1;
1093 if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
1094 &v, sizeof(v)) == -1)
1095 fatal("setsockopt(SO_REUSEADDR)");
1097 if (bind(fd, res->ai_addr, res->ai_addrlen) == -1) {
1098 cause = "bind";
1099 save_errno = errno;
1100 close(fd);
1101 errno = save_errno;
1102 continue;
1105 if (listen(fd, 5) == -1)
1106 err(1, "listen");
1108 if (ev_add(fd, POLLIN, web_accept, NULL) == -1)
1109 fatal("ev_add");
1110 nsock++;
1112 if (nsock == 0)
1113 err(1, "%s", cause);
1114 freeaddrinfo(res0);
1116 if (pledge("stdio inet", NULL) == -1)
1117 err(1, "pledge");
1119 log_info("listening on port %s", port);
1120 ev_loop();
1121 return (1);