251 lines
7.2 KiB
C++
251 lines
7.2 KiB
C++
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cctype>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <mutex>
|
|
#include <string>
|
|
|
|
#include "node.h"
|
|
#include "utils/logger.h"
|
|
|
|
#if defined(RK3588_ENABLE_ZLMEDIAKIT)
|
|
extern "C" {
|
|
#include "mk_events.h"
|
|
#include "mk_mediakit.h"
|
|
}
|
|
#endif
|
|
|
|
namespace rk3588 {
|
|
|
|
#if defined(RK3588_ENABLE_ZLMEDIAKIT)
|
|
namespace {
|
|
|
|
struct HttpServeState {
|
|
std::mutex mu;
|
|
std::string root;
|
|
std::string prefix;
|
|
uint16_t port = 0;
|
|
bool started = false;
|
|
};
|
|
|
|
HttpServeState& GState() {
|
|
static HttpServeState st;
|
|
return st;
|
|
}
|
|
|
|
static inline bool StartsWith(const std::string& s, const std::string& prefix) {
|
|
return s.size() >= prefix.size() && std::equal(prefix.begin(), prefix.end(), s.begin());
|
|
}
|
|
|
|
static inline std::string TrimLeadingSlash(std::string s) {
|
|
while (!s.empty() && s.front() == '/') s.erase(s.begin());
|
|
return s;
|
|
}
|
|
|
|
static inline bool HasPathTraversal(const std::string& rel) {
|
|
// Minimal hardening: reject obvious traversal and Windows drive/UNC paths.
|
|
if (rel.find("..") != std::string::npos) return true;
|
|
if (rel.find('\\') != std::string::npos) return true;
|
|
if (rel.find(':') != std::string::npos) return true;
|
|
return false;
|
|
}
|
|
|
|
static inline const char* ContentTypeForPath(const std::filesystem::path& p) {
|
|
auto ext = p.extension().string();
|
|
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return (char)std::tolower(c); });
|
|
if (ext == ".m3u8") return "application/vnd.apple.mpegurl";
|
|
if (ext == ".ts") return "video/mp2t";
|
|
if (ext == ".mp4") return "video/mp4";
|
|
if (ext == ".m4s") return "video/iso.segment";
|
|
if (ext == ".mpd") return "application/dash+xml";
|
|
if (ext == ".vtt") return "text/vtt";
|
|
return "application/octet-stream";
|
|
}
|
|
|
|
void API_CALL OnHttpRequest(const mk_parser parser,
|
|
const mk_http_response_invoker invoker,
|
|
int* consumed,
|
|
const mk_sock_info /*sender*/) {
|
|
if (!parser || !invoker || !consumed) return;
|
|
|
|
const char* method_c = mk_parser_get_method(parser);
|
|
const char* url_c = mk_parser_get_url(parser);
|
|
if (!method_c || !url_c) return;
|
|
|
|
const std::string method(method_c);
|
|
const std::string url(url_c);
|
|
|
|
std::string root;
|
|
std::string prefix;
|
|
{
|
|
auto& st = GState();
|
|
std::lock_guard<std::mutex> lock(st.mu);
|
|
root = st.root;
|
|
prefix = st.prefix;
|
|
}
|
|
if (root.empty()) return;
|
|
|
|
std::string rel_url = url;
|
|
if (!prefix.empty() && prefix != "/") {
|
|
if (!StartsWith(rel_url, prefix)) return;
|
|
rel_url.erase(0, prefix.size());
|
|
if (rel_url.empty()) rel_url = "/";
|
|
}
|
|
rel_url = TrimLeadingSlash(rel_url);
|
|
if (rel_url.empty()) return;
|
|
if (HasPathTraversal(rel_url)) {
|
|
*consumed = 1;
|
|
const char* hdr[] = {
|
|
"Access-Control-Allow-Origin", "*",
|
|
"Cache-Control", "no-cache",
|
|
nullptr
|
|
};
|
|
mk_http_response_invoker_do_string(invoker, 403, hdr, "Forbidden");
|
|
return;
|
|
}
|
|
|
|
std::filesystem::path full = std::filesystem::path(root) / std::filesystem::path(rel_url);
|
|
std::error_code ec;
|
|
if (std::filesystem::is_directory(full, ec)) {
|
|
full /= "index.m3u8";
|
|
}
|
|
|
|
if (ec || !std::filesystem::exists(full, ec) || ec || !std::filesystem::is_regular_file(full, ec) || ec) {
|
|
return; // fallthrough to ZLMediaKit default handlers
|
|
}
|
|
|
|
const char* ct = ContentTypeForPath(full);
|
|
if (method == "OPTIONS") {
|
|
*consumed = 1;
|
|
const char* hdr[] = {
|
|
"Access-Control-Allow-Origin", "*",
|
|
"Access-Control-Allow-Methods", "GET, OPTIONS",
|
|
"Access-Control-Allow-Headers", "Range, Origin, Accept, Content-Type",
|
|
"Access-Control-Max-Age", "86400",
|
|
"Cache-Control", "no-cache",
|
|
nullptr
|
|
};
|
|
mk_http_response_invoker_do_string(invoker, 204, hdr, "");
|
|
return;
|
|
}
|
|
|
|
if (method != "GET") return;
|
|
|
|
*consumed = 1;
|
|
const char* hdr[] = {
|
|
"Content-Type", ct,
|
|
"Access-Control-Allow-Origin", "*",
|
|
"Cache-Control", "no-cache",
|
|
nullptr
|
|
};
|
|
const std::string file_path = full.string();
|
|
mk_http_response_invoker_do_file(invoker, parser, hdr, file_path.c_str());
|
|
}
|
|
|
|
void EnsureZlmEnvOnce() {
|
|
static std::once_flag once;
|
|
std::call_once(once, [] {
|
|
mk_config cfg;
|
|
std::memset(&cfg, 0, sizeof(cfg));
|
|
cfg.thread_num = 0;
|
|
cfg.log_level = 2;
|
|
cfg.log_mask = LOG_CONSOLE;
|
|
mk_env_init(&cfg);
|
|
});
|
|
}
|
|
|
|
void EnsureHttpEventsOnce() {
|
|
static std::once_flag once;
|
|
std::call_once(once, [] {
|
|
mk_events ev;
|
|
std::memset(&ev, 0, sizeof(ev));
|
|
ev.on_mk_http_request = &OnHttpRequest;
|
|
mk_events_listen(&ev);
|
|
});
|
|
}
|
|
|
|
uint16_t StartHttpServerOnce(uint16_t port, bool ssl) {
|
|
auto& st = GState();
|
|
std::lock_guard<std::mutex> lock(st.mu);
|
|
if (st.started) return st.port;
|
|
auto actual = mk_http_server_start(port, ssl ? 1 : 0);
|
|
if (actual == 0) return 0;
|
|
st.started = true;
|
|
st.port = actual;
|
|
return actual;
|
|
}
|
|
|
|
} // namespace
|
|
#endif
|
|
|
|
class ZlmHttpNode final : public INode {
|
|
public:
|
|
std::string Id() const override { return id_; }
|
|
std::string Type() const override { return "zlm_http"; }
|
|
|
|
bool Init(const SimpleJson& config, const NodeContext& /*ctx*/) override {
|
|
id_ = config.ValueOr<std::string>("id", "zlm_http");
|
|
|
|
#if !defined(RK3588_ENABLE_ZLMEDIAKIT)
|
|
(void)config;
|
|
LogError("[zlm_http] RK3588_ENABLE_ZLMEDIAKIT is OFF at build time");
|
|
return false;
|
|
#else
|
|
root_ = config.ValueOr<std::string>("root", "");
|
|
if (root_.empty()) root_ = config.ValueOr<std::string>("path", "");
|
|
prefix_ = config.ValueOr<std::string>("prefix", "/");
|
|
port_ = static_cast<uint16_t>(std::max(0, config.ValueOr<int>("port", 8080)));
|
|
ssl_ = config.ValueOr<bool>("ssl", false);
|
|
|
|
if (root_.empty()) {
|
|
LogError("[zlm_http] missing config: root (or path)");
|
|
return false;
|
|
}
|
|
if (prefix_.empty()) prefix_ = "/";
|
|
if (prefix_.front() != '/') prefix_ = "/" + prefix_;
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
bool Start() override {
|
|
#if !defined(RK3588_ENABLE_ZLMEDIAKIT)
|
|
return false;
|
|
#else
|
|
EnsureZlmEnvOnce();
|
|
EnsureHttpEventsOnce();
|
|
|
|
{
|
|
auto& st = GState();
|
|
std::lock_guard<std::mutex> lock(st.mu);
|
|
st.root = root_;
|
|
st.prefix = prefix_;
|
|
}
|
|
|
|
auto actual = StartHttpServerOnce(port_, ssl_);
|
|
if (actual == 0) {
|
|
LogError("[zlm_http] failed to start HTTP server on port " + std::to_string(port_));
|
|
return false;
|
|
}
|
|
LogInfo("[zlm_http] http file server started, port=" + std::to_string(actual) +
|
|
" root=" + root_ + " prefix=" + prefix_);
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
void Stop() override {
|
|
// ZLMediaKit mk_api doesn't expose per-server stop; keep it running for process lifetime.
|
|
}
|
|
|
|
private:
|
|
std::string id_;
|
|
std::string root_;
|
|
std::string prefix_ = "/";
|
|
uint16_t port_ = 8080;
|
|
bool ssl_ = false;
|
|
};
|
|
|
|
REGISTER_NODE(ZlmHttpNode, "zlm_http");
|
|
|
|
} // namespace rk3588
|