From 4741fe195229424025f6586e201478923b21ad2b Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Fri, 22 Mar 2019 02:51:33 +0300 Subject: [PATCH] http static server --- README.md | 3 - README.ru.md | 3 - src/http/mime.c | 73 ++++++++++++++++++ src/http/mime.h | 26 +++++++ src/http/path.c | 183 ++++++++++++++++++++++++++++++++++++++++++++++ src/http/path.h | 26 +++++++ src/http/server.c | 37 +++++++++- src/http/static.c | 88 ++++++++++++++++++++++ src/http/static.h | 26 +++++++ src/main.c | 4 +- 10 files changed, 460 insertions(+), 9 deletions(-) create mode 100644 src/http/mime.c create mode 100644 src/http/mime.h create mode 100644 src/http/path.c create mode 100644 src/http/path.h create mode 100644 src/http/static.c create mode 100644 src/http/static.h diff --git a/README.md b/README.md index 5ae5ccb..6dfdc85 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,12 @@ | Streaming via UNIX domain socket | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No | | Debug logs without recompiling,
performance statistics log,
access to HTTP broadcast parameters | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No | | Access to webcam controls (focus, servos)
and settings such as brightness via HTTP | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | -| Option to serve files
with a built-in HTTP server | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No 3 | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | Footnotes: * ```1``` Long before µStreamer, I made a [patch](https://github.com/jacksonliam/mjpg-streamer/pull/164) to add DV-timings support to mjpg-streamer and to keep it from hanging up no device disconnection. Alas, the patch is far from perfect and I can't guarantee it will work every time - mjpg-streamer's source code is very complicated and its structure is hard to understand. With this in mind, along with needing multithreading and JPEG hardware acceleration in the future, I decided to make my own stream server from scratch instead of supporting legacy code. * ```2``` This feature allows to cut down outgoing traffic several-fold when broadcasting HDMI, but it increases CPU usage a little bit. The idea is that HDMI is a fully digital interface and each captured frame can be identical to the previous one byte-wise. There's no need to broadcast the same image over the net several times a second. With the `--drop-same-frames=20` option enabled, µStreamer will drop all the matching frames (with a limit of 20 in a row). Each new frame is matched with the previous one first by length, then using ```memcmp()```. - * ```3``` ...and there'll never be. µStreamer is designed UNIX-way, so if you need a small website with your broadcast, install NGINX. - ----- # TL;DR If you're going to live-stream from your backyard webcam and need to control it, use mjpg-streamer. If you need a high-quality image with high FPS - µStreamer for the win. diff --git a/README.ru.md b/README.ru.md index f2f119d..99c9c6f 100644 --- a/README.ru.md +++ b/README.ru.md @@ -16,15 +16,12 @@ | Стрим через UNIX domain socket | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет | | Дебаг-логи без перекомпиляции,
логгирование статистики производительности,
возможность получения параметров
трансляции по HTTP | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет | | Поддержка контролов веб-камер (фокус,
движение сервами) и всяких настроек,
типа яркости, через HTTP | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | -| Возможность сервить файлы встроенным
HTTP-сервером | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет 3 | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | Сносочки: * ```1``` Еще до написания µStreamer, я запилил [патч](https://github.com/jacksonliam/mjpg-streamer/pull/164), добавляющий в mjpg-streamer поддержку DV-таймингов и предотвращающий его зависание при отключении устройства. Однако патч, увы, далек от совершенства и я не гарантирую его стопроцентную работоспособность, поскольку код mjpg-streamer чрезвычайно запутан и очень плохо структурирован. Учитывая это, а также то, что в дальнейшем мне потребовались многопоточность и аппаратное кодирование JPEG, было принято решение написать свой стрим-сервер с нуля, чтобы не тратить силы на поддержку лишнего легаси. * ```2``` Это фича позволяет в несколько раз снизить объем исходящего трафика при трансляции HDMI, однако немного увеличивает загрузку процессора. Суть в том, что HDMI - полностью цифровой интерфейс, и новый захваченный фрейм может быть идентичен предыдущему в точности до байта. В этом случае нет нужды передавать одну и ту же картинку по сети несколько раз в секунду. При использовании опции `--drop-same-frames=20`, µStreamer будет дропать все одинаковые фреймы, но не более 20 подряд. Новый фрейм сравнивается с предыдущим сначала по длине, а затем помощью ```memcmp()```. - * ```3``` ... и не будет. µStreamer придерживается концепции UNIX-way, так что если вам нужно нарисовать маленький сайтик со встроенной трансляцией - просто поставьте NGINX. - ----- # TL;DR Если вам нужно вещать стрим с уличной камеры и управлять ее параметрами - возьмите mjpg-streamer. Если же вам нужно очень качественное изображение с высоким FPS - µStreamer ваш бро. diff --git a/src/http/mime.c b/src/http/mime.c new file mode 100644 index 0000000..aaa9dd0 --- /dev/null +++ b/src/http/mime.c @@ -0,0 +1,73 @@ +/***************************************************************************** +# # +# uStreamer - Lightweight and fast MJPG-HTTP streamer. # +# # +# Copyright (C) 2018 Maxim Devaev # +# # +# 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 "../tools.h" + +#include "mime.h" + + +static const struct { + const char *ext; + const char *mime; +} _MIME_TYPES[] = { + {"html", "text/html"}, + {"htm", "text/html"}, + {"css", "text/css"}, + {"js", "text/javascript"}, + {"txt", "text/plain"}, + {"jpg", "image/jpeg"}, + {"jpeg", "image/jpeg"}, + {"png", "image/png"}, + {"gif", "image/gif"}, + {"ico", "image/x-icon"}, + {"bmp", "image/bmp"}, + {"svg", "image/svg+xml"}, + {"swf", "application/x-shockwave-flash"}, + {"cab", "application/x-shockwave-flash"}, + {"jar", "application/java-archive"}, + {"json", "application/json"}, +}; + + +const char *guess_mime_type(const char *path) { + char *dot; + char *ext; + + dot = strchr(path, '.'); + if (dot == NULL || strchr(dot, '/') != NULL) { + goto misc; + } + + ext = dot + 1; + for (unsigned index = 0; index < ARRAY_LEN(_MIME_TYPES); ++index) { + if (!evutil_ascii_strcasecmp(ext, _MIME_TYPES[index].ext)) { + return _MIME_TYPES[index].mime; + } + } + + misc: + return "application/misc"; +} diff --git a/src/http/mime.h b/src/http/mime.h new file mode 100644 index 0000000..c06c9b4 --- /dev/null +++ b/src/http/mime.h @@ -0,0 +1,26 @@ +/***************************************************************************** +# # +# uStreamer - Lightweight and fast MJPG-HTTP streamer. # +# # +# Copyright (C) 2018 Maxim Devaev # +# # +# 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 . # +# # +*****************************************************************************/ + + +#pragma once + + +const char *guess_mime_type(const char *str); diff --git a/src/http/path.c b/src/http/path.c new file mode 100644 index 0000000..257ddaf --- /dev/null +++ b/src/http/path.c @@ -0,0 +1,183 @@ +/***************************************************************************** +# # +# uStreamer - Lightweight and fast MJPG-HTTP streamer. # +# # +# Copyright (C) 2018 Maxim Devaev # +# # +# 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 +#include + +#include "../tools.h" + +#include "path.h" + + +char *simplify_request_path(const char *str) { + // Based on Lighttpd sources: + // - https://github.com/lighttpd/lighttpd1.4/blob/b31e7840d5403bc640579135b7004793b9ccd6c0/src/buffer.c#L840 + // - https://github.com/lighttpd/lighttpd1.4/blob/77c01f981725512653c01cde5ca74c11633dfec4/src/t/test_buffer.c + + char ch; // Current character + char pre1; // The one before + char pre2; // The one before that + char *simplified; + char *start; + char *out; + char *slash; + + A_CALLOC(simplified, strlen(str) + 1); + + if (str[0] == '\0') { + simplified[0] = '\0'; + return simplified; + } + + start = simplified; + out = simplified; + slash = simplified; + + // Skip leading spaces + for (; *str == ' '; ++str); + + if (*str == '.') { + if (str[1] == '/' || str[1] == '\0') { + ++str; + } else if (str[1] == '.' && (str[2] == '/' || str[2] == '\0')) { + str += 2; + } + } + + pre1 = '\0'; + ch = *(str++); + + while (ch != '\0') { + pre2 = pre1; + pre1 = ch; + + // Possibly: out == str - need to read first + ch = *str; + *out = pre1; + + out++; + str++; + // (out <= str) still true; also now (slash < out) + + if (ch == '/' || ch == '\0') { + size_t toklen = out - slash; + + if (toklen == 3 && pre2 == '.' && pre1 == '.' && *slash == '/') { + // "/../" or ("/.." at end of string) + out = slash; + // If there is something before "/..", there is at least one + // component, which needs to be removed + if (out > start) { + --out; + for (; out > start && *out != '/'; --out); + } + + // Don't kill trailing '/' at end of path + if (ch == '\0') { + ++out; + } + // slash < out before, so out_new <= slash + 1 <= out_before <= str + } else if (toklen == 1 || (pre2 == '/' && pre1 == '.')) { + // "//" or "/./" or (("/" or "/.") at end of string) + out = slash; + // Don't kill trailing '/' at end of path + if (ch == '\0') { + ++out; + } + // Slash < out before, so out_new <= slash + 1 <= out_before <= str + } + + slash = out; + } + } + + *out = '\0'; + return simplified; +} + +#if 0 + +int test_simplify_request_path(const char *sample, const char *expected) { + char *result = simplify_request_path(sample); + int retval = -!!strcmp(result, expected); + + printf("Testing '%s' -> '%s' ... ", sample, expected); + if (retval == 0) { + printf("ok\n"); + } else { + printf("FAILED; got '%s'\n", result); + } + free(result); + return retval; +} + +int main(void) { + int retval = 0; + +# define TEST_SIMPLIFY_REQUEST_PATH(_sample, _expected) { \ + retval += test_simplify_request_path(_sample, _expected); \ + } + + TEST_SIMPLIFY_REQUEST_PATH("", ""); + TEST_SIMPLIFY_REQUEST_PATH(" ", ""); + TEST_SIMPLIFY_REQUEST_PATH("/", "/"); + TEST_SIMPLIFY_REQUEST_PATH("//", "/"); + TEST_SIMPLIFY_REQUEST_PATH("abc", "abc"); + TEST_SIMPLIFY_REQUEST_PATH("abc//", "abc/"); + TEST_SIMPLIFY_REQUEST_PATH("abc/./xyz", "abc/xyz"); + TEST_SIMPLIFY_REQUEST_PATH("abc/.//xyz", "abc/xyz"); + TEST_SIMPLIFY_REQUEST_PATH("abc/../xyz", "/xyz"); + TEST_SIMPLIFY_REQUEST_PATH("/abc/./xyz", "/abc/xyz"); + TEST_SIMPLIFY_REQUEST_PATH("/abc//./xyz", "/abc/xyz"); + TEST_SIMPLIFY_REQUEST_PATH("/abc/../xyz", "/xyz"); + TEST_SIMPLIFY_REQUEST_PATH("abc/../xyz/.", "/xyz/"); + TEST_SIMPLIFY_REQUEST_PATH("/abc/../xyz/.", "/xyz/"); + TEST_SIMPLIFY_REQUEST_PATH("abc/./xyz/..", "abc/"); + TEST_SIMPLIFY_REQUEST_PATH("/abc/./xyz/..", "/abc/"); + TEST_SIMPLIFY_REQUEST_PATH(".", ""); + TEST_SIMPLIFY_REQUEST_PATH("..", ""); + TEST_SIMPLIFY_REQUEST_PATH("...", "..."); + TEST_SIMPLIFY_REQUEST_PATH("....", "...."); + TEST_SIMPLIFY_REQUEST_PATH(".../", ".../"); + TEST_SIMPLIFY_REQUEST_PATH("./xyz/..", "/"); + TEST_SIMPLIFY_REQUEST_PATH(".//xyz/..", "/"); + TEST_SIMPLIFY_REQUEST_PATH("/./xyz/..", "/"); + TEST_SIMPLIFY_REQUEST_PATH(".././xyz/..", "/"); + TEST_SIMPLIFY_REQUEST_PATH("/.././xyz/..", "/"); + TEST_SIMPLIFY_REQUEST_PATH("/.././xyz/..", "/"); + TEST_SIMPLIFY_REQUEST_PATH("../../../etc/passwd", "/etc/passwd"); + TEST_SIMPLIFY_REQUEST_PATH("/../../../etc/passwd", "/etc/passwd"); + TEST_SIMPLIFY_REQUEST_PATH(" ../../../etc/passwd", "/etc/passwd"); + TEST_SIMPLIFY_REQUEST_PATH(" /../../../etc/passwd", "/etc/passwd"); + TEST_SIMPLIFY_REQUEST_PATH(" /foo/bar/../../../etc/passwd", "/etc/passwd"); + +# undef TEST_SIMPLIFY_REQUEST_PATH + + if (retval < 0) { + printf("===== TEST FAILED =====\n"); + } + return retval; +} + +#endif diff --git a/src/http/path.h b/src/http/path.h new file mode 100644 index 0000000..29dc4c2 --- /dev/null +++ b/src/http/path.h @@ -0,0 +1,26 @@ +/***************************************************************************** +# # +# uStreamer - Lightweight and fast MJPG-HTTP streamer. # +# # +# Copyright (C) 2018 Maxim Devaev # +# # +# 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 . # +# # +*****************************************************************************/ + + +#pragma once + + +char *simplify_request_path(const char *str); diff --git a/src/http/server.c b/src/http/server.c index 1d12e72..d3cbea2 100644 --- a/src/http/server.c +++ b/src/http/server.c @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -52,6 +53,8 @@ #include "../stream.h" #include "base64.h" +#include "mime.h" +#include "static.h" #include "server.h" #include "data/index_html.h" @@ -325,6 +328,9 @@ static void _http_callback_static(struct evhttp_request *request, void *v_server struct evhttp_uri *uri = NULL; char *uri_path; char *decoded_path = NULL; + char *static_path = NULL; + int fd = -1; + struct stat st; PREPROCESS_REQUEST; @@ -340,7 +346,26 @@ static void _http_callback_static(struct evhttp_request *request, void *v_server } assert((buf = evbuffer_new())); - assert(evbuffer_add_printf(buf, "|%s|", decoded_path)); // TODO + + if ((static_path = find_static_file_path(server->static_path, decoded_path)) == NULL) { + goto not_found; + } + + if ((fd = open(static_path, O_RDONLY)) < 0) { + LOG_PERROR("HTTP: Can't open found static file %s", static_path); + goto not_found; + } + + if (fstat(fd, &st) < 0) { + LOG_PERROR("HTTP: Can't stat() found static file %s", static_path); + goto not_found; + } + + if (evbuffer_add_file(buf, fd, 0, st.st_size) < 0) { + LOG_ERROR("HTTP: Can't serve static file %s", static_path); + goto not_found; + } + ADD_HEADER("Content-Type", guess_mime_type(static_path)); evhttp_send_reply(request, HTTP_OK, "OK", buf); goto cleanup; @@ -348,7 +373,17 @@ static void _http_callback_static(struct evhttp_request *request, void *v_server evhttp_send_error(request, HTTP_BADREQUEST, NULL); goto cleanup; + not_found: + evhttp_send_error(request, HTTP_NOTFOUND, NULL); + goto cleanup; + cleanup: + if (fd >= 0) { + close(fd); + } + if (static_path) { + free(static_path); + } if (buf) { evbuffer_free(buf); } diff --git a/src/http/static.c b/src/http/static.c new file mode 100644 index 0000000..4da6007 --- /dev/null +++ b/src/http/static.c @@ -0,0 +1,88 @@ +/***************************************************************************** +# # +# uStreamer - Lightweight and fast MJPG-HTTP streamer. # +# # +# Copyright (C) 2018 Maxim Devaev # +# # +# 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 +#include + +#include +#include + +#include "../tools.h" +#include "../logging.h" + +#include "path.h" +#include "static.h" + + +char *find_static_file_path(const char *root_path, const char *request_path) { + char *simplified_path; + char *path = NULL; + struct stat st; + + simplified_path = simplify_request_path(request_path); + if (simplified_path[0] == '\0') { + goto error; + } + + A_CALLOC(path, strlen(root_path) + strlen(simplified_path) + 32); + sprintf(path, "%s/%s", root_path, simplified_path); + +# define LOAD_STAT { \ + if (lstat(path, &st) < 0) { \ + /* LOG_PERROR("Can't stat() file %s", path); */ \ + goto error; \ + } \ + } + + LOAD_STAT; + if (S_ISDIR(st.st_mode)) { + strcat(path, "/index.html"); + LOAD_STAT; + } + +# undef LOAD_STAT + + if (!S_ISREG(st.st_mode)) { + // LOG_ERROR("Not a regulary file: %s", path); + goto error; + } + + if (access(path, R_OK) < 0) { + // LOG_PERROR("Can't access() R_OK file %s", path); + goto error; + } + + goto ok; + + error: + if (path) { + free(path); + } + path = NULL; + + ok: + free(simplified_path); + + return path; +} diff --git a/src/http/static.h b/src/http/static.h new file mode 100644 index 0000000..e493f36 --- /dev/null +++ b/src/http/static.h @@ -0,0 +1,26 @@ +/***************************************************************************** +# # +# uStreamer - Lightweight and fast MJPG-HTTP streamer. # +# # +# Copyright (C) 2018 Maxim Devaev # +# # +# 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 . # +# # +*****************************************************************************/ + + +#pragma once + + +char *find_static_file_path(const char *root_path, const char *request_path); diff --git a/src/main.c b/src/main.c index 97823b0..6ef453e 100644 --- a/src/main.c +++ b/src/main.c @@ -164,8 +164,8 @@ static void _help(struct device_t *dev, struct encoder_t *encoder, struct http_s printf(" -o|--unix-mode ────── Set UNIX socket file permissions (like 777). Default: disabled\n\n"); printf(" --user ────────────── HTTP basic auth user. Default: disabled\n\n"); printf(" --passwd ───────────── HTTP basic auth passwd. Default: empty\n\n"); - printf(" --static ───────────── Serve static files instead of embedded root index page.\n"); - printf(" Default: disabled.\n\n"); + printf(" --static ───────────── Path to dir with static files instead of embedded root index page.\n"); + printf(" Symlinks are not supported for security reasons. Default: disabled.\n\n"); printf(" -e|--drop-same-frames ── Don't send same frames to clients, but no more than specified number.\n"); printf(" It can significantly reduce the outgoing traffic, but will increase\n"); printf(" the CPU loading. Don't use this option with analog signal sources\n");