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:
Sergey Kuznetsov
2025-09-03 15:22:47 +01:00
committed by GitHub
parent 0a2930d861
commit 3a667f558c
39 changed files with 1042 additions and 125 deletions

View File

@@ -26,11 +26,13 @@
#include "util/Taggable.hpp"
#include "util/TestHttpClient.hpp"
#include "util/TestWebSocketClient.hpp"
#include "util/config/Array.hpp"
#include "util/config/ConfigConstraints.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigFileJson.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/ProxyIpResolver.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/ProcessingPolicy.hpp"
@@ -58,6 +60,7 @@
#include <optional>
#include <ranges>
#include <string>
#include <unordered_set>
using namespace web::ng;
using namespace util::config;
@@ -85,6 +88,8 @@ TEST_P(MakeServerTest, Make)
{"server.ip", ConfigValue{ConfigType::String}.optional()},
{"server.port", ConfigValue{ConfigType::Integer}.optional()},
{"server.processing_policy", ConfigValue{ConfigType::String}.defaultValue("parallel")},
{"server.proxy.ips.[]", Array{ConfigValue{ConfigType::String}}},
{"server.proxy.tokens.[]", Array{ConfigValue{ConfigType::String}}},
{"server.parallel_requests_limit", ConfigValue{ConfigType::Integer}.optional()},
{"server.ws_max_sending_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(1500)},
{"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
@@ -95,8 +100,13 @@ TEST_P(MakeServerTest, Make)
auto const errors = config.parse(json);
ASSERT_TRUE(!errors.has_value());
auto const expectedServer =
makeServer(config, [](auto&&) -> std::expected<void, Response> { return {}; }, [](auto&&) {}, ioContext_);
auto const expectedServer = makeServer(
config,
[](auto&&) -> std::expected<void, Response> { return {}; },
[](auto&&, auto&&) {},
[](auto&&) {},
ioContext_
);
EXPECT_EQ(expectedServer.has_value(), GetParam().expectSuccess);
}
@@ -173,6 +183,8 @@ protected:
{"server.admin_password", ConfigValue{ConfigType::String}.optional()},
{"server.local_admin", ConfigValue{ConfigType::Boolean}.optional()},
{"server.parallel_requests_limit", ConfigValue{ConfigType::Integer}.optional()},
{"server.proxy.ips.[]", Array{ConfigValue{ConfigType::String}}},
{"server.proxy.tokens.[]", Array{ConfigValue{ConfigType::String}}},
{"server.ws_max_sending_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(1500)},
{"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
{"ssl_key_file", ConfigValue{ConfigType::String}.optional()},
@@ -180,7 +192,8 @@ protected:
};
Server::OnConnectCheck emptyOnConnectCheck_ = [](auto&&) -> std::expected<void, Response> { return {}; };
std::expected<Server, std::string> server_ = makeServer(config_, emptyOnConnectCheck_, [](auto&&) {}, ctx_);
std::expected<Server, std::string> server_ =
makeServer(config_, emptyOnConnectCheck_, [](auto&&, auto&&) {}, [](auto&&) {}, ctx_);
std::string requestMessage_ = "some request";
std::string const headerName_ = "Some-header";
@@ -210,9 +223,13 @@ TEST_F(ServerTest, BadEndpoint)
ProcessingPolicy::Sequential,
std::nullopt,
tagDecoratorFactory,
web::ProxyIpResolver{{}, {}},
std::nullopt,
emptyOnConnectCheck_,
[](auto&&) {}
Server::Hooks{
.onConnectCheck = emptyOnConnectCheck_,
.onIpChangeHook = [](auto&&, auto&&) {},
.onDisconnectHook = [](auto&&) {}
}
};
auto maybeError = server.run();
@@ -274,9 +291,13 @@ TEST_F(ServerHttpTest, OnConnectCheck)
ProcessingPolicy::Sequential,
std::nullopt,
tagDecoratorFactory,
web::ProxyIpResolver{{}, {}},
std::nullopt,
onConnectCheck.AsStdFunction(),
[](auto&&) {}
Server::Hooks{
.onConnectCheck = onConnectCheck.AsStdFunction(),
.onIpChangeHook = [](auto&&, auto&&) {},
.onDisconnectHook = [](auto&&) {}
}
};
HttpAsyncClient client{ctx_};
@@ -334,9 +355,13 @@ TEST_F(ServerHttpTest, OnConnectCheckFailed)
ProcessingPolicy::Sequential,
std::nullopt,
tagDecoratorFactory,
web::ProxyIpResolver{{}, {}},
std::nullopt,
onConnectCheck.AsStdFunction(),
[](auto&&) {}
Server::Hooks{
.onConnectCheck = onConnectCheck.AsStdFunction(),
.onIpChangeHook = [](auto&&, auto&&) {},
.onDisconnectHook = [](auto&&) {}
}
};
HttpAsyncClient client{ctx_};
@@ -393,9 +418,13 @@ TEST_F(ServerHttpTest, OnDisconnectHook)
ProcessingPolicy::Sequential,
std::nullopt,
tagDecoratorFactory,
web::ProxyIpResolver{{}, {}},
std::nullopt,
emptyOnConnectCheck_,
onDisconnectHookMock.AsStdFunction()
Server::Hooks{
.onConnectCheck = emptyOnConnectCheck_,
.onIpChangeHook = [](auto&&, auto&&) {},
.onDisconnectHook = onDisconnectHookMock.AsStdFunction()
}
};
HttpAsyncClient client{ctx_};

View File

@@ -25,6 +25,7 @@
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/ProxyIpResolver.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
@@ -40,11 +41,13 @@
#include <boost/asio/steady_timer.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/error.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/websocket/error.hpp>
#include <fmt/format.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
@@ -60,7 +63,6 @@ using namespace web::ng::impl;
using namespace web::ng;
using namespace util;
using testing::Return;
namespace beast = boost::beast;
namespace http = boost::beast::http;
namespace websocket = boost::beast::websocket;
@@ -69,7 +71,15 @@ struct ConnectionHandlerTest : prometheus::WithPrometheus, SyncAsioContextTest {
: tagFactory{util::config::ClioConfigDefinition{
{"log.tag_style", config::ConfigValue{config::ConfigType::String}.defaultValue("uint")}
}}
, connectionHandler{policy, maxParallelConnections, tagFactory, std::nullopt, onDisconnectMock.AsStdFunction()}
, connectionHandler{
policy,
maxParallelConnections,
tagFactory,
std::nullopt,
proxyIpResolver,
onDisconnectMock.AsStdFunction(),
onIpChangeMock.AsStdFunction()
}
{
}
@@ -98,6 +108,12 @@ struct ConnectionHandlerTest : prometheus::WithPrometheus, SyncAsioContextTest {
return Request{std::forward<Args>(args)...};
}
std::string const clientIp = "1.2.3.4";
std::string const proxyIp = "5.6.7.8";
std::string const proxyToken = "some_proxy_token";
web::ProxyIpResolver proxyIpResolver{{proxyIp}, {proxyToken}};
testing::StrictMock<testing::MockFunction<void(std::string const&, std::string const&)>> onIpChangeMock;
testing::StrictMock<testing::MockFunction<void(Connection const&)>> onDisconnectMock;
util::TagDecoratorFactory tagFactory;
ConnectionHandler connectionHandler;
@@ -106,9 +122,9 @@ struct ConnectionHandlerTest : prometheus::WithPrometheus, SyncAsioContextTest {
{"log.tag_style", config::ConfigValue{config::ConfigType::String}.defaultValue("uint")}
}};
StrictMockHttpConnectionPtr mockHttpConnection =
std::make_unique<StrictMockHttpConnection>("1.2.3.4", beast::flat_buffer{}, tagDecoratorFactory);
std::make_unique<StrictMockHttpConnection>(clientIp, boost::beast::flat_buffer{}, tagDecoratorFactory);
StrictMockWsConnectionPtr mockWsConnection =
std::make_unique<StrictMockWsConnection>("1.2.3.4", beast::flat_buffer{}, tagDecoratorFactory);
std::make_unique<StrictMockWsConnection>(clientIp, boost::beast::flat_buffer{}, tagDecoratorFactory);
Request::HttpHeaders headers;
};
@@ -452,6 +468,50 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_SendError)
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, OnIpChangeHookCalledWhenSentFromProxy)
{
std::string const target = "/some/target";
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
getHandlerMock;
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
connectionHandler.onGet(target, getHandlerMock.AsStdFunction());
StrictMockHttpConnectionPtr mockHttpConnectionFromProxy =
std::make_unique<StrictMockHttpConnection>(proxyIp, boost::beast::flat_buffer{}, tagDecoratorFactory);
auto request = http::request<http::string_body>{http::verb::get, target, 11, requestMessage};
request.set(http::field::forwarded, fmt::format("for={}", clientIp));
EXPECT_CALL(*mockHttpConnectionFromProxy, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockHttpConnectionFromProxy, receive).WillOnce(Return(makeRequest(request)));
EXPECT_CALL(onIpChangeMock, Call(proxyIp, clientIp));
EXPECT_CALL(getHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockHttpConnectionFromProxy, send).WillOnce([&responseMessage](Response response, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return makeError(http::error::end_of_stream).error();
});
EXPECT_CALL(onDisconnectMock, Call)
.WillOnce([this, connectionPtr = mockHttpConnectionFromProxy.get()](Connection const& c) {
EXPECT_EQ(&c, connectionPtr);
EXPECT_EQ(c.ip(), clientIp);
});
runSpawn([this, c = std::move(mockHttpConnectionFromProxy)](boost::asio::yield_context yield) mutable {
connectionHandler.processConnection(std::move(c), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
{
testing::StrictMock<testing::MockFunction<
@@ -613,6 +673,48 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send)
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, OnIpChangeHookCalledWhenSentFromProxy)
{
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler.onWs(wsHandlerMock.AsStdFunction());
StrictMockWsConnectionPtr mockWsConnectionFromProxy =
std::make_unique<StrictMockWsConnection>(proxyIp, boost::beast::flat_buffer{}, tagDecoratorFactory);
headers.set(http::field::forwarded, fmt::format("for={}", clientIp));
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
EXPECT_CALL(*mockWsConnectionFromProxy, wasUpgraded).WillOnce(Return(true));
EXPECT_CALL(*mockWsConnectionFromProxy, receive)
.WillOnce(Return(makeRequest(requestMessage, headers)))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(onIpChangeMock, Call(proxyIp, clientIp));
EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockWsConnectionFromProxy, send).WillOnce([&responseMessage](Response response, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
EXPECT_CALL(onDisconnectMock, Call)
.WillOnce([this, connectionPtr = mockWsConnectionFromProxy.get()](Connection const& c) {
EXPECT_EQ(&c, connectionPtr);
EXPECT_EQ(c.ip(), clientIp);
});
runSpawn([this, c = std::move(mockWsConnectionFromProxy)](boost::asio::yield_context yield) mutable {
connectionHandler.processConnection(std::move(c), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop)
{
testing::StrictMock<testing::MockFunction<

View File

@@ -38,6 +38,7 @@
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <fmt/format.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>

View File

@@ -39,7 +39,9 @@
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/status.hpp>
#include <fmt/format.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
@@ -65,7 +67,10 @@ struct WebWsConnectionTests : SyncAsioContextTest {
auto ip = expectedSocket->remote_endpoint().address().to_string();
PlainHttpConnection httpConnection{
std::move(expectedSocket).value(), std::move(ip), boost::beast::flat_buffer{}, tagDecoratorFactory_
std::move(expectedSocket).value(),
std::move(ip),
boost::beast::flat_buffer{},
tagDecoratorFactory_,
};
auto expectedTrue = httpConnection.isUpgradeRequested(yield);