Compare commits

..

3 Commits

Author SHA1 Message Date
Sergey Kuznetsov
14645e1494 fix: Proxy support (#3103)
Port of two changes onto release/2.7.1:
- #3006 (d3381a1d): resolve proxy ip before processing any request
- #3043 (d7bcf6e7): re-resolve client ip when a proxy reuses a TCP
connection for different clients (resolveClientIp now returns
std::optional; extractClientIp made public; isProxyConnection_ tracked)
2026-06-09 16:25:02 +01:00
Sergey Kuznetsov
ce44aec245 ci: Use docker images with glibc version in conan (#3100) 2026-06-08 15:16:50 +01:00
Sergey Kuznetsov
94da8459dd ci: Use glibc version in conan profile (#3099) 2026-06-08 14:01:33 +01:00
22 changed files with 277 additions and 57 deletions

View File

@@ -4,7 +4,7 @@ import json
LINUX_OS = ["heavy", "heavy-arm64"]
LINUX_CONTAINERS = [
'{ "image": "ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996" }'
'{ "image": "ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031" }'
]
LINUX_COMPILERS = ["gcc", "clang"]

View File

@@ -49,7 +49,7 @@ jobs:
build_type: [Release, Debug]
container:
[
'{ "image": "ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996" }',
'{ "image": "ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031" }',
]
static: [true]
@@ -79,7 +79,7 @@ jobs:
uses: ./.github/workflows/reusable-build.yml
with:
runs_on: heavy
container: '{ "image": "ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031" }'
conan_profile: gcc
build_type: Debug
download_ccache: true
@@ -97,7 +97,7 @@ jobs:
needs: build-and-test
runs-on: heavy
container:
image: ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996
image: ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

View File

@@ -21,7 +21,7 @@ jobs:
name: Build Clio / `libXRPL ${{ github.event.client_payload.version }}`
runs-on: heavy
container:
image: ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996
image: ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
@@ -69,7 +69,7 @@ jobs:
needs: build
runs-on: heavy
container:
image: ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996
image: ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031
steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0

View File

@@ -31,7 +31,7 @@ jobs:
if: github.event_name != 'push' || contains(github.event.head_commit.message, 'clang-tidy auto fixes')
runs-on: heavy
container:
image: ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996
image: ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031
permissions:
contents: write

View File

@@ -18,7 +18,7 @@ jobs:
build:
runs-on: ubuntu-latest
container:
image: ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996
image: ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031
steps:
- name: Checkout

View File

@@ -55,17 +55,17 @@ jobs:
conan_profile: gcc
build_type: Release
static: true
container: '{ "image": "ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031" }'
- os: heavy
conan_profile: gcc
build_type: Debug
static: true
container: '{ "image": "ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031" }'
- os: heavy
conan_profile: gcc.ubsan
build_type: Release
static: false
container: '{ "image": "ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031" }'
uses: ./.github/workflows/reusable-build-test.yml
with:
@@ -88,7 +88,7 @@ jobs:
uses: ./.github/workflows/reusable-build.yml
with:
runs_on: heavy
container: '{ "image": "ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031" }'
conan_profile: gcc
build_type: Release
download_ccache: false
@@ -111,7 +111,7 @@ jobs:
include:
- os: heavy
conan_profile: clang
container: '{ "image": "ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031" }'
static: true
- os: macos15
conan_profile: apple-clang

View File

@@ -11,4 +11,4 @@ jobs:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@282890f46d6921249d5659dd38babcb0bd8aef48
with:
runs_on: heavy
container: '{ "image": "ghcr.io/xrplf/clio-pre-commit:6bb4953f1643b999781609ca79d5ec467289c996" }'
container: '{ "image": "ghcr.io/xrplf/clio-pre-commit:94da8459ddc30e2f0d88a98cba63a57b2cda3031" }'

View File

@@ -29,7 +29,7 @@ jobs:
conan_profile: gcc
build_type: Release
static: true
container: '{ "image": "ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031" }'
uses: ./.github/workflows/reusable-build-test.yml
with:
@@ -51,7 +51,7 @@ jobs:
uses: ./.github/workflows/reusable-build.yml
with:
runs_on: heavy
container: '{ "image": "ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031" }'
conan_profile: gcc
build_type: Release
download_ccache: false

View File

@@ -46,7 +46,7 @@ jobs:
release:
runs-on: heavy
container:
image: ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996
image: ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}

View File

@@ -44,7 +44,7 @@ jobs:
uses: ./.github/workflows/reusable-build-test.yml
with:
runs_on: heavy
container: '{ "image": "ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031" }'
download_ccache: false
upload_ccache: false
conan_profile: ${{ matrix.compiler }}${{ matrix.sanitizer_ext }}

View File

@@ -3,7 +3,7 @@
This image contains an environment to build [Clio](https://github.com/XRPLF/clio), check code and documentation.
It is used in [Clio Github Actions](https://github.com/XRPLF/clio/actions) but can also be used to compile Clio locally.
The image is based on Ubuntu 20.04 and contains:
The image is based on Ubuntu 22.04 and contains:
- ccache 4.12.2
- Clang 19

View File

@@ -10,3 +10,5 @@ os=Linux
[conf]
tools.build:compiler_executables={"c": "/usr/bin/clang-19", "cpp": "/usr/bin/clang++-19"}
grpc/1.50.1:tools.build:cxxflags+=["-Wno-missing-template-arg-list-after-template-kw"]
user.package:libc_version=2.32
tools.info.package_id:confs+=["user.package:libc_version"]

View File

@@ -9,3 +9,5 @@ os=Linux
[conf]
tools.build:compiler_executables={"c": "/usr/bin/gcc-15", "cpp": "/usr/bin/g++-15"}
user.package:libc_version=2.32
tools.info.package_id:confs+=["user.package:libc_version"]

View File

@@ -1,6 +1,6 @@
services:
clio_develop:
image: ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996
image: ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031
volumes:
- clio_develop_conan_data:/root/.conan2/p
- clio_develop_ccache:/root/.ccache

View File

@@ -175,7 +175,7 @@ Open the `index.html` file in your browser to see the documentation pages.
It is also possible to build Clio using [Docker](https://www.docker.com/) if you don't want to install all the dependencies on your machine.
```sh
docker run -it ghcr.io/xrplf/clio-ci:6bb4953f1643b999781609ca79d5ec467289c996
docker run -it ghcr.io/xrplf/clio-ci:94da8459ddc30e2f0d88a98cba63a57b2cda3031
git clone https://github.com/XRPLF/clio
cd clio
```

View File

@@ -26,6 +26,7 @@
#include <boost/beast/http/field.hpp>
#include <algorithm>
#include <optional>
#include <string>
#include <string_view>
@@ -63,20 +64,20 @@ ProxyIpResolver::fromConfig(util::config::ClioConfigDefinition const& config)
return ProxyIpResolver{std::move(ips), std::move(tokens)};
}
std::string
std::optional<std::string>
ProxyIpResolver::resolveClientIp(std::string const& connectionIp, HttpHeaders const& headers) const
{
if (proxyIps_.contains(connectionIp)) {
return extractClientIp(headers).value_or(connectionIp);
return extractClientIp(headers);
}
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 extractClientIp(headers);
}
}
return connectionIp;
return std::nullopt;
}
std::optional<std::string>
@@ -92,14 +93,17 @@ ProxyIpResolver::extractClientIp(HttpHeaders const& headers)
auto const headerValue = util::toLower(it->value());
static constexpr std::string_view kFOR_PREFIX = "for=";
auto const startPos = headerValue.find(kFOR_PREFIX);
auto const startPos = headerValue.rfind(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);
static constexpr char kSECTION_DELIMITER = ';';
static constexpr char kCHAIN_DELIMITER = ',';
auto const sectionEnd = value.find(kSECTION_DELIMITER);
auto const chainEnd = value.find(kCHAIN_DELIMITER);
auto const endPos = std::min(sectionEnd, chainEnd);
auto const ip = value.substr(0, endPos);
static constexpr auto kMIN_IP_LENGTH = 7; // minimum 3 dots + 4 digits

View File

@@ -75,16 +75,15 @@ public:
*
* 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.
* Otherwise, returns std::nullopt.
*
* @param connectionIp The IP address of the direct connection.
* @param headers The HTTP request headers.
* @return The resolved client IP address as a string.
* @return The resolved client IP address if the connection is from a trusted proxy, otherwise std::nullopt.
*/
std::string
std::optional<std::string>
resolveClientIp(std::string const& connectionIp, HttpHeaders const& headers) const;
private:
/**
* @brief Extracts the client IP from the `Forwarded` HTTP header.
*

View File

@@ -43,8 +43,10 @@
#include <boost/beast/http/error.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/message_fwd.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/string_body_fwd.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/core/ignore_unused.hpp>
@@ -139,6 +141,7 @@ class HttpBase : public ConnectionBase {
SendLambda sender_;
std::shared_ptr<AdminVerificationStrategy> adminVerification_;
std::shared_ptr<ProxyIpResolver> proxyIpResolver_;
bool isProxyConnection_ = false;
protected:
boost::beast::flat_buffer buffer_;
@@ -239,6 +242,23 @@ public:
if (ec)
return httpFail(ec, "read");
auto const updateClientIp = [&](std::string newIp) {
if (newIp == clientIp_)
return;
LOG(log_.info()) << tag() << "Detected a forwarded request from proxy. Resolved client ip: " << newIp;
dosGuard_.get().decrement(clientIp_);
clientIp_ = std::move(newIp);
dosGuard_.get().increment(clientIp_);
};
if (isProxyConnection_) {
if (auto resolvedIp = ProxyIpResolver::extractClientIp(req_); resolvedIp.has_value())
updateClientIp(std::move(*resolvedIp));
} else if (auto resolvedIp = proxyIpResolver_->resolveClientIp(clientIp_, req_); resolvedIp.has_value()) {
updateClientIp(std::move(*resolvedIp));
isProxyConnection_ = true;
}
if (req_.method() == http::verb::get and req_.target() == "/health")
return sender_(httpResponse(http::status::ok, "text/html", kHEALTH_CHECK_HTML));
@@ -249,14 +269,6 @@ public:
return sender_(httpResponse(http::status::service_unavailable, "text/html", kCACHE_CHECK_NOT_LOADED_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_, clientIp_);

View File

@@ -45,6 +45,7 @@ class ConnectionMetadata : public util::Taggable {
protected:
std::string ip_; // client ip
std::optional<bool> isAdmin_;
bool isProxyConnection_ = false;
public:
/**
@@ -82,6 +83,26 @@ public:
ip_ = std::move(newIp);
}
/**
* @brief Mark this connection as coming through a trusted proxy.
*/
void
markAsProxyConnection()
{
isProxyConnection_ = true;
}
/**
* @brief Whether this connection was identified as coming through a trusted proxy.
*
* @return true if the connection is a proxy connection.
*/
[[nodiscard]] bool
isProxyConnection() const
{
return isProxyConnection_;
}
/**
* @brief Get whether the client is an admin.
*

View File

@@ -395,12 +395,22 @@ 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));
auto const updateIp = [&](std::string newIp) {
if (newIp == connection.ip())
return;
LOG(log_.info()) << connection.tag()
<< "Detected a forwarded request from proxy. Resolved client ip: " << newIp;
onIpChangeHook_(connection.ip(), newIp);
connection.setIp(std::move(newIp));
};
if (connection.isProxyConnection()) {
if (auto resolvedIp = ProxyIpResolver::extractClientIp(request.httpHeaders()); resolvedIp.has_value())
updateIp(std::move(*resolvedIp));
} else if (auto resolvedIp = proxyIpResolver_.resolveClientIp(connection.ip(), request.httpHeaders());
resolvedIp.has_value()) {
updateIp(std::move(*resolvedIp));
connection.markAsProxyConnection();
}
}

View File

@@ -29,6 +29,7 @@
#include <fmt/format.h>
#include <gtest/gtest.h>
#include <optional>
#include <string>
#include <unordered_set>
#include <utility>
@@ -44,7 +45,7 @@ struct ProxyIpResolverTestParams {
std::unordered_set<std::string> proxyTokens;
std::vector<std::pair<std::string, std::string>> headers;
std::string connectionIp;
std::string expectedIp;
std::optional<std::string> expectedIp;
};
class ProxyIpResolverTest : public ::testing::TestWithParam<ProxyIpResolverTestParams> {};
@@ -79,11 +80,11 @@ TEST_F(ProxyIpResolverTest, FromConfig)
auto const proxyIpResolver = ProxyIpResolver::fromConfig(config);
ProxyIpResolver::HttpHeaders headers;
EXPECT_EQ(proxyIpResolver.resolveClientIp(clientIp, headers), clientIp);
EXPECT_EQ(proxyIpResolver.resolveClientIp(proxyIp, headers), proxyIp);
EXPECT_EQ(proxyIpResolver.resolveClientIp(clientIp, headers), std::nullopt);
EXPECT_EQ(proxyIpResolver.resolveClientIp(proxyIp, headers), std::nullopt);
headers.set(boost::beast::http::field::forwarded, fmt::format("for={}", clientIp));
EXPECT_EQ(proxyIpResolver.resolveClientIp(clientIp, headers), clientIp);
EXPECT_EQ(proxyIpResolver.resolveClientIp(clientIp, headers), std::nullopt);
EXPECT_EQ(proxyIpResolver.resolveClientIp(proxyIp, headers), clientIp);
headers.set(ProxyIpResolver::kPROXY_TOKEN_HEADER, proxyToken);
@@ -113,7 +114,7 @@ INSTANTIATE_TEST_SUITE_P(
.proxyTokens = {},
.headers = {},
.connectionIp = "1.2.3.4",
.expectedIp = "1.2.3.4"
.expectedIp = std::nullopt
},
ProxyIpResolverTestParams{
.testName = "TrustedProxyIpWithForwardedHeader",
@@ -129,7 +130,7 @@ INSTANTIATE_TEST_SUITE_P(
.proxyTokens = {},
.headers = {},
.connectionIp = "5.6.7.8",
.expectedIp = "5.6.7.8"
.expectedIp = std::nullopt
},
ProxyIpResolverTestParams{
.testName = "UntrustedProxyIpWithForwardedHeader",
@@ -137,7 +138,7 @@ INSTANTIATE_TEST_SUITE_P(
.proxyTokens = {},
.headers = {{std::string(http::to_string(http::field::forwarded)), "for=1.2.3.4"}},
.connectionIp = "5.6.7.8",
.expectedIp = "5.6.7.8"
.expectedIp = std::nullopt
},
ProxyIpResolverTestParams{
.testName = "TrustedProxyTokenWithForwardedHeader",
@@ -155,7 +156,7 @@ INSTANTIATE_TEST_SUITE_P(
.proxyTokens = {"test_token"},
.headers = {{std::string(ProxyIpResolver::kPROXY_TOKEN_HEADER), "test_token"}},
.connectionIp = "5.6.7.8",
.expectedIp = "5.6.7.8"
.expectedIp = std::nullopt
},
ProxyIpResolverTestParams{
.testName = "UntrustedProxyTokenWithForwardedHeader",
@@ -165,7 +166,7 @@ INSTANTIATE_TEST_SUITE_P(
{{std::string(ProxyIpResolver::kPROXY_TOKEN_HEADER), "test_token"},
{std::string(http::to_string(http::field::forwarded)), "for=1.2.3.4"}},
.connectionIp = "5.6.7.8",
.expectedIp = "5.6.7.8"
.expectedIp = std::nullopt
},
ProxyIpResolverTestParams{
.testName = "ForwardedHeaderWithAdditionalFields",
@@ -191,7 +192,7 @@ INSTANTIATE_TEST_SUITE_P(
.proxyTokens = {},
.headers = {{std::string(http::to_string(http::field::forwarded)), "by=1.2.3.4"}},
.connectionIp = "5.6.7.8",
.expectedIp = "5.6.7.8"
.expectedIp = std::nullopt
},
ProxyIpResolverTestParams{
.testName = "ForwardedHeaderWithIpInQuotes",
@@ -207,7 +208,25 @@ INSTANTIATE_TEST_SUITE_P(
.proxyTokens = {},
.headers = {{std::string(http::to_string(http::field::forwarded)), "for=\";some_other_text"}},
.connectionIp = "5.6.7.8",
.expectedIp = "5.6.7.8"
.expectedIp = std::nullopt
},
ProxyIpResolverTestParams{
.testName = "ForwardedHeaderWithMultipleForValues",
.proxyIps = {"5.6.7.8"},
.proxyTokens = {},
.headers = {{std::string(http::to_string(http::field::forwarded)), "for=1.2.3.4, for=9.10.11.12"}},
.connectionIp = "5.6.7.8",
.expectedIp = "9.10.11.12"
},
ProxyIpResolverTestParams{
.testName = "ForwardedHeaderWithMultipleForValuesAndSectionDelimiters",
.proxyIps = {"5.6.7.8"},
.proxyTokens = {},
.headers =
{{std::string(http::to_string(http::field::forwarded)),
"for=1.2.3.4; proto=http, for=9.10.11.12; proto=https"}},
.connectionIp = "5.6.7.8",
.expectedIp = "9.10.11.12"
}
),
tests::util::kNAME_GENERATOR

View File

@@ -523,6 +523,88 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, OnIpChangeHookCalledWhenSentFr
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, ProxyConnection_SameClientReuses_HookCalledOnce)
{
std::string const target = "/some/target";
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
getHandlerMock;
connectionHandler.onGet(target, getHandlerMock.AsStdFunction());
StrictMockHttpConnectionPtr mockProxyConnection =
std::make_unique<StrictMockHttpConnection>(proxyIp, boost::beast::flat_buffer{}, tagDecoratorFactory);
auto request = http::request<http::string_body>{http::verb::get, target, 11, ""};
request.set(http::field::forwarded, fmt::format("for={}", clientIp));
EXPECT_CALL(*mockProxyConnection, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockProxyConnection, receive)
.WillOnce(Return(makeRequest(request)))
.WillOnce(Return(makeRequest(request)))
.WillOnce(Return(makeError(http::error::end_of_stream)));
EXPECT_CALL(onIpChangeMock, Call(proxyIp, clientIp));
EXPECT_CALL(getHandlerMock, Call).Times(2).WillRepeatedly([](Request const& req, auto&&, auto&&, auto&&) {
return Response(http::status::ok, "ok", req);
});
EXPECT_CALL(*mockProxyConnection, send).Times(2).WillRepeatedly(Return(std::expected<void, web::ng::Error>{}));
EXPECT_CALL(onDisconnectMock, Call).WillOnce([this, ptr = mockProxyConnection.get()](Connection const& c) {
EXPECT_EQ(&c, ptr);
EXPECT_EQ(c.ip(), clientIp);
});
runSpawn([this, c = std::move(mockProxyConnection)](boost::asio::yield_context yield) mutable {
connectionHandler.processConnection(std::move(c), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, ProxyConnection_DifferentClientReuses_HookCalledForEachIpChange)
{
std::string const target = "/some/target";
std::string const anotherClientIp = "9.10.11.12";
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
getHandlerMock;
connectionHandler.onGet(target, getHandlerMock.AsStdFunction());
StrictMockHttpConnectionPtr mockProxyConnection =
std::make_unique<StrictMockHttpConnection>(proxyIp, boost::beast::flat_buffer{}, tagDecoratorFactory);
auto request1 = http::request<http::string_body>{http::verb::get, target, 11, ""};
request1.set(http::field::forwarded, fmt::format("for={}", clientIp));
auto request2 = http::request<http::string_body>{http::verb::get, target, 11, ""};
request2.set(http::field::forwarded, fmt::format("for={}", anotherClientIp));
EXPECT_CALL(*mockProxyConnection, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockProxyConnection, receive)
.WillOnce(Return(makeRequest(request1)))
.WillOnce(Return(makeRequest(request2)))
.WillOnce(Return(makeError(http::error::end_of_stream)));
EXPECT_CALL(onIpChangeMock, Call(proxyIp, clientIp));
EXPECT_CALL(onIpChangeMock, Call(clientIp, anotherClientIp));
EXPECT_CALL(getHandlerMock, Call).Times(2).WillRepeatedly([](Request const& req, auto&&, auto&&, auto&&) {
return Response(http::status::ok, "ok", req);
});
EXPECT_CALL(*mockProxyConnection, send).Times(2).WillRepeatedly(Return(std::expected<void, web::ng::Error>{}));
EXPECT_CALL(onDisconnectMock, Call)
.WillOnce([anotherClientIp, ptr = mockProxyConnection.get()](Connection const& c) {
EXPECT_EQ(&c, ptr);
EXPECT_EQ(c.ip(), anotherClientIp);
});
runSpawn([this, c = std::move(mockProxyConnection)](boost::asio::yield_context yield) mutable {
connectionHandler.processConnection(std::move(c), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
{
testing::StrictMock<testing::MockFunction<
@@ -728,6 +810,75 @@ TEST_F(ConnectionHandlerParallelProcessingTest, OnIpChangeHookCalledWhenSentFrom
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, ProxyConnection_SameClientReuses_HookCalledOnce)
{
connectionHandler.onWs([](Request const& req, auto&&, auto&&, auto&&) {
return Response(http::status::ok, "ok", req);
});
StrictMockWsConnectionPtr mockProxyConnection =
std::make_unique<StrictMockWsConnection>(proxyIp, boost::beast::flat_buffer{}, tagDecoratorFactory);
headers.set(http::field::forwarded, fmt::format("for={}", clientIp));
EXPECT_CALL(*mockProxyConnection, wasUpgraded).WillOnce(Return(true));
EXPECT_CALL(*mockProxyConnection, receive)
.WillOnce(Return(makeRequest("msg", headers)))
.WillOnce(Return(makeRequest("msg", headers)))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(onIpChangeMock, Call(proxyIp, clientIp));
EXPECT_CALL(*mockProxyConnection, send).Times(2).WillRepeatedly(Return(std::expected<void, web::ng::Error>{}));
EXPECT_CALL(onDisconnectMock, Call).WillOnce([this, ptr = mockProxyConnection.get()](Connection const& c) {
EXPECT_EQ(&c, ptr);
EXPECT_EQ(c.ip(), clientIp);
});
runSpawn([this, c = std::move(mockProxyConnection)](boost::asio::yield_context yield) mutable {
connectionHandler.processConnection(std::move(c), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, ProxyConnection_DifferentClientReuses_HookCalledForEachIpChange)
{
std::string const anotherClientIp = "9.10.11.12";
connectionHandler.onWs([](Request const& req, auto&&, auto&&, auto&&) {
return Response(http::status::ok, "ok", req);
});
StrictMockWsConnectionPtr mockProxyConnection =
std::make_unique<StrictMockWsConnection>(proxyIp, boost::beast::flat_buffer{}, tagDecoratorFactory);
Request::HttpHeaders headers1;
headers1.set(http::field::forwarded, fmt::format("for={}", clientIp));
Request::HttpHeaders headers2;
headers2.set(http::field::forwarded, fmt::format("for={}", anotherClientIp));
EXPECT_CALL(*mockProxyConnection, wasUpgraded).WillOnce(Return(true));
EXPECT_CALL(*mockProxyConnection, receive)
.WillOnce(Return(makeRequest("msg", headers1)))
.WillOnce(Return(makeRequest("msg", headers2)))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(onIpChangeMock, Call(proxyIp, clientIp));
EXPECT_CALL(onIpChangeMock, Call(clientIp, anotherClientIp));
EXPECT_CALL(*mockProxyConnection, send).Times(2).WillRepeatedly(Return(std::expected<void, web::ng::Error>{}));
EXPECT_CALL(onDisconnectMock, Call)
.WillOnce([anotherClientIp, ptr = mockProxyConnection.get()](Connection const& c) {
EXPECT_EQ(&c, ptr);
EXPECT_EQ(c.ip(), anotherClientIp);
});
runSpawn([this, c = std::move(mockProxyConnection)](boost::asio::yield_context yield) mutable {
connectionHandler.processConnection(std::move(c), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop)
{
testing::StrictMock<testing::MockFunction<