mirror of
https://gitee.com/an-tao/drogon.git
synced 2024-11-29 18:27:43 +08:00
Optimize plugins with redirection functions (#1776)
Note: after this submission, users who use the SecureSSLRedirector plugin and the SlashRemover plugin should add the following line to the configuration file: { "name": "drogon::plugin::Redirector", "dependencies": [], "config": { } } and add the plugin name "drogon::plugin::Redirector" to the dependencies list of the SecureSSLRedirector plugin and the SlashRemover plugin.
This commit is contained in:
parent
cedeeb59f4
commit
112d19ff12
@ -268,6 +268,7 @@ set(DROGON_SOURCES
|
|||||||
lib/src/RateLimiter.cc
|
lib/src/RateLimiter.cc
|
||||||
lib/src/RealIpResolver.cc
|
lib/src/RealIpResolver.cc
|
||||||
lib/src/SecureSSLRedirector.cc
|
lib/src/SecureSSLRedirector.cc
|
||||||
|
lib/src/Redirector.cc
|
||||||
lib/src/SessionManager.cc
|
lib/src/SessionManager.cc
|
||||||
lib/src/SlashRemover.cc
|
lib/src/SlashRemover.cc
|
||||||
lib/src/SlidingWindowRateLimiter.cc
|
lib/src/SlidingWindowRateLimiter.cc
|
||||||
@ -710,6 +711,7 @@ install(FILES ${DROGON_MONITORING_HEADERS}
|
|||||||
|
|
||||||
set(DROGON_PLUGIN_HEADERS
|
set(DROGON_PLUGIN_HEADERS
|
||||||
lib/inc/drogon/plugins/Plugin.h
|
lib/inc/drogon/plugins/Plugin.h
|
||||||
|
lib/inc/drogon/plugins/Redirector.h
|
||||||
lib/inc/drogon/plugins/SecureSSLRedirector.h
|
lib/inc/drogon/plugins/SecureSSLRedirector.h
|
||||||
lib/inc/drogon/plugins/AccessLogger.h
|
lib/inc/drogon/plugins/AccessLogger.h
|
||||||
lib/inc/drogon/plugins/RealIpResolver.h
|
lib/inc/drogon/plugins/RealIpResolver.h
|
||||||
|
@ -416,6 +416,7 @@ class DROGON_EXPORT HttpRequest
|
|||||||
|
|
||||||
/// Set the path of the request
|
/// Set the path of the request
|
||||||
virtual void setPath(const std::string &path) = 0;
|
virtual void setPath(const std::string &path) = 0;
|
||||||
|
virtual void setPath(std::string &&path) = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The default behavior is to encode the value of setPath
|
* @brief The default behavior is to encode the value of setPath
|
||||||
|
119
lib/inc/drogon/plugins/Redirector.h
Normal file
119
lib/inc/drogon/plugins/Redirector.h
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* @file Redirector.h
|
||||||
|
* @author An Tao
|
||||||
|
*
|
||||||
|
* Copyright 2018, An Tao. All rights reserved.
|
||||||
|
* https://github.com/an-tao/drogon
|
||||||
|
* Use of this source code is governed by a MIT license
|
||||||
|
* that can be found in the License file.
|
||||||
|
*
|
||||||
|
* Drogon
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <drogon/plugins/Plugin.h>
|
||||||
|
#include <drogon/HttpRequest.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace drogon
|
||||||
|
{
|
||||||
|
namespace plugin
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief The RedirectorHandler is a function object that can be registered to
|
||||||
|
* the Redirector plugin. It is used to redirect requests to proper URLs. Users
|
||||||
|
* can modify the protocol, host and path of the request. If a false value is
|
||||||
|
* returned, the request will be considered as invalid and a 404 response will
|
||||||
|
* be sent to the client.
|
||||||
|
*/
|
||||||
|
using RedirectorHandler =
|
||||||
|
std::function<bool(const drogon::HttpRequestPtr &,
|
||||||
|
std::string &, //"http://" or "https://"
|
||||||
|
std::string &, // host
|
||||||
|
bool &)>; // path changed or not
|
||||||
|
/**
|
||||||
|
* @brief The PathRewriteHandler is a function object that can be registered to
|
||||||
|
* the Redirector plugin. It is used to rewrite the path of the request. The
|
||||||
|
* Redirector plugin will call all registered PathRewriteHandlers in the order
|
||||||
|
* of registration. If one or more handlers return true, the request will be
|
||||||
|
* redirected to the new path.
|
||||||
|
*/
|
||||||
|
using PathRewriteHandler = std::function<bool(const drogon::HttpRequestPtr &)>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The ForwardHandler is a function object that can be registered to the
|
||||||
|
* Redirector plugin. It is used to forward the request to next processing steps
|
||||||
|
* in the framework. The Redirector plugin will call all registered
|
||||||
|
* ForwardHandlers in the order of registration. Users can use this handler to
|
||||||
|
* change the request path or any other part of the request.
|
||||||
|
*/
|
||||||
|
using ForwardHandler = std::function<void(const drogon::HttpRequestPtr &)>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief This plugin is used to redirect requests to proper URLs. It is a
|
||||||
|
* helper plugin for other plugins, e.g. SlashRemover.
|
||||||
|
* Users can register a handler to this plugin to redirect requests. All
|
||||||
|
* handlers will be called in the order of registration.
|
||||||
|
* The json configuration is as follows:
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
{
|
||||||
|
"name": "drogon::plugin::Redirector",
|
||||||
|
"dependencies": [],
|
||||||
|
"config": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@endcode
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class DROGON_EXPORT Redirector : public drogon::Plugin<Redirector>,
|
||||||
|
public std::enable_shared_from_this<Redirector>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Redirector()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void initAndStart(const Json::Value &config) override;
|
||||||
|
void shutdown() override;
|
||||||
|
|
||||||
|
void registerRedirectHandler(RedirectorHandler &&handler)
|
||||||
|
{
|
||||||
|
handlers_.emplace_back(std::move(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerRedirectHandler(const RedirectorHandler &handler)
|
||||||
|
{
|
||||||
|
handlers_.emplace_back(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerPathRewriteHandler(PathRewriteHandler &&handler)
|
||||||
|
{
|
||||||
|
pathRewriteHandlers_.emplace_back(std::move(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerPathRewriteHandler(const PathRewriteHandler &handler)
|
||||||
|
{
|
||||||
|
pathRewriteHandlers_.emplace_back(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerForwardHandler(ForwardHandler &&handler)
|
||||||
|
{
|
||||||
|
forwardHandlers_.emplace_back(std::move(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerForwardHandler(const ForwardHandler &handler)
|
||||||
|
{
|
||||||
|
forwardHandlers_.emplace_back(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<RedirectorHandler> handlers_;
|
||||||
|
std::vector<PathRewriteHandler> pathRewriteHandlers_;
|
||||||
|
std::vector<ForwardHandler> forwardHandlers_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace plugin
|
||||||
|
} // namespace drogon
|
@ -25,7 +25,7 @@ namespace plugin
|
|||||||
* @code
|
* @code
|
||||||
{
|
{
|
||||||
"name": "drogon::plugin::SecureSSLRedirector",
|
"name": "drogon::plugin::SecureSSLRedirector",
|
||||||
"dependencies": [],
|
"dependencies": ["drogon::plugin::Redirector"],
|
||||||
"config": {
|
"config": {
|
||||||
"ssl_redirect_exempt": ["^/.*\\.jpg", ...],
|
"ssl_redirect_exempt": ["^/.*\\.jpg", ...],
|
||||||
"secure_ssl_host": "localhost:8849"
|
"secure_ssl_host": "localhost:8849"
|
||||||
@ -64,8 +64,12 @@ class DROGON_EXPORT SecureSSLRedirector
|
|||||||
void shutdown() override;
|
void shutdown() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
HttpResponsePtr redirectingAdvice(const HttpRequestPtr &) const;
|
bool redirectingAdvice(const HttpRequestPtr &,
|
||||||
HttpResponsePtr redirectToSSL(const HttpRequestPtr &) const;
|
std::string &,
|
||||||
|
std::string &) const;
|
||||||
|
bool redirectToSSL(const HttpRequestPtr &,
|
||||||
|
std::string &,
|
||||||
|
std::string &) const;
|
||||||
|
|
||||||
std::regex exemptPegex_;
|
std::regex exemptPegex_;
|
||||||
bool regexFlag_{false};
|
bool regexFlag_{false};
|
||||||
|
@ -27,7 +27,7 @@ namespace drogon::plugin
|
|||||||
* @code
|
* @code
|
||||||
{
|
{
|
||||||
"name": "drogon::plugin::SlashRemover",
|
"name": "drogon::plugin::SlashRemover",
|
||||||
"dependencies": [],
|
"dependencies": ["drogon::plugin::Redirector"],
|
||||||
"config": {
|
"config": {
|
||||||
// If true, it removes all trailing slashes, e.g.
|
// If true, it removes all trailing slashes, e.g.
|
||||||
///home// -> ///home
|
///home// -> ///home
|
||||||
|
@ -159,6 +159,11 @@ class HttpRequestImpl : public HttpRequest
|
|||||||
path_ = path;
|
path_ = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setPath(std::string &&path) override
|
||||||
|
{
|
||||||
|
path_ = std::move(path);
|
||||||
|
}
|
||||||
|
|
||||||
void setPathEncode(bool pathEncode) override
|
void setPathEncode(bool pathEncode) override
|
||||||
{
|
{
|
||||||
pathEncode_ = pathEncode;
|
pathEncode_ = pathEncode;
|
||||||
|
87
lib/src/Redirector.cc
Normal file
87
lib/src/Redirector.cc
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
* @file Redirector.cc
|
||||||
|
* An Tao
|
||||||
|
*
|
||||||
|
* Copyright 2018, An Tao. All rights reserved.
|
||||||
|
* https://github.com/an-tao/drogon
|
||||||
|
* Use of this source code is governed by a MIT license
|
||||||
|
* that can be found in the License file.
|
||||||
|
*
|
||||||
|
* Drogon
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <drogon/drogon.h>
|
||||||
|
#include <drogon/plugins/Redirector.h>
|
||||||
|
|
||||||
|
using namespace drogon;
|
||||||
|
using namespace drogon::plugin;
|
||||||
|
|
||||||
|
void Redirector::initAndStart(const Json::Value &config)
|
||||||
|
{
|
||||||
|
auto weakPtr = std::weak_ptr<Redirector>(shared_from_this());
|
||||||
|
drogon::app().registerSyncAdvice(
|
||||||
|
[weakPtr](const HttpRequestPtr &req) -> HttpResponsePtr {
|
||||||
|
auto thisPtr = weakPtr.lock();
|
||||||
|
if (!thisPtr)
|
||||||
|
{
|
||||||
|
return HttpResponsePtr{};
|
||||||
|
}
|
||||||
|
std::string protocol, host;
|
||||||
|
bool pathChanged{false};
|
||||||
|
for (auto &handler : thisPtr->handlers_)
|
||||||
|
{
|
||||||
|
if (!handler(req, protocol, host, pathChanged))
|
||||||
|
{
|
||||||
|
return HttpResponse::newNotFoundResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto &handler : thisPtr->pathRewriteHandlers_)
|
||||||
|
{
|
||||||
|
pathChanged |= handler(req);
|
||||||
|
}
|
||||||
|
if (!protocol.empty() || !host.empty() || pathChanged)
|
||||||
|
{
|
||||||
|
std::string url;
|
||||||
|
if (protocol.empty())
|
||||||
|
{
|
||||||
|
if (!host.empty())
|
||||||
|
{
|
||||||
|
url = req->isOnSecureConnection() ? "https://"
|
||||||
|
: "http://";
|
||||||
|
url.append(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
url = std::move(protocol);
|
||||||
|
if (!host.empty())
|
||||||
|
{
|
||||||
|
url.append(host);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
url.append(req->getHeader("host"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
url.append(req->path());
|
||||||
|
auto &query = req->query();
|
||||||
|
if (!query.empty())
|
||||||
|
{
|
||||||
|
url.append("?").append(query);
|
||||||
|
}
|
||||||
|
return HttpResponse::newRedirectionResponse(url);
|
||||||
|
}
|
||||||
|
for (auto &handler : thisPtr->forwardHandlers_)
|
||||||
|
{
|
||||||
|
handler(req);
|
||||||
|
}
|
||||||
|
return HttpResponsePtr{};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Redirector::shutdown()
|
||||||
|
{
|
||||||
|
LOG_TRACE << "Redirector plugin is shutdown!";
|
||||||
|
}
|
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
#include <drogon/drogon.h>
|
#include <drogon/drogon.h>
|
||||||
#include <drogon/plugins/SecureSSLRedirector.h>
|
#include <drogon/plugins/SecureSSLRedirector.h>
|
||||||
|
#include <drogon/plugins/Redirector.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
using namespace drogon;
|
using namespace drogon;
|
||||||
@ -42,14 +43,24 @@ void SecureSSLRedirector::initAndStart(const Json::Value &config)
|
|||||||
}
|
}
|
||||||
secureHost_ = config.get("secure_ssl_host", "").asString();
|
secureHost_ = config.get("secure_ssl_host", "").asString();
|
||||||
std::weak_ptr<SecureSSLRedirector> weakPtr = shared_from_this();
|
std::weak_ptr<SecureSSLRedirector> weakPtr = shared_from_this();
|
||||||
app().registerSyncAdvice([weakPtr](const HttpRequestPtr &req) {
|
auto redirector = drogon::app().getPlugin<Redirector>();
|
||||||
auto thisPtr = weakPtr.lock();
|
if (!redirector)
|
||||||
if (!thisPtr)
|
{
|
||||||
{
|
LOG_ERROR << "Redirector plugin is not found!";
|
||||||
return HttpResponsePtr{};
|
return;
|
||||||
}
|
}
|
||||||
return thisPtr->redirectingAdvice(req);
|
redirector->registerRedirectHandler(
|
||||||
});
|
[weakPtr](const drogon::HttpRequestPtr &req,
|
||||||
|
std::string &protocol,
|
||||||
|
std::string &host,
|
||||||
|
bool &) -> bool {
|
||||||
|
auto thisPtr = weakPtr.lock();
|
||||||
|
if (!thisPtr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return thisPtr->redirectingAdvice(req, protocol, host);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void SecureSSLRedirector::shutdown()
|
void SecureSSLRedirector::shutdown()
|
||||||
@ -57,60 +68,58 @@ void SecureSSLRedirector::shutdown()
|
|||||||
/// Shutdown the plugin
|
/// Shutdown the plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpResponsePtr SecureSSLRedirector::redirectingAdvice(
|
bool SecureSSLRedirector::redirectingAdvice(const HttpRequestPtr &req,
|
||||||
const HttpRequestPtr &req) const
|
std::string &protocol,
|
||||||
|
std::string &host) const
|
||||||
{
|
{
|
||||||
if (req->isOnSecureConnection())
|
if (req->isOnSecureConnection() || protocol == "https://")
|
||||||
{
|
{
|
||||||
return HttpResponsePtr{};
|
return true;
|
||||||
}
|
}
|
||||||
else if (regexFlag_)
|
else if (regexFlag_)
|
||||||
{
|
{
|
||||||
std::smatch regexResult;
|
std::smatch regexResult;
|
||||||
if (std::regex_match(req->path(), regexResult, exemptPegex_))
|
if (std::regex_match(req->path(), regexResult, exemptPegex_))
|
||||||
{
|
{
|
||||||
return HttpResponsePtr{};
|
return true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return redirectToSSL(req);
|
return redirectToSSL(req, protocol, host);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return redirectToSSL(req);
|
return redirectToSSL(req, protocol, host);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpResponsePtr SecureSSLRedirector::redirectToSSL(
|
bool SecureSSLRedirector::redirectToSSL(const HttpRequestPtr &req,
|
||||||
const HttpRequestPtr &req) const
|
std::string &protocol,
|
||||||
|
std::string &host) const
|
||||||
{
|
{
|
||||||
if (!secureHost_.empty())
|
if (!secureHost_.empty())
|
||||||
{
|
{
|
||||||
static std::string urlPrefix{"https://" + secureHost_};
|
host = secureHost_;
|
||||||
std::string query{urlPrefix + req->path()};
|
protocol = "https://";
|
||||||
if (!req->query().empty())
|
return true;
|
||||||
{
|
|
||||||
query += "?" + req->query();
|
|
||||||
}
|
|
||||||
return HttpResponse::newRedirectionResponse(query);
|
|
||||||
}
|
}
|
||||||
else
|
else if (host.empty())
|
||||||
{
|
{
|
||||||
const auto &host = req->getHeader("host");
|
const auto &reqHost = req->getHeader("host");
|
||||||
if (!host.empty())
|
if (!reqHost.empty())
|
||||||
{
|
{
|
||||||
std::string query{"https://" + host};
|
protocol = "https://";
|
||||||
query += req->path();
|
return true;
|
||||||
if (!req->query().empty())
|
|
||||||
{
|
|
||||||
query += "?" + req->query();
|
|
||||||
}
|
|
||||||
return HttpResponse::newRedirectionResponse(query);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return HttpResponse::newNotFoundResponse();
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
protocol = "https://";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
#include "drogon/plugins/SlashRemover.h"
|
#include <drogon/plugins/SlashRemover.h>
|
||||||
#include "drogon/HttpAppFramework.h"
|
#include <drogon/plugins/Redirector.h>
|
||||||
|
#include <drogon/HttpAppFramework.h>
|
||||||
#include "drogon/utils/FunctionTraits.h"
|
#include "drogon/utils/FunctionTraits.h"
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <regex>
|
||||||
|
|
||||||
using namespace drogon;
|
using namespace drogon;
|
||||||
using namespace plugin;
|
using namespace drogon::plugin;
|
||||||
using std::string;
|
using std::string;
|
||||||
|
|
||||||
#define TRAILING_SLASH_REGEX ".+\\/$"
|
#define TRAILING_SLASH_REGEX ".+\\/$"
|
||||||
@ -64,6 +66,31 @@ static inline void removeExcessiveSlashes(string& url)
|
|||||||
removeDuplicateSlashes(url);
|
removeDuplicateSlashes(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline bool handleReq(const drogon::HttpRequestPtr& req, int removeMode)
|
||||||
|
{
|
||||||
|
static const std::regex regex(regexes[removeMode - 1]);
|
||||||
|
if (std::regex_match(req->path(), regex))
|
||||||
|
{
|
||||||
|
string newPath = req->path();
|
||||||
|
switch (removeMode)
|
||||||
|
{
|
||||||
|
case trailing:
|
||||||
|
removeTrailingSlashes(newPath);
|
||||||
|
break;
|
||||||
|
case duplicate:
|
||||||
|
removeDuplicateSlashes(newPath);
|
||||||
|
break;
|
||||||
|
case both:
|
||||||
|
default:
|
||||||
|
removeExcessiveSlashes(newPath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
req->setPath(std::move(newPath));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void SlashRemover::initAndStart(const Json::Value& config)
|
void SlashRemover::initAndStart(const Json::Value& config)
|
||||||
{
|
{
|
||||||
trailingSlashes_ = config.get("remove_trailing_slashes", true).asBool();
|
trailingSlashes_ = config.get("remove_trailing_slashes", true).asBool();
|
||||||
@ -73,33 +100,23 @@ void SlashRemover::initAndStart(const Json::Value& config)
|
|||||||
(trailingSlashes_ * trailing) | (duplicateSlashes_ * duplicate);
|
(trailingSlashes_ * trailing) | (duplicateSlashes_ * duplicate);
|
||||||
if (!removeMode)
|
if (!removeMode)
|
||||||
return;
|
return;
|
||||||
app().registerHandlerViaRegex(
|
auto redirector = app().getPlugin<Redirector>();
|
||||||
regexes[removeMode - 1],
|
if (!redirector)
|
||||||
[removeMode,
|
{
|
||||||
this](const HttpRequestPtr& req,
|
LOG_ERROR << "Redirector plugin is not found!";
|
||||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
return;
|
||||||
string newPath = req->path();
|
}
|
||||||
switch (removeMode)
|
auto func = [removeMode](const HttpRequestPtr& req) -> bool {
|
||||||
{
|
return handleReq(req, removeMode);
|
||||||
case trailing:
|
};
|
||||||
removeTrailingSlashes(newPath);
|
if (redirect_)
|
||||||
break;
|
{
|
||||||
case duplicate:
|
redirector->registerPathRewriteHandler(std::move(func));
|
||||||
removeDuplicateSlashes(newPath);
|
}
|
||||||
break;
|
else
|
||||||
case both:
|
{
|
||||||
default:
|
redirector->registerForwardHandler(std::move(func));
|
||||||
removeExcessiveSlashes(newPath);
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (redirect_)
|
|
||||||
callback(HttpResponse::newRedirectionResponse(newPath));
|
|
||||||
else
|
|
||||||
{
|
|
||||||
req->setPath(newPath);
|
|
||||||
app().forward(req, std::move(callback));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SlashRemover::shutdown()
|
void SlashRemover::shutdown()
|
||||||
|
Loading…
Reference in New Issue
Block a user