diff --git a/src/Http/HttpFileManager.cpp b/src/Http/HttpFileManager.cpp new file mode 100644 index 00000000..800c0a6e --- /dev/null +++ b/src/Http/HttpFileManager.cpp @@ -0,0 +1,501 @@ +/* + * MIT License + * + * Copyright (c) 2016-2019 xiongziliang <771730766@qq.com> + * + * This file is part of ZLMediaKit(https://github.com/xiongziliang/ZLMediaKit). + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#if !defined(_WIN32) +#include +#endif //!defined(_WIN32) +#include +#include "HttpFileManager.h" +#include "Util/File.h" +#include "HttpSession.h" + +namespace mediakit { + +static int kHlsCookieSecond = 10 * 60; +static const string kCookieName = "ZL_COOKIE"; +static const string kCookiePathKey = "kCookiePathKey"; +static const string kAccessErrKey = "kAccessErrKey"; + +static const string &getMimeType(const char *name) { + const char *dot; + dot = strrchr(name, '.'); + static StrCaseMap mapType; + static onceToken token([&]() { + mapType.emplace(".html", "text/html"); + mapType.emplace(".htm", "text/html"); + mapType.emplace(".mp4", "video/mp4"); + mapType.emplace(".mkv", "video/x-matroska"); + mapType.emplace(".rmvb", "application/vnd.rn-realmedia"); + mapType.emplace(".rm", "application/vnd.rn-realmedia"); + mapType.emplace(".m3u8", "application/vnd.apple.mpegurl"); + mapType.emplace(".jpg", "image/jpeg"); + mapType.emplace(".jpeg", "image/jpeg"); + mapType.emplace(".gif", "image/gif"); + mapType.emplace(".png", "image/png"); + mapType.emplace(".ico", "image/x-icon"); + mapType.emplace(".css", "text/css"); + mapType.emplace(".js", "application/javascript"); + mapType.emplace(".au", "audio/basic"); + mapType.emplace(".wav", "audio/wav"); + mapType.emplace(".avi", "video/x-msvideo"); + mapType.emplace(".mov", "video/quicktime"); + mapType.emplace(".qt", "video/quicktime"); + mapType.emplace(".mpeg", "video/mpeg"); + mapType.emplace(".mpe", "video/mpeg"); + mapType.emplace(".vrml", "model/vrml"); + mapType.emplace(".wrl", "model/vrml"); + mapType.emplace(".midi", "audio/midi"); + mapType.emplace(".mid", "audio/midi"); + mapType.emplace(".mp3", "audio/mpeg"); + mapType.emplace(".ogg", "application/ogg"); + mapType.emplace(".pac", "application/x-ns-proxy-autoconfig"); + mapType.emplace(".flv", "video/x-flv"); + }); + static string defaultType = "text/plain"; + if (!dot) { + return defaultType; + } + auto it = mapType.find(dot); + if (it == mapType.end()) { + return defaultType; + } + return it->second; +} + +static string searchIndexFile(const string &dir){ + DIR *pDir; + dirent *pDirent; + if ((pDir = opendir(dir.data())) == NULL) { + return ""; + } + set setFile; + while ((pDirent = readdir(pDir)) != NULL) { + static set indexSet = {"index.html","index.htm","index"}; + if(indexSet.find(pDirent->d_name) != indexSet.end()){ + string ret = pDirent->d_name; + closedir(pDir); + return ret; + } + } + closedir(pDir); + return ""; +} + +static bool makeFolderMenu(const string &httpPath, const string &strFullPath, string &strRet) { + string strPathPrefix(strFullPath); + string last_dir_name; + if(strPathPrefix.back() == '/'){ + strPathPrefix.pop_back(); + }else{ + last_dir_name = split(strPathPrefix,"/").back(); + } + + if (!File::is_dir(strPathPrefix.data())) { + return false; + } + stringstream ss; + ss << "\r\n" + "\r\n" + "文件索引\r\n" + "\r\n" + "\r\n" + "

文件索引:"; + + ss << httpPath; + ss << "

\r\n"; + if (httpPath != "/") { + ss << "
  • "; + ss << "根目录"; + ss << "
  • \r\n"; + + ss << "
  • "; + ss << "上级目录"; + ss << "
  • \r\n"; + } + + DIR *pDir; + dirent *pDirent; + if ((pDir = opendir(strPathPrefix.data())) == NULL) { + return false; + } + set setFile; + while ((pDirent = readdir(pDir)) != NULL) { + if (File::is_special_dir(pDirent->d_name)) { + continue; + } + if(pDirent->d_name[0] == '.'){ + continue; + } + setFile.emplace(pDirent->d_name); + } + int i = 0; + for(auto &strFile :setFile ){ + string strAbsolutePath = strPathPrefix + "/" + strFile; + bool isDir = File::is_dir(strAbsolutePath.data()); + ss << "
  • " << i++ << "\t"; + ss << ""; + ss << strFile; + if (isDir) { + ss << "/
  • \r\n"; + continue; + } + //是文件 + struct stat fileData; + if (0 == stat(strAbsolutePath.data(), &fileData)) { + auto &fileSize = fileData.st_size; + if (fileSize < 1024) { + ss << " (" << fileData.st_size << "B)" << endl; + } else if (fileSize < 1024 * 1024) { + ss << fixed << setprecision(2) << " (" << fileData.st_size / 1024.0 << "KB)"; + } else if (fileSize < 1024 * 1024 * 1024) { + ss << fixed << setprecision(2) << " (" << fileData.st_size / 1024 / 1024.0 << "MB)"; + } else { + ss << fixed << setprecision(2) << " (" << fileData.st_size / 1024 / 1024 / 1024.0 << "GB)"; + } + } + ss << "\r\n"; + } + closedir(pDir); + ss << "
      \r\n"; + ss << "
    \r\n"; + ss.str().swap(strRet); + return true; +} + +//字符串是否以xx结尾 +static bool end_of(const string &str, const string &substr){ + auto pos = str.rfind(substr); + return pos != string::npos && pos == str.size() - substr.size(); +}; + +//拦截hls的播放请求 +static bool checkHls(BroadcastHttpAccessArgs){ + if(!end_of(args._streamid,("/hls.m3u8"))) { + //不是hls + return false; + } + //访问的hls.m3u8结尾,我们转换成kBroadcastMediaPlayed事件 + Broadcast::AuthInvoker mediaAuthInvoker = [invoker,path](const string &err){ + //cookie有效期为kHlsCookieSecond + invoker(err,"",kHlsCookieSecond); + }; + + auto args_copy = args; + replace(args_copy._streamid,"/hls.m3u8",""); + return NoticeCenter::Instance().emitEvent(Broadcast::kBroadcastMediaPlayed,args_copy,mediaAuthInvoker,sender); +} + + +/** + * 判断http客户端是否有权限访问文件的逻辑步骤 + * 1、根据http请求头查找cookie,找到进入步骤3 + * 2、根据http url参数查找cookie,如果还是未找到cookie则进入步骤5 + * 3、cookie标记是否有权限访问文件,如果有权限,直接返回文件 + * 4、cookie中记录的url参数是否跟本次url参数一致,如果一致直接返回客户端错误码 + * 5、触发kBroadcastHttpAccess事件 + */ +static void canAccessPath(TcpSession &sender, const Parser &parser, const MediaInfo &mediaInfo, bool is_dir, + const function &callback) { + //获取用户唯一id + auto uid = parser.Params(); + auto path = parser.Url(); + + //先根据http头中的cookie字段获取cookie + HttpServerCookie::Ptr cookie = HttpCookieManager::Instance().getCookie(kCookieName, parser.getValues()); + //如果不是从http头中找到的cookie,我们让http客户端设置下cookie + bool cookie_from_header = true; + if (!cookie && !uid.empty()) { + //客户端请求中无cookie,再根据该用户的用户id获取cookie + cookie = HttpCookieManager::Instance().getCookieByUid(kCookieName, uid); + cookie_from_header = false; + } + + if (cookie) { + //找到了cookie,对cookie上锁先 + auto lck = cookie->getLock(); + auto accessErr = (*cookie)[kAccessErrKey].get(); + auto cookiePath = (*cookie)[kCookiePathKey].get(); + if (path.find(cookiePath) == 0) { + //上次cookie是限定本目录 + if (accessErr.empty()) { + //上次鉴权成功 + callback("", cookie_from_header ? nullptr : cookie); + return; + } + //上次鉴权失败,但是如果url参数发生变更,那么也重新鉴权下 + if (parser.Params().empty() || parser.Params() == cookie->getUid()) { + //url参数未变,或者本来就没有url参数,那么判断本次请求为重复请求,无访问权限 + callback(accessErr, cookie_from_header ? nullptr : cookie); + return; + } + } + //如果url参数变了或者不是限定本目录,那么旧cookie失效,重新鉴权 + HttpCookieManager::Instance().delCookie(cookie); + } + + //该用户从来未获取过cookie,这个时候我们广播是否允许该用户访问该http目录 + HttpSession::HttpAccessPathInvoker accessPathInvoker = [callback, uid, path, is_dir](const string &errMsg, + const string &cookie_path_in, + int cookieLifeSecond) { + HttpServerCookie::Ptr cookie; + if (cookieLifeSecond) { + //本次鉴权设置了有效期,我们把鉴权结果缓存在cookie中 + string cookie_path = cookie_path_in; + if (cookie_path.empty()) { + //如果未设置鉴权目录,那么我们采用当前目录 + cookie_path = is_dir ? path : path.substr(0, path.rfind("/") + 1); + } + + cookie = HttpCookieManager::Instance().addCookie(kCookieName, uid, cookieLifeSecond); + //对cookie上锁 + auto lck = cookie->getLock(); + //记录用户能访问的路径 + (*cookie)[kCookiePathKey].set(cookie_path); + //记录能否访问 + (*cookie)[kAccessErrKey].set(errMsg); + } + callback(errMsg, cookie); + }; + + if (checkHls(parser, mediaInfo, path, is_dir, accessPathInvoker, sender)) { + //是hls的播放鉴权,拦截之 + return; + } + + //事件未被拦截,则认为是http下载请求 + bool flag = NoticeCenter::Instance().emitEvent(Broadcast::kBroadcastHttpAccess, parser, mediaInfo, path, is_dir, accessPathInvoker, sender); + if (!flag) { + //此事件无人监听,我们默认都有权限访问 + callback("", nullptr); + } +} + +/** + * 发送404 Not Found + */ +static void sendNotFound(const HttpFileManager::invoker &cb) { + GET_CONFIG(string,notFound,Http::kNotFound); + cb("404 Not Found","text/html",StrCaseMap(),std::make_shared(notFound)); +} + +/** + * 拼接文件路径 + */ +static string pathCat(const string &a, const string &b){ + if(a.back() == '/'){ + return a + b; + } + return a + '/' + b; +} + +/** + * 访问文件 + * @param sender 事件触发者 + * @param parser http请求 + * @param mediaInfo http url信息 + * @param strFile 文件绝对路径 + * @param cb 回调对象 + */ +static void accessFile(TcpSession &sender, const Parser &parser, const MediaInfo &mediaInfo, const string &strFile, const HttpFileManager::invoker &cb) { + if (!File::is_file(strFile.data())) { + sendNotFound(cb); + return; + } + //判断是否有权限访问该文件 + canAccessPath(sender, parser, mediaInfo, false, [cb, strFile, parser](const string &errMsg, const HttpServerCookie::Ptr &cookie) { + if (!errMsg.empty()) { + StrCaseMap headerOut; + if (cookie) { + headerOut["Set-Cookie"] = cookie->getCookie((*cookie)[kCookiePathKey].get()); + } + cb("401 Unauthorized", "", headerOut, std::make_shared(errMsg)); + return; + } + + StrCaseMap httpHeader; + if (cookie) { + httpHeader["Set-Cookie"] = cookie->getCookie((*cookie)[kCookiePathKey].get()); + } + HttpSession::HttpResponseInvoker invoker = [&](const string &codeOut, const StrCaseMap &headerOut, const HttpBody::Ptr &body) { + cb(codeOut.data(), getMimeType(strFile.data()), headerOut, body); + }; + invoker.responseFile(parser.getValues(), httpHeader, strFile); + }); +} + +/** + * 访问文件或文件夹 + * @param sender 事件触发者 + * @param parser http请求 + * @param cb 回调对象 + */ +void HttpFileManager::onAccessPath(TcpSession &sender, Parser &parser, const HttpFileManager::invoker &cb) { + auto fullUrl = string(HTTP_SCHEMA) + "://" + parser["Host"] + parser.FullUrl(); + MediaInfo mediaInfo(fullUrl); + + GET_CONFIG(bool, enableVhost, General::kEnableVhost); + GET_CONFIG(string, rootPath, Http::kRootPath); + auto strFile = File::absolutePath(enableVhost ? mediaInfo._vhost + parser.Url() : parser.Url(), rootPath); + + //访问的是文件夹 + if (strFile.back() == '/' || File::is_dir(strFile.data())) { + auto indexFile = searchIndexFile(strFile); + if (!indexFile.empty()) { + //发现该文件夹下有index文件 + strFile = pathCat(strFile, indexFile); + parser.setUrl(pathCat(parser.Url(), indexFile)); + accessFile(sender, parser, mediaInfo, strFile, cb); + return; + } + string strMenu; + //生成文件夹菜单索引 + if (!makeFolderMenu(parser.Url(), strFile, strMenu)) { + //文件夹不存在 + sendNotFound(cb); + return; + } + //判断是否有权限访问该目录 + canAccessPath(sender, parser, mediaInfo, true, [strMenu, cb](const string &errMsg, const HttpServerCookie::Ptr &cookie) { + if (!errMsg.empty()) { + const_cast(strMenu) = errMsg; + } + StrCaseMap headerOut; + if (cookie) { + headerOut["Set-Cookie"] = cookie->getCookie((*cookie)[kCookiePathKey].get()); + } + cb(errMsg.empty() ? "200 OK" : "401 Unauthorized", "text/html", headerOut, std::make_shared(strMenu)); + }); + return; + } + + //访问的是文件 + accessFile(sender, parser, mediaInfo, strFile, cb); +}; + + +////////////////////////////////////HttpResponseInvokerImp////////////////////////////////////// + +void HttpResponseInvokerImp::operator()(const string &codeOut, const StrCaseMap &headerOut, const HttpBody::Ptr &body) const{ + if(_lambad){ + _lambad(codeOut,headerOut,body); + } +} + +void HttpResponseInvokerImp::operator()(const string &codeOut, const StrCaseMap &headerOut, const string &body) const{ + this->operator()(codeOut,headerOut,std::make_shared(body)); +} + +HttpResponseInvokerImp::HttpResponseInvokerImp(const HttpResponseInvokerImp::HttpResponseInvokerLambda0 &lambda){ + _lambad = lambda; +} + +HttpResponseInvokerImp::HttpResponseInvokerImp(const HttpResponseInvokerImp::HttpResponseInvokerLambda1 &lambda){ + if(!lambda){ + _lambad = nullptr; + return; + } + _lambad = [lambda](const string &codeOut, const StrCaseMap &headerOut, const HttpBody::Ptr &body){ + string str; + if(body && body->remainSize()){ + str = body->readData(body->remainSize())->toString(); + } + lambda(codeOut,headerOut,str); + }; +} + +void HttpResponseInvokerImp::responseFile(const StrCaseMap &requestHeader, + const StrCaseMap &responseHeader, + const string &filePath) const { + StrCaseMap &httpHeader = const_cast(responseHeader); + std::shared_ptr fp(fopen(filePath.data(), "rb"), [](FILE *fp) { + if (fp) { + fclose(fp); + } + }); + + if (!fp) { + //打开文件失败 + GET_CONFIG(string,notFound,Http::kNotFound); + GET_CONFIG(string,charSet,Http::kCharSet); + + auto strContentType = StrPrinter << "text/html; charset=" << charSet << endl; + httpHeader["Content-Type"] = strContentType; + (*this)("404 Not Found", httpHeader, notFound); + return; + } + + auto &strRange = const_cast(requestHeader)["Range"]; + int64_t iRangeStart = 0; + int64_t iRangeEnd = 0 ; + int64_t fileSize = HttpMultiFormBody::fileSize(fp.get()); + + const char *pcHttpResult = NULL; + if (strRange.size() == 0) { + //全部下载 + pcHttpResult = "200 OK"; + iRangeEnd = fileSize - 1; + } else { + //分节下载 + pcHttpResult = "206 Partial Content"; + iRangeStart = atoll(FindField(strRange.data(), "bytes=", "-").data()); + iRangeEnd = atoll(FindField(strRange.data(), "-", "\r\n").data()); + if (iRangeEnd == 0) { + iRangeEnd = fileSize - 1; + } + //分节下载返回Content-Range头 + httpHeader.emplace("Content-Range", StrPrinter << "bytes " << iRangeStart << "-" << iRangeEnd << "/" << fileSize << endl); + } + + //回复文件 + HttpBody::Ptr fileBody = std::make_shared(fp, iRangeStart, iRangeEnd - iRangeStart + 1); + (*this)(pcHttpResult, httpHeader, fileBody); +} + +HttpResponseInvokerImp::operator bool(){ + return _lambad.operator bool(); +} + + +}//namespace mediakit \ No newline at end of file diff --git a/src/Http/HttpFileManager.h b/src/Http/HttpFileManager.h new file mode 100644 index 00000000..4421431a --- /dev/null +++ b/src/Http/HttpFileManager.h @@ -0,0 +1,80 @@ +/* + * MIT License + * + * Copyright (c) 2016-2019 xiongziliang <771730766@qq.com> + * + * This file is part of ZLMediaKit(https://github.com/xiongziliang/ZLMediaKit). + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef ZLMEDIAKIT_HTTPFILEMANAGER_H +#define ZLMEDIAKIT_HTTPFILEMANAGER_H + +#include "HttpBody.h" +#include "HttpCookie.h" +#include "Common/Parser.h" +#include "Network/TcpSession.h" +#include "Util/function_traits.h" + +namespace mediakit { + +class HttpResponseInvokerImp{ +public: + typedef std::function HttpResponseInvokerLambda0; + typedef std::function HttpResponseInvokerLambda1; + + HttpResponseInvokerImp(){} + ~HttpResponseInvokerImp(){} + template + HttpResponseInvokerImp(const C &c):HttpResponseInvokerImp(typename function_traits::stl_function_type(c)) {} + HttpResponseInvokerImp(const HttpResponseInvokerLambda0 &lambda); + HttpResponseInvokerImp(const HttpResponseInvokerLambda1 &lambda); + + void operator()(const string &codeOut, const StrCaseMap &headerOut, const HttpBody::Ptr &body) const; + void operator()(const string &codeOut, const StrCaseMap &headerOut, const string &body) const; + void responseFile(const StrCaseMap &requestHeader,const StrCaseMap &responseHeader,const string &filePath) const; + operator bool(); +private: + HttpResponseInvokerLambda0 _lambad; +}; + +/** + * 该对象用于控制http静态文件夹服务器的访问权限 + */ +class HttpFileManager { +public: + typedef function invoker; + + /** + * 访问文件或文件夹 + * @param sender 事件触发者 + * @param parser http请求 + * @param cb 回调对象 + */ + static void onAccessPath(TcpSession &sender, Parser &parser, const invoker &cb); +private: + HttpFileManager() = delete; + ~HttpFileManager() = delete; +}; + +} + + +#endif //ZLMEDIAKIT_HTTPFILEMANAGER_H diff --git a/src/Http/HttpSession.cpp b/src/Http/HttpSession.cpp index a6ccb2b9..3f01f567 100644 --- a/src/Http/HttpSession.cpp +++ b/src/Http/HttpSession.cpp @@ -35,163 +35,12 @@ #include "Common/config.h" #include "strCoding.h" #include "HttpSession.h" -#include "Util/File.h" -#include "Util/util.h" -#include "Util/TimeTicker.h" -#include "Util/onceToken.h" -#include "Util/mini.h" -#include "Util/NoticeCenter.h" #include "Util/base64.h" #include "Util/SHA1.h" -#include "Rtmp/utils.h" using namespace toolkit; namespace mediakit { -static int kHlsCookieSecond = 10 * 60; -static const string kCookieName = "ZL_COOKIE"; -static const string kCookiePathKey = "kCookiePathKey"; -static const string kAccessErrKey = "kAccessErrKey"; - -string dateStr() { - char buf[64]; - time_t tt = time(NULL); - strftime(buf, sizeof buf, "%a, %b %d %Y %H:%M:%S GMT", gmtime(&tt)); - return buf; -} - -const char *HttpSession::get_mime_type(const char *name) { - const char *dot; - dot = strrchr(name, '.'); - static HttpSession::KeyValue mapType; - static onceToken token([&]() { - mapType.emplace(".html", "text/html"); - mapType.emplace(".htm", "text/html"); - mapType.emplace(".mp4", "video/mp4"); - mapType.emplace(".mkv", "video/x-matroska"); - mapType.emplace(".rmvb", "application/vnd.rn-realmedia"); - mapType.emplace(".rm", "application/vnd.rn-realmedia"); - mapType.emplace(".m3u8", "application/vnd.apple.mpegurl"); - mapType.emplace(".jpg", "image/jpeg"); - mapType.emplace(".jpeg", "image/jpeg"); - mapType.emplace(".gif", "image/gif"); - mapType.emplace(".png", "image/png"); - mapType.emplace(".ico", "image/x-icon"); - mapType.emplace(".css", "text/css"); - mapType.emplace(".js", "application/javascript"); - mapType.emplace(".au", "audio/basic"); - mapType.emplace(".wav", "audio/wav"); - mapType.emplace(".avi", "video/x-msvideo"); - mapType.emplace(".mov", "video/quicktime"); - mapType.emplace(".qt", "video/quicktime"); - mapType.emplace(".mpeg", "video/mpeg"); - mapType.emplace(".mpe", "video/mpeg"); - mapType.emplace(".vrml", "model/vrml"); - mapType.emplace(".wrl", "model/vrml"); - mapType.emplace(".midi", "audio/midi"); - mapType.emplace(".mid", "audio/midi"); - mapType.emplace(".mp3", "audio/mpeg"); - mapType.emplace(".ogg", "application/ogg"); - mapType.emplace(".pac", "application/x-ns-proxy-autoconfig"); - mapType.emplace(".flv", "video/x-flv"); - }, nullptr); - if (!dot) { - return "text/plain"; - } - auto it = mapType.find(dot); - if (it == mapType.end()) { - return "text/plain"; - } - return it->second.data(); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -void HttpResponseInvokerImp::operator()(const string &codeOut, const StrCaseMap &headerOut, const HttpBody::Ptr &body) const{ - if(_lambad){ - _lambad(codeOut,headerOut,body); - } -} - -void HttpResponseInvokerImp::operator()(const string &codeOut, const StrCaseMap &headerOut, const string &body) const{ - this->operator()(codeOut,headerOut,std::make_shared(body)); -} - -HttpResponseInvokerImp::HttpResponseInvokerImp(const HttpResponseInvokerImp::HttpResponseInvokerLambda0 &lambda){ - _lambad = lambda; -} - -HttpResponseInvokerImp::HttpResponseInvokerImp(const HttpResponseInvokerImp::HttpResponseInvokerLambda1 &lambda){ - if(!lambda){ - _lambad = nullptr; - return; - } - _lambad = [lambda](const string &codeOut, const StrCaseMap &headerOut, const HttpBody::Ptr &body){ - string str; - if(body && body->remainSize()){ - str = body->readData(body->remainSize())->toString(); - } - lambda(codeOut,headerOut,str); - }; -} - -void HttpResponseInvokerImp::responseFile(const StrCaseMap &requestHeader, - const StrCaseMap &responseHeader, - const string &filePath) const { - StrCaseMap &httpHeader = const_cast(responseHeader); - do { - std::shared_ptr fp(fopen(filePath.data(), "rb"), [](FILE *fp) { - if (fp) { - fclose(fp); - } - }); - if (!fp) { - //打开文件失败 - break; - } - - auto &strRange = const_cast(requestHeader)["Range"]; - int64_t iRangeStart = 0; - int64_t iRangeEnd = 0 ; - int64_t fileSize = HttpMultiFormBody::fileSize(fp.get()); - - const char *pcHttpResult = NULL; - if (strRange.size() == 0) { - //全部下载 - pcHttpResult = "200 OK"; - iRangeEnd = fileSize - 1; - } else { - //分节下载 - pcHttpResult = "206 Partial Content"; - iRangeStart = atoll(FindField(strRange.data(), "bytes=", "-").data()); - iRangeEnd = atoll(FindField(strRange.data(), "-", "\r\n").data()); - if (iRangeEnd == 0) { - iRangeEnd = fileSize - 1; - } - //分节下载返回Content-Range头 - httpHeader.emplace("Content-Range", StrPrinter << "bytes " << iRangeStart << "-" << iRangeEnd << "/" << fileSize << endl); - } - - //回复文件 - HttpBody::Ptr fileBody = std::make_shared(fp, iRangeStart, iRangeEnd - iRangeStart + 1); - (*this)(pcHttpResult, httpHeader, fileBody); - return; - }while(false); - - GET_CONFIG(string,notFound,Http::kNotFound); - GET_CONFIG(string,charSet,Http::kCharSet); - - auto strContentType = StrPrinter << "text/html; charset=" << charSet << endl; - httpHeader["Content-Type"] = strContentType; - (*this)("404 Not Found", httpHeader, notFound); -} - -HttpResponseInvokerImp::operator bool(){ - return _lambad.operator bool(); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// - HttpSession::HttpSession(const Socket::Ptr &pSock) : TcpSession(pSock) { TraceP(this); GET_CONFIG(uint32_t,keep_alive_sec,Http::kKeepAliveSecond); @@ -206,19 +55,18 @@ HttpSession::~HttpSession() { int64_t HttpSession::onRecvHeader(const char *header,uint64_t len) { typedef void (HttpSession::*HttpCMDHandle)(int64_t &); - static unordered_map g_mapCmdIndex; + static unordered_map s_func_map; static onceToken token([]() { - g_mapCmdIndex.emplace("GET",&HttpSession::Handle_Req_GET); - g_mapCmdIndex.emplace("POST",&HttpSession::Handle_Req_POST); + s_func_map.emplace("GET",&HttpSession::Handle_Req_GET); + s_func_map.emplace("POST",&HttpSession::Handle_Req_POST); }, nullptr); _parser.Parse(header); urlDecode(_parser); string cmd = _parser.Method(); - auto it = g_mapCmdIndex.find(cmd); - if (it == g_mapCmdIndex.end()) { + auto it = s_func_map.find(cmd); + if (it == s_func_map.end()) { sendResponse("403 Forbidden", true); - shutdown(SockException(Err_shutdown,StrPrinter << "403 Forbidden:" << cmd)); return 0; } @@ -230,10 +78,6 @@ int64_t HttpSession::onRecvHeader(const char *header,uint64_t len) { auto &fun = it->second; try { (this->*fun)(content_len); - }catch (SockException &ex){ - if(ex){ - shutdown(ex); - } }catch (exception &ex){ shutdown(SockException(Err_shutdown,ex.what())); } @@ -324,12 +168,12 @@ bool HttpSession::checkWebSocket(){ //如果checkLiveFlvStream返回false,则代表不是websocket-flv,而是普通的websocket连接 if(!onWebSocketConnect(_parser)){ sendResponse("501 Not Implemented",true, nullptr, headerOut); - shutdown(SockException(Err_shutdown,"WebSocket server not implemented")); return true; } sendResponse("101 Switching Protocols",false, nullptr,headerOut); return true; } + //http-flv 链接格式:http://vhost-url:port/app/streamid.flv?key1=value1&key2=value2 //如果url(除去?以及后面的参数)后缀是.flv,那么表明该url是一个http-flv直播。 bool HttpSession::checkLiveFlvStream(const function &cb){ @@ -365,9 +209,6 @@ bool HttpSession::checkLiveFlvStream(const function &cb){ if(!rtmp_src){ //未找到该流 sendNotFound(bClose); - if(bClose){ - shutdown(SockException(Err_shutdown,"flv stream not found")); - } return; } //找到流了 @@ -375,7 +216,6 @@ bool HttpSession::checkLiveFlvStream(const function &cb){ bool authSuccess = err.empty(); if(!authSuccess){ sendResponse("401 Unauthorized", true, nullptr, KeyValue(), std::make_shared(err)); - shutdown(SockException(Err_shutdown,StrPrinter << "401 Unauthorized:" << err)); return ; } @@ -421,159 +261,6 @@ bool HttpSession::checkLiveFlvStream(const function &cb){ return true; } -bool makeMeun(const string &httpPath,const string &strFullPath, string &strRet) ; - -static string findIndexFile(const string &dir){ - DIR *pDir; - dirent *pDirent; - if ((pDir = opendir(dir.data())) == NULL) { - return ""; - } - set setFile; - while ((pDirent = readdir(pDir)) != NULL) { - static set indexSet = {"index.html","index.htm","index"}; - if(indexSet.find(pDirent->d_name) != indexSet.end()){ - string ret = pDirent->d_name; - closedir(pDir); - return ret; - } - } - closedir(pDir); - return ""; -} - -string HttpSession::getClientUid(){ - //如果http客户端不支持cookie,那么我们可以通过url参数来追踪用户 - //如果url参数也没有,那么只能通过ip+端口号来追踪用户 - //追踪用户的目的是为了减少http短链接情况的重复鉴权验证,通过缓存记录鉴权结果,提高性能 - string uid = _parser.Params(); - if(uid.empty()){ - uid = StrPrinter << get_peer_ip() << ":" << get_peer_port(); - } - return uid; -} - - -//字符串是否以xx结尾 -static bool end_of(const string &str, const string &substr){ - auto pos = str.rfind(substr); - return pos != string::npos && pos == str.size() - substr.size(); -}; - -//拦截hls的播放请求 -static bool checkHls(BroadcastHttpAccessArgs){ - if(!end_of(args._streamid,("/hls.m3u8"))) { - //不是hls - return false; - } - //访问的hls.m3u8结尾,我们转换成kBroadcastMediaPlayed事件 - Broadcast::AuthInvoker mediaAuthInvoker = [invoker,path](const string &err){ - //cookie有效期为kHlsCookieSecond - invoker(err,"",kHlsCookieSecond); - }; - - auto args_copy = args; - replace(args_copy._streamid,"/hls.m3u8",""); - return NoticeCenter::Instance().emitEvent(Broadcast::kBroadcastMediaPlayed,args_copy,mediaAuthInvoker,sender); -} - -void HttpSession::canAccessPath(const string &path,bool is_dir,const function &callback_in){ - auto callback = [callback_in,this](const string &errMsg,const HttpServerCookie::Ptr &cookie){ - try { - callback_in(errMsg,cookie); - }catch (SockException &ex){ - if(ex){ - shutdown(ex); - } - }catch (exception &ex){ - shutdown(SockException(Err_shutdown,ex.what())); - } - }; - - //获取用户唯一id - auto uid = getClientUid(); - //先根据http头中的cookie字段获取cookie - HttpServerCookie::Ptr cookie = HttpCookieManager::Instance().getCookie(kCookieName, _parser.getValues()); - //如果不是从http头中找到的cookie,我们让http客户端设置下cookie - bool cookie_from_header = true; - if(!cookie){ - //客户端请求中无cookie,再根据该用户的用户id获取cookie - cookie = HttpCookieManager::Instance().getCookieByUid(kCookieName, uid); - cookie_from_header = false; - } - - if(cookie){ - //找到了cookie,对cookie上锁先 - auto lck = cookie->getLock(); - auto accessErr = (*cookie)[kAccessErrKey].get(); - auto cookiePath = (*cookie)[kCookiePathKey].get(); - if(path.find(cookiePath) == 0){ - //上次cookie是限定本目录 - if(accessErr.empty()){ - //上次鉴权成功 - callback("", cookie_from_header ? nullptr : cookie); - return; - } - //上次鉴权失败,但是如果url参数发生变更,那么也重新鉴权下 - if (_parser.Params().empty() || _parser.Params() == cookie->getUid()) { - //url参数未变,或者本来就没有url参数,那么判断本次请求为重复请求,无访问权限 - callback(accessErr, cookie_from_header ? nullptr : cookie); - return; - } - } - //如果url参数变了或者不是限定本目录,那么旧cookie失效,重新鉴权 - HttpCookieManager::Instance().delCookie(cookie); - } - - //该用户从来未获取过cookie,这个时候我们广播是否允许该用户访问该http目录 - weak_ptr weakSelf = dynamic_pointer_cast(shared_from_this()); - HttpAccessPathInvoker accessPathInvoker = [weakSelf,callback,uid,path,is_dir] (const string &errMsg,const string &cookie_path_in, int cookieLifeSecond) { - HttpServerCookie::Ptr cookie ; - if(cookieLifeSecond) { - //本次鉴权设置了有效期,我们把鉴权结果缓存在cookie中 - string cookie_path = cookie_path_in; - if(cookie_path.empty()){ - //如果未设置鉴权目录,那么我们采用当前目录 - cookie_path = is_dir ? path : path.substr(0,path.rfind("/") + 1); - } - - cookie = HttpCookieManager::Instance().addCookie(kCookieName, uid, cookieLifeSecond); - //对cookie上锁 - auto lck = cookie->getLock(); - //记录用户能访问的路径 - (*cookie)[kCookiePathKey].set(cookie_path); - //记录能否访问 - (*cookie)[kAccessErrKey].set(errMsg); - } - - auto strongSelf = weakSelf.lock(); - if (!strongSelf) { - //自己已经销毁 - return; - } - strongSelf->async([weakSelf,callback,cookie,errMsg]() { - //切换到自己线程 - auto strongSelf = weakSelf.lock(); - if (!strongSelf) { - //自己已经销毁 - return; - } - callback(errMsg, cookie); - }); - }; - - if(checkHls(_parser,_mediaInfo,path,is_dir,accessPathInvoker,*this)){ - //是hls的播放鉴权,拦截之 - return; - } - - bool flag = NoticeCenter::Instance().emitEvent(Broadcast::kBroadcastHttpAccess,_parser,_mediaInfo,path,is_dir,accessPathInvoker,*this); - if(!flag){ - //此事件无人监听,我们默认都有权限访问 - callback("", nullptr); - } - -} void HttpSession::Handle_Req_GET(int64_t &content_len) { //先看看是否为WebSocket请求 @@ -597,182 +284,41 @@ void HttpSession::Handle_Req_GET(int64_t &content_len) { return; } - //事件未被拦截,则认为是http下载请求 - auto fullUrl = string(HTTP_SCHEMA) + "://" + _parser["Host"] + _parser.FullUrl(); - _mediaInfo.parse(fullUrl); - - /////////////HTTP连接是否需要被关闭//////////////// GET_CONFIG(uint32_t,reqCnt,Http::kMaxReqCount); - GET_CONFIG(bool,enableVhost,General::kEnableVhost); - GET_CONFIG(string,rootPath,Http::kRootPath); - auto strFile = File::absolutePath(enableVhost ? _mediaInfo._vhost + _parser.Url() : _parser.Url(),rootPath); bool bClose = (strcasecmp(_parser["Connection"].data(),"close") == 0) || ( ++_iReqCnt > reqCnt); - do{ - //访问的是文件夹 - if (strFile.back() == '/' || File::is_dir(strFile.data())) { - auto indexFile = findIndexFile(strFile); - if(!indexFile.empty()){ - //发现该文件夹下有index文件 - strFile = strFile + "/" + indexFile; - _parser.setUrl(_parser.Url() + "/" + indexFile); - break; - } - string strMeun; - //生成文件夹菜单索引 - if (!makeMeun(_parser.Url(),strFile,strMeun)) { - //文件夹不存在 - sendNotFound(bClose); - throw SockException(bClose ? Err_shutdown : Err_success,"close connection after send 404 not found on folder"); - } - - //判断是否有权限访问该目录 - canAccessPath(_parser.Url(),true,[this,bClose,strFile,strMeun](const string &errMsg,const HttpServerCookie::Ptr &cookie){ - if(!errMsg.empty()){ - const_cast(strMeun) = errMsg; - } - KeyValue headerOut; - if(cookie){ - headerOut["Set-Cookie"] = cookie->getCookie((*cookie)[kCookiePathKey].get()); - } - sendResponse(errMsg.empty() ? "200 OK" : "401 Unauthorized" ,bClose, "text/html", headerOut, std::make_shared(strMeun)); - throw SockException(bClose ? Err_shutdown : Err_success,"close connection after access folder"); - }); + weak_ptr weakSelf = dynamic_pointer_cast(shared_from_this()); + HttpFileManager::onAccessPath(*this, _parser, [weakSelf, bClose](const string &status_code, const string &content_type, + const StrCaseMap &responseHeader, const HttpBody::Ptr &body) { + auto strongSelf = weakSelf.lock(); + if (!strongSelf) { return; } - }while(0); - - auto parser = _parser; - //判断是否有权限访问该文件 - canAccessPath(_parser.Url(),false,[this,parser,bClose,strFile](const string &errMsg,const HttpServerCookie::Ptr &cookie){ - if(!errMsg.empty()){ - KeyValue headerOut; - if(cookie){ - headerOut["Set-Cookie"] = cookie->getCookie((*cookie)[kCookiePathKey].get()); + strongSelf->async([weakSelf, bClose, status_code, content_type, responseHeader, body]() { + auto strongSelf = weakSelf.lock(); + if (!strongSelf) { + return; } - sendResponse("401 Unauthorized" ,bClose, nullptr, headerOut, std::make_shared(errMsg)); - throw SockException(bClose ? Err_shutdown : Err_success,"close connection after access file failed"); - } - - KeyValue httpHeader; - if(cookie){ - httpHeader["Set-Cookie"] = cookie->getCookie((*cookie)[kCookiePathKey].get()); - } - - HttpResponseInvoker invoker = [this,bClose,&strFile](const string &codeOut, const KeyValue &headerOut, const HttpBody::Ptr &body){ - sendResponse(codeOut.data(), bClose, get_mime_type(strFile.data()), headerOut, body); - }; - invoker.responseFile(parser.getValues(),httpHeader,strFile); + strongSelf->sendResponse(status_code.data(), bClose, + content_type.empty() ? nullptr : content_type.data(), + responseHeader, body); + }); }); } -bool makeMeun(const string &httpPath,const string &strFullPath, string &strRet) { - string strPathPrefix(strFullPath); - string last_dir_name; - if(strPathPrefix.back() == '/'){ - strPathPrefix.pop_back(); - }else{ - last_dir_name = split(strPathPrefix,"/").back(); - } - - if (!File::is_dir(strPathPrefix.data())) { - return false; - } - stringstream ss; - ss << "\r\n" - "\r\n" - "文件索引\r\n" - "\r\n" - "\r\n" - "

    文件索引:"; - - ss << httpPath; - ss << "

    \r\n"; - if (httpPath != "/") { - ss << "
  • "; - ss << "根目录"; - ss << "
  • \r\n"; - - ss << "
  • "; - ss << "上级目录"; - ss << "
  • \r\n"; - } - - DIR *pDir; - dirent *pDirent; - if ((pDir = opendir(strPathPrefix.data())) == NULL) { - return false; - } - set setFile; - while ((pDirent = readdir(pDir)) != NULL) { - if (File::is_special_dir(pDirent->d_name)) { - continue; - } - if(pDirent->d_name[0] == '.'){ - continue; - } - setFile.emplace(pDirent->d_name); - } - int i = 0; - for(auto &strFile :setFile ){ - string strAbsolutePath = strPathPrefix + "/" + strFile; - bool isDir = File::is_dir(strAbsolutePath.data()); - ss << "
  • " << i++ << "\t"; - ss << ""; - ss << strFile; - if (isDir) { - ss << "/
  • \r\n"; - continue; - } - //是文件 - struct stat fileData; - if (0 == stat(strAbsolutePath.data(), &fileData)) { - auto &fileSize = fileData.st_size; - if (fileSize < 1024) { - ss << " (" << fileData.st_size << "B)" << endl; - } else if (fileSize < 1024 * 1024) { - ss << fixed << setprecision(2) << " (" << fileData.st_size / 1024.0 << "KB)"; - } else if (fileSize < 1024 * 1024 * 1024) { - ss << fixed << setprecision(2) << " (" << fileData.st_size / 1024 / 1024.0 << "MB)"; - } else { - ss << fixed << setprecision(2) << " (" << fileData.st_size / 1024 / 1024 / 1024.0 << "GB)"; - } - } - ss << "\r\n"; - } - closedir(pDir); - ss << "
      \r\n"; - ss << "
    \r\n"; - ss.str().swap(strRet); - return true; +static string dateStr() { + char buf[64]; + time_t tt = time(NULL); + strftime(buf, sizeof buf, "%a, %b %d %Y %H:%M:%S GMT", gmtime(&tt)); + return buf; } - void HttpSession::sendResponse(const char *pcStatus, bool bClose, const char *pcContentType, const HttpSession::KeyValue &header, const HttpBody::Ptr &body, bool set_content_len ){ - GET_CONFIG(string,charSet,Http::kCharSet); GET_CONFIG(uint32_t,keepAliveSec,Http::kKeepAliveSecond); GET_CONFIG(uint32_t,reqCnt,Http::kMaxReqCount); @@ -838,7 +384,7 @@ void HttpSession::sendResponse(const char *pcStatus, if(!size){ //没有body if(bClose){ - shutdown(SockException(Err_shutdown,"close connection after send http header completed")); + shutdown(SockException(Err_shutdown,StrPrinter << "close connection after send http header completed with status code:" << pcStatus)); } return; } @@ -847,7 +393,8 @@ void HttpSession::sendResponse(const char *pcStatus, GET_CONFIG(uint32_t,sendBufSize,Http::kSendBufSize); weak_ptr weakSelf = dynamic_pointer_cast(shared_from_this()); - auto onFlush = [body,bClose,weakSelf]() { + string status_code = pcStatus; + auto onFlush = [body,bClose,weakSelf,status_code]() { auto strongSelf = weakSelf.lock(); if(!strongSelf){ //本对象已经销毁 @@ -884,7 +431,7 @@ void HttpSession::sendResponse(const char *pcStatus, if(bClose) { //最后一次flush事件,文件也发送完毕了,可以关闭socket了 - strongSelf->shutdown(SockException(Err_shutdown,"close connection after send http body completed")); + strongSelf->shutdown(SockException(Err_shutdown, StrPrinter << "close connection after send http body completed with status code:" << status_code)); } //不再监听onFlush事件 return false; @@ -935,13 +482,6 @@ bool HttpSession::emitHttpEvent(bool doInvoke){ //本对象已经销毁 return; } - - if(codeOut.empty()){ - //回调提供的参数异常 - strongSelf->sendNotFound(bClose); - return; - } - strongSelf->sendResponse(codeOut.data(), bClose, nullptr, headerOut, body); }); }; @@ -951,10 +491,6 @@ bool HttpSession::emitHttpEvent(bool doInvoke){ if(!consumed && doInvoke){ //该事件无人消费,所以返回404 invoker("404 Not Found",KeyValue(), HttpBody::Ptr()); - if(bClose){ - //close类型,回复完毕,关闭连接 - shutdown(SockException(Err_shutdown,"404 Not Found")); - } } return consumed; } diff --git a/src/Http/HttpSession.h b/src/Http/HttpSession.h index 98f234c3..1d9ead78 100644 --- a/src/Http/HttpSession.h +++ b/src/Http/HttpSession.h @@ -27,46 +27,19 @@ #define SRC_HTTP_HTTPSESSION_H_ #include -#include "Common/config.h" -#include "Common/Parser.h" #include "Network/TcpSession.h" -#include "Network/TcpServer.h" #include "Rtmp/RtmpMediaSource.h" #include "Rtmp/FlvMuxer.h" #include "HttpRequestSplitter.h" #include "WebSocketSplitter.h" #include "HttpCookieManager.h" -#include "HttpBody.h" -#include "Util/function_traits.h" +#include "HttpFileManager.h" using namespace std; using namespace toolkit; namespace mediakit { -/** - * 该类实现与老代码的兼容适配 - */ -class HttpResponseInvokerImp{ -public: - typedef std::function HttpResponseInvokerLambda0; - typedef std::function HttpResponseInvokerLambda1; - - HttpResponseInvokerImp(){} - ~HttpResponseInvokerImp(){} - template - HttpResponseInvokerImp(const C &c):HttpResponseInvokerImp(typename function_traits::stl_function_type(c)) {} - HttpResponseInvokerImp(const HttpResponseInvokerLambda0 &lambda); - HttpResponseInvokerImp(const HttpResponseInvokerLambda1 &lambda); - - void operator()(const string &codeOut, const StrCaseMap &headerOut, const HttpBody::Ptr &body) const; - void operator()(const string &codeOut, const StrCaseMap &headerOut, const string &body) const; - void responseFile(const StrCaseMap &requestHeader,const StrCaseMap &responseHeader,const string &filePath) const; - operator bool(); -private: - HttpResponseInvokerLambda0 _lambad; -}; - class HttpSession: public TcpSession, public FlvMuxer, public HttpRequestSplitter, @@ -88,9 +61,7 @@ public: virtual void onRecv(const Buffer::Ptr &) override; virtual void onError(const SockException &err) override; virtual void onManager() override; - static string urlDecode(const string &str); - static const char* get_mime_type(const char* name); protected: //FlvMuxer override void onWrite(const Buffer::Ptr &data) override ; @@ -145,26 +116,6 @@ private: void sendResponse(const char *pcStatus, bool bClose, const char *pcContentType = nullptr, const HttpSession::KeyValue &header = HttpSession::KeyValue(), const HttpBody::Ptr &body = nullptr,bool set_content_len = true); - /** - * 判断http客户端是否有权限访问文件的逻辑步骤 - * - * 1、根据http请求头查找cookie,找到进入步骤3 - * 2、根据http url参数(如果没有根据ip+端口号)查找cookie,如果还是未找到cookie则进入步骤5 - * 3、cookie标记是否有权限访问文件,如果有权限,直接返回文件 - * 4、cookie中记录的url参数是否跟本次url参数一致,如果一致直接返回客户端错误码 - * 5、触发kBroadcastHttpAccess事件 - * @param path 文件或目录 - * @param is_dir path是否为目录 - * @param callback 有权限或无权限的回调 - */ - void canAccessPath(const string &path,bool is_dir,const function &callback); - - /** - * 获取用户唯一识别id - * 有url参数返回参数,无参数返回ip+端口号 - * @return - */ - string getClientUid(); //设置socket标志 void setSocketFlags();