commit - 96621d9401fa4c4a0c9233330f154c02b9172467
commit + 04e4e99327c1aa645f591ea2a47ac2f4c13fe4c1
blob - abf3c24bc711201666a48e52c79d2bdde00dea31
blob + caf21c548f295f0990d283b0a6ae722a5557384c
--- .gitignore
+++ .gitignore
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
+.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
+.\" Copyright (c) 2023 Omar Polo <op@omarpolo.com>
+.\"
+.\" 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
+/*
+ * Copyright (c) 2023 Omar Polo <op@omarpolo.com>
+ *
+ * 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 <sys/uio.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#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
+/*
+ * Copyright (c) 2023 Omar Polo <op@omarpolo.com>
+ *
+ * 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
+/*
+ * Copyright (c) 2023 Omar Polo <op@omarpolo.com>
+ * Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org>
+ *
+ * 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 <sys/socket.h>
+#include <sys/types.h>
+#include <sys/un.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <fnmatch.h>
+#include <limits.h>
+#include <locale.h>
+#include <netdb.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#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 = "<!doctype html>"
+ "<html>"
+ "<head>"
+ "<meta name='viewport' content='width=device-width, initial-scale=1'/>"
+ "<title>Amused Web</title>"
+ "<style>"
+ "*{box-sizing:border-box}"
+ "html,body{"
+ " padding: 0;"
+ " border: 0;"
+ " margin: 0;"
+ "}"
+ "main{"
+ " display: flex;"
+ " flex-direction: column;"
+ "}"
+ "button{cursor:pointer}"
+ ".searchbox{"
+ " position: sticky;"
+ " top: 0;"
+ "}"
+ ".searchbox input{"
+ " width: 100%;"
+ " padding: 9px;"
+ "}"
+ ".playlist-wrapper{min-height:80vh}"
+ ".playlist{"
+ " list-style: none;"
+ " padding: 0;"
+ " margin: 0;"
+ "}"
+ ".playlist button{"
+ " font-family: monospace;"
+ " text-align: left;"
+ " width: 100%;"
+ " padding: 5px;"
+ " border: 0;"
+ " background: transparent;"
+ " transition: background-color .25s ease-in-out;"
+ "}"
+ ".playlist button::before{"
+ " content: \"\";"
+ " width: 2ch;"
+ " display: inline-block;"
+ "}"
+ ".playlist button:hover{"
+ " background-color: #dfdddd;"
+ "}"
+ ".playlist #current button{"
+ " font-weight: bold;"
+ "}"
+ ".playlist #current button::before{"
+ " content: \"→ \";"
+ " font-weight: bold;"
+ "}"
+ ".controls{"
+ " position: sticky;"
+ " width: 100%;"
+ " max-width: 800px;"
+ " margin: 0 auto;"
+ " bottom: 0;"
+ " background-color: white;"
+ " background: #3d3d3d;"
+ " color: white;"
+ " border-radius: 10px 10px 0 0;"
+ " padding: 10px;"
+ " text-align: center;"
+ " order: 2;"
+ "}"
+ ".controls p{"
+ " margin: .4rem;"
+ "}"
+ ".controls a{"
+ " color: white;"
+ "}"
+ ".controls .status{"
+ " font-size: 0.9rem;"
+ "}"
+ ".controls button{"
+ " margin: 5px;"
+ " padding: 5px 20px;"
+ "}"
+ ".mode-active{"
+ " color: #0064ff;"
+ "}"
+ "</style>"
+ "</head>"
+ "<body>";
+
+const char *foot = "<script>"
+ "function cur(e) {"
+ " if (e) {e.preventDefault()}"
+ " let cur = document.querySelector('#current');"
+ " if (cur) {cur.scrollIntoView(); window.scrollBy(0, -100);}"
+ "}"
+ "cur();"
+ "document.querySelector('.controls a').addEventListener('click',cur)"
+ "</script></body></html>";
+
+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, "<section class='playlist-wrapper'>");
+ http_writes(res, "<form action=jump method=post"
+ " enctype='"FORM_URLENCODED"'>");
+ http_writes(res, "<ul class=playlist>");
+
+ 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, "<li%s>",
+ current ? " id=current" : "");
+ http_writes(res,
+ "<button type=submit name=jump value=\"");
+ http_htmlescape(res, ps.path);
+ http_writes(res, "\">");
+ http_htmlescape(res, p);
+ http_writes(res, "</button></li>");
+
+ imsg_free(&imsg);
+ }
+ }
+
+ http_writes(res, "</ul>");
+ http_writes(res, "</form>");
+ http_writes(res, "</section>");
+}
+
+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, "<section class=controls>") == -1 ||
+ http_writes(res, "<p><a href='#current'>") == -1 ||
+ http_htmlescape(res, p) == -1 ||
+ http_writes(res, "</a></p>") == -1 ||
+ http_writes(res, "<form action=ctrls method=post"
+ " enctype='"FORM_URLENCODED"'>") == -1 ||
+ http_writes(res, "<button type=submit name=ctl value=prev>"
+ ICON_PREV"</button>") == -1 ||
+ http_fmt(res, "<button type=submit name=ctl value=%s>"
+ "%s</button>", playing ? "pause" : "play",
+ playing ? ICON_PAUSE : ICON_PLAY) == -1 ||
+ http_writes(res, "<button type=submit name=ctl value=next>"
+ ICON_NEXT"</button>") == -1 ||
+ http_writes(res, "</form>") == -1 ||
+ http_writes(res, "<form action=mode method=post"
+ " enctype='"FORM_URLENCODED"'>") == -1 ||
+ http_fmt(res, "<button%s type=submit name=mode value=all>"
+ ICON_REPEAT_ALL"</button>", ac) == -1 ||
+ http_fmt(res, "<button%s type=submit name=mode value=one>"
+ ICON_REPEAT_ONE"</button>", oc) == -1 ||
+ http_writes(res, "</form>") == -1 ||
+ http_writes(res, "</section>") == -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, "<main>") == -1)
+ return;
+
+ if (http_writes(res, "<section class=searchbox>"
+ "<input type=search name=filter aria-label='Filter playlist'"
+ " placeholder='Filter playlist' id=search />"
+ "</section>") == -1)
+ return;
+
+ render_controls(res);
+ render_playlist(res);
+
+ if (http_writes(res, "</main>") == -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);
+ }
+ }
+}