mirror of
https://github.com/XRPLF/clio.git
synced 2026-04-29 15:37:53 +00:00
feat: Proxy support (#2490)
Add client IP resolving support in case when there is a proxy in front of Clio.
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
|
||||
#include "util/JsonUtils.hpp"
|
||||
#include "util/Shasum.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
|
||||
#include <boost/beast/http/field.hpp>
|
||||
@@ -42,15 +43,8 @@ IPAdminVerificationStrategy::isAdmin(RequestHeader const&, std::string_view ip)
|
||||
}
|
||||
|
||||
PasswordAdminVerificationStrategy::PasswordAdminVerificationStrategy(std::string const& password)
|
||||
: passwordSha256_(util::toUpper(util::sha256sumString(password)))
|
||||
{
|
||||
ripple::sha256_hasher hasher;
|
||||
hasher(password.data(), password.size());
|
||||
auto const d = static_cast<ripple::sha256_hasher::result_type>(hasher);
|
||||
ripple::uint256 sha256;
|
||||
std::memcpy(sha256.data(), d.data(), d.size());
|
||||
passwordSha256_ = ripple::to_string(sha256);
|
||||
// make sure it's uppercase
|
||||
passwordSha256_ = util::toUpper(std::move(passwordSha256_));
|
||||
}
|
||||
|
||||
bool
|
||||
|
||||
@@ -15,6 +15,7 @@ target_sources(
|
||||
ng/Response.cpp
|
||||
ng/Server.cpp
|
||||
ng/SubscriptionContext.cpp
|
||||
ProxyIpResolver.cpp
|
||||
Resolver.cpp
|
||||
SubscriptionContext.cpp
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/PlainWsSession.hpp"
|
||||
#include "web/ProxyIpResolver.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/impl/HttpBase.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
@@ -64,6 +65,7 @@ public:
|
||||
* @param socket The socket. Ownership is transferred to HttpSession
|
||||
* @param ip Client's IP address
|
||||
* @param adminVerification The admin verification strategy to use
|
||||
* @param proxyIpResolver The client ip resolver if a request was forwarded by a proxy
|
||||
* @param tagFactory A factory that is used to generate tags to track requests and sessions
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
@@ -74,6 +76,7 @@ public:
|
||||
tcp::socket&& socket,
|
||||
std::string const& ip,
|
||||
std::shared_ptr<AdminVerificationStrategy> const& adminVerification,
|
||||
std::shared_ptr<ProxyIpResolver> proxyIpResolver,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
@@ -84,6 +87,7 @@ public:
|
||||
ip,
|
||||
tagFactory,
|
||||
adminVerification,
|
||||
std::move(proxyIpResolver),
|
||||
dosGuard,
|
||||
handler,
|
||||
std::move(buffer)
|
||||
@@ -129,7 +133,7 @@ public:
|
||||
{
|
||||
std::make_shared<WsUpgrader<HandlerType>>(
|
||||
std::move(stream_),
|
||||
this->clientIp,
|
||||
this->clientIp_,
|
||||
tagFactory_,
|
||||
this->dosGuard_,
|
||||
this->handler_,
|
||||
|
||||
119
src/web/ProxyIpResolver.cpp
Normal file
119
src/web/ProxyIpResolver.cpp
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2025, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "web/ProxyIpResolver.hpp"
|
||||
|
||||
#include "util/JsonUtils.hpp"
|
||||
#include "util/Shasum.hpp"
|
||||
#include "util/config/ArrayView.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ValueView.hpp"
|
||||
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace web {
|
||||
|
||||
ProxyIpResolver::ProxyIpResolver(std::unordered_set<std::string> proxyIps, std::unordered_set<std::string> proxyTokens)
|
||||
: proxyIps_(std::move(proxyIps))
|
||||
{
|
||||
proxyTokens_.reserve(proxyTokens.size());
|
||||
for (auto const& t : proxyTokens) {
|
||||
proxyTokens_.insert(util::sha256sum(t));
|
||||
}
|
||||
}
|
||||
|
||||
ProxyIpResolver
|
||||
ProxyIpResolver::fromConfig(util::config::ClioConfigDefinition const& config)
|
||||
{
|
||||
using util::config::ValueView;
|
||||
|
||||
std::unordered_set<std::string> ips;
|
||||
auto const ipsFromConfig = config.getArray("server.proxy.ips");
|
||||
for (auto it = ipsFromConfig.begin<ValueView>(); it != ipsFromConfig.end<ValueView>(); ++it) {
|
||||
ips.insert((*it).asString());
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> tokens;
|
||||
auto const tokensFromConfig = config.getArray("server.proxy.tokens");
|
||||
for (auto it = tokensFromConfig.begin<ValueView>(); it != tokensFromConfig.end<ValueView>(); ++it) {
|
||||
tokens.insert((*it).asString());
|
||||
}
|
||||
|
||||
return ProxyIpResolver{std::move(ips), std::move(tokens)};
|
||||
}
|
||||
|
||||
std::string
|
||||
ProxyIpResolver::resolveClientIp(std::string const& connectionIp, HttpHeaders const& headers) const
|
||||
{
|
||||
if (proxyIps_.contains(connectionIp)) {
|
||||
return extractClientIp(headers).value_or(connectionIp);
|
||||
}
|
||||
|
||||
if (auto it = headers.find(kPROXY_TOKEN_HEADER); it != headers.end()) {
|
||||
auto const tokenHash = util::sha256sum(it->value());
|
||||
if (proxyTokens_.contains(tokenHash)) {
|
||||
return extractClientIp(headers).value_or(connectionIp);
|
||||
}
|
||||
}
|
||||
return connectionIp;
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
ProxyIpResolver::extractClientIp(HttpHeaders const& headers)
|
||||
{
|
||||
auto const it = headers.find(boost::beast::http::field::forwarded);
|
||||
if (it == headers.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Forwarded header is case insensitive:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Forwarded#using_the_forwarded_header
|
||||
auto const headerValue = util::toLower(it->value());
|
||||
|
||||
static constexpr std::string_view kFOR_PREFIX = "for=";
|
||||
auto const startPos = headerValue.find(kFOR_PREFIX);
|
||||
if (startPos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto value = it->value().substr(startPos + kFOR_PREFIX.size());
|
||||
|
||||
static constexpr char kDELIMITER = ';';
|
||||
auto const endPos = value.find(kDELIMITER);
|
||||
auto const ip = value.substr(0, endPos);
|
||||
|
||||
static constexpr auto kMIN_IP_LENGTH = 7; // minimum 3 dots + 4 digits
|
||||
if (ip.size() < kMIN_IP_LENGTH) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (ip.starts_with('"')) {
|
||||
return ip.substr(1, ip.size() - 2);
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
} // namespace web
|
||||
101
src/web/ProxyIpResolver.hpp
Normal file
101
src/web/ProxyIpResolver.hpp
Normal file
@@ -0,0 +1,101 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2025, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ValueView.hpp"
|
||||
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief Resolves the client's IP address, considering proxy servers.
|
||||
*
|
||||
* This class is designed to determine the original IP address of a client when the connection
|
||||
* is forwarded through a proxy server. It uses a configurable list of trusted proxy IPs
|
||||
* and proxy tokens to decide whether to trust the `Forwarded` HTTP header.
|
||||
*/
|
||||
class ProxyIpResolver {
|
||||
std::unordered_set<std::string> proxyIps_;
|
||||
// ripple::uint256 doesn't have hash implementation
|
||||
std::unordered_set<ripple::uint256, ripple::uint256::hasher> proxyTokens_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a ProxyIpResolver.
|
||||
*
|
||||
* @param proxyIps A set of trusted proxy IP addresses.
|
||||
* @param proxyTokens A set of trusted proxy tokens. The tokens will be hashed with SHA-256.
|
||||
*/
|
||||
ProxyIpResolver(std::unordered_set<std::string> proxyIps, std::unordered_set<std::string> proxyTokens);
|
||||
|
||||
/**
|
||||
* @brief Creates a ProxyIpResolver from a configuration.
|
||||
*
|
||||
* The configuration should contain `server.proxy.ips` and `server.proxy.tokens` arrays.
|
||||
*
|
||||
* @param config The Clio configuration.
|
||||
* @return A new ProxyIpResolver instance.
|
||||
*/
|
||||
static ProxyIpResolver
|
||||
fromConfig(util::config::ClioConfigDefinition const& config);
|
||||
|
||||
using HttpHeaders = boost::beast::http::request<boost::beast::http::string_body>::header_type;
|
||||
|
||||
static constexpr std::string_view kPROXY_TOKEN_HEADER = "X-Proxy-Token";
|
||||
|
||||
/**
|
||||
* @brief Resolves the client's IP address from the connection IP and HTTP headers.
|
||||
*
|
||||
* If the connection IP is in the trusted proxy list, or if a valid proxy token is provided in the headers,
|
||||
* this method will attempt to extract the client's IP from the `Forwarded` header.
|
||||
* Otherwise, it returns the connection IP.
|
||||
*
|
||||
* @param connectionIp The IP address of the direct connection.
|
||||
* @param headers The HTTP request headers.
|
||||
* @return The resolved client IP address as a string.
|
||||
*/
|
||||
std::string
|
||||
resolveClientIp(std::string const& connectionIp, HttpHeaders const& headers) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Extracts the client IP from the `Forwarded` HTTP header.
|
||||
*
|
||||
* The `Forwarded` header is expected to be in the format specified by RFC 7239.
|
||||
* This function looks for the `for` parameter.
|
||||
*
|
||||
* @param headers The HTTP request headers.
|
||||
* @return The client IP address as a string if found, otherwise std::nullopt.
|
||||
*/
|
||||
static std::optional<std::string>
|
||||
extractClientIp(HttpHeaders const& headers);
|
||||
};
|
||||
|
||||
} // namespace web
|
||||
@@ -107,7 +107,7 @@ public:
|
||||
void
|
||||
operator()(std::string const& request, std::shared_ptr<web::ConnectionBase> const& connection)
|
||||
{
|
||||
if (not dosguard_.get().isOk(connection->clientIp)) {
|
||||
if (not dosguard_.get().isOk(connection->clientIp())) {
|
||||
connection->sendSlowDown(request);
|
||||
return;
|
||||
}
|
||||
@@ -119,7 +119,7 @@ public:
|
||||
if (not connection->upgraded and shouldReplaceParams(req))
|
||||
req[JS(params)] = boost::json::array({boost::json::object{}});
|
||||
|
||||
if (not dosguard_.get().request(connection->clientIp, req)) {
|
||||
if (not dosguard_.get().request(connection->clientIp(), req)) {
|
||||
connection->sendSlowDown(request);
|
||||
return;
|
||||
}
|
||||
@@ -128,7 +128,7 @@ public:
|
||||
[this, request = std::move(req), connection](boost::asio::yield_context yield) mutable {
|
||||
handleRequest(yield, std::move(request), connection);
|
||||
},
|
||||
connection->clientIp
|
||||
connection->clientIp()
|
||||
)) {
|
||||
rpcEngine_->notifyTooBusy();
|
||||
web::impl::ErrorHelper(connection).sendTooBusyError();
|
||||
@@ -160,7 +160,7 @@ private:
|
||||
{
|
||||
LOG(log_.info()) << connection->tag() << (connection->upgraded ? "ws" : "http")
|
||||
<< " received request from work queue: " << util::removeSecret(request)
|
||||
<< " ip = " << connection->clientIp;
|
||||
<< " ip = " << connection->clientIp();
|
||||
|
||||
try {
|
||||
auto const range = backend_->fetchLedgerRange();
|
||||
@@ -180,7 +180,7 @@ private:
|
||||
connection->makeSubscriptionContext(tagFactory_),
|
||||
tagFactory_.with(connection->tag()),
|
||||
*range,
|
||||
connection->clientIp,
|
||||
connection->clientIp(),
|
||||
std::cref(apiVersionParser_),
|
||||
connection->isAdmin()
|
||||
);
|
||||
@@ -190,7 +190,7 @@ private:
|
||||
request,
|
||||
tagFactory_.with(connection->tag()),
|
||||
*range,
|
||||
connection->clientIp,
|
||||
connection->clientIp(),
|
||||
std::cref(apiVersionParser_),
|
||||
connection->isAdmin()
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/HttpSession.hpp"
|
||||
#include "web/ProxyIpResolver.hpp"
|
||||
#include "web/SslHttpSession.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
@@ -87,6 +88,7 @@ class Detector : public std::enable_shared_from_this<Detector<PlainSessionType,
|
||||
boost::beast::flat_buffer buffer_;
|
||||
std::shared_ptr<AdminVerificationStrategy> const adminVerification_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
std::shared_ptr<ProxyIpResolver> proxyIpResolver_;
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -99,6 +101,7 @@ public:
|
||||
* @param handler The server handler to use
|
||||
* @param adminVerification The admin verification strategy to use
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
* @param proxyIpResolver The client ip resolver if a request was forwarded by a proxy
|
||||
*/
|
||||
Detector(
|
||||
tcp::socket&& socket,
|
||||
@@ -107,7 +110,8 @@ public:
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> handler,
|
||||
std::shared_ptr<AdminVerificationStrategy> adminVerification,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
std::uint32_t maxWsSendingQueueSize,
|
||||
std::shared_ptr<ProxyIpResolver> proxyIpResolver
|
||||
)
|
||||
: stream_(std::move(socket))
|
||||
, ctx_(ctx)
|
||||
@@ -116,6 +120,7 @@ public:
|
||||
, handler_(std::move(handler))
|
||||
, adminVerification_(std::move(adminVerification))
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
, proxyIpResolver_(std::move(proxyIpResolver))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -169,6 +174,7 @@ public:
|
||||
stream_.release_socket(),
|
||||
ip,
|
||||
adminVerification_,
|
||||
proxyIpResolver_,
|
||||
*ctx_,
|
||||
tagFactory_,
|
||||
dosGuard_,
|
||||
@@ -184,6 +190,7 @@ public:
|
||||
stream_.release_socket(),
|
||||
ip,
|
||||
adminVerification_,
|
||||
proxyIpResolver_,
|
||||
tagFactory_,
|
||||
dosGuard_,
|
||||
handler_,
|
||||
@@ -219,6 +226,7 @@ class Server : public std::enable_shared_from_this<Server<PlainSessionType, SslS
|
||||
tcp::acceptor acceptor_;
|
||||
std::shared_ptr<AdminVerificationStrategy> adminVerification_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
std::shared_ptr<ProxyIpResolver> proxyIpResolver_;
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -232,6 +240,7 @@ public:
|
||||
* @param handler The server handler to use
|
||||
* @param adminVerification The admin verification strategy to use
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
* @param proxyIpResolver The client ip resolver if a request was forwarded by a proxy
|
||||
*/
|
||||
Server(
|
||||
boost::asio::io_context& ioc,
|
||||
@@ -241,7 +250,8 @@ public:
|
||||
dosguard::DOSGuardInterface& dosGuard,
|
||||
std::shared_ptr<HandlerType> handler,
|
||||
std::shared_ptr<AdminVerificationStrategy> adminVerification,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
std::uint32_t maxWsSendingQueueSize,
|
||||
ProxyIpResolver proxyIpResolver
|
||||
)
|
||||
: ioc_(std::ref(ioc))
|
||||
, ctx_(std::move(ctx))
|
||||
@@ -251,6 +261,7 @@ public:
|
||||
, acceptor_(boost::asio::make_strand(ioc))
|
||||
, adminVerification_(std::move(adminVerification))
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
, proxyIpResolver_(std::make_shared<ProxyIpResolver>(std::move(proxyIpResolver)))
|
||||
{
|
||||
boost::beast::error_code ec;
|
||||
|
||||
@@ -310,7 +321,8 @@ private:
|
||||
dosGuard_,
|
||||
handler_,
|
||||
adminVerification_,
|
||||
maxWsSendingQueueSize_
|
||||
maxWsSendingQueueSize_,
|
||||
proxyIpResolver_
|
||||
)
|
||||
->run();
|
||||
}
|
||||
@@ -364,6 +376,8 @@ makeHttpServer(
|
||||
// each ledger. we allow user delay 3 ledgers by default
|
||||
auto const maxWsSendingQueueSize = serverConfig.get<uint32_t>("ws_max_sending_queue_size");
|
||||
|
||||
auto proxyIpResolver = ProxyIpResolver::fromConfig(config);
|
||||
|
||||
auto server = std::make_shared<HttpServer<HandlerType>>(
|
||||
ioc,
|
||||
std::move(expectedSslContext).value(),
|
||||
@@ -372,7 +386,8 @@ makeHttpServer(
|
||||
dosGuard,
|
||||
handler,
|
||||
std::move(expectedAdminVerification).value(),
|
||||
maxWsSendingQueueSize
|
||||
maxWsSendingQueueSize,
|
||||
std::move(proxyIpResolver)
|
||||
);
|
||||
|
||||
server->run();
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/ProxyIpResolver.hpp"
|
||||
#include "web/SslWsSession.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/impl/HttpBase.hpp"
|
||||
@@ -70,6 +71,7 @@ public:
|
||||
* @param socket The socket. Ownership is transferred to HttpSession
|
||||
* @param ip Client's IP address
|
||||
* @param adminVerification The admin verification strategy to use
|
||||
* @param proxyIpResolver The client ip resolver if a request was forwarded by a proxy
|
||||
* @param ctx The SSL context
|
||||
* @param tagFactory A factory that is used to generate tags to track requests and sessions
|
||||
* @param dosGuard The denial of service guard to use
|
||||
@@ -81,6 +83,7 @@ public:
|
||||
tcp::socket&& socket,
|
||||
std::string const& ip,
|
||||
std::shared_ptr<AdminVerificationStrategy> const& adminVerification,
|
||||
std::shared_ptr<ProxyIpResolver> proxyIpResolver,
|
||||
boost::asio::ssl::context& ctx,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
@@ -92,6 +95,7 @@ public:
|
||||
ip,
|
||||
tagFactory,
|
||||
adminVerification,
|
||||
std::move(proxyIpResolver),
|
||||
dosGuard,
|
||||
handler,
|
||||
std::move(buffer)
|
||||
@@ -173,7 +177,7 @@ public:
|
||||
{
|
||||
std::make_shared<SslWsUpgrader<HandlerType>>(
|
||||
std::move(stream_),
|
||||
this->clientIp,
|
||||
this->clientIp_,
|
||||
tagFactory_,
|
||||
this->dosGuard_,
|
||||
this->handler_,
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "util/prometheus/Http.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/ProxyIpResolver.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
@@ -120,6 +121,7 @@ class HttpBase : public ConnectionBase {
|
||||
std::shared_ptr<void> res_;
|
||||
SendLambda sender_;
|
||||
std::shared_ptr<AdminVerificationStrategy> adminVerification_;
|
||||
std::shared_ptr<ProxyIpResolver> proxyIpResolver_;
|
||||
|
||||
protected:
|
||||
boost::beast::flat_buffer buffer_;
|
||||
@@ -164,6 +166,7 @@ public:
|
||||
std::string const& ip,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::shared_ptr<AdminVerificationStrategy> adminVerification,
|
||||
std::shared_ptr<ProxyIpResolver> proxyIpResolver,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> handler,
|
||||
boost::beast::flat_buffer buffer
|
||||
@@ -171,6 +174,7 @@ public:
|
||||
: ConnectionBase(tagFactory, ip)
|
||||
, sender_(*this)
|
||||
, adminVerification_(std::move(adminVerification))
|
||||
, proxyIpResolver_(std::move(proxyIpResolver))
|
||||
, buffer_(std::move(buffer))
|
||||
, dosGuard_(dosGuard)
|
||||
, handler_(std::move(handler))
|
||||
@@ -183,7 +187,7 @@ public:
|
||||
{
|
||||
LOG(perfLog_.debug()) << tag() << "http session closed";
|
||||
if (not upgraded)
|
||||
dosGuard_.get().decrement(this->clientIp);
|
||||
dosGuard_.get().decrement(clientIp_);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -218,11 +222,19 @@ public:
|
||||
if (req_.method() == http::verb::get and req_.target() == "/health")
|
||||
return sender_(httpResponse(http::status::ok, "text/html", kHEALTH_CHECK_HTML));
|
||||
|
||||
if (auto resolvedIp = proxyIpResolver_->resolveClientIp(clientIp_, req_); resolvedIp != clientIp_) {
|
||||
LOG(log_.info()) << tag() << "Detected a forwarded request from proxy. Proxy ip: " << clientIp_
|
||||
<< ". Resolved client ip: " << resolvedIp;
|
||||
dosGuard_.get().decrement(clientIp_);
|
||||
clientIp_ = std::move(resolvedIp);
|
||||
dosGuard_.get().increment(clientIp_);
|
||||
}
|
||||
|
||||
// Update isAdmin property of the connection
|
||||
ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, this->clientIp);
|
||||
ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, clientIp_);
|
||||
|
||||
if (boost::beast::websocket::is_upgrade(req_)) {
|
||||
if (dosGuard_.get().isOk(this->clientIp)) {
|
||||
if (dosGuard_.get().isOk(clientIp_)) {
|
||||
// Disable the timeout. The websocket::stream uses its own timeout settings.
|
||||
boost::beast::get_lowest_layer(derived().stream()).expires_never();
|
||||
|
||||
@@ -240,7 +252,7 @@ public:
|
||||
return sender_(httpResponse(http::status::bad_request, "text/html", "Expected a POST request"));
|
||||
}
|
||||
|
||||
LOG(log_.info()) << tag() << "Received request from ip = " << clientIp;
|
||||
LOG(log_.info()) << tag() << "Received request from ip = " << clientIp_;
|
||||
|
||||
try {
|
||||
(*handler_)(req_.body(), derived().shared_from_this());
|
||||
@@ -271,7 +283,7 @@ public:
|
||||
void
|
||||
send(std::string&& msg, http::status status = http::status::ok) override
|
||||
{
|
||||
if (!dosGuard_.get().add(clientIp, msg.size())) {
|
||||
if (!dosGuard_.get().add(clientIp_, msg.size())) {
|
||||
auto jsonResponse = boost::json::parse(msg).as_object();
|
||||
jsonResponse["warning"] = "load";
|
||||
if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
|
||||
|
||||
@@ -124,7 +124,7 @@ public:
|
||||
~WsBase() override
|
||||
{
|
||||
LOG(perfLog_.debug()) << tag() << "session closed";
|
||||
dosGuard_.get().decrement(clientIp);
|
||||
dosGuard_.get().decrement(clientIp_);
|
||||
}
|
||||
|
||||
Derived<HandlerType>&
|
||||
@@ -217,7 +217,7 @@ public:
|
||||
void
|
||||
send(std::string&& msg, http::status) override
|
||||
{
|
||||
if (!dosGuard_.get().add(clientIp, msg.size())) {
|
||||
if (!dosGuard_.get().add(clientIp_, msg.size())) {
|
||||
auto jsonResponse = boost::json::parse(msg).as_object();
|
||||
jsonResponse["warning"] = "load";
|
||||
|
||||
@@ -283,7 +283,7 @@ public:
|
||||
if (ec)
|
||||
return wsFail(ec, "read");
|
||||
|
||||
LOG(perfLog_.info()) << tag() << "Received request from ip = " << this->clientIp;
|
||||
LOG(perfLog_.info()) << tag() << "Received request from ip = " << clientIp_;
|
||||
|
||||
std::string requestStr{static_cast<char const*>(buffer_.data().data()), buffer_.size()};
|
||||
|
||||
|
||||
@@ -45,9 +45,9 @@ struct ConnectionBase : public util::Taggable {
|
||||
protected:
|
||||
boost::system::error_code ec_;
|
||||
bool isAdmin_ = false;
|
||||
std::string clientIp_;
|
||||
|
||||
public:
|
||||
std::string const clientIp;
|
||||
bool upgraded = false;
|
||||
|
||||
/**
|
||||
@@ -57,7 +57,7 @@ public:
|
||||
* @param ip The IP address of the connected peer
|
||||
*/
|
||||
ConnectionBase(util::TagDecoratorFactory const& tagFactory, std::string ip)
|
||||
: Taggable(tagFactory), clientIp(std::move(ip))
|
||||
: Taggable(tagFactory), clientIp_(std::move(ip))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -119,5 +119,16 @@ public:
|
||||
{
|
||||
return isAdmin_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the IP address of the client.
|
||||
*
|
||||
* @return The IP address of the client.
|
||||
*/
|
||||
[[nodiscard]] std::string const&
|
||||
clientIp() const
|
||||
{
|
||||
return clientIp_;
|
||||
}
|
||||
};
|
||||
} // namespace web
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
@@ -70,6 +71,17 @@ public:
|
||||
std::string const&
|
||||
ip() const;
|
||||
|
||||
/**
|
||||
* @brief Set the ip of the client.
|
||||
*
|
||||
* @param newIp The new ip to set.
|
||||
*/
|
||||
void
|
||||
setIp(std::string newIp)
|
||||
{
|
||||
ip_ = std::move(newIp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get whether the client is an admin.
|
||||
*
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ObjectView.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/ProxyIpResolver.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/ProcessingPolicy.hpp"
|
||||
@@ -205,16 +206,16 @@ Server::Server(
|
||||
ProcessingPolicy processingPolicy,
|
||||
std::optional<size_t> parallelRequestLimit,
|
||||
util::TagDecoratorFactory tagDecoratorFactory,
|
||||
ProxyIpResolver proxyIpResolver,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize,
|
||||
OnConnectCheck onConnectCheck,
|
||||
OnDisconnectHook onDisconnectHook
|
||||
Hooks hooks
|
||||
)
|
||||
: ctx_{ctx}
|
||||
, sslContext_{std::move(sslContext)}
|
||||
, tagDecoratorFactory_{tagDecoratorFactory}
|
||||
, connectionHandler_{processingPolicy, parallelRequestLimit, tagDecoratorFactory_, maxSubscriptionSendQueueSize, std::move(onDisconnectHook)}
|
||||
, connectionHandler_{processingPolicy, parallelRequestLimit, tagDecoratorFactory_, maxSubscriptionSendQueueSize, std::move(proxyIpResolver), std::move(hooks.onDisconnectHook), std::move(hooks.onIpChangeHook)}
|
||||
, endpoint_{std::move(endpoint)}
|
||||
, onConnectCheck_{std::move(onConnectCheck)}
|
||||
, onConnectCheck_{std::move(hooks.onConnectCheck)}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -337,6 +338,7 @@ std::expected<Server, std::string>
|
||||
makeServer(
|
||||
util::config::ClioConfigDefinition const& config,
|
||||
Server::OnConnectCheck onConnectCheck,
|
||||
Server::OnIpChangeHook onIpChangeHook,
|
||||
Server::OnDisconnectHook onDisconnectHook,
|
||||
boost::asio::io_context& context
|
||||
)
|
||||
@@ -365,6 +367,8 @@ makeServer(
|
||||
|
||||
auto const maxSubscriptionSendQueueSize = serverConfig.get<size_t>("ws_max_sending_queue_size");
|
||||
|
||||
auto proxyIpResolver = ProxyIpResolver::fromConfig(config);
|
||||
|
||||
return std::expected<Server, std::string>{
|
||||
std::in_place,
|
||||
context,
|
||||
@@ -373,9 +377,13 @@ makeServer(
|
||||
processingPolicy,
|
||||
parallelRequestLimit,
|
||||
util::TagDecoratorFactory(config),
|
||||
std::move(proxyIpResolver),
|
||||
maxSubscriptionSendQueueSize,
|
||||
std::move(onConnectCheck),
|
||||
std::move(onDisconnectHook)
|
||||
Server::Hooks{
|
||||
.onConnectCheck = std::move(onConnectCheck),
|
||||
.onIpChangeHook = std::move(onIpChangeHook),
|
||||
.onDisconnectHook = std::move(onDisconnectHook)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/ProxyIpResolver.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/ProcessingPolicy.hpp"
|
||||
@@ -62,11 +63,22 @@ public:
|
||||
*/
|
||||
using OnConnectCheck = std::function<std::expected<void, Response>(Connection const&)>;
|
||||
|
||||
using OnIpChangeHook = impl::ConnectionHandler::OnIpChangeHook;
|
||||
|
||||
/**
|
||||
* @brief Hook called when any connection disconnects
|
||||
*/
|
||||
using OnDisconnectHook = impl::ConnectionHandler::OnDisconnectHook;
|
||||
|
||||
/**
|
||||
* @brief A struct that holds all the hooks for the server.
|
||||
*/
|
||||
struct Hooks {
|
||||
OnConnectCheck onConnectCheck;
|
||||
OnIpChangeHook onIpChangeHook;
|
||||
OnDisconnectHook onDisconnectHook;
|
||||
};
|
||||
|
||||
private:
|
||||
util::Logger log_{"WebServer"};
|
||||
util::Logger perfLog_{"Performance"};
|
||||
@@ -94,9 +106,9 @@ public:
|
||||
* @param parallelRequestLimit The limit of requests for one connection that can be processed in parallel. Only used
|
||||
* if processingPolicy is parallel.
|
||||
* @param tagDecoratorFactory The tag decorator factory.
|
||||
* @param proxyIpResolver The client ip resolver if a request was forwarded by a proxy
|
||||
* @param maxSubscriptionSendQueueSize The maximum size of the subscription send queue.
|
||||
* @param onConnectCheck The check to perform on each connection.
|
||||
* @param onDisconnectHook The hook to call on each disconnection.
|
||||
* @param hooks The server hooks
|
||||
*/
|
||||
Server(
|
||||
boost::asio::io_context& ctx,
|
||||
@@ -105,9 +117,9 @@ public:
|
||||
ProcessingPolicy processingPolicy,
|
||||
std::optional<size_t> parallelRequestLimit,
|
||||
util::TagDecoratorFactory tagDecoratorFactory,
|
||||
ProxyIpResolver proxyIpResolver,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize,
|
||||
OnConnectCheck onConnectCheck,
|
||||
OnDisconnectHook onDisconnectHook
|
||||
Hooks hooks
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -185,6 +197,7 @@ std::expected<Server, std::string>
|
||||
makeServer(
|
||||
util::config::ClioConfigDefinition const& config,
|
||||
Server::OnConnectCheck onConnectCheck,
|
||||
Server::OnIpChangeHook onIpChangeHook,
|
||||
Server::OnDisconnectHook onDisconnectHook,
|
||||
boost::asio::io_context& context
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "util/CoroutineGroup.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/ProxyIpResolver.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
@@ -90,13 +91,17 @@ ConnectionHandler::ConnectionHandler(
|
||||
std::optional<size_t> maxParallelRequests,
|
||||
util::TagDecoratorFactory& tagFactory,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize,
|
||||
OnDisconnectHook onDisconnectHook
|
||||
ProxyIpResolver proxyIpResolver,
|
||||
OnDisconnectHook onDisconnectHook,
|
||||
OnIpChangeHook onIpChangeHook
|
||||
)
|
||||
: processingPolicy_{processingPolicy}
|
||||
, maxParallelRequests_{maxParallelRequests}
|
||||
, tagFactory_{tagFactory}
|
||||
, maxSubscriptionSendQueueSize_{maxSubscriptionSendQueueSize}
|
||||
, proxyIpResolver_(std::move(proxyIpResolver))
|
||||
, onDisconnectHook_{std::move(onDisconnectHook)}
|
||||
, onIpChangeHook_(std::move(onIpChangeHook))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -277,9 +282,9 @@ ConnectionHandler::sequentRequestResponseLoop(
|
||||
if (not expectedRequest)
|
||||
return handleError(expectedRequest.error(), connection);
|
||||
|
||||
LOG(log_.info()) << connection.tag() << "Received request from ip = " << connection.ip();
|
||||
resolveClientIp(connection, *expectedRequest);
|
||||
|
||||
auto maybeReturnValue = processRequest(connection, subscriptionContext, expectedRequest.value(), yield);
|
||||
auto maybeReturnValue = processRequest(connection, subscriptionContext, *expectedRequest, yield);
|
||||
if (maybeReturnValue.has_value())
|
||||
return maybeReturnValue.value();
|
||||
}
|
||||
@@ -308,6 +313,8 @@ ConnectionHandler::parallelRequestResponseLoop(
|
||||
break;
|
||||
}
|
||||
|
||||
resolveClientIp(connection, *expectedRequest);
|
||||
|
||||
if (not tasksGroup.isFull()) {
|
||||
bool const spawnSuccess = tasksGroup.spawn(
|
||||
yield, // spawn on the same strand
|
||||
@@ -385,4 +392,16 @@ ConnectionHandler::handleRequest(
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ConnectionHandler::resolveClientIp(Connection& connection, Request const& request) const
|
||||
{
|
||||
if (auto resolvedClientIp = proxyIpResolver_.resolveClientIp(connection.ip(), request.httpHeaders());
|
||||
resolvedClientIp != connection.ip()) {
|
||||
LOG(log_.info()) << connection.tag() << "Detected a forwarded request from proxy. Proxy ip: " << connection.ip()
|
||||
<< ". Resolved client ip: " << resolvedClientIp;
|
||||
onIpChangeHook_(connection.ip(), resolvedClientIp);
|
||||
connection.setIp(std::move(resolvedClientIp));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace web::ng::impl
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "util/prometheus/Gauge.hpp"
|
||||
#include "util/prometheus/Label.hpp"
|
||||
#include "util/prometheus/Prometheus.hpp"
|
||||
#include "web/ProxyIpResolver.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
@@ -52,6 +53,7 @@ namespace web::ng::impl {
|
||||
class ConnectionHandler {
|
||||
public:
|
||||
using OnDisconnectHook = std::function<void(Connection const&)>;
|
||||
using OnIpChangeHook = std::function<void(std::string const&, std::string const&)>;
|
||||
using TargetToHandlerMap = std::unordered_map<std::string, MessageHandler, util::StringHash, std::equal_to<>>;
|
||||
|
||||
private:
|
||||
@@ -64,7 +66,10 @@ private:
|
||||
std::reference_wrapper<util::TagDecoratorFactory> tagFactory_;
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize_;
|
||||
|
||||
ProxyIpResolver proxyIpResolver_;
|
||||
|
||||
OnDisconnectHook onDisconnectHook_;
|
||||
OnIpChangeHook onIpChangeHook_;
|
||||
|
||||
TargetToHandlerMap getHandlers_;
|
||||
TargetToHandlerMap postHandlers_;
|
||||
@@ -84,7 +89,9 @@ public:
|
||||
std::optional<size_t> maxParallelRequests,
|
||||
util::TagDecoratorFactory& tagFactory,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize,
|
||||
OnDisconnectHook onDisconnectHook
|
||||
ProxyIpResolver proxyIpResolver,
|
||||
OnDisconnectHook onDisconnectHook,
|
||||
OnIpChangeHook onIpChangeHook
|
||||
);
|
||||
|
||||
ConnectionHandler(ConnectionHandler&&) = delete;
|
||||
@@ -159,6 +166,9 @@ private:
|
||||
Request const& request,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
||||
void
|
||||
resolveClientIp(Connection& connection, Request const& request) const;
|
||||
};
|
||||
|
||||
} // namespace web::ng::impl
|
||||
|
||||
Reference in New Issue
Block a user