commit 04e4e99327c1aa645f591ea2a47ac2f4c13fe4c1 from: Omar Polo date: Mon Aug 14 18:53:28 2023 UTC add amused-web, a web interface to control amused It's a first stab at it, some planned features (like the search) are missing but the basic ones work. It's not hooked in the main build yet. commit - 96621d9401fa4c4a0c9233330f154c02b9172467 commit + 04e4e99327c1aa645f591ea2a47ac2f4c13fe4c1 blob - abf3c24bc711201666a48e52c79d2bdde00dea31 blob + caf21c548f295f0990d283b0a6ae722a5557384c --- .gitignore +++ .gitignore @@ -4,6 +4,7 @@ config.h config.h.old config.log config.log.old +web/amused-web **/*.d **/*.o **/obj blob - /dev/null blob + 77fe3eda0d6433c7422354a75b2ddd420c92ce1b (mode 644) --- /dev/null +++ web/Makefile @@ -0,0 +1,61 @@ +.PHONY: all clean + +PROG = amused-web + +SOURCES = web.c http.c ../log.c ../playlist.c ../xmalloc.c + +OBJS = ${SOURCES:.c=.o} + +DISTFILES = Makefile amused-web.1 http.c web.h + +all: ${PROG} + +../Makefile.configure ../config.h: ../configure ../tests.c + @echo "$@ is out of date; please run ../configure" + @exit 1 + +include ../Makefile.configure + +# --- targets --- + +${PROG}: ${OBJS} + ${CC} -o $@ ${OBJS} ${LDFLAGS} ${LDADD} + +clean: + rm -f ${OBJS} ${OBJS:.o=.d} ${PROG} + +distclean: clean + +install: + mkdir -p ${DESTDIR}${BINDIR} + mkdir -p ${DESTDIR}${MANDIR}/man1 + ${INSTALL_PROGRAM} ${PROG} ${DESTDIR}${BINDIR} + ${INSTALL_MAN} amused-web.1 ${DESTDIR}${MANDIR}/man1/${PROG}.1 + +install-local: + mkdir -p ${HOME}/bin + ${INSTALL_PROGRAM} ${PROG} ${HOME}/bin + +uninstall: + rm ${DESTDIR}${BINDIR}/${PROG} + rm ${DESTDIR}${MANDIR}/man1/${PROG}.1 + +.c.o: + ${CC} ${CFLAGS} -I../ -c $< -o $@ + +# --- maintainer targets --- + +dist: + mkdir -p ${DESTDIR}/web + ${INSTALL} -m 0644 ${DISTFILES} ${DESTDIR}/web + +# --- dependency management --- + +# these .d files are produced during the first build if the compiler +# supports it. + +-include http.d +-include web.d +-include ../log.d +-include ../playlist.d +-include ../xmalloc.d blob - /dev/null blob + 114332657a43551572bd150e1374088a6f49b783 (mode 644) --- /dev/null +++ web/amused-web.1 @@ -0,0 +1,64 @@ +.\" Copyright (c) 2023 Omar Polo +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd August 14, 2023 +.Dt AMUSED-WEB 1 +.Os +.Sh NAME +.Nm amused-web +.Nd web interface for the amused music player +.Sh SYNOPSIS +.Nm +.Op Fl v +.Op Fl s Ar socket +.Op Fl t Ar prefix +.Op Oo Ar host Oc Ar port +.Sh DESCRIPTION +.Nm +is a web interface to control the +.Xr amused 1 +music player. +It exposes a web server that listen on +.Ar host +at the given +.Ar port . +By default all IPv4 and IPv6 address at port 9090. +.Pp +The following options are available: +.Bl -tag -width tenletters +.It Fl s Ar socket +Path to the +.Xr amused 1 +control socket. +By default +.Pa /tmp/amused-UID +is used. +.It Fl t Ar prefix +Strip +.Ar prefix +from the paths showed on the interface. +.It Fl v +Produce more verbose output. +.El +.Sh SEE ALSO +.Xr amused 1 +.Sh AUTHORS +.An -nosplit +The +.Nm +program was written by +.An Omar Polo Aq Mt op@omarpolo.com . +.Sh BUGS +The web interface doesn't update itself automatically. +Periodically refresh are needed. blob - /dev/null blob + 14d0338cf00535a9a279b66898aceaaa98565666 (mode 644) --- /dev/null +++ web/http.c @@ -0,0 +1,426 @@ +/* + * Copyright (c) 2023 Omar Polo + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "http.h" +#include "log.h" +#include "xmalloc.h" + +#ifndef nitems +#define nitems(x) (sizeof(x)/sizeof(x[0])) +#endif + +static int +writeall(struct reswriter *res, const char *buf, size_t buflen) +{ + ssize_t nw; + size_t off; + + for (off = 0; off < buflen; off += nw) + if ((nw = write(res->fd, buf + off, buflen - off)) == 0 || + nw == -1) { + if (nw == 0) + log_warnx("Unexpected EOF"); + else + log_warn("write"); + res->err = 1; + return -1; + } + + return 0; +} + +int +http_parse(struct request *req, int fd) +{ + ssize_t nr; + size_t avail, len; + int done = 0, first = 1; + char *s, *t, *line, *endln; + const char *errstr, *m; + + memset(req, 0, sizeof(*req)); + + line = req->buf; + + while (!done) { + if (req->len == sizeof(req->buf)) { + log_warnx("not enough space"); + return -1; + } + + avail = sizeof(req->buf) - req->len; + nr = read(fd, req->buf + req->len, avail); + if (nr <= 0) { + if (errno == EAGAIN || errno == EINTR) + continue; + if (nr == 0) + log_warnx("Unexpected EOF"); + else + log_warn("read"); + return -1; + } + req->len += nr; + + while ((endln = memmem(req->buf, req->len, "\r\n", 2))) { + if (endln == req->buf) + done = 1; + + len = endln - req->buf + 2; + while (len > 0 && (line[len - 1] == '\r' || + line[len - 1] == '\n' || line[len - 1] == ' ' || + line[len - 1] == '\t')) + line[--len] = '\0'; + + if (first) { + first = 0; + if (!strncmp("GET ", line, 4)) { + req->method = METHOD_GET; + s = line + 4; + } else if (!strncmp("POST ", line, 5)) { + req->method = METHOD_POST; + s = line + 5; + } + + t = strchr(s, ' '); + if (t == NULL) + t = s; + if (*t != '\0') + *t++ = '\0'; + req->path = xstrdup(s); + if (strcmp("HTTP/1.0", t) != 0 && + strcmp("HTTP/1.1", t) != 0) { + log_warnx("unknown http version: %s", + t); + return -1; + } + } + + if (!strncasecmp(line, "Content-Length:", 15)) { + line += 15; + line += strspn(line, " \t"); + req->clen = strtonum(line, 0, LONG_MAX, + &errstr); + if (errstr != NULL) { + log_warnx("content-length is %s: %s", + errstr, line); + return -1; + } + } + + len = endln - req->buf + 2; + memmove(req->buf, req->buf + len, req->len - len); + req->len -= len; + } + } + + if (req->method == METHOD_GET) + m = "GET"; + else if (req->method == METHOD_POST) + m = "POST"; + else + m = "unknown"; + log_debug("< %s %s", m, req->path); + + return 0; +} + +int +http_read(struct request *req, int fd) +{ + size_t left; + ssize_t nr; + + if (req->clen > sizeof(req->buf) - 1) + return -1; + if (req->len == req->clen) { + req->buf[req->len] = '\0'; + return 0; + } + if (req->len > req->clen) { + log_warnx("got more data than what advertised!"); + return -1; + } + + left = req->clen - req->len; + while (left > 0) { + nr = read(fd, req->buf + req->len, left); + if (nr <= 0) { + if (nr == -1 && errno == EAGAIN) + continue; + if (nr == 0) + log_warnx("Unexpected EOF"); + else + log_warn("read"); + return -1; + } + req->len += nr; + left -= nr; + } + + req->buf[req->len] = '\0'; + return 0; +} + +void +http_response_init(struct reswriter *res, int fd) +{ + memset(res, 0, sizeof(*res)); + res->fd = fd; +} + +int +http_reply(struct reswriter *res, int code, const char *reason, + const char *ctype) +{ + const char *location = NULL; + int r; + + res->len = 0; /* discard any leftover from reading */ + res->chunked = ctype != NULL; + + log_debug("> %d %s", code, reason); + + if (code >= 300 && code < 400) { + location = ctype; + ctype = NULL; + } + + r = snprintf(res->buf, sizeof(res->buf), "HTTP/1.1 %d %s\r\n" + "Connection: close\r\n" + "Cache-Control: no-store\r\n" + "%s%s%s" + "%s%s%s" + "%s" + "\r\n", + code, reason, + ctype == NULL ? "" : "Content-Type: ", + ctype == NULL ? "" : ctype, + ctype == NULL ? "" : "\r\n", + location == NULL ? "" : "Location: ", + location == NULL ? "" : location, + location == NULL ? "" : "\r\n", + ctype == NULL ? "" : "Transfer-Encoding: chunked\r\n"); + if (r < 0 || (size_t)r >= sizeof(res->buf)) + return -1; + + return writeall(res, res->buf, r); +} + +int +http_flush(struct reswriter *res) +{ + struct iovec iov[3]; + char buf[64]; + ssize_t nw; + size_t i, tot; + int r; + + if (res->err) + return -1; + + if (res->len == 0) + return 0; + + r = snprintf(buf, sizeof(buf), "%zx\r\n", res->len); + if (r < 0 || (size_t)r >= sizeof(buf)) { + log_warn("snprintf failed"); + res->err = 1; + return -1; + } + + memset(iov, 0, sizeof(iov)); + + iov[0].iov_base = buf; + iov[0].iov_len = r; + + iov[1].iov_base = res->buf; + iov[1].iov_len = res->len; + + iov[2].iov_base = "\r\n"; + iov[2].iov_len = 2; + + tot = iov[0].iov_len + iov[1].iov_len + iov[2].iov_len; + while (tot > 0) { + nw = writev(res->fd, iov, nitems(iov)); + if (nw <= 0) { + if (nw == -1 && errno == EAGAIN) + continue; + if (nw == 0) + log_warnx("Unexpected EOF"); + else + log_warn("writev"); + res->err = 1; + return -1; + } + + tot -= nw; + for (i = 0; i < nitems(iov); ++i) { + if (nw < iov[i].iov_len) { + iov[i].iov_base += nw; + iov[i].iov_len -= nw; + break; + } + nw -= iov[i].iov_len; + iov[i].iov_len = 0; + } + } + + res->len = 0; + return 0; +} + +int +http_write(struct reswriter *res, const char *d, size_t len) +{ + size_t avail; + + if (res->err) + return -1; + + while (len > 0) { + avail = sizeof(res->buf) - res->len; + if (avail > len) + avail = len; + + memcpy(res->buf + res->len, d, avail); + res->len += avail; + len -= avail; + d += avail; + if (res->len == sizeof(res->buf)) { + if (http_flush(res) == -1) + return -1; + } + } + + return 0; +} + +int +http_writes(struct reswriter *res, const char *str) +{ + return http_write(res, str, strlen(str)); +} + +int +http_fmt(struct reswriter *res, const char *fmt, ...) +{ + va_list ap; + char *str; + int r; + + va_start(ap, fmt); + r = vasprintf(&str, fmt, ap); + va_end(ap); + + if (r == -1) { + log_warn("vasprintf"); + res->err = 1; + return -1; + } + + r = http_write(res, str, r); + free(str); + return r; +} + +int +http_urlescape(struct reswriter *res, const char *str) +{ + int r; + char tmp[4]; + + for (; *str; ++str) { + if (iscntrl((unsigned char)*str) || + isspace((unsigned char)*str) || + *str == '\'' || *str == '"' || *str == '\\') { + r = snprintf(tmp, sizeof(tmp), "%%%2X", + (unsigned char)*str); + if (r < 0 || (size_t)r >= sizeof(tmp)) { + log_warn("snprintf failed"); + res->err = 1; + return -1; + } + if (http_write(res, tmp, r) == -1) + return -1; + } else if (http_write(res, str, 1) == -1) + return -1; + } + + return 0; +} + +int +http_htmlescape(struct reswriter *res, const char *str) +{ + int r; + + for (; *str; ++str) { + switch (*str) { + case '<': + r = http_writes(res, "<"); + break; + case '>': + r = http_writes(res, ">"); + break; + case '&': + r = http_writes(res, ">"); + break; + case '"': + r = http_writes(res, """); + break; + case '\'': + r = http_writes(res, "'"); + break; + default: + r = http_write(res, str, 1); + break; + } + + if (r == -1) + return -1; + } + + return 0; +} + +int +http_close(struct reswriter *res) +{ + if (!res->chunked) + return 0; + + return writeall(res, "0\r\n\r\n", 5); +} + +void +http_free_request(struct request *req) +{ + free(req->path); + free(req->ctype); +} blob - /dev/null blob + c457f7765fa8d07caa8b0cb7f255800debb1787f (mode 644) --- /dev/null +++ web/http.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 Omar Polo + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +enum http_method { + METHOD_UNKNOWN, + METHOD_GET, + METHOD_POST, +}; + +struct request { + char buf[BUFSIZ]; + size_t len; + + char *path; + int method; + char *ctype; + size_t clen; +}; + +struct reswriter { + int fd; + int err; + int chunked; + char buf[BUFSIZ]; + size_t len; +}; + +int http_parse(struct request *, int); +int http_read(struct request *, int); +void http_response_init(struct reswriter *, int); +int http_reply(struct reswriter *, int, const char *, const char *); +int http_flush(struct reswriter *); +int http_write(struct reswriter *, const char *, size_t); +int http_writes(struct reswriter *, const char *); +int http_fmt(struct reswriter *, const char *, ...); +int http_urlescape(struct reswriter *, const char *); +int http_htmlescape(struct reswriter *, const char *); +int http_close(struct reswriter *); +void http_free_request(struct request *); blob - /dev/null blob + 3904a24bfcba03acd812a6e6f3be7564415b3978 (mode 644) --- /dev/null +++ web/web.c @@ -0,0 +1,787 @@ +/* + * Copyright (c) 2023 Omar Polo + * Copyright (c) 2014 Reyk Floeter + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "config.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "amused.h" +#include "http.h" +#include "log.h" +#include "playlist.h" +#include "xmalloc.h" + +#ifndef nitems +#define nitems(x) (sizeof(x)/sizeof(x[0])) +#endif + +#define FORM_URLENCODED "application/x-www-form-urlencoded" + +#define ICON_REPEAT_ALL "🔁" +#define ICON_REPEAT_ONE "🔂" +#define ICON_PREV "⏮" +#define ICON_NEXT "⏭" +#define ICON_STOP "⏹" +#define ICON_PAUSE "⏸" +#define ICON_TOGGLE "⏯" +#define ICON_PLAY "⏵" + +static struct imsgbuf ibuf; +static const char *prefix = ""; +static size_t prefixlen; + +const char *head = "" + "" + "" + "" + "Amused Web" + "" + "" + ""; + +const char *foot = ""; + +static int +dial(const char *sock) +{ + struct sockaddr_un sa; + size_t len; + int s; + + memset(&sa, 0, sizeof(sa)); + sa.sun_family = AF_UNIX; + len = strlcpy(sa.sun_path, sock, sizeof(sa.sun_path)); + if (len >= sizeof(sa.sun_path)) + err(1, "path too long: %s", sock); + + if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) + err(1, "socket"); + if (connect(s, (struct sockaddr *)&sa, sizeof(sa)) == -1) + err(1, "failed to connect to %s", sock); + + return s; +} + + +/* + * Adapted from usr.sbin/httpd/httpd.c' url_decode. + */ +static int +url_decode(char *url) +{ + char*p, *q; + char hex[3] = {0}; + unsigned long x; + + p = q = url; + while (*p != '\0') { + switch (*p) { + case '%': + /* Encoding character is followed by two hex chars */ + if (!isxdigit((unsigned char)p[1]) || + !isxdigit((unsigned char)p[2]) || + (p[1] == '0' && p[2] == '0')) + return (-1); + + hex[0] = p[1]; + hex[1] = p[2]; + + /* + * We don't have to validate "hex" because it is + * guaranteed to include two hex chars followed + * by NUL. + */ + x = strtoul(hex, NULL, 16); + *q = (char)x; + p += 2; + break; + case '+': + *q = ' '; + break; + default: + *q = *p; + break; + } + p++; + q++; + } + *q = '\0'; + + return (0); +} + +static void +route_notfound(struct reswriter *res, struct request *req) +{ + if (http_reply(res, 404, "Not Found", "text/plain") == -1 || + http_writes(res, "Page not found\n") == -1) + return; +} + +static void +render_playlist(struct reswriter *res) +{ + struct imsg imsg; + struct player_status ps; + ssize_t n; + const char *p; + int current, done; + + imsg_compose(&ibuf, IMSG_CTL_SHOW, 0, 0, -1, NULL, 0); + imsg_flush(&ibuf); + + http_writes(res, "
"); + http_writes(res, "
"); + http_writes(res, "
    "); + + done = 0; + while (!done) { + if ((n = imsg_read(&ibuf)) == -1) + fatal("imsg_read"); + if (n == 0) + fatalx("pipe closed"); + + for (;;) { + if ((n = imsg_get(&ibuf, &imsg)) == -1) + fatal("imsg_get"); + if (n == 0) + break; + + if (imsg.hdr.type != IMSG_CTL_SHOW) { + log_warnx("got event %d while expecting SHOW", + imsg.hdr.type); + imsg_free(&imsg); + continue; + } + + if (IMSG_DATA_SIZE(imsg) == 0) { + done = 1; + break; + } + + if (IMSG_DATA_SIZE(imsg) != sizeof(ps)) + fatalx("wrong size for seek ctl"); + memcpy(&ps, imsg.data, sizeof(ps)); + if (ps.path[sizeof(ps.path) - 1] != '\0') + fatalx("received corrupted data"); + + current = ps.status == STATE_PLAYING; + + p = ps.path; + if (!strncmp(p, prefix, prefixlen)) + p += prefixlen; + + http_fmt(res, "", + current ? " id=current" : ""); + http_writes(res, + ""); + + imsg_free(&imsg); + } + } + + http_writes(res, "
"); + http_writes(res, "
"); + http_writes(res, "
"); +} + +static void +render_controls(struct reswriter *res) +{ + struct imsg imsg; + struct player_status ps; + ssize_t n; + const char *oc, *ac, *p; + int playing; + + imsg_compose(&ibuf, IMSG_CTL_STATUS, 0, 0, -1, NULL, 0); + imsg_flush(&ibuf); + + if ((n = imsg_read(&ibuf)) == -1) + fatal("imsg_read"); + if (n == 0) + fatalx("pipe closed"); + + if ((n = imsg_get(&ibuf, &imsg)) == -1) + fatal("imsg_get"); + if (n == 0) + return; + + if (imsg.hdr.type != IMSG_CTL_STATUS) { + log_warnx("got event %d while expecting CTL_STATUS", + imsg.hdr.type); + goto done; + } + if (IMSG_DATA_SIZE(imsg) != sizeof(ps)) + fatalx("wrong size for IMSG_CTL_STATUS"); + memcpy(&ps, imsg.data, sizeof(ps)); + if (ps.path[sizeof(ps.path) - 1] != '\0') + fatalx("received corrupted data"); + + ac = ps.mode.repeat_all ? " class='mode-active'" : ""; + oc = ps.mode.repeat_one ? " class='mode-active'" : ""; + playing = ps.status == STATE_PLAYING; + + if ((p = strrchr(ps.path, '/')) != NULL) + p++; + else + p = ps.path; + + if (http_writes(res, "
") == -1 || + http_writes(res, "

") == -1 || + http_htmlescape(res, p) == -1 || + http_writes(res, "

") == -1 || + http_writes(res, "
") == -1 || + http_writes(res, "") == -1 || + http_fmt(res, "", playing ? "pause" : "play", + playing ? ICON_PAUSE : ICON_PLAY) == -1 || + http_writes(res, "") == -1 || + http_writes(res, "
") == -1 || + http_writes(res, "
") == -1 || + http_fmt(res, "" + ICON_REPEAT_ALL"", ac) == -1 || + http_fmt(res, "" + ICON_REPEAT_ONE"", oc) == -1 || + http_writes(res, "") == -1 || + http_writes(res, "
") == -1) + return; + + done: + imsg_free(&imsg); +} + +static void +route_home(struct reswriter *res, struct request *req) +{ + if (http_reply(res, 200, "OK", "text/html;charset=UTF-8") == -1) + return; + + if (http_write(res, head, strlen(head)) == -1) + return; + + if (http_writes(res, "
") == -1) + return; + + if (http_writes(res, "") == -1) + return; + + render_controls(res); + render_playlist(res); + + if (http_writes(res, "
") == -1) + return; + + http_write(res, foot, strlen(foot)); +} + +static void +route_jump(struct reswriter *res, struct request *req) +{ + struct imsg imsg; + struct player_status ps; + ssize_t n; + char path[PATH_MAX]; + char *form, *field; + int found = 0; + + if (http_read(req, res->fd) == -1) + return; + + form = req->buf; + while ((field = strsep(&form, "&")) != NULL) { + if (url_decode(field) == -1) + goto badreq; + + if (strncmp(field, "jump=", 5) != 0) + continue; + field += 5; + found = 1; + + if (strlcpy(path, field, sizeof(path)) >= sizeof(path)) + goto badreq; + + log_warnx("path is %s", path); + imsg_compose(&ibuf, IMSG_CTL_JUMP, 0, 0, -1, + path, sizeof(path)); + imsg_flush(&ibuf); + + if ((n = imsg_read(&ibuf)) == -1) + fatal("imsg_read"); + if (n == 0) + fatalx("pipe closed"); + + for (;;) { + if ((n = imsg_get(&ibuf, &imsg)) == -1) + fatal("imsg_get"); + if (n == 0) + break; + + if (imsg.hdr.type != IMSG_CTL_STATUS) { + log_warnx("got event %d while expecting" + " IMSG_CTL_STATUS", imsg.hdr.type); + imsg_free(&imsg); + continue; + } + + if (IMSG_DATA_SIZE(imsg) != sizeof(ps)) + fatalx("data size mismatch"); + memcpy(&ps, imsg.data, sizeof(ps)); + if (ps.path[sizeof(ps.path) - 1] != '\0') + fatalx("received corrupted data"); + log_debug("jumped to %s", ps.path); + } + + break; + } + + if (!found) + goto badreq; + + http_reply(res, 302, "See Other", "/"); + return; + + badreq: + http_reply(res, 400, "Bad Request", "text/plain"); + http_writes(res, "Bad Request.\n"); +} + +static void +route_controls(struct reswriter *res, struct request *req) +{ + char *form, *field; + int cmd, found = 0; + + if (http_read(req, res->fd) == -1) + return; + + form = req->buf; + while ((field = strsep(&form, "&")) != NULL) { + if (url_decode(field) == -1) + goto badreq; + + if (strncmp(field, "ctl=", 4) != 0) + continue; + field += 4; + found = 1; + + if (!strcmp(field, "play")) + cmd = IMSG_CTL_PLAY; + else if (!strcmp(field, "pause")) + cmd = IMSG_CTL_PAUSE; + else if (!strcmp(field, "next")) + cmd = IMSG_CTL_NEXT; + else if (!strcmp(field, "prev")) + cmd = IMSG_CTL_PREV; + else + goto badreq; + + imsg_compose(&ibuf, cmd, 0, 0, -1, NULL, 0); + imsg_flush(&ibuf); + break; + } + + if (!found) + goto badreq; + + http_reply(res, 302, "See Other", "/"); + return; + + badreq: + http_reply(res, 400, "Bad Request", "text/plain"); + http_writes(res, "Bad Request.\n"); +} + +static void +route_mode(struct reswriter *res, struct request *req) +{ + char *form, *field; + int found = 0; + ssize_t n; + struct player_status ps; + struct player_mode pm; + struct imsg imsg; + + pm.repeat_one = pm.repeat_all = pm.consume = MODE_UNDEF; + + if (http_read(req, res->fd) == -1) + return; + + form = req->buf; + while ((field = strsep(&form, "&")) != NULL) { + if (url_decode(field) == -1) + goto badreq; + + if (strncmp(field, "mode=", 5) != 0) + continue; + field += 5; + found = 1; + + if (!strcmp(field, "all")) + pm.repeat_all = MODE_TOGGLE; + else if (!strcmp(field, "one")) + pm.repeat_one = MODE_TOGGLE; + else + goto badreq; + + imsg_compose(&ibuf, IMSG_CTL_MODE, 0, 0, -1, &pm, sizeof(pm)); + imsg_flush(&ibuf); + + if ((n = imsg_read(&ibuf)) == -1) + fatal("imsg_read"); + if (n == 0) + fatalx("pipe closed"); + + for (;;) { + if ((n = imsg_get(&ibuf, &imsg)) == -1) + fatal("imsg_get"); + if (n == 0) + break; + + if (imsg.hdr.type != IMSG_CTL_STATUS) { + log_warnx("got event %d while expecting" + " IMSG_CTL_STATUS", imsg.hdr.type); + imsg_free(&imsg); + continue; + } + + if (IMSG_DATA_SIZE(imsg) != sizeof(ps)) + fatalx("data size mismatch"); + memcpy(&ps, imsg.data, sizeof(ps)); + if (ps.path[sizeof(ps.path) - 1] != '\0') + fatalx("received corrupted data"); + } + + break; + } + + if (!found) + goto badreq; + + http_reply(res, 302, "See Other", "/"); + return; + + badreq: + http_reply(res, 400, "Bad Request", "text/plain"); + http_writes(res, "Bad Request.\n"); +} + +static void +route_dispatch(struct reswriter *res, struct request *req) +{ + static const struct route { + int method; + const char *path; + void (*fn)(struct reswriter *, struct request *); + } routes[] = { + { METHOD_GET, "/", &route_home }, + { METHOD_POST, "/jump", &route_jump }, + { METHOD_POST, "/ctrls", &route_controls }, + { METHOD_POST, "/mode", &route_mode }, + + { METHOD_GET, "*", &route_notfound }, + { METHOD_POST, "*", &route_notfound }, + }; + size_t i; + + if ((req->method != METHOD_GET && req->method != METHOD_POST) || + (req->ctype != NULL && strcmp(req->ctype, FORM_URLENCODED) != 0) || + req->path == NULL) { + http_reply(res, 400, "Bad Request", NULL); + return; + } + + for (i = 0; i < nitems(routes); ++i) { + if (req->method != routes[i].method || + fnmatch(routes[i].path, req->path, 0) != 0) + continue; + routes[i].fn(res, req); + return; + } +} + +static void +handle_client(int psock) +{ + struct reswriter res; + struct request req; + int sock;; + + if ((sock = accept(psock, NULL, NULL)) == -1) { + warn("accept"); + return; + } + if (http_parse(&req, sock) == -1) { + close(sock); + return; + } + http_response_init(&res, sock); + route_dispatch(&res, &req); + http_flush(&res); + http_close(&res); + http_free_request(&req); + close(sock); + return; +} + +void __dead +usage(void) +{ + fprintf(stderr, "usage: %s [-v] [-s sock] [-t prefix] [[host] port]\n", + getprogname()); + exit(1); +} + +int +main(int argc, char **argv) +{ + struct pollfd pfds[16]; + struct addrinfo hints, *res, *res0; + const char *cause = NULL; + const char *host = NULL; + const char *port = "9090"; + char *sock = NULL; + size_t i, nsock, error, save_errno; + int ch, v, amused_sock; + int verbose = 0; + + setlocale(LC_ALL, NULL); + + memset(&pfds, 0, sizeof(pfds)); + for (i = 0; i < nitems(pfds); ++i) + pfds[i].fd = -1; + + log_init(1, LOG_DAEMON); + + if (pledge("stdio rpath unix inet dns", NULL) == -1) + err(1, "pledge"); + + while ((ch = getopt(argc, argv, "s:t:v")) != -1) { + switch (ch) { + case 's': + sock = optarg; + break; + case 't': + prefix = optarg; + prefixlen = strlen(prefix); + break; + case 'v': + verbose = 1; + break; + default: + usage(); + } + } + argc -= optind; + argv += optind; + + if (argc == 1) + port = argv[0]; + if (argc == 2) { + host = argv[0]; + port = argv[1]; + } + if (argc > 2) + usage(); + + log_setverbose(verbose); + + if (sock == NULL) + xasprintf(&sock, "/tmp/amused-%d", getuid()); + + signal(SIGPIPE, SIG_IGN); + + amused_sock = dial(sock); + imsg_init(&ibuf, amused_sock); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + error = getaddrinfo(host, port, &hints, &res0); + if (error) + errx(1, "%s", gai_strerror(error)); + + nsock = 0; + for (res = res0; res && nsock < nitems(pfds); res = res->ai_next) { + pfds[nsock].fd = socket(res->ai_family, res->ai_socktype, + res->ai_protocol); + if (pfds[nsock].fd == -1) { + cause = "socket"; + continue; + } + + v = 1; + if (setsockopt(pfds[nsock].fd, SOL_SOCKET, SO_REUSEADDR, + &v, sizeof(v)) == -1) + fatal("setsockopt(SO_REUSEADDR)"); + + if (bind(pfds[nsock].fd, res->ai_addr, res->ai_addrlen) == -1) { + cause = "bind"; + save_errno = errno; + close(pfds[nsock].fd); + errno = save_errno; + continue; + } + + if (listen(pfds[nsock].fd, 5) == -1) + err(1, "listen"); + + pfds[nsock].events = POLLIN; + nsock++; + } + if (nsock == 0) + err(1, "%s", cause); + freeaddrinfo(res0); + + if (pledge("stdio inet", NULL) == -1) + err(1, "pledge"); + + log_info("starting"); + + for (;;) { + if (poll(pfds, nitems(pfds), INFTIM) == -1) { + if (errno == EINTR) + continue; + err(1, "poll"); + } + + for (i = 0; i < nitems(pfds); ++i) { + if (pfds[i].fd == -1) + continue; + if (!(pfds[i].revents & POLLIN)) + continue; + handle_client(pfds[i].fd); + } + } +}