Unverified Commit 059171b1 authored by jiankyu's avatar jiankyu Committed by GitHub

Add middleware support (#779)

This commit partially resolves #24, which is a common requirement on
many RESTful services.

A vector of middleware handlers is added to Router, these handlers are
executed before the URL match and handler invocation, in the order of
registration.

A middleware function has the signature:

bool func(Pistache::Http::Request &request,
          Pistache::Http::ResponseWriter &response);

The first parameter is a reference to Http::Request, not Rest::Request,
at the moment middlewares are called, the request parameters are not yet
filled. This allows some generic operation on the request, such as
authentication or rate limiting.

Parameters are passed as references, to allow the middlewares be able to
modify them, such as injecting custom headers.

If a function returns true, the next function is executed, until all
middleware functions are called and all of them return true, the
registered handler is called.

If one of them returns false, this means there is no need to further
call the subsequent functions and the handler, the handling of this
request completes. Typical case is missing auth info or reaching limit.
Basically, the middleware function that returns false is responsible to
call response.send() to send the response to client.

Like Route::bind, a new method, Route::middleware(), with different
signatures, is added, to convert different callables into a middleware.

There is also requirements on the "post-handling" handlers, such as
logging response, measuring handler latency, and so on, in a centralized
place. This commit doesn't implement it: The response is passed to
handler using std::move, it's not retrievable again after handler
processing.
Signed-off-by: default avatarJiankun Yu <yujiankun@hotmail.com>
parent 14ecdf51
......@@ -65,6 +65,8 @@ struct Route {
typedef std::function<Result(const Request, Http::ResponseWriter)> Handler;
typedef std::function<bool(Http::Request& req, Http::ResponseWriter& resp)> Middleware;
explicit Route(Route::Handler handler) : handler_(std::move(handler)) {}
template <typename... Args> void invokeHandler(Args &&... args) const {
......@@ -218,6 +220,7 @@ public:
void head(const std::string &resource, Route::Handler handler);
void addCustomHandler(Route::Handler handler);
void addMiddleware(Route::Middleware middleware);
void addNotFoundHandler(Route::Handler handler);
inline bool hasNotFoundHandler() { return notFoundHandler != nullptr; }
......@@ -227,13 +230,15 @@ public:
Route::Status route(const Http::Request &request,
Http::ResponseWriter response);
Router() : routes(), customHandlers(), notFoundHandler() {}
Router() : routes(), customHandlers(), middlewares(), notFoundHandler() {}
private:
std::unordered_map<Http::Method, SegmentTreeNode> routes;
std::vector<Route::Handler> customHandlers;
std::vector<Route::Middleware> middlewares;
Route::Handler notFoundHandler;
};
......@@ -354,6 +359,33 @@ Route::Handler bind(Result (*func)(Args...)) {
};
}
template <typename Cls, typename... Args, typename Obj>
Route::Middleware middleware(bool (Cls::*func)(Args...), Obj obj) {
details::static_checks<Args...>();
return [=](Http::Request &request, Http::ResponseWriter &response) {
return (obj->*func)(request, response);
};
}
template <typename Cls, typename... Args, typename Obj>
Route::Middleware middleware(bool (Cls::*func)(Args...), std::shared_ptr<Obj> objPtr) {
details::static_checks<Args...>();
return [=](Http::Request &request, Http::ResponseWriter &response) {
return (objPtr.get()->*func)(request, response);
};
}
template <typename... Args>
Route::Middleware middleware(bool (*func)(Args...)) {
details::static_checks<Args...>();
return [=](Http::Request &request, Http::ResponseWriter &response) {
return func(request, response);
};
}
} // namespace Routes
} // namespace Rest
} // namespace Pistache
......@@ -381,6 +381,10 @@ void Router::addCustomHandler(Route::Handler handler) {
customHandlers.push_back(std::move(handler));
}
void Router::addMiddleware(Route::Middleware middleware) {
middlewares.push_back(std::move(middleware));
}
void Router::addNotFoundHandler(Route::Handler handler) {
notFoundHandler = std::move(handler);
}
......@@ -392,12 +396,23 @@ void Router::invokeNotFoundHandler(const Http::Request &req,
std::move(resp));
}
Route::Status Router::route(const Http::Request &req,
Route::Status Router::route(const Http::Request &http_req,
Http::ResponseWriter response) {
const auto resource = req.resource();
const auto resource = http_req.resource();
if (resource.empty())
throw std::runtime_error("Invalid zero-length URL.");
auto req = http_req;
auto resp = response.clone();
for (const auto &middleware : middlewares) {
auto result = middleware(req, resp);
// Handler returns true, go to the next piped handler, otherwise break and return
if (! result)
return Route::Status::Match;
}
auto &r = routes[req.method()];
const auto sanitized = SegmentTreeNode::sanitizeResource(resource);
const std::string_view path{sanitized.data(), sanitized.size()};
......@@ -408,7 +423,7 @@ Route::Status Router::route(const Http::Request &req,
auto params = std::get<1>(result);
auto splats = std::get<2>(result);
route->invokeHandler(Request(req, std::move(params), std::move(splats)),
std::move(response));
std::move(resp));
return Route::Status::Match;
}
......
......@@ -278,3 +278,97 @@ TEST(router_test, test_bind_shared_ptr) {
endpoint->shutdown();
}
class HandlerWithAuthMiddleware :public MyHandler {
public:
HandlerWithAuthMiddleware()
: MyHandler(), auth_count(0), auth_succ_count(0) {}
bool do_auth(Pistache::Http::Request &request, Pistache::Http::ResponseWriter &response) {
auth_count++;
try {
auto auth = request.headers().get<Pistache::Http::Header::Authorization>();
if (auth->getMethod() == Pistache::Http::Header::Authorization::Method::Basic) {
auth_succ_count++;
return true;
} else {
response.send(Pistache::Http::Code::Unauthorized);
return false;
}
} catch (std::runtime_error&) {
return false;
}
}
int getAuthCount() { return auth_count; }
int getSuccAuthCount() { return auth_succ_count; }
private:
int auth_count;
int auth_succ_count;
};
bool fill_auth_header(Pistache::Http::Request &request, Pistache::Http::ResponseWriter &response) {
auto au = Pistache::Http::Header::Authorization();
au.setBasicUserPassword("foo", "bar");
request.headers().add<decltype(au)>(au);
return true;
}
bool stop_processing(Pistache::Http::Request &request, Pistache::Http::ResponseWriter &response) {
response.send(Pistache::Http::Code::No_Content);
return false;
}
TEST(router_test, test_middleware_stop_processing) {
Address addr(Ipv4::any(), 0);
auto endpoint = std::make_shared<Http::Endpoint>(addr);
auto opts = Http::Endpoint::options().threads(1);
endpoint->init(opts);
auto sharedPtr = std::make_shared<HandlerWithAuthMiddleware>();
Rest::Router router;
router.addMiddleware(Routes::middleware(&stop_processing));
Routes::Head(router, "/tinkywinky", Routes::bind(&HandlerWithAuthMiddleware::handle, sharedPtr));
endpoint->setHandler(router.handler());
endpoint->serveThreaded();
const auto bound_port = endpoint->getPort();
httplib::Client client("localhost", bound_port);
ASSERT_EQ(sharedPtr->getCount(), 0);
auto response = client.Head("/tinkywinky");
ASSERT_EQ(sharedPtr->getCount(), 0);
ASSERT_EQ(response->status, int(Pistache::Http::Code::No_Content));
}
TEST(router_test, test_auth_middleware) {
Address addr(Ipv4::any(), 0);
auto endpoint = std::make_shared<Http::Endpoint>(addr);
auto opts = Http::Endpoint::options().threads(1);
endpoint->init(opts);
HandlerWithAuthMiddleware handler;
Rest::Router router;
router.addMiddleware(Routes::middleware(&fill_auth_header));
router.addMiddleware(Routes::middleware(&HandlerWithAuthMiddleware::do_auth, &handler));
Routes::Head(router, "/tinkywinky", Routes::bind(&HandlerWithAuthMiddleware::handle, &handler));
endpoint->setHandler(router.handler());
endpoint->serveThreaded();
const auto bound_port = endpoint->getPort();
httplib::Client client("localhost", bound_port);
ASSERT_EQ(handler.getCount(), 0);
auto response = client.Head("/tinkywinky");
ASSERT_EQ(handler.getCount(), 1);
ASSERT_EQ(handler.getAuthCount(), 1);
ASSERT_EQ(handler.getSuccAuthCount(), 1);
ASSERT_EQ(response->status, int(Pistache::Http::Code::Ok));
}
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment