diff --git a/usr.sbin/virtual_oss/virtual_oss/httpd.c b/usr.sbin/virtual_oss/virtual_oss/httpd.c index c05d5839e96b..faea44f07f26 100644 --- a/usr.sbin/virtual_oss/virtual_oss/httpd.c +++ b/usr.sbin/virtual_oss/virtual_oss/httpd.c @@ -1,844 +1,844 @@ /*- * Copyright (c) 2020 Hans Petter Selasky * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "int.h" #define VOSS_HTTPD_BIND_MAX 8 #define VOSS_HTTPD_MAX_STREAM_TIME (60 * 60 * 3) /* seconds */ struct http_state { int fd; uint64_t ts; }; struct rtp_raw_packet { struct { uint32_t padding; uint8_t dhost[6]; uint8_t shost[6]; uint16_t ether_type; } __packed eth; struct { uint8_t hl_ver; uint8_t tos; uint16_t len; uint16_t ident; uint16_t offset; uint8_t ttl; uint8_t protocol; uint16_t chksum; union { uint32_t sourceip; uint16_t source16[2]; }; union { uint32_t destip; uint16_t dest16[2]; }; } __packed ip; struct { uint16_t srcport; uint16_t dstport; uint16_t len; uint16_t chksum; } __packed udp; union { uint8_t header8[12]; uint16_t header16[6]; uint32_t header32[3]; } __packed rtp; } __packed; static const char * voss_httpd_bind_rtp(vclient_t *pvc, const char *ifname, int *pfd) { const char *perr = NULL; struct vlanreq vr = {}; struct ifreq ifr = {}; int fd; fd = socket(AF_LOCAL, SOCK_DGRAM, 0); if (fd < 0) { perr = "Cannot open raw RTP socket"; goto done; } strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name)); ifr.ifr_data = (void *)&vr; if (ioctl(fd, SIOCGETVLAN, &ifr) == 0) pvc->profile->http.rtp_vlanid = vr.vlr_tag; else pvc->profile->http.rtp_vlanid = 0; close(fd); ifr.ifr_data = NULL; *pfd = fd = open("/dev/bpf", O_RDWR); if (fd < 0) { perr = "Cannot open BPF device"; goto done; } if (ioctl(fd, BIOCSETIF, &ifr) != 0) { perr = "Cannot bind BPF device to network interface"; goto done; } done: if (perr != NULL && fd > -1) close(fd); return (perr); } static uint16_t voss_ipv4_csum(const uint16_t *ptr, size_t count) { uint32_t sum = 0; while (count--) sum += *ptr++; sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); return (~sum); } static uint16_t voss_udp_csum(uint32_t sum, const uint16_t *hdr, size_t count, const uint16_t *ptr, size_t length) { while (count--) sum += *hdr++; while (length > 1) { sum += *ptr++; length -= 2; } if (length & 1) sum += *__DECONST(uint8_t *, ptr); sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); return (~sum); } static void voss_httpd_send_rtp_sub(vclient_t *pvc, int fd, void *ptr, size_t len, uint32_t ts) { struct rtp_raw_packet pkt = {}; struct iovec iov[2]; size_t total_ip; uint16_t port = atoi(pvc->profile->http.rtp_port); size_t x; /* NOTE: BPF filter will insert VLAN header for us */ memset(pkt.eth.dhost, 255, sizeof(pkt.eth.dhost)); memset(pkt.eth.shost, 1, sizeof(pkt.eth.shost)); pkt.eth.ether_type = htobe16(0x0800); total_ip = sizeof(pkt.ip) + sizeof(pkt.udp) + sizeof(pkt.rtp) + len; iov[0].iov_base = pkt.eth.dhost; iov[0].iov_len = 14 + total_ip - len; iov[1].iov_base = alloca(len); iov[1].iov_len = len; /* byte swap data - WAV files are 16-bit little endian */ for (x = 0; x != (len / 2); x++) ((uint16_t *)iov[1].iov_base)[x] = bswap16(((uint16_t *)ptr)[x]); pkt.ip.hl_ver = 0x45; pkt.ip.len = htobe16(total_ip); pkt.ip.ttl = 8; pkt.ip.protocol = 17; /* UDP */ pkt.ip.sourceip = 0x01010101U; pkt.ip.destip = htobe32((239 << 24) + (255 << 16) + (1 << 0)); pkt.ip.chksum = voss_ipv4_csum((void *)&pkt.ip, sizeof(pkt.ip) / 2); pkt.udp.srcport = htobe16(port); pkt.udp.dstport = htobe16(port); pkt.udp.len = htobe16(total_ip - sizeof(pkt.ip)); pkt.rtp.header8[0] = (2 << 6); pkt.rtp.header8[1] = ((pvc->channels == 2) ? 10 : 11) | 0x80; pkt.rtp.header16[1] = htobe16(pvc->profile->http.rtp_seqnum); pkt.rtp.header32[1] = htobe32(ts); pkt.rtp.header32[2] = htobe32(0); pkt.udp.chksum = voss_udp_csum(pkt.ip.dest16[0] + pkt.ip.dest16[1] + pkt.ip.source16[0] + pkt.ip.source16[1] + 0x1100 + pkt.udp.len, (void *)&pkt.udp, sizeof(pkt.udp) / 2 + sizeof(pkt.rtp) / 2, iov[1].iov_base, iov[1].iov_len); pvc->profile->http.rtp_seqnum++; pvc->profile->http.rtp_ts += len / (2 * pvc->channels); if (writev(fd, iov, 2) < 0) ; } static void voss_httpd_send_rtp(vclient_t *pvc, int fd, void *ptr, size_t len, uint32_t ts) { const uint32_t mod = pvc->channels * vclient_sample_bytes(pvc); const uint32_t max = 1420 - (1420 % mod); while (len >= max) { voss_httpd_send_rtp_sub(pvc, fd, ptr, max, ts); len -= max; ptr = (uint8_t *)ptr + max; } if (len != 0) voss_httpd_send_rtp_sub(pvc, fd, ptr, len, ts); } static size_t voss_httpd_usage(vclient_t *pvc) { size_t usage = 0; size_t x; for (x = 0; x < pvc->profile->http.nstate; x++) usage += (pvc->profile->http.state[x].fd != -1); return (usage); } static char * voss_httpd_read_line(FILE *io, char *linebuffer, size_t linelen) { char buffer[2]; size_t size = 0; if (fread(buffer, 1, 2, io) != 2) return (NULL); while (1) { if (buffer[0] == '\r' && buffer[1] == '\n') break; if (size == (linelen - 1)) return (NULL); linebuffer[size++] = buffer[0]; buffer[0] = buffer[1]; if (fread(buffer + 1, 1, 1, io) != 1) return (NULL); } linebuffer[size++] = 0; return (linebuffer); } static int voss_http_generate_wav_header(vclient_t *pvc, FILE *io, uintmax_t r_start, uintmax_t r_end, bool is_partial) { uint8_t buffer[256]; uint8_t *ptr; uintmax_t dummy_len; uintmax_t delta; size_t mod; size_t len; size_t buflen; ptr = buffer; mod = pvc->channels * vclient_sample_bytes(pvc); if (mod == 0 || sizeof(buffer) < (44 + mod - 1)) return (-1); /* align to next sample */ len = 44 + mod - 1; len -= len % mod; buflen = len; /* clear block */ memset(ptr, 0, len); /* fill out data header */ ptr[len - 8] = 'd'; ptr[len - 7] = 'a'; ptr[len - 6] = 't'; ptr[len - 5] = 'a'; /* magic for unspecified length */ ptr[len - 4] = 0x00; ptr[len - 3] = 0xF0; ptr[len - 2] = 0xFF; ptr[len - 1] = 0x7F; /* fill out header */ *ptr++ = 'R'; *ptr++ = 'I'; *ptr++ = 'F'; *ptr++ = 'F'; /* total chunk size - unknown */ *ptr++ = 0; *ptr++ = 0; *ptr++ = 0; *ptr++ = 0; *ptr++ = 'W'; *ptr++ = 'A'; *ptr++ = 'V'; *ptr++ = 'E'; *ptr++ = 'f'; *ptr++ = 'm'; *ptr++ = 't'; *ptr++ = ' '; /* make sure header fits in PCM block */ len -= 28; *ptr++ = len; *ptr++ = len >> 8; *ptr++ = len >> 16; *ptr++ = len >> 24; /* audioformat = PCM */ *ptr++ = 0x01; *ptr++ = 0x00; /* number of channels */ len = pvc->channels; *ptr++ = len; *ptr++ = len >> 8; /* sample rate */ len = pvc->sample_rate; *ptr++ = len; *ptr++ = len >> 8; *ptr++ = len >> 16; *ptr++ = len >> 24; /* byte rate */ len = pvc->sample_rate * pvc->channels * vclient_sample_bytes(pvc); *ptr++ = len; *ptr++ = len >> 8; *ptr++ = len >> 16; *ptr++ = len >> 24; /* block align */ len = pvc->channels * vclient_sample_bytes(pvc); *ptr++ = len; *ptr++ = len >> 8; /* bits per sample */ len = vclient_sample_bytes(pvc) * 8; *ptr++ = len; *ptr++ = len >> 8; /* check if alignment is correct */ if (r_start >= buflen && (r_start % mod) != 0) return (2); dummy_len = pvc->sample_rate * pvc->channels * vclient_sample_bytes(pvc); dummy_len *= VOSS_HTTPD_MAX_STREAM_TIME; /* fixup end */ if (r_end >= dummy_len) r_end = dummy_len - 1; delta = r_end - r_start + 1; if (is_partial) { fprintf(io, "HTTP/1.1 206 Partial Content\r\n" "Content-Type: audio/wav\r\n" "Server: virtual_oss/1.0\r\n" "Cache-Control: no-cache, no-store\r\n" "Expires: Mon, 26 Jul 1997 05:00:00 GMT\r\n" "Connection: Close\r\n" "Content-Range: bytes %ju-%ju/%ju\r\n" "Content-Length: %ju\r\n" "\r\n", r_start, r_end, dummy_len, delta); } else { fprintf(io, "HTTP/1.0 200 OK\r\n" "Content-Type: audio/wav\r\n" "Server: virtual_oss/1.0\r\n" "Cache-Control: no-cache, no-store\r\n" "Expires: Mon, 26 Jul 1997 05:00:00 GMT\r\n" "Connection: Close\r\n" "Content-Length: %ju\r\n" "\r\n", dummy_len); } /* check if we should insert a header */ if (r_start < buflen) { buflen -= r_start; if (buflen > delta) buflen = delta; /* send data */ if (fwrite(buffer + r_start, buflen, 1, io) != 1) return (-1); /* check if all data was read */ if (buflen == delta) return (1); } return (0); } static void voss_httpd_handle_connection(vclient_t *pvc, int fd, const struct sockaddr_in *sa) { char linebuffer[2048]; uintmax_t r_start = 0; uintmax_t r_end = -1ULL; bool is_partial = false; char *line; FILE *io; size_t x; int page; io = fdopen(fd, "r+"); if (io == NULL) goto done; page = -1; /* dump HTTP request header */ while (1) { line = voss_httpd_read_line(io, linebuffer, sizeof(linebuffer)); if (line == NULL) goto done; if (line[0] == 0) break; if (page < 0 && (strstr(line, "GET / ") == line || strstr(line, "GET /index.html") == line)) { page = 0; } else if (page < 0 && strstr(line, "GET /stream.wav") == line) { page = 1; } else if (page < 0 && strstr(line, "GET /stream.m3u") == line) { page = 2; } else if (strstr(line, "Range: bytes=") == line && - sscanf(line, "Range: bytes=%zu-%zu", &r_start, &r_end) >= 1) { + sscanf(line, "Range: bytes=%ju-%ju", &r_start, &r_end) >= 1) { is_partial = true; } } switch (page) { case 0: x = voss_httpd_usage(pvc); fprintf(io, "HTTP/1.0 200 OK\r\n" "Content-Type: text/html\r\n" "Server: virtual_oss/1.0\r\n" "Cache-Control: no-cache, no-store\r\n" "Expires: Mon, 26 Jul 1997 05:00:00 GMT\r\n" "\r\n" "Welcome to live streaming" "" "" "" "" "" "

Live HD stream

" "
" "
" "

Alternative 1 (recommended)

" "
    " "
  1. Install VideoLanClient (VLC), from App- or Play-store free of charge
  2. " "
  3. Open VLC and select Network Stream
  4. " "
  5. Enter, copy or share this network address to VLC: http://%s:%s/stream.m3u
  6. " "
" "
" "
" "

Alternative 2 (on your own)

" "
" "
" "" "
" "
", pvc->profile->http.host, pvc->profile->http.port, pvc->profile->http.host, pvc->profile->http.port); if (x == pvc->profile->http.nstate) fprintf(io, "

There are currently no free slots (%zu active). Try again later!

", x); else fprintf(io, "

There are %zu free slots (%zu active)

", pvc->profile->http.nstate - x, x); fprintf(io, ""); break; case 1: for (x = 0; x < pvc->profile->http.nstate; x++) { if (pvc->profile->http.state[x].fd >= 0) continue; switch (voss_http_generate_wav_header(pvc, io, r_start, r_end, is_partial)) { static const int enable = 1; case 0: fflush(io); fdclose(io, NULL); if (ioctl(fd, FIONBIO, &enable) != 0) { close(fd); return; } pvc->profile->http.state[x].ts = virtual_oss_timestamp() - 1000000000ULL; pvc->profile->http.state[x].fd = fd; return; case 1: fclose(io); return; case 2: fprintf(io, "HTTP/1.1 416 Range Not Satisfiable\r\n" "Server: virtual_oss/1.0\r\n" "\r\n"); goto done; default: goto done; } } fprintf(io, "HTTP/1.0 503 Out of Resources\r\n" "Server: virtual_oss/1.0\r\n" "\r\n"); break; case 2: fprintf(io, "HTTP/1.0 200 OK\r\n" "Content-Type: audio/mpegurl\r\n" "Server: virtual_oss/1.0\r\n" "Cache-Control: no-cache, no-store\r\n" "Expires: Mon, 26 Jul 1997 05:00:00 GMT\r\n" "\r\n"); if (sa->sin_family == AF_INET && pvc->profile->http.rtp_port != NULL) { fprintf(io, "rtp://239.255.0.1:%s\r\n", pvc->profile->http.rtp_port); } else { fprintf(io, "http://%s:%s/stream.wav\r\n", pvc->profile->http.host, pvc->profile->http.port); } break; default: fprintf(io, "HTTP/1.0 404 Not Found\r\n" "Content-Type: text/html\r\n" "Server: virtual_oss/1.0\r\n" "\r\n" "virtual_oss" "" "

Invalid page requested! " "Click here to go back.


" "" ""); break; } done: if (io != NULL) fclose(io); else close(fd); } static int voss_httpd_do_listen(vclient_t *pvc, const char *host, const char *port, struct pollfd *pfd, int num_sock, int buffer) { static const struct timeval timeout = {.tv_sec = 1}; struct addrinfo hints = {}; struct addrinfo *res; struct addrinfo *res0; int error; int flag; int s; int ns = 0; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; hints.ai_flags = AI_PASSIVE; if ((error = getaddrinfo(host, port, &hints, &res))) return (-1); res0 = res; do { if ((s = socket(res0->ai_family, res0->ai_socktype, res0->ai_protocol)) < 0) continue; flag = 1; setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &flag, (int)sizeof(flag)); setsockopt(s, SOL_SOCKET, SO_SNDBUF, &buffer, (int)sizeof(buffer)); setsockopt(s, SOL_SOCKET, SO_RCVBUF, &buffer, (int)sizeof(buffer)); setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &timeout, (int)sizeof(timeout)); setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, (int)sizeof(timeout)); if (bind(s, res0->ai_addr, res0->ai_addrlen) == 0) { if (listen(s, pvc->profile->http.nstate) == 0) { if (ns < num_sock) { pfd[ns++].fd = s; continue; } close(s); break; } } close(s); } while ((res0 = res0->ai_next) != NULL); freeaddrinfo(res); return (ns); } static size_t voss_httpd_buflimit(vclient_t *pvc) { /* don't buffer more than 250ms */ return ((pvc->sample_rate / 4) * pvc->channels * vclient_sample_bytes(pvc)); }; static void voss_httpd_server(vclient_t *pvc) { const size_t bufferlimit = voss_httpd_buflimit(pvc); const char *host = pvc->profile->http.host; const char *port = pvc->profile->http.port; struct sockaddr sa = {}; struct pollfd fds[VOSS_HTTPD_BIND_MAX] = {}; int nfd; nfd = voss_httpd_do_listen(pvc, host, port, fds, VOSS_HTTPD_BIND_MAX, bufferlimit); if (nfd < 1) { errx(EX_SOFTWARE, "Could not bind to " "'%s' and '%s'", host, port); } while (1) { struct sockaddr_in si; int ns = nfd; int c; int f; for (c = 0; c != ns; c++) { fds[c].events = (POLLIN | POLLRDNORM | POLLRDBAND | POLLPRI | POLLERR | POLLHUP | POLLNVAL); fds[c].revents = 0; } if (poll(fds, ns, -1) < 0) errx(EX_SOFTWARE, "Polling failed"); for (c = 0; c != ns; c++) { socklen_t socklen = sizeof(sa); if (fds[c].revents == 0) continue; f = accept(fds[c].fd, &sa, &socklen); if (f < 0) continue; memcpy(&si, &sa, sizeof(sa)); voss_httpd_handle_connection(pvc, f, &si); } } } static void voss_httpd_streamer(vclient_t *pvc) { const size_t bufferlimit = voss_httpd_buflimit(pvc); uint8_t *ptr; size_t len; uint64_t ts; size_t x; atomic_lock(); while (1) { if (vclient_export_read_locked(pvc) != 0) { atomic_wait(); continue; } vring_get_read(&pvc->rx_ring[1], &ptr, &len); if (len == 0) { /* try to avoid ring wraps */ vring_reset(&pvc->rx_ring[1]); atomic_wait(); continue; } atomic_unlock(); ts = virtual_oss_timestamp(); /* check if we should send RTP data, if any */ if (pvc->profile->http.rtp_fd > -1) { voss_httpd_send_rtp(pvc, pvc->profile->http.rtp_fd, ptr, len, pvc->profile->http.rtp_ts); } /* send HTTP data, if any */ for (x = 0; x < pvc->profile->http.nstate; x++) { int fd = pvc->profile->http.state[x].fd; uint64_t delta = ts - pvc->profile->http.state[x].ts; uint8_t buf[1]; int write_len; if (fd < 0) { /* do nothing */ } else if (delta >= (8ULL * 1000000000ULL)) { /* no data for 8 seconds - terminate */ pvc->profile->http.state[x].fd = -1; close(fd); } else if (read(fd, buf, sizeof(buf)) != -1 || errno != EWOULDBLOCK) { pvc->profile->http.state[x].fd = -1; close(fd); } else if (ioctl(fd, FIONWRITE, &write_len) < 0) { pvc->profile->http.state[x].fd = -1; close(fd); } else if ((ssize_t)(bufferlimit - write_len) < (ssize_t)len) { /* do nothing */ } else if (write(fd, ptr, len) != (ssize_t)len) { pvc->profile->http.state[x].fd = -1; close(fd); } else { /* update timestamp */ pvc->profile->http.state[x].ts = ts; } } atomic_lock(); vring_inc_read(&pvc->rx_ring[1], len); } } const char * voss_httpd_start(vprofile_t *pvp) { vclient_t *pvc; pthread_t td; int error; size_t x; if (pvp->http.host == NULL || pvp->http.port == NULL || pvp->http.nstate == 0) return (NULL); pvp->http.state = malloc(sizeof(pvp->http.state[0]) * pvp->http.nstate); if (pvp->http.state == NULL) return ("Could not allocate HTTP states"); for (x = 0; x != pvp->http.nstate; x++) { pvp->http.state[x].fd = -1; pvp->http.state[x].ts = 0; } pvc = vclient_alloc(); if (pvc == NULL) return ("Could not allocate client for HTTP server"); pvc->profile = pvp; if (pvp->http.rtp_ifname != NULL) { const char *perr; if (pvc->channels > 2) return ("RTP only supports 44.1kHz, 1 or 2 channels at 16-bit depth"); /* bind to UDP port */ perr = voss_httpd_bind_rtp(pvc, pvp->http.rtp_ifname, &pvp->http.rtp_fd); if (perr != NULL) return (perr); /* setup buffers */ error = vclient_setup_buffers(pvc, 0, 0, pvp->channels, AFMT_S16_LE, 44100); } else { pvp->http.rtp_fd = -1; /* setup buffers */ error = vclient_setup_buffers(pvc, 0, 0, pvp->channels, vclient_get_default_fmt(pvp, VTYPE_WAV_HDR), voss_dsp_sample_rate); } if (error != 0) { vclient_free(pvc); return ("Could not allocate buffers for HTTP server"); } /* trigger enabled */ pvc->rx_enabled = 1; pvc->type = VTYPE_OSS_DAT; atomic_lock(); TAILQ_INSERT_TAIL(&pvp->head, pvc, entry); atomic_unlock(); if (pthread_create(&td, NULL, (void *)&voss_httpd_server, pvc)) return ("Could not create HTTP daemon thread"); if (pthread_create(&td, NULL, (void *)&voss_httpd_streamer, pvc)) return ("Could not create HTTP streamer thread"); return (NULL); }