Add the OPTIONS method for CORS

This commit is contained in:
antao 2019-04-12 21:45:43 +08:00
parent 2d9ca5ae5f
commit d5e8bd0b73
13 changed files with 320 additions and 91 deletions

View File

@ -10,9 +10,9 @@ class TestController : public drogon::HttpSimpleController<TestController>
PATH_LIST_BEGIN
//list path definations here;
//PATH_ADD("/path","filter1","filter2",...);
PATH_ADD("/");
PATH_ADD("/", Get);
PATH_ADD("/Test", "nonFilter");
PATH_ADD("/tpost", Post);
PATH_ADD("/tpost", Post, Options);
PATH_ADD("/slow", "TimeFilter", Get);
PATH_LIST_END
TestController()

View File

@ -9,5 +9,6 @@ class TestViewCtl : public drogon::HttpSimpleController<TestViewCtl>
//list path definations here;
//PATH_ADD("/path","filter1","filter2",...);
PATH_ADD("/view");
PATH_ADD("/", Post);
PATH_LIST_END
};

View File

@ -10,11 +10,12 @@ class ApiTest : public drogon::HttpController<ApiTest>
public:
METHOD_LIST_BEGIN
//use METHOD_ADD to add your custom processing function here;
METHOD_ADD(ApiTest::rootGet, "", "TimeFilter", Get, "drogon::LocalHostFilter", "drogon::IntranetIpFilter");
METHOD_ADD(ApiTest::rootPost, "", Post);
METHOD_ADD(ApiTest::rootGet, "", "TimeFilter", Get, Options, "drogon::LocalHostFilter", "drogon::IntranetIpFilter");
METHOD_ADD(ApiTest::rootPost, "", Post, Options);
METHOD_ADD(ApiTest::get, "/get/{2}/{1}", Get); //path is /api/v1/apitest/get/{arg2}/{arg1}
METHOD_ADD(ApiTest::your_method_name, "/{1}/List?P2={2}", Get); //path is /api/v1/apitest/{arg1}/list
METHOD_ADD(ApiTest::staticApi, "/static", Get, Post);
METHOD_ADD(ApiTest::staticApi, "/static", Get, Options); //CORS
METHOD_ADD(ApiTest::staticApi, "/static", Post, Put, Delete);
METHOD_ADD(ApiTest::get2, "/get/{1}", Get); //path is /api/v1/apitest/get/{arg1}
ADD_METHOD_TO(ApiTest::get2, "/absolute/{1}", Get); //path is /absolute/{arg1}
METHOD_ADD(ApiTest::jsonTest, "/json", Post);
@ -29,6 +30,7 @@ class ApiTest : public drogon::HttpController<ApiTest>
void rootPost(const HttpRequestPtr &req, const std::function<void(const HttpResponsePtr &)> &callback);
void jsonTest(const HttpRequestPtr &req, const std::function<void(const HttpResponsePtr &)> &callback);
void formTest(const HttpRequestPtr &req, const std::function<void(const HttpResponsePtr &)> &callback);
public:
ApiTest()
{

View File

@ -199,6 +199,130 @@ void doTest(const HttpClientPtr &client, std::promise<int> &pro, bool isHttps =
}
});
/// 4. Http OPTIONS Method
req = HttpRequest::newHttpRequest();
req->setMethod(drogon::Options);
req->setPath("/tpost");
client->sendRequest(req, [=](ReqResult result, const HttpResponsePtr &resp) {
if (result == ReqResult::Ok)
{
//LOG_DEBUG << resp->getBody();
auto allow = resp->getHeader("allow");
if (resp->statusCode() == k200OK && allow.find("POST") != std::string::npos)
{
outputGood(req, isHttps);
}
else
{
LOG_ERROR << "Error!";
exit(1);
}
}
else
{
LOG_ERROR << "Error!";
exit(1);
}
});
req = HttpRequest::newHttpRequest();
req->setMethod(drogon::Options);
req->setPath("/api/v1/apitest");
client->sendRequest(req, [=](ReqResult result, const HttpResponsePtr &resp) {
if (result == ReqResult::Ok)
{
//LOG_DEBUG << resp->getBody();
auto allow = resp->getHeader("allow");
if (resp->statusCode() == k200OK && allow == "OPTIONS,GET,HEAD,POST")
{
outputGood(req, isHttps);
}
else
{
LOG_ERROR << "Error!";
exit(1);
}
}
else
{
LOG_ERROR << "Error!";
exit(1);
}
});
req = HttpRequest::newHttpRequest();
req->setMethod(drogon::Options);
req->setPath("/slow");
client->sendRequest(req, [=](ReqResult result, const HttpResponsePtr &resp) {
if (result == ReqResult::Ok)
{
if (resp->statusCode() == k403Forbidden)
{
outputGood(req, isHttps);
}
else
{
LOG_ERROR << "Error!";
exit(1);
}
}
else
{
LOG_ERROR << "Error!";
exit(1);
}
});
req = HttpRequest::newHttpRequest();
req->setMethod(drogon::Options);
req->setPath("/*");
client->sendRequest(req, [=](ReqResult result, const HttpResponsePtr &resp) {
if (result == ReqResult::Ok)
{
//LOG_DEBUG << resp->getBody();
auto allow = resp->getHeader("allow");
if (resp->statusCode() == k200OK && allow == "GET,HEAD,POST,PUT,DELETE,OPTIONS")
{
outputGood(req, isHttps);
}
else
{
LOG_ERROR << "Error!";
exit(1);
}
}
else
{
LOG_ERROR << "Error!";
exit(1);
}
});
req = HttpRequest::newHttpRequest();
req->setMethod(drogon::Options);
req->setPath("/api/v1/apitest/static");
client->sendRequest(req, [=](ReqResult result, const HttpResponsePtr &resp) {
if (result == ReqResult::Ok)
{
//LOG_DEBUG << resp->getBody();
auto allow = resp->getHeader("allow");
if (resp->statusCode() == k200OK && allow == "OPTIONS,GET,HEAD")
{
outputGood(req, isHttps);
}
else
{
LOG_ERROR << "Error!";
exit(1);
}
}
else
{
LOG_ERROR << "Error!";
exit(1);
}
});
/// 4. Test HttpController
req = HttpRequest::newHttpRequest();
req->setMethod(drogon::Post);

View File

@ -106,6 +106,7 @@ enum HttpMethod
Head,
Put,
Delete,
Options,
Invalid
};

View File

@ -526,7 +526,6 @@ void HttpAppFrameworkImpl::onConnection(const TcpConnectionPtr &conn)
}
}
void HttpAppFrameworkImpl::setUploadPath(const std::string &uploadPath)
{
assert(!uploadPath.empty());
@ -555,24 +554,16 @@ void HttpAppFrameworkImpl::onAsyncRequest(const HttpRequestImplPtr &req, std::fu
{
LOG_TRACE << "new request:" << req->peerAddr().toIpPort() << "->" << req->localAddr().toIpPort();
LOG_TRACE << "Headers " << req->methodString() << " " << req->path();
#if 0
const std::map<std::string, std::string>& headers = req->headers();
for (std::map<std::string, std::string>::const_iterator it = headers.begin();
it != headers.end();
++it) {
LOG_TRACE << it->first << ": " << it->second;
}
LOG_TRACE<<"cookies:";
auto cookies = req->cookies();
for(auto it=cookies.begin();it!=cookies.end();++it)
{
LOG_TRACE<<it->first<<"="<<it->second;
}
#endif
LOG_TRACE << "http path=" << req->path();
if (req->method() == Options && (req->path() == "*" || req->path() == "/*"))
{
auto resp = HttpResponse::newHttpResponse();
resp->setContentTypeCode(ContentType::CT_TEXT_PLAIN);
resp->addHeader("ALLOW", "GET,HEAD,POST,PUT,DELETE,OPTIONS");
resp->setExpiredTime(0);
callback(resp);
return;
}
// LOG_TRACE << "query: " << req->query() ;
std::string sessionId = req->getCookie("JSESSIONID");

View File

@ -101,7 +101,7 @@ void HttpControllersRouter::addHttpPath(const std::string &path,
}
}
auto pathParameterPattern = std::regex_replace(originPath, regex, "([^/]*)");
auto binderInfo = CtrlBinderPtr(new CtrlBinder);
auto binderInfo = std::make_shared<CtrlBinder>();
binderInfo->_filterNames = filters;
binderInfo->_binderPtr = binder;
binderInfo->_parameterPlaces = std::move(places);
@ -117,10 +117,13 @@ void HttpControllersRouter::addHttpPath(const std::string &path,
for (auto const &method : validMethods)
{
router._binders[method] = binderInfo;
if (method == Options)
binderInfo->_isCORS = true;
}
}
else
{
binderInfo->_isCORS = true;
for (int i = 0; i < Invalid; i++)
router._binders[i] = binderInfo;
}
@ -136,10 +139,13 @@ void HttpControllersRouter::addHttpPath(const std::string &path,
for (auto const &method : validMethods)
{
router._binders[method] = binderInfo;
if (method == Options)
binderInfo->_isCORS = true;
}
}
else
{
binderInfo->_isCORS = true;
for (int i = 0; i < Invalid; i++)
router._binders[i] = binderInfo;
}
@ -176,7 +182,14 @@ void HttpControllersRouter::route(const HttpRequestImplPtr &req,
{
//Invalid Http Method
auto res = drogon::HttpResponse::newHttpResponse();
res->setStatusCode(k405MethodNotAllowed);
if (req->method() != Options)
{
res->setStatusCode(k405MethodNotAllowed);
}
else
{
res->setStatusCode(k403Forbidden);
}
callback(res);
return;
}
@ -222,6 +235,33 @@ void HttpControllersRouter::doControllerHandler(const CtrlBinderPtr &ctrlBinderP
bool needSetJsessionid,
std::string &&sessionId)
{
if (req->method() == Options)
{
auto resp = HttpResponse::newHttpResponse();
resp->setContentTypeCode(ContentType::CT_TEXT_PLAIN);
std::string methods = "OPTIONS,";
if (routerItem._binders[Get] && routerItem._binders[Get]->_isCORS)
{
methods.append("GET,HEAD,");
}
if (routerItem._binders[Post] && routerItem._binders[Post]->_isCORS)
{
methods.append("POST,");
}
if (routerItem._binders[Put] && routerItem._binders[Put]->_isCORS)
{
methods.append("PUT,");
}
if (routerItem._binders[Delete] && routerItem._binders[Delete]->_isCORS)
{
methods.append("DELETE,");
}
methods.resize(methods.length() - 1);
resp->addHeader("ALLOW", methods);
callback(resp);
return;
}
HttpResponsePtr &responsePtr = ctrlBinderPtr->_responsePtrMap[req->getLoop()];
if (responsePtr && (responsePtr->expiredTime() == 0 || (trantor::Date::now() < responsePtr->creationDate().after(responsePtr->expiredTime()))))
{
@ -240,6 +280,7 @@ void HttpControllersRouter::doControllerHandler(const CtrlBinderPtr &ctrlBinderP
}
return;
}
std::vector<std::string> params(ctrlBinderPtr->_parameterPlaces.size());
std::smatch r;
if (std::regex_match(req->path(), r, routerItem._regex))

View File

@ -51,6 +51,7 @@ class HttpControllersRouter : public trantor::NonCopyable
std::vector<size_t> _parameterPlaces;
std::map<std::string, size_t> _queryParametersPlaces;
std::map<trantor::EventLoop *,std::shared_ptr<HttpResponse>> _responsePtrMap;
bool _isCORS = false;
};
typedef std::shared_ptr<CtrlBinder> CtrlBinderPtr;
struct HttpControllerRouterItem
@ -58,7 +59,7 @@ class HttpControllersRouter : public trantor::NonCopyable
std::string _pathParameterPattern;
std::string _pathPattern;
std::regex _regex;
CtrlBinderPtr _binders[Invalid]; //The enum value Invalid is the http methods number
CtrlBinderPtr _binders[Invalid]={nullptr}; //The enum value Invalid is the http methods number
};
std::vector<HttpControllerRouterItem> _ctrlVector;
std::mutex _ctrlMutex;

View File

@ -113,6 +113,9 @@ void HttpRequestImpl::appendToBuffer(MsgBuffer *output) const
case Delete:
output->append("DELETE ");
break;
case Options:
output->append("OPTIONS ");
break;
default:
return;
}

View File

@ -109,6 +109,16 @@ class HttpRequestImpl : public HttpRequest
_method = Invalid;
}
break;
case 7:
if (m == "OPTIONS")
{
_method = Options;
}
else
{
_method = Invalid;
}
break;
default:
_method = Invalid;
break;
@ -161,6 +171,9 @@ class HttpRequestImpl : public HttpRequest
case Delete:
result = "DELETE";
break;
case Options:
result = "OPTIONS";
break;
default:
break;
}
@ -384,7 +397,7 @@ class HttpRequestImpl : public HttpRequest
virtual const std::string &matchedPathPattern() const override
{
if(_matchedPathPattern.empty())
if (_matchedPathPattern.empty())
return _path;
return _matchedPathPattern;
}

View File

@ -118,8 +118,8 @@ bool HttpResponseParser::parseResponse(MsgBuffer *buf)
}
else
{
if (_response->statusCode() == k101SwitchingProtocols &&
_response->getHeaderBy("upgrade") == "websocket")
if (_response->statusCode() == k204NoContent || (_response->statusCode() == k101SwitchingProtocols &&
_response->getHeaderBy("upgrade") == "websocket"))
{
//The Websocket response may not have a content-length header.
_state = HttpResponseParseState::kGotAll;

View File

@ -53,22 +53,30 @@ void HttpSimpleControllersRouter::registerHttpSimpleController(const std::string
}
auto &item = _simpCtrlMap[path];
item._controllerName = ctrlName;
item._filterNames = filters;
item._validMethodsFlags.clear(); //There may be old data, first clear
auto binder = std::make_shared<CtrlBinder>();
binder->_filterNames = filters;
auto &_object = DrClassMap::getSingleInstance(ctrlName);
auto controller = std::dynamic_pointer_cast<HttpSimpleControllerBase>(_object);
binder->_controller = controller;
if (validMethods.size() > 0)
{
item._validMethodsFlags.resize(Invalid, 0);
for (auto const &method : validMethods)
{
item._validMethodsFlags[method] = 1;
item._binders[method] = binder;
if (method == Options)
{
binder->_isCORS = true;
}
}
}
auto controller = item._controller;
if (!controller)
else
{
auto &_object = DrClassMap::getSingleInstance(ctrlName);
controller = std::dynamic_pointer_cast<HttpSimpleControllerBase>(_object);
item._controller = controller;
//All HTTP methods are valid
for (size_t i = 0; i < Invalid; i++)
{
item._binders[i] = binder;
}
binder->_isCORS = true;
}
}
@ -83,47 +91,77 @@ void HttpSimpleControllersRouter::route(const HttpRequestImplPtr &req,
if (iter != _simpCtrlMap.end())
{
auto &ctrlInfo = iter->second;
if (!ctrlInfo._validMethodsFlags.empty())
auto &binder = ctrlInfo._binders[req->method()];
if (!binder)
{
assert(ctrlInfo._validMethodsFlags.size() > req->method());
if (ctrlInfo._validMethodsFlags[req->method()] == 0)
//Invalid Http Method
auto res = drogon::HttpResponse::newHttpResponse();
if (req->method() != Options)
{
//Invalid Http Method
auto res = drogon::HttpResponse::newHttpResponse();
res->setStatusCode(k405MethodNotAllowed);
callback(res);
return;
}
else
{
res->setStatusCode(k403Forbidden);
}
callback(res);
return;
}
auto &filters = ctrlInfo._filters;
auto &filters = ctrlInfo._binders[req->method()]->_filters;
if (!filters.empty())
{
auto sessionIdPtr = std::make_shared<std::string>(std::move(sessionId));
auto callbackPtr = std::make_shared<std::function<void(const HttpResponsePtr &)>>(std::move(callback));
FiltersFunction::doFilters(filters, req, callbackPtr, needSetJsessionid, sessionIdPtr, [=, &ctrlInfo]() mutable {
doControllerHandler(ctrlInfo, req, std::move(*callbackPtr), needSetJsessionid, std::move(*sessionIdPtr));
FiltersFunction::doFilters(filters, req, callbackPtr, needSetJsessionid, sessionIdPtr, [=, &binder]() mutable {
doControllerHandler(binder, ctrlInfo, req, std::move(*callbackPtr), needSetJsessionid, std::move(*sessionIdPtr));
});
}
else
{
doControllerHandler(ctrlInfo, req, std::move(callback), needSetJsessionid, std::move(sessionId));
doControllerHandler(binder, ctrlInfo, req, std::move(callback), needSetJsessionid, std::move(sessionId));
}
return;
}
_httpCtrlsRouter.route(req, std::move(callback), needSetJsessionid, std::move(sessionId));
}
void HttpSimpleControllersRouter::doControllerHandler(SimpleControllerRouterItem &item,
void HttpSimpleControllersRouter::doControllerHandler(const CtrlBinderPtr &ctrlBinderPtr,
const SimpleControllerRouterItem &routerItem,
const HttpRequestImplPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
bool needSetJsessionid,
std::string &&sessionId)
{
const std::string &ctrlName = item._controllerName;
auto &controller = item._controller;
auto &controller = ctrlBinderPtr->_controller;
if (controller)
{
HttpResponsePtr &responsePtr = item._responsePtrMap[req->getLoop()];
if (req->method() == Options)
{
auto resp = HttpResponse::newHttpResponse();
resp->setContentTypeCode(ContentType::CT_TEXT_PLAIN);
std::string methods = "OPTIONS,";
if (routerItem._binders[Get] && routerItem._binders[Get]->_isCORS)
{
methods.append("GET,HEAD,");
}
if (routerItem._binders[Post] && routerItem._binders[Post]->_isCORS)
{
methods.append("POST,");
}
if (routerItem._binders[Put] && routerItem._binders[Put]->_isCORS)
{
methods.append("PUT,");
}
if (routerItem._binders[Delete] && routerItem._binders[Delete]->_isCORS)
{
methods.append("DELETE,");
}
methods.resize(methods.length() - 1);
resp->addHeader("ALLOW", methods);
callback(resp);
return;
}
HttpResponsePtr &responsePtr = ctrlBinderPtr->_responsePtrMap[req->getLoop()];
if (responsePtr && (responsePtr->expiredTime() == 0 || (trantor::Date::now() < responsePtr->creationDate().after(responsePtr->expiredTime()))))
{
//use cached response!
@ -142,7 +180,7 @@ void HttpSimpleControllersRouter::doControllerHandler(SimpleControllerRouterItem
}
else
{
controller->asyncHandleHttpRequest(req, [=, callback = std::move(callback), &item, sessionId = std::move(sessionId)](const HttpResponsePtr &resp) {
controller->asyncHandleHttpRequest(req, [=, callback = std::move(callback), &ctrlBinderPtr, sessionId = std::move(sessionId)](const HttpResponsePtr &resp) {
auto newResp = resp;
if (resp->expiredTime() >= 0)
{
@ -151,12 +189,12 @@ void HttpSimpleControllersRouter::doControllerHandler(SimpleControllerRouterItem
auto loop = req->getLoop();
if (loop->isInLoopThread())
{
item._responsePtrMap[loop] = resp;
ctrlBinderPtr->_responsePtrMap[loop] = resp;
}
else
{
loop->queueInLoop([loop, resp, &item]() {
item._responsePtrMap[loop] = resp;
loop->queueInLoop([loop, resp, &ctrlBinderPtr]() {
ctrlBinderPtr->_responsePtrMap[loop] = resp;
});
}
}
@ -178,6 +216,7 @@ void HttpSimpleControllersRouter::doControllerHandler(SimpleControllerRouterItem
}
else
{
const std::string &ctrlName = routerItem._controllerName;
LOG_ERROR << "can't find controller " << ctrlName;
auto res = drogon::HttpResponse::newNotFoundResponse();
if (needSetJsessionid)
@ -192,10 +231,17 @@ void HttpSimpleControllersRouter::init(const std::vector<trantor::EventLoop *> &
for (auto &iter : _simpCtrlMap)
{
auto &item = iter.second;
item._filters = FiltersFunction::createFilters(item._filterNames);
for (auto ioloop : ioLoops)
for (size_t i = 0; i < Invalid; i++)
{
item._responsePtrMap[ioloop] = std::shared_ptr<HttpResponse>();
auto &binder = item._binders[i];
if(binder)
{
binder->_filters = FiltersFunction::createFilters(binder->_filterNames);
for (auto ioloop : ioLoops)
{
binder->_responsePtrMap[ioloop] = nullptr;
}
}
}
}
}

View File

@ -34,38 +34,44 @@ class HttpAppFrameworkImpl;
class HttpControllersRouter;
class HttpSimpleControllersRouter : public trantor::NonCopyable
{
public:
explicit HttpSimpleControllersRouter(HttpControllersRouter &httpCtrlRouter)
: _httpCtrlsRouter(httpCtrlRouter) {}
void registerHttpSimpleController(const std::string &pathName,
const std::string &ctrlName,
const std::vector<any> &filtersAndMethods);
void route(const HttpRequestImplPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
bool needSetJsessionid,
std::string &&sessionId);
void init(const std::vector<trantor::EventLoop *> &ioLoops);
public:
explicit HttpSimpleControllersRouter(HttpControllersRouter &httpCtrlRouter)
: _httpCtrlsRouter(httpCtrlRouter) {}
void registerHttpSimpleController(const std::string &pathName,
const std::string &ctrlName,
const std::vector<any> &filtersAndMethods);
void route(const HttpRequestImplPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
bool needSetJsessionid,
std::string &&sessionId);
void init(const std::vector<trantor::EventLoop *> &ioLoops);
private:
HttpControllersRouter &_httpCtrlsRouter;
struct SimpleControllerRouterItem
{
std::string _controllerName;
std::vector<std::string> _filterNames;
std::vector<std::shared_ptr<HttpFilterBase>> _filters;
std::vector<int> _validMethodsFlags;
std::shared_ptr<HttpSimpleControllerBase> _controller;
std::map<trantor::EventLoop *, std::shared_ptr<HttpResponse>> _responsePtrMap;
//std::atomic<bool> _mutex = ATOMIC_VAR_INIT(false);
//std::atomic_flag _mutex = ATOMIC_FLAG_INIT;
};
std::unordered_map<std::string, SimpleControllerRouterItem> _simpCtrlMap;
std::mutex _simpCtrlMutex;
private:
HttpControllersRouter &_httpCtrlsRouter;
void doControllerHandler(SimpleControllerRouterItem &item,
const HttpRequestImplPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
bool needSetJsessionid,
std::string &&sessionId);
struct CtrlBinder
{
std::shared_ptr<HttpSimpleControllerBase> _controller;
std::vector<std::string> _filterNames;
std::vector<std::shared_ptr<HttpFilterBase>> _filters;
std::map<trantor::EventLoop *, std::shared_ptr<HttpResponse>> _responsePtrMap;
bool _isCORS = false;
};
typedef std::shared_ptr<CtrlBinder> CtrlBinderPtr;
struct SimpleControllerRouterItem
{
std::string _controllerName;
CtrlBinderPtr _binders[Invalid] = {nullptr};
};
std::unordered_map<std::string, SimpleControllerRouterItem> _simpCtrlMap;
std::mutex _simpCtrlMutex;
void doControllerHandler(const CtrlBinderPtr &ctrlBinderPtr,
const SimpleControllerRouterItem &routerItem,
const HttpRequestImplPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
bool needSetJsessionid,
std::string &&sessionId);
};
} // namespace drogon