diff options
Diffstat (limited to 'src/multipart-replace-http-server.c')
-rw-r--r-- | src/multipart-replace-http-server.c | 367 |
1 files changed, 367 insertions, 0 deletions
diff --git a/src/multipart-replace-http-server.c b/src/multipart-replace-http-server.c new file mode 100644 index 0000000..726714f --- /dev/null +++ b/src/multipart-replace-http-server.c @@ -0,0 +1,367 @@ +/* Copyright 2016 Luke Shumaker */ + +#include <errno.h> +#include <error.h> +#include <fcntl.h> /* for open */ +#include <netdb.h> /* for {get,free}addrinfo() */ +#include <stdio.h> +#include <stdlib.h> /* atexit */ +#include <stdlib.h> /* for EXIT_FAILURE */ +#include <string.h> +#include <sys/socket.h> +#include <sys/stat.h> /* for open */ +#include <sys/un.h> +#include <unistd.h> + +#include "util.h" +#include "wg.h" +#include "multipart-replace.h" + +struct httpfile { + char name[256]; + struct multipart_replace_stream *stream; +}; + +size_t filec = 0; +struct httpfile *filev = NULL; + +struct reader_thread_args { + struct multipart_replace_stream *stream; + int fd; + const char *filename; + const char *boundary; +}; + + +bool running = true; +int sock; +struct wg wg; + +void stop(int sig) { + log("Caught %d", sig); + log("STOPPING=1"); + running = false; + close(sock); +} + +#define threaderror(stat, errnum, ...) do { error(0, errnum, __VA_ARGS__); stop(0); } while(0) + +void *reader_thread(void *args_anon) { + struct reader_thread_args *args = args_anon; + + char *name = alloca(strlen("read ")+strlen(args->filename)+1); + strcpy(name, "read "); + strcpy(&name[strlen("read ")], args->filename); + pthread_setname_np(pthread_self(), name); + debug("starting thread: %s", name); + + multipart_replace_reader(args->stream, args->fd, args->boundary); + sleep(5); + threaderror(EXIT_FAILURE, 0, "multipart_replace stream ended: %s", args->filename); + wg_sub(&wg, 1); + return NULL; +} + +void start_multipart_replace_reader(struct multipart_replace_stream *s, int fd, const char *filename, const char *boundary) { + struct reader_thread_args *args = xrealloc(NULL, sizeof(struct reader_thread_args)); + args->stream = s; + args->fd = fd; + args->filename = strdup(filename); + args->boundary = boundary; + pthread_t thread; + wg_add(&wg, 1); + pthread_create(&thread, NULL, reader_thread, args); +} + +void file_add(const char *filename) { + struct multipart_replace_stream *stream = xrealloc(NULL, sizeof(struct multipart_replace_stream)); + init_multipart_replace_stream(stream); + int fd = open(filename, O_RDONLY); + if (fd < 0) { + threaderror(EXIT_FAILURE, errno, "opening file: %s", filename); + return; + } + filev = xrealloc(filev, (++filec)*sizeof(*filev)); + char *shortname = strrchr(filename, '/'); + if (shortname == NULL) { + shortname = alloca(strlen(filename)+2); + shortname[0] = '/'; + strcpy(&shortname[1], filename); + } + strncpy(filev[filec-1].name, shortname, sizeof(filev[filec-1].name)); + log("added file #%zd: %s", filec-1, filev[filec-1].name); + filev[filec-1].stream = stream; + start_multipart_replace_reader(stream, fd, filev[filec-1].name, "ffserver" /* FIXME */); +} + +struct multipart_replace_stream *file_get(const char *filename) { + for (size_t i = 0; i < filec; i++) { + if (strncmp(filename, filev[i].name, sizeof(filev[i].name)) == 0) { + return filev[i].stream; + } + } + return NULL; +} + +void connection_handler(int fd) { + FILE *netstream = fdopen(fd, "r"); + char *line_buf = NULL; + size_t line_cap = 0; + ssize_t line_len = 0; + + line_len = getline(&line_buf, &line_cap, netstream); + /* expect line to be "GET /path.mjpg HTTP/1.1" */ + if (strncmp(line_buf, "GET ", 4) != 0) { + dprintf(fd, + "HTTP/1.1 405 Method Not Allowed\r\n" + "Allow: GET\r\n" + "\r\n"); + close(fd); + return; + } + char *version = &line_buf[4]; + char *path = strsep(&version, " "); + if (strcmp(version, "HTTP/1.1\r\n") != 0 && strcmp(version, "HTTP/1.0\r\n")) { + close(fd); + return; + } + path = strdupa(path); + + /* run through the rest of the headers */ + while (strcmp(line_buf, "\r\n") != 0 && line_len >= 0) + line_len = getline(&line_buf, &line_cap, netstream); + + if (strcmp(path, "/") == 0) { + log("200 %s", path); + dprintf(fd, + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html; charset=utf-8\r\n" + "\r\n" + "<!DOCTYPE html>\n" + "<html>\n" + "<head>\n" + " <title>multipart/x-mixed-replace HTTP server</title>\n" + "</head>\n" + "<body>\n" + " <ul>\n" + ); + for (size_t i = 0; i < filec; i++) + dprintf(fd, " <li><a href=\"%s\">%s</a></li>\n", filev[i].name, filev[i].name); + dprintf(fd, + " </ul>\n" + "</body>\n" + "</html>\n"); + close(fd); + return; + } + struct multipart_replace_stream *vidstream = file_get(path); + if (vidstream == NULL) { + log("404 %s", path); + dprintf(fd, + "HTTP/1.1 404 Not Found\r\n" + "\r\n"); + return; + } + + const char *boundary = "boundary" /* FIXME */; + log("200 %s", path); + dprintf(fd, + "HTTP/1.1 200 OK\r\n" + "Content-Type: multipart/x-mixed-replace;boundary=%s\r\n" + "\r\n", + boundary); + multipart_replace_writer(vidstream, fd, boundary); + close(fd); +} + +void *connection_thread(void *arg_anon) { + pthread_setname_np(pthread_self(), "connection"); + int fd = (int)(intptr_t)arg_anon; + log("Connection %d opened", fd); + connection_handler(fd); + log("Connection %d closed", fd); + wg_sub(&wg, 1); + return NULL; +} + + +/* same error codes values as -getaddrinfo(); */ +int sockstream_listen(const char *type, const char *addr) { + int sock; + if (strcmp(type, "fd") == 0) { + sock = get_fd(addr); + if (sock < 0) { + errno = -sock; + return -EAI_SYSTEM; + } + /* make sure it's a socket */ + struct stat st; + if (fstat(sock, &st) < 0) + return -EAI_SYSTEM; + if (!S_ISSOCK(st.st_mode)) { + errno = ENOTSOCK; + return -EAI_SYSTEM; + } + /* make sure the socket is a stream */ + int fd_socktype = 0; + socklen_t l = sizeof(fd_socktype); + if (getsockopt(sock, SOL_SOCKET, SO_TYPE, &fd_socktype, &l) < 0) + return -EAI_SYSTEM; + if (l != sizeof(fd_socktype)) { + errno = EINVAL; + return -EAI_SYSTEM; + } + if (fd_socktype != SOCK_STREAM) { + errno = ENOSTR; + return -EAI_SYSTEM; + } + /* make sure the socket is listening */ + int fd_listening = 0; + l = sizeof(fd_listening); + if (getsockopt(sock, SOL_SOCKET, SO_ACCEPTCONN, &fd_listening, &l) < 0) + return -EAI_SYSTEM; + if (l != sizeof(fd_listening)) { + errno = EINVAL; + return -EAI_SYSTEM; + } + if (!fd_listening) + listen(sock, SOMAXCONN); + /* return the socket */ + return sock; + } else if (strcmp(type, "unix") == 0) { + struct sockaddr_un un_addr = { 0 }; + if (strlen(addr)+1 > sizeof(un_addr.sun_path)) { + errno = ENAMETOOLONG; + return -EAI_SYSTEM; + } + un_addr.sun_family = AF_UNIX; + strcpy(un_addr.sun_path, addr); + int sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock < 0) + return -EAI_SYSTEM; + unlink(addr); + if (bind(sock, (struct sockaddr *)&un_addr, sizeof(un_addr)) < 0) + return -EAI_SYSTEM; + if (listen(sock, SOMAXCONN) < 0) + return -EAI_SYSTEM; + return sock; + } else if (strcmp(type, "tcp4") == 0 || strcmp(type, "tcp6") == 0) { + struct addrinfo *ai_addr = NULL; + struct addrinfo ai_hints = { 0 }; + + char *host = strdupa(addr); + char *col = strrchr(host, ':'); + if (col == NULL) { + errno = EINVAL; + return -EAI_SYSTEM; + } + char *port = &col[1]; + *col = '\0'; + if (host[0] == '\0') + host = NULL; + + ai_hints.ai_family = (strcmp(type, "tcp6") == 0) ? AF_INET6 : AF_INET; + ai_hints.ai_socktype = SOCK_STREAM; + ai_hints.ai_flags = AI_PASSIVE; + + int r = getaddrinfo(host, port, &ai_hints, &ai_addr); + if (r < 0) + return r; + if (r > 0) + return -r; + + int sock = socket(ai_addr->ai_family, ai_addr->ai_socktype, ai_addr->ai_protocol); + if (sock < 0) + return -EAI_SYSTEM; + + int yes = 1; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + + if (bind(sock, ai_addr->ai_addr, ai_addr->ai_addrlen) < 0) + return -EAI_SYSTEM; + + if (listen(sock, SOMAXCONN) < 0) + return -EAI_SYSTEM; + + freeaddrinfo(ai_addr); + return sock; + } else { + errno = EINVAL; + return -EAI_SYSTEM; + } +} + +void usage() { + printf("Usage: %s [-h] ADDRTYPE ADDR [FILENAME...]\n", program_invocation_name); + printf("Multiplex several multipart/x-mixed-replace streams over HTTP.\n" + "\n" + "ADDRTYPE is \"tcp4\", \"tcp6\", \"fd\", or \"unix\". For tcp4/6, ADDR\n" + "is a socket address in standard notation. For fd, ADDR is \"stdin\",\n" + "\"stdout\", \"stder\", \"systemd\", or an unsigned integer. For unix,\n" + "ADDR is a file path.\n" + "\n" + "The leading directories of the FILENAMEs are stripped; they are served\n" + "at just the base filename. As common sense should eventually suggest,\n" + "the FILENAMEs should be pipes or sockets.\n"); +} + +void cleanup() { + fflush(stderr); + sleep(5); /* work around systemd bug dropping log messages */ +} + +pthread_t main_thread; + +void progname() { + char threadname[16]; + if (pthread_self() != main_thread && pthread_getname_np(pthread_self(), threadname, sizeof(threadname)) == 0) + fprintf(stderr, "%s:%s: ", program_invocation_name, threadname); + else + fprintf(stderr, "%s: ", program_invocation_name); +} + +int main(int argc, char *argv[]) { + if (argc >=2 && strcmp(argv[1], "-h") == 0) { + usage(); + return EXIT_SUCCESS; + } + if (argc < 3) { + dup2(2, 1); + usage(); + return EXIT_FAILURE; + } + + main_thread = pthread_self(); + error_print_progname = progname; + atexit(cleanup); + + sock = sockstream_listen(argv[1], argv[2]); + if (sock < 0) { + if (sock == -EAI_SYSTEM) + error(EXIT_FAILURE, errno, "Opening socket %s %s", argv[1], argv[2]); + else + error(EXIT_FAILURE, 0, "Opening socket %s %s: %s", argv[1], argv[2], gai_strerror(-sock)); + } + + wg_init(&wg); + for (int i = 3; i < argc; i++) + file_add(argv[i]); + + signal(SIGPIPE, SIG_IGN); + signal(SIGTERM, stop); + signal(SIGQUIT, stop); + signal(SIGINT, stop); + while (running) { + int conn = accept(sock, NULL, NULL); + if (conn < 0) + continue; + pthread_t thread; + wg_add(&wg, 1); + pthread_create(&thread, NULL, connection_thread, (void*)(intptr_t)conn); + } + wg_wait(&wg); + for (size_t i = 0; i < filec; i++) + destroy_multipart_replace_stream(filev[i].stream); + free(filev); + return 0; +} |