From 42d382878de21d41b8cf167672e3210c6b1cf8f7 Mon Sep 17 00:00:00 2001 From: xiongziliang <771730766@qq.com> Date: Fri, 8 Dec 2017 22:37:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84HTTP=20API=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Common/config.h | 2 +- src/Http/HttpSession.cpp | 110 +++++++++++++++++-------------- src/Http/HttpSession.h | 6 +- src/Rtsp/Rtsp.h | 62 ++++++++++++++---- tests/test_httpApi.cpp | 136 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 252 insertions(+), 64 deletions(-) create mode 100644 tests/test_httpApi.cpp diff --git a/src/Common/config.h b/src/Common/config.h index 720bc3a1..def1326b 100644 --- a/src/Common/config.h +++ b/src/Common/config.h @@ -70,7 +70,7 @@ extern const char kBroadcastRecordMP4[]; #define BroadcastRecordMP4Args const Mp4Info &info extern const char kBroadcastHttpRequest[]; -#define BroadcastHttpRequestArgs const Parser &parser,HttpSession::HttpResponseInvoker &invoker +#define BroadcastHttpRequestArgs const Parser &parser,HttpSession::HttpResponseInvoker &invoker,bool &consumed } //namespace Broadcast diff --git a/src/Http/HttpSession.cpp b/src/Http/HttpSession.cpp index 13310066..8bad9172 100644 --- a/src/Http/HttpSession.cpp +++ b/src/Http/HttpSession.cpp @@ -106,7 +106,6 @@ HttpSession::HttpSession(const std::shared_ptr &pTh, const Socket::P static onceToken token([]() { g_mapCmdIndex.emplace("GET",&HttpSession::Handle_Req_GET); g_mapCmdIndex.emplace("POST",&HttpSession::Handle_Req_POST); - g_mapCmdIndex.emplace("OPTIONS",&HttpSession::Handle_Req_POST); }, nullptr); } @@ -150,6 +149,7 @@ void HttpSession::onRecv(const char *data,int size){ } inline HttpSession::HttpCode HttpSession::parserHttpReq(const string &str) { m_parser.Parse(str.data()); + urlDecode(m_parser); string cmd = m_parser.Method(); auto it = g_mapCmdIndex.find(cmd); if (it == g_mapCmdIndex.end()) { @@ -174,42 +174,43 @@ void HttpSession::onManager() { } inline HttpSession::HttpCode HttpSession::Handle_Req_GET() { - string strUrl = strCoding::UrlUTF8Decode(m_parser.Url()); -#ifdef _WIN32 - static bool isGb2312 = !strcasecmp(mINI::Instance()[Config::Http::kCharSet].data(), "gb2312"); - if (isGb2312) { - strUrl = strCoding::UTF8ToGB2312(strUrl); + //先看看该http事件是否被拦截 + if(emitHttpEvent(false)){ + return Http_success; } -#endif // _WIN32 - string strFile = m_strPath + strUrl; - string strConType = m_parser["Connection"]; + //事件未被拦截,则认为是http下载请求 + + string strFile = m_strPath + m_parser.Url(); + /////////////HTTP连接是否需要被关闭//////////////// static uint32_t reqCnt = mINI::Instance()[Config::Http::kMaxReqCount].as(); - bool bClose = (strcasecmp(strConType.data(),"close") == 0) && ( ++m_iReqCnt < reqCnt); + bool bClose = (strcasecmp(m_parser["Connection"].data(),"close") == 0) && ( ++m_iReqCnt < reqCnt); HttpCode eHttpCode = bClose ? Http_failed : Http_success; + //访问的是文件夹 if (strFile.back() == '/') { - //index the folder + //生成文件夹菜单索引 string strMeun; if (!makeMeun(strFile, strMeun)) { + //文件夹不存在 sendNotFound(bClose); return eHttpCode; } sendResponse("200 OK", makeHttpHeader(bClose,strMeun.size() ), strMeun); return eHttpCode; } - //download the file + //访问的是文件 struct stat tFileStat; if (0 != stat(strFile.data(), &tFileStat)) { + //文件不存在 sendNotFound(bClose); return eHttpCode; } - - TimeTicker(); FILE *pFile = fopen(strFile.data(), "rb"); if (pFile == NULL) { + //打开文件失败 sendNotFound(bClose); return eHttpCode; } - + //判断是不是分节下载 auto &strRange = m_parser["Range"]; int64_t iRangeStart = 0, iRangeEnd = 0; iRangeStart = atoll(FindField(strRange.data(), "bytes=", "-").data()); @@ -219,24 +220,25 @@ inline HttpSession::HttpCode HttpSession::Handle_Req_GET() { } const char *pcHttpResult = NULL; if (strRange.size() == 0) { + //全部下载 pcHttpResult = "200 OK"; } else { + //分节下载 pcHttpResult = "206 Partial Content"; fseek(pFile, iRangeStart, SEEK_SET); } - - auto httpHeader=makeHttpHeader(bClose, iRangeEnd - iRangeStart + 1, get_mime_type(strUrl.data())); + 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); } - + //先回复HTTP头部分 sendResponse(pcHttpResult, httpHeader, ""); if (iRangeEnd - iRangeStart < 0) { - //file is empty! + //文件时空的! return eHttpCode; } - - //send the file + //回复Content部分 std::shared_ptr piLeft(new int64_t(iRangeEnd - iRangeStart + 1)); std::shared_ptr pFilePtr(pFile, [](FILE *pFp) { fclose(pFp); @@ -398,35 +400,32 @@ inline HttpSession::KeyValue HttpSession::makeHttpHeader(bool bClose, int64_t iC } return headerOut; } -inline HttpSession::HttpCode HttpSession::Handle_Req_POST() { - int iContentLen = atoi(m_parser["Content-Length"].data()); - /*if (!iContentLen) { - return Http_failed; - }*/ - if ((int) m_strRcvBuf.size() < iContentLen) { - return Http_moreData; //需要更多数据 - } - auto strContent = m_strRcvBuf.substr(0, iContentLen); - m_strRcvBuf.erase(0, iContentLen); - string strUrl = strCoding::UrlUTF8Decode(m_parser.Url()); +string HttpSession::urlDecode(const string &str){ + auto ret = strCoding::UrlUTF8Decode(str); #ifdef _WIN32 static bool isGb2312 = !strcasecmp(mINI::Instance()[Config::Http::kCharSet].data(), "gb2312"); if (isGb2312) { - strUrl = strCoding::UTF8ToGB2312(strUrl); + ret = strCoding::UTF8ToGB2312(ret); } #endif // _WIN32 + return ret; +} - string strConType = m_parser["Connection"]; +inline void HttpSession::urlDecode(Parser &parser){ + parser.setUrl(urlDecode(parser.Url())); + for(auto &pr : m_parser.getUrlArgs()){ + const_cast(pr.second) = urlDecode(pr.second); + } +} + +inline bool HttpSession::emitHttpEvent(bool doInvoke){ + ///////////////////是否断开本链接/////////////////////// static uint32_t reqCnt = mINI::Instance()[Config::Http::kMaxReqCount].as(); - bool bClose = (strcasecmp(strConType.data(),"close") == 0) && ( ++m_iReqCnt < reqCnt); - m_parser.setUrl(strUrl); - m_parser.setContent(strContent); - + bool bClose = (strcasecmp(m_parser["Connection"].data(),"close") == 0) && ( ++m_iReqCnt < reqCnt); + /////////////////////异步回复Invoker/////////////////////////////// weak_ptr weakSelf = dynamic_pointer_cast(shared_from_this()); - HttpResponseInvoker invoker = [weakSelf,bClose](const string &codeOut, - const KeyValue &headerOut, - const string &contentOut){ + HttpResponseInvoker invoker = [weakSelf,bClose](const string &codeOut, const KeyValue &headerOut, const string &contentOut){ auto strongSelf = weakSelf.lock(); if(!strongSelf) { return; @@ -436,27 +435,40 @@ inline HttpSession::HttpCode HttpSession::Handle_Req_POST() { if(!strongSelf) { return; } - strongSelf->responseDelay(bClose, - const_cast(codeOut), - const_cast(headerOut), - const_cast(contentOut)); + strongSelf->responseDelay(bClose,codeOut,headerOut,contentOut); if(bClose){ strongSelf->shutdown(); } }); }; - if(!NoticeCenter::Instance().emitEvent(Config::Broadcast::kBroadcastHttpRequest,m_parser,invoker)){ + ///////////////////广播HTTP事件/////////////////////////// + bool consumed = false;//该事件是否被消费 + NoticeCenter::Instance().emitEvent(Config::Broadcast::kBroadcastHttpRequest,m_parser,invoker,(bool &)consumed); + if(!consumed && doInvoke){ + //该事件无人消费,所以返回404 invoker("404 Not Found",KeyValue(),""); } + return consumed; +} +inline HttpSession::HttpCode HttpSession::Handle_Req_POST() { + //////////////获取HTTP POST Content///////////// + int iContentLen = atoi(m_parser["Content-Length"].data()); + if ((int) m_strRcvBuf.size() < iContentLen) { + return Http_moreData; //需要更多数据 + } + m_parser.setContent(m_strRcvBuf.substr(0, iContentLen)); + m_strRcvBuf.erase(0, iContentLen); + //广播事件 + emitHttpEvent(true); return Http_success; } -void HttpSession::responseDelay(bool bClose,string &codeOut,KeyValue &headerOut, string &contentOut){ +void HttpSession::responseDelay(bool bClose,const string &codeOut,const KeyValue &headerOut, const string &contentOut){ if(codeOut.empty()){ sendNotFound(bClose); return; } - auto headerOther=makeHttpHeader(bClose,contentOut.size(),"text/json"); - headerOut.insert(headerOther.begin(), headerOther.end()); + auto headerOther=makeHttpHeader(bClose,contentOut.size(),"text/plain"); + const_cast(headerOut).insert(headerOther.begin(), headerOther.end()); sendResponse(codeOut.data(), headerOut, contentOut); } inline void HttpSession::sendNotFound(bool bClose) { diff --git a/src/Http/HttpSession.h b/src/Http/HttpSession.h index 3a65e8d5..c41edfc6 100644 --- a/src/Http/HttpSession.h +++ b/src/Http/HttpSession.h @@ -51,6 +51,8 @@ public: virtual void onRecv(const Socket::Buffer::Ptr &) override; virtual void onError(const SockException &err) override; virtual void onManager() override; + + static string urlDecode(const string &str); protected: void onRecv(const char *data,int size); private: @@ -73,11 +75,13 @@ private: inline HttpCode parserHttpReq(const string &); inline HttpCode Handle_Req_GET(); inline HttpCode Handle_Req_POST(); + inline bool emitHttpEvent(bool doInvoke); + inline void urlDecode(Parser &parser); inline bool makeMeun(const string &strFullPath, string &strRet); inline void sendNotFound(bool bClose); inline void sendResponse(const char *pcStatus,const KeyValue &header,const string &strContent); inline static KeyValue makeHttpHeader(bool bClose=false,int64_t iContentSize=-1,const char *pcContentType="text/html"); - void responseDelay(bool bClose,string &codeOut,KeyValue &headerOut, string &contentOut); + void responseDelay(bool bClose,const string &codeOut,const KeyValue &headerOut, const string &contentOut); }; } /* namespace Http */ diff --git a/src/Rtsp/Rtsp.h b/src/Rtsp/Rtsp.h index 14d80d93..44ace8df 100644 --- a/src/Rtsp/Rtsp.h +++ b/src/Rtsp/Rtsp.h @@ -88,29 +88,31 @@ typedef map StrCaseMap; class Parser { public: - Parser() { - } - virtual ~Parser() { - } + Parser() {} + virtual ~Parser() {} void Parse(const char *buf) { //解析 const char *start = buf; - string line; - string field; - string value; Clear(); while (true) { - line = FindField(start, NULL, "\r\n"); + auto line = FindField(start, NULL, "\r\n"); if (line.size() == 0) { break; } if (start == buf) { m_strMethod = FindField(line.c_str(), NULL, " "); - m_strUrl = FindField(line.c_str(), " ", " "); - m_strTail = FindField(line.c_str(), (m_strUrl + " ").c_str(), NULL); + auto full_url = FindField(line.c_str(), " ", " "); + auto args_pos = full_url.find('?'); + if(args_pos != string::npos){ + m_strUrl = full_url.substr(0,args_pos); + m_mapUrlArgs = parseArgs(full_url.substr(args_pos + 1 )); + }else{ + m_strUrl = full_url; + } + m_strTail = FindField(line.c_str(), (full_url + " ").c_str(), NULL); } else { - field = FindField(line.c_str(), NULL, ": "); - value = FindField(line.c_str(), ": ", NULL); + auto field = FindField(line.c_str(), NULL, ": "); + auto value = FindField(line.c_str(), ": ", NULL); if (field.size() != 0) { m_mapValues[field] = value; } @@ -151,6 +153,7 @@ public: m_strTail.clear(); m_strContent.clear(); m_mapValues.clear(); + m_mapUrlArgs.clear(); } void setUrl(const string& url) { @@ -163,7 +166,40 @@ public: const StrCaseMap& getValues() const { return m_mapValues; } + const StrCaseMap& getUrlArgs() const { + return m_mapUrlArgs; + } + //注意:当字符串为空时,也会返回一个空字符串 + static vector split(const string& s, const char *delim) { + size_t last = 0; + size_t index = s.find_first_of(delim, last); + vector ret; + while (index != string::npos) { + ret.push_back(s.substr(last, index - last)); + last = index + 1; + index = s.find_first_of(delim, last); + } + if (index - last > 0) { + ret.push_back(s.substr(last, index - last)); + } + return ret; + } + + static StrCaseMap parseArgs(const string &str){ + StrCaseMap ret; + auto arg_vec = split(str, "&"); + for (string &key_val : arg_vec) { + if (!key_val.size()) { + continue; + } + auto key_val_vec = split(key_val, "="); + if (key_val_vec.size() >= 2) { + ret[key_val_vec[0]] = key_val_vec[1]; + } + } + return ret; + } private: string m_strMethod; string m_strUrl; @@ -171,7 +207,7 @@ private: string m_strContent; string m_strNull; StrCaseMap m_mapValues; - + StrCaseMap m_mapUrlArgs; }; typedef struct { diff --git a/tests/test_httpApi.cpp b/tests/test_httpApi.cpp new file mode 100644 index 00000000..e67a38df --- /dev/null +++ b/tests/test_httpApi.cpp @@ -0,0 +1,136 @@ +/* + * MIT License + * + * Copyright (c) 2016 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 +#include +#include +#include "Common/config.h" + +#ifdef ENABLE_OPENSSL +#include "Util/SSLBox.h" +#include "Http/HttpsSession.h" +#endif//ENABLE_OPENSSL + +#include "Util/File.h" +#include "Util/logger.h" +#include "Util/onceToken.h" +#include "Network/TcpServer.h" +#include "Poller/EventPoller.h" +#include "Thread/WorkThreadPool.h" +#include "Util/NoticeCenter.h" + +using namespace std; +using namespace ZL::Util; +using namespace ZL::Http; +using namespace ZL::Thread; +using namespace ZL::Network; + +static onceToken s_token([](){ + NoticeCenter::Instance().addListener(nullptr,Config::Broadcast::kBroadcastHttpRequest,[](BroadcastHttpRequestArgs){ + //const Parser &parser,HttpSession::HttpResponseInvoker &invoker,bool &consumed + if(strstr(parser.Url().data(),"/api/") != parser.Url().data()){ + return; + } + //url以"/api/起始,说明是http api" + consumed = true;//该http请求已被消费 + + _StrPrinter printer; + ////////////////method//////////////////// + printer << "\r\nmethod:\r\n\t" << parser.Method(); + ////////////////url///////////////// + printer << "\r\nurl:\r\n\t" << parser.Url(); + ////////////////protocol///////////////// + printer << "\r\nprotocol:\r\n\t" << parser.Tail(); + ///////////////args////////////////// + printer << "\r\nargs:\r\n"; + for(auto &pr : parser.getUrlArgs()){ + printer << "\t" << pr.first << " : " << pr.second << "\r\n"; + } + ///////////////header////////////////// + printer << "\r\nheader:\r\n"; + for(auto &pr : parser.getValues()){ + printer << "\t" << pr.first << " : " << pr.second << "\r\n"; + } + ////////////////content///////////////// + printer << "\r\ncontent:\r\n" << parser.Content(); + auto contentOut = printer << endl; + + ////////////////我们测算异步回复,当然你也可以同步回复///////////////// + EventPoller::Instance().sync([invoker,contentOut](){ + HttpSession::KeyValue headerOut; + //你可以自定义header,如果跟默认header重名,则会覆盖之 + //默认header有:Server,Connection,Date,Content-Type,Content-Length + //请勿覆盖Connection、Content-Length键 + //键名覆盖时不区分大小写 + headerOut["TestHeader"] = "HeaderValue"; + invoker("200 OK",headerOut,contentOut); + }); + }); +}, nullptr); + +int main(int argc,char *argv[]){ + //设置退出信号处理函数 + signal(SIGINT, [](int){EventPoller::Instance().shutdown();}); + //设置日志 + Logger::Instance().add(std::make_shared("stdout", LTrace)); + Logger::Instance().setWriter(std::make_shared()); + //加载配置文件,如果配置文件不存在就创建一个 + Config::loadIniConfig(); + +#ifdef ENABLE_OPENSSL + //请把证书"test_httpApi.pem"放置在本程序可执行程序同目录下 + try{ + //加载证书,证书包含公钥和私钥 + SSL_Initor::Instance().loadServerPem((exePath() + ".pem").data()); + }catch(...){ + FatalL << "请把证书:" << (exeName() + ".pem") << "放置在本程序可执行程序同目录下:" << exeDir() << endl; + return 0; + } +#endif //ENABLE_OPENSSL + + //开启http服务器 + TcpServer::Ptr httpSrv(new TcpServer()); + httpSrv->start(mINI::Instance()[Config::Http::kPort]);//默认80 + +#ifdef ENABLE_OPENSSL + //如果支持ssl,还可以开启https服务器 + TcpServer::Ptr httpsSrv(new TcpServer()); + httpsSrv->start(mINI::Instance()[Config::Http::kSSLPort]);//默认443 +#endif //ENABLE_OPENSSL + + InfoL << "你可以在浏览器输入:http://127.0.0.1/api/my_api?key0=val0&key1=参数1" << endl; + + EventPoller::Instance().runLoop(); + + static onceToken s_token(nullptr,[]() { + //TcpServer用到了WorkThreadPool + WorkThreadPool::Destory(); + EventPoller::Destory(); + Logger::Destory(); + }); + return 0; +} +