/* multipart-replace-http-server - A server to multiplex
* mixed/x-multipart-replace streams over HTTP
*
* Copyright (C) 2016 Luke Shumaker
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include
#include
#include /* for open */
#include /* for {get,free}addrinfo() */
#include
#include /* atexit */
#include /* for EXIT_FAILURE */
#include
#include
#include
#include "util.h"
#include "wg.h"
#include "multipart-replace.h"
void stop(int sig);
int exitcode = 0;
bool running = true;
struct wg wg;
#define threaderror(stat, errnum, ...) do { \
error(0, errnum, __VA_ARGS__); \
exitcode = stat; \
stop(0); \
} while(0)
/* File management */
void start_multipart_replace_reader(struct multipart_replace_stream *s, int fd, const char *filename, const char *boundary);
struct httpfile {
char name[256];
struct multipart_replace_stream *stream;
};
size_t filec = 0;
struct httpfile *filev = NULL;
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;
}
/* Reader */
struct reader_thread_args {
struct multipart_replace_stream *stream;
int fd;
char *filename;
char *boundary;
};
void *reader_thread(void *args_anon) {
struct reader_thread_args *args = args_anon;
char name[16];
strncpy(name, args->filename, sizeof(name));
name[15] = '\0';
pthread_setname_np(pthread_self(), name);
debug("starting thread: %s", name);
int ret = multipart_replace_reader(args->stream, args->fd, args->boundary);
threaderror(ret, 0, "multipart_replace stream ended: %s", args->filename);
free(args->filename);
free(args->boundary);
free(args);
wg_sub(&wg);
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 = strdup(boundary);
pthread_t thread;
wg_add(&wg);
pthread_create(&thread, NULL, reader_thread, args);
}
/* Writer */
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");
fclose(netstream);
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")) {
fclose(netstream);
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);
free(line_buf);
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"
"\n"
"\n"
"\n"
" multipart/x-mixed-replace HTTP server\n"
"\n"
"\n"
" \n"
);
for (size_t i = 0; i < filec; i++)
dprintf(fd, " - %s
\n", filev[i].name, filev[i].name);
dprintf(fd,
"
\n"
"\n"
"\n");
fclose(netstream);
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);
fclose(netstream);
}
void *connection_thread(void *arg_anon) {
int fd = (int)(intptr_t)arg_anon;
char name[16];
snprintf(name, sizeof(name), "connection %d", fd);
pthread_setname_np(pthread_self(), name);
log("Connection %d opened", fd);
connection_handler(fd);
log("Connection %d closed", fd);
wg_sub(&wg);
return NULL;
}
/* Main program lifecycle */
pthread_t main_thread;
void cleanup(void);
int sock;
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);
}
void stop(int sig) {
if (sig != 0)
log("Caught %d", sig);
sd_notify(0, "STOPPING=1");
running = false;
close(sock);
}
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");
}
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);
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[i].stream);
}
free(filev);
return 0;
}
void cleanup() {
fflush(stderr);
if (sd_booted() > 0) sleep(5); /* work around systemd bug dropping log messages */
_exit(exitcode);
}