diff --git a/conf/config.ini b/conf/config.ini index 14b4b919..af8f071a 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -15,6 +15,8 @@ secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc snapRoot=./www/snap/ #默认截图图片,在启动FFmpeg截图后但是截图还未生成时,可以返回默认的预设图片 defaultSnap=./www/logo.png +#downloadFile http接口可访问文件的根目录,支持多个目录,不同目录通过分号(;)分隔 +downloadRoot=./www [ffmpeg] #FFmpeg可执行程序路径,支持相对路径/绝对路径 diff --git a/postman/ZLMediaKit.postman_collection.json b/postman/ZLMediaKit.postman_collection.json index b8bbd0f7..1111850f 100644 --- a/postman/ZLMediaKit.postman_collection.json +++ b/postman/ZLMediaKit.postman_collection.json @@ -2184,6 +2184,38 @@ } }, "response": [] + }, + { + "name": "下载文件(downloadFile)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ZLMediaKit_URL}}/index/api/downloadFile?file_path=/path/to/file.ext", + "host": [ + "{{ZLMediaKit_URL}}" + ], + "path": [ + "index", + "api", + "downloadFile" + ], + "query": [ + { + "key": "file_path", + "value": "/path/to/file.ext", + "description": "文件绝对路径,根据文件名生成Content-Type;该接口将触发on_http_access hook" + }, + { + "key": "save_name", + "value": "test", + "description": "浏览器下载文件后保存文件名;可选参数", + "disabled": true + } + ] + } + }, + "response": [] } ], "event": [ diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 2dec2d22..1500b2c1 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -72,12 +72,14 @@ const string kApiDebug = API_FIELD"apiDebug"; const string kSecret = API_FIELD"secret"; const string kSnapRoot = API_FIELD"snapRoot"; const string kDefaultSnap = API_FIELD"defaultSnap"; +const string kDownloadRoot = API_FIELD"downloadRoot"; static onceToken token([]() { mINI::Instance()[kApiDebug] = "1"; mINI::Instance()[kSecret] = "035c73f7-bb6b-4889-a715-d9eb2d1925cc"; mINI::Instance()[kSnapRoot] = "./www/snap/"; mINI::Instance()[kDefaultSnap] = "./www/logo.png"; + mINI::Instance()[kDownloadRoot] = "./www"; }); }//namespace API @@ -1824,6 +1826,57 @@ void installWebApi() { // sample_ms设置为0,从配置文件加载;file_repeat可以指定,如果配置文件也指定循环解复用,那么强制开启 reader->startReadMP4(0, true, allArgs["file_repeat"]); }); + + GET_CONFIG_FUNC(std::set, download_roots, API::kDownloadRoot, [](const string &str) -> std::set { + std::set ret; + auto vec = toolkit::split(str, ";"); + for (auto &item : vec) { + auto root = File::absolutePath(item, "", true); + ret.emplace(std::move(root)); + } + return ret; + }); + + api_regist("/index/api/downloadFile", [](API_ARGS_MAP_ASYNC) { + CHECK_ARGS("file_path"); + auto file_path = allArgs["file_path"]; + + if (file_path.find("..") != std::string::npos) { + invoker(401, StrCaseMap{}, "You can not access parent directory"); + return; + } + bool safe = false; + for (auto &root : download_roots) { + if (start_with(file_path, root)) { + safe = true; + break; + } + } + if (!safe) { + invoker(401, StrCaseMap{}, "You can not download files outside the root directory"); + return; + } + + // 通过on_http_access完成文件下载鉴权,请务必确认访问鉴权url参数以及访问文件路径是否合法 + HttpSession::HttpAccessPathInvoker file_invoker = [allArgs, invoker](const string &err_msg, const string &cookie_path_in, int life_second) mutable { + if (!err_msg.empty()) { + invoker(401, StrCaseMap{}, err_msg); + } else { + StrCaseMap res_header; + auto save_name = allArgs["save_name"]; + if (!save_name.empty()) { + res_header.emplace("Content-Disposition", "attachment;filename=\"" + save_name + "\""); + } + invoker.responseFile(allArgs.getParser().getHeader(), res_header, allArgs["file_path"]); + } + }; + + bool flag = NOTICE_EMIT(BroadcastHttpAccessArgs, Broadcast::kBroadcastHttpAccess, allArgs.getParser(), file_path, false, file_invoker, sender); + if (!flag) { + // 文件下载鉴权事件无人监听,不允许下载 + invoker(401, StrCaseMap {}, "None http access event listener"); + } + }); } void unInstallWebApi(){ diff --git a/src/Http/HttpFileManager.cpp b/src/Http/HttpFileManager.cpp index 09a638f1..3233d48f 100644 --- a/src/Http/HttpFileManager.cpp +++ b/src/Http/HttpFileManager.cpp @@ -725,6 +725,9 @@ void HttpResponseInvokerImp::responseFile(const StrCaseMap &requestHeader, return; } + // 尝试添加Content-Type + httpHeader.emplace("Content-Type", HttpConst::getHttpContentType(file.data())); + auto &strRange = const_cast(requestHeader)["Range"]; int code = 200; if (!strRange.empty()) {