Changeset View
Changeset View
Standalone View
Standalone View
usr.sbin/virtual_oss/virtual_oss/httpd.c
- This file was added.
| /*- | |||||
| * 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 <sys/types.h> | |||||
| #include <sys/queue.h> | |||||
| #include <sys/ioctl.h> | |||||
| #include <sys/socket.h> | |||||
| #include <sys/endian.h> | |||||
| #include <sys/uio.h> | |||||
| #include <sys/soundcard.h> | |||||
| #include <stdio.h> | |||||
| #include <stdint.h> | |||||
| #include <stdbool.h> | |||||
| #include <stdlib.h> | |||||
| #include <string.h> | |||||
| #include <fcntl.h> | |||||
| #include <unistd.h> | |||||
| #include <err.h> | |||||
| #include <errno.h> | |||||
| #include <poll.h> | |||||
| #include <sysexits.h> | |||||
| #include <netdb.h> | |||||
| #include <netinet/in.h> | |||||
| #include <netinet/tcp.h> | |||||
| #include <net/if.h> | |||||
| #include <net/if_vlan_var.h> | |||||
| #include <net/bpf.h> | |||||
| #include <arpa/inet.h> | |||||
| #include <pthread.h> | |||||
| #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) { | |||||
| 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" | |||||
| "<html><head><title>Welcome to live streaming</title>" | |||||
| "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />" | |||||
| "<meta http-equiv=\"Pragma\" content=\"no-cache\" />" | |||||
| "<meta http-equiv=\"Expires\" content=\"0\" />" | |||||
| "</head>" | |||||
| "<body>" | |||||
| "<h1>Live HD stream</h1>" | |||||
| "<br>" | |||||
| "<br>" | |||||
| "<h2>Alternative 1 (recommended)</h2>" | |||||
| "<ol type=\"1\">" | |||||
| "<li>Install <a href=\"https://www.videolan.org\">VideoLanClient (VLC)</a>, from App- or Play-store free of charge</li>" | |||||
| "<li>Open VLC and select Network Stream</li>" | |||||
| "<li>Enter, copy or share this network address to VLC: <a href=\"http://%s:%s/stream.m3u\">http://%s:%s/stream.m3u</a></li>" | |||||
| "</ol>" | |||||
| "<br>" | |||||
| "<br>" | |||||
| "<h2>Alternative 2 (on your own)</h2>" | |||||
| "<br>" | |||||
| "<br>" | |||||
| "<audio id=\"audio\" controls=\"true\" src=\"stream.wav\" preload=\"none\"></audio>" | |||||
| "<br>" | |||||
| "<br>", | |||||
| pvc->profile->http.host, pvc->profile->http.port, | |||||
| pvc->profile->http.host, pvc->profile->http.port); | |||||
| if (x == pvc->profile->http.nstate) | |||||
| fprintf(io, "<h2>There are currently no free slots (%zu active). Try again later!</h2>", x); | |||||
| else | |||||
| fprintf(io, "<h2>There are %zu free slots (%zu active)</h2>", pvc->profile->http.nstate - x, x); | |||||
| fprintf(io, "</body></html>"); | |||||
| 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" | |||||
| "<html><head><title>Virtual OSS</title></head>" | |||||
| "<body>" | |||||
| "<h1>Invalid page requested! " | |||||
| "<a HREF=\"index.html\">Click here to go back</a>.</h1><br>" | |||||
| "</body>" | |||||
| "</html>"); | |||||
| 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); | |||||
| } | |||||