diff --git a/server/WebApi.cpp b/server/WebApi.cpp index d7724f78..8521a475 100644 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -673,34 +673,23 @@ void installWebApi() { }; API_REGIST(hook,on_http_access,{ -#if 0 - //能访问根目录以及根目录下所有文件10分钟 - val["path"] = "/"; - val["second"] = 10 * 60; -#else //在这里根据allArgs["params"](url参数)来判断该http客户端是否有权限访问该文件 if(!checkAccess(allArgs["params"])){ //无访问权限 + val["err"] = "无访问权限"; + //仅限制访问当前目录 val["path"] = ""; - //标记该客户端无权限1分钟,1分钟之内它凭此cookie访问将都无权限 - //如果客户端不支持cookie,那么可以根据url参数来追踪用户,请参考kBroadcastTrackHttpClient事件 - //如果服务器未处理kBroadcastTrackHttpClient事件,那么ZLMediaKit会根据ip和端口追踪用户 + //标记该客户端无权限1分钟 val["second"] = 60; return; } - //只能访问本文件,且只授权10分钟,访问其他文件都要另外授权 - if(allArgs["is_dir"].as()){ - //访问的是目录,该授权cookie只对该目录有效 - val["path"] = (string)allArgs["path"]; - }else{ - //访问的是文件,那么我们授予客户端访问所在目录的权限 - string dir = allArgs["path"].substr(0,allArgs["path"].rfind("/") + 1); - val["path"] = dir; - } - //该http客户端用户被授予10分钟的访问权限,该权限仅限访问特定目录 + //可以访问 + val["err"] = ""; + //只能访问当前目录 + val["path"] = ""; + //该http客户端用户被授予10分钟的访问权限,该权限仅限访问当前目录 val["second"] = 10 * 60; -#endif }); diff --git a/server/WebHook.cpp b/server/WebHook.cpp index 714a5ee5..02136f10 100644 --- a/server/WebHook.cpp +++ b/server/WebHook.cpp @@ -399,13 +399,13 @@ void installWebHook(){ NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastHttpAccess,[](BroadcastHttpAccessArgs){ if(sender.get_peer_ip() == "127.0.0.1" && args._param_strs == hook_adminparams){ //如果是本机或超级管理员访问,那么不做访问鉴权;权限有效期1个小时 - invoker("/",60 * 60); + invoker("","",60 * 60); return; } if(!hook_enable || hook_http_access.empty()){ //未开启http文件访问鉴权,那么允许访问,但是每次访问都要鉴权; //因为后续随时都可能开启鉴权(重载配置文件后可能重新开启鉴权) - invoker("/",0); + invoker("","",0); return; } @@ -423,15 +423,13 @@ void installWebHook(){ do_http_hook(hook_http_access,body, [invoker](const Value &obj,const string &err){ if(!err.empty()){ //如果接口访问失败,那么仅限本次没有访问http服务器的权限 - invoker("",0); + invoker(err,"",0); return; } - //path参数是该客户端能访问的顶端目录,该目录下的所有文件它都能访问 - //second参数规定该cookie超时时间,超过这个时间后,用户需要重新鉴权 - //如果path为空字符串,则为禁止访问任何目录 - //如果second为0,本次鉴权结果不缓存 - //如果被禁止访问文件,在cookie有效期内,假定再次访问的url参数变了,那么也能立即触发重新鉴权操作 - invoker(obj["path"].asString(),obj["second"].asInt()); + //err参数代表不能访问的原因,空则代表可以访问 + //path参数是该客户端能访问或被禁止的顶端目录,如果path为空字符串,则表述为当前目录 + //second参数规定该cookie超时时间,如果second为0,本次鉴权结果不缓存 + invoker(obj["err"].asString(),obj["path"].asString(),obj["second"].asInt()); }); }); } diff --git a/src/Http/HttpCookieManager.cpp b/src/Http/HttpCookieManager.cpp index 2b1baa75..46fbad6a 100644 --- a/src/Http/HttpCookieManager.cpp +++ b/src/Http/HttpCookieManager.cpp @@ -76,6 +76,10 @@ bool HttpServerCookie::isExpired() { return _ticker.elapsedTime() > _max_elapsed * 1000; } +std::shared_ptr > HttpServerCookie::getLock(){ + return std::make_shared >(_mtx); +} + string HttpServerCookie::cookieExpireTime() const{ char buf[64]; time_t tt = time(NULL) + _max_elapsed; @@ -105,7 +109,7 @@ void HttpCookieManager::onManager() { for (auto it_cookie = it_name->second.begin() ; it_cookie != it_name->second.end() ; ){ if(it_cookie->second->isExpired()){ //cookie过期,移除记录 - WarnL << it_cookie->second->getUid() << " cookie过期"; + DebugL << it_cookie->second->getUid() << " cookie过期:" << it_cookie->second->getCookie(); it_cookie = it_name->second.erase(it_cookie); continue; } @@ -114,7 +118,7 @@ void HttpCookieManager::onManager() { if(it_name->second.empty()){ //该类型下没有任何cooki记录,移除之 - WarnL << "该path下没有任何cooki记录:" << it_name->first; + DebugL << "该path下没有任何cooki记录:" << it_name->first; it_name = _map_cookie.erase(it_name); continue; } @@ -152,6 +156,7 @@ HttpServerCookie::Ptr HttpCookieManager::getCookie(const string &cookie_name,con } if(it_cookie->second->isExpired()){ //cookie过期 + DebugL << "cookie过期:" << it_cookie->second->getCookie(); it_name->second.erase(it_cookie); return nullptr; } diff --git a/src/Http/HttpCookieManager.h b/src/Http/HttpCookieManager.h index 3267216d..1dbfb21f 100644 --- a/src/Http/HttpCookieManager.h +++ b/src/Http/HttpCookieManager.h @@ -102,6 +102,12 @@ public: * @return */ bool isExpired(); + + /** + * 获取区域锁 + * @return + */ + std::shared_ptr > getLock(); private: string cookieExpireTime() const ; private: @@ -110,6 +116,7 @@ private: string _cookie_uuid; uint64_t _max_elapsed; Ticker _ticker; + mutex _mtx; std::weak_ptr _manager; }; diff --git a/src/Http/HttpSession.cpp b/src/Http/HttpSession.cpp index 308c750e..02f8c499 100644 --- a/src/Http/HttpSession.cpp +++ b/src/Http/HttpSession.cpp @@ -49,11 +49,10 @@ using namespace toolkit; namespace mediakit { static int kSockFlags = SOCKET_DEFAULE_FLAGS | FLAG_MORE; +static int kHlsCookieSecond = 10 * 60; static const string kCookieName = "ZL_COOKIE"; -static const string kAccessPathKey = "kAccessPathKey"; -static const string kAccessDirUnauthorized = "你没有权限访问该目录"; -static const string kAccessFileUnauthorized = "你没有权限访问该文件"; - +static const string kCookiePathKey = "kCookiePathKey"; +static const string kAccessErrKey = "kAccessErrKey"; string dateStr() { char buf[64]; @@ -346,13 +345,8 @@ static inline bool checkHls(BroadcastHttpAccessArgs){ } //访问的hls.m3u8结尾,我们转换成kBroadcastMediaPlayed事件 Broadcast::AuthInvoker mediaAuthInvoker = [invoker,path](const string &err){ - if(err.empty() ){ - //鉴权通过,允许播放一个小时 - invoker(path.substr(0,path.rfind("/") + 1),60 * 60); - }else{ - //鉴权失败,10秒内不允许播放hls - invoker("",10); - } + //cookie有效期为kHlsCookieSecond + invoker(err,"",kHlsCookieSecond); }; auto args_copy = args; @@ -360,13 +354,13 @@ static inline bool checkHls(BroadcastHttpAccessArgs){ return NoticeCenter::Instance().emitEvent(Broadcast::kBroadcastMediaPlayed,args_copy,mediaAuthInvoker,sender); } -inline void HttpSession::canAccessPath(const string &path_in,bool is_dir,const function &callback_in){ +inline void HttpSession::canAccessPath(const string &path_in,bool is_dir,const function &callback_in){ auto path = path_in; replace(const_cast(path),"//","/"); - auto callback = [callback_in,this](bool canAccess,const HttpServerCookie::Ptr &cookie){ + auto callback = [callback_in,this](const string &errMsg,const HttpServerCookie::Ptr &cookie){ try { - callback_in(canAccess,cookie); + callback_in(errMsg,cookie); }catch (SockException &ex){ if(ex){ shutdown(ex); @@ -386,48 +380,63 @@ inline void HttpSession::canAccessPath(const string &path_in,bool is_dir,const f } if(cookie){ - //找到了cookie - auto accessPath = (*cookie)[kAccessPathKey]; - if (!accessPath.empty() && path.find(accessPath) == 0) { - //用户是有权限访问该目录 - callback(true, nullptr); + //找到了cookie,对cookie上锁先 + auto lck = cookie->getLock(); + auto accessErr = (*cookie)[kAccessErrKey]; + if (accessErr.empty() && path.find((*cookie)[kCookiePathKey]) == 0) { + //用户有权限访问该目录 + callback("", nullptr); return; } + //用户无权限访问,我们看看用户的url参数变了没有 - //如果url参数变了,那么重新鉴权 - if (cookie->getUid() == _parser.Params()) { + if (_parser.Params().empty() || _parser.Params() == cookie->getUid()) { //url参数未变,那么判断无权限访问 - callback(false, nullptr); + callback(accessErr.empty() ? "无权限访问该目录" : accessErr, nullptr); return; } + //如果url参数变了,那么旧cookie失效,我们重新鉴权 + HttpCookieManager::Instance().delCookie(cookie); } //该用户从来未获取过cookie,这个时候我们广播是否允许该用户访问该http目录 weak_ptr weakSelf = dynamic_pointer_cast(shared_from_this()); - HttpAccessPathInvoker accessPathInvoker = [weakSelf, callback, uid , path] (const string &accessPath, int cookieLifeSecond) { + HttpAccessPathInvoker accessPathInvoker = [weakSelf,callback,uid,path,is_dir] (const string &errMsg,const string &cookie_path_in, int cookieLifeSecond) { + string cookie_path = cookie_path_in; + if(cookie_path.empty()){ + //如果未设置鉴权目录,那么我们采用当前目录 + if(is_dir){ + cookie_path = path; + }else{ + cookie_path = path.substr(0,path.rfind("/") + 1); + } + } + + HttpServerCookie::Ptr cookie ; + if(cookieLifeSecond) { + //本次鉴权设置了有效期,我们把鉴权结果缓存在cookie中 + cookie = HttpCookieManager::Instance().addCookie(kCookieName, uid, cookieLifeSecond); + //对cookie上锁 + auto lck = cookie->getLock(); + //记录用户能访问的路径 + (*cookie)[kCookiePathKey] = cookie_path; + //记录能否访问 + (*cookie)[kAccessErrKey] = errMsg; + } + auto strongSelf = weakSelf.lock(); if (!strongSelf) { //自己已经销毁 return; } - strongSelf->async([weakSelf, callback, accessPath, cookieLifeSecond, uid , path]() { + strongSelf->async([weakSelf,callback,cookie,errMsg]() { //切换到自己线程 auto strongSelf = weakSelf.lock(); if (!strongSelf) { //自己已经销毁 return; } - if(cookieLifeSecond){ - //我们给用户生成追踪cookie - auto cookie = HttpCookieManager::Instance().addCookie(kCookieName,uid,cookieLifeSecond); - //记录用户能访问的路径 - (*cookie)[kAccessPathKey] = accessPath; - //判断该用户是否有权限访问该目录,并且设置客户端cookie - callback(!accessPath.empty() && path.find(accessPath) == 0, cookie); - }else{ - //仅限本次访问文件 - callback(!accessPath.empty() && path.find(accessPath) == 0, nullptr); - } + callback(errMsg, cookie); }); }; @@ -439,7 +448,7 @@ inline void HttpSession::canAccessPath(const string &path_in,bool is_dir,const f bool flag = NoticeCenter::Instance().emitEvent(Broadcast::kBroadcastHttpAccess,_parser,_mediaInfo,path,is_dir,accessPathInvoker,*this); if(!flag){ //此事件无人监听,我们默认都有权限访问 - callback(true, nullptr); + callback("", nullptr); } } @@ -497,15 +506,16 @@ inline void HttpSession::Handle_Req_GET(int64_t &content_len) { } //判断是否有权限访问该目录 - canAccessPath(_parser.Url(),true,[this,bClose,strFile,strMeun](bool canAccess,const HttpServerCookie::Ptr &cookie){ - if(!canAccess){ - const_cast(strMeun) = kAccessDirUnauthorized; + auto path = _parser.Url(); + canAccessPath(_parser.Url(),true,[this,bClose,strFile,strMeun,path](const string &errMsg,const HttpServerCookie::Ptr &cookie){ + if(!errMsg.empty()){ + const_cast(strMeun) = errMsg; } auto headerOut = makeHttpHeader(bClose,strMeun.size()); if(cookie){ - headerOut["Set-Cookie"] = cookie->getCookie((*cookie)[kAccessPathKey]); + headerOut["Set-Cookie"] = cookie->getCookie((*cookie)[kCookiePathKey]); } - sendResponse(canAccess ? "200 OK" : "401 Unauthorized" , headerOut, strMeun); + sendResponse(errMsg.empty() ? "200 OK" : "401 Unauthorized" , headerOut, strMeun); throw SockException(bClose ? Err_shutdown : Err_success,"close connection after access folder"); }); return; @@ -534,7 +544,16 @@ inline void HttpSession::Handle_Req_GET(int64_t &content_len) { auto parser = _parser; //判断是否有权限访问该文件 - canAccessPath(_parser.Url(),false,[this,parser,tFileStat,pFilePtr,bClose,strFile](bool canAccess,const HttpServerCookie::Ptr &cookie){ + canAccessPath(_parser.Url(),false,[this,parser,tFileStat,pFilePtr,bClose,strFile](const string &errMsg,const HttpServerCookie::Ptr &cookie){ + if(!errMsg.empty()){ + auto headerOut = makeHttpHeader(bClose,errMsg.size()); + if(cookie){ + headerOut["Set-Cookie"] = cookie->getCookie((*cookie)[kCookiePathKey]); + } + sendResponse("401 Unauthorized" , headerOut, errMsg); + throw SockException(bClose ? Err_shutdown : Err_success,"close connection after access file failed"); + } + //判断是不是分节下载 auto &strRange = parser["Range"]; int64_t iRangeStart = 0, iRangeEnd = 0; @@ -552,8 +571,7 @@ inline void HttpSession::Handle_Req_GET(int64_t &content_len) { pcHttpResult = "206 Partial Content"; fseek(pFilePtr.get(), iRangeStart, SEEK_SET); } - auto httpHeader = canAccess ? makeHttpHeader(bClose, iRangeEnd - iRangeStart + 1, get_mime_type(strFile.data())) - : makeHttpHeader(bClose, kAccessFileUnauthorized.size()); + auto httpHeader = makeHttpHeader(bClose, iRangeEnd - iRangeStart + 1, get_mime_type(strFile.data())); if (strRange.size() != 0) { //分节下载返回Content-Range头 httpHeader.emplace("Content-Range",StrPrinter<<"bytes " << iRangeStart << "-" << iRangeEnd << "/" << tFileStat.st_size<< endl); @@ -564,12 +582,10 @@ inline void HttpSession::Handle_Req_GET(int64_t &content_len) { httpHeader["Access-Control-Allow-Credentials"] = "true"; } - if(cookie){ - httpHeader["Set-Cookie"] = cookie->getCookie((*cookie)[kAccessPathKey]); - } //先回复HTTP头部分 - sendResponse(canAccess ? pcHttpResult : "401 Unauthorized" , httpHeader,canAccess ? "" : kAccessFileUnauthorized); - if (!canAccess || iRangeEnd - iRangeStart < 0) { + sendResponse(pcHttpResult,httpHeader,""); + + if (iRangeEnd - iRangeStart < 0) { //文件是空的! throw SockException(bClose ? Err_shutdown : Err_success,"close connection after access file"); } diff --git a/src/Http/HttpSession.h b/src/Http/HttpSession.h index ab339902..d6b4829f 100644 --- a/src/Http/HttpSession.h +++ b/src/Http/HttpSession.h @@ -52,7 +52,12 @@ public: const KeyValue &headerOut, const string &contentOut)> HttpResponseInvoker; - typedef std::function HttpAccessPathInvoker; + /** + * @param errMsg 如果为空,则代表鉴权通过,否则为错误提示 + * @param accessPath 运行或禁止访问的根目录 + * @param cookieLifeSecond 鉴权cookie有效期 + **/ + typedef std::function HttpAccessPathInvoker; HttpSession(const Socket::Ptr &pSock); virtual ~HttpSession(); @@ -125,7 +130,7 @@ private: * @param is_dir path是否为目录 * @param callback 有权限或无权限的回调 */ - inline void canAccessPath(const string &path,bool is_dir,const function &callback); + inline void canAccessPath(const string &path,bool is_dir,const function &callback); /** * 获取用户唯一识别id