#include #include #include #include #include #include #include #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 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 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("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("root", ""); if (root_.empty()) root_ = config.ValueOr("path", ""); prefix_ = config.ValueOr("prefix", "/"); port_ = static_cast(std::max(0, config.ValueOr("port", 8080))); ssl_ = config.ValueOr("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 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