flexisip/tester/module-router-tester.cc
2025-06-16 13:54:12 +07:00

520 lines
No EOL
21 KiB
C++

/*
Flexisip, a flexible SIP proxy server with media capabilities.
Copyright (C) 2010-2024 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <chrono>
#include <memory>
#include <string>
#include <unistd.h>
#include "belle-sip/types.h"
#include "flexisip/logmanager.hh"
#include "flexisip/module-router.hh"
#include "sofia-wrapper/nta-agent.hh"
#include "utils/asserts.hh"
#include "utils/bellesip-utils.hh"
#include "utils/core-assert.hh"
#include "utils/string-utils.hh"
#include "utils/test-patterns/registrardb-test.hh"
#include "utils/test-patterns/test.hh"
#include "utils/test-suite.hh"
using namespace std;
using namespace std::chrono;
using namespace sofiasip;
namespace flexisip::tester {
namespace {
void fallbackRouteFilter() {
const auto fallbackPort = 8282;
Server server{{
{"module::DoSProtection/enabled", "false"},
{"module::Registrar/reg-domains", "127.0.0.1"},
{"module::Router/enabled", "true"},
{"module::Router/fallback-route", "sip:127.0.0.1:" + to_string(fallbackPort) + ";transport=udp"},
{"module::Router/fallback-route-filter", "request.method != 'INVITE'"},
}};
server.start();
bool requestReceived = false;
BellesipUtils belleSipUtilsFallback{
"0.0.0.0",
fallbackPort,
"UDP",
static_cast<BellesipUtils::ProcessResponseStatusCb>(nullptr),
[&requestReceived](const belle_sip_request_event_t*) { requestReceived = true; },
};
bool responseReceived = false;
BellesipUtils belleSipUtils{
"0.0.0.0",
BELLE_SIP_LISTENING_POINT_RANDOM_PORT,
"UDP",
[&responseReceived](int status) {
if (status != 100) {
BC_ASSERT_CPP_EQUAL(status, 200);
responseReceived = true;
}
},
};
// Send a request matching the filter.
stringstream requestMatchingFilter{};
requestMatchingFilter << "OPTIONS sip:participant1@127.0.0.1:" << server.getFirstPort() << " SIP/2.0\r\n"
<< "Via: SIP/2.0/UDP 10.10.10.10:5060;branch=z9hG4bK1439638806\r\n"
<< "From: <sip:anthony@127.0.0.1>;tag=465687829\r\n"
<< "To: <sip:participant1@127.0.0.1>\r\n"
<< "CSeq: 1 OPTIONS\r\n"
<< "Call-ID: 1053183492\r\n"
<< "Contact: <sip:jehan-mac@192.168.1.8:5062>\r\n"
<< "Max-Forwards: 70\r\n"
<< "User-Agent: BelleSipUtils\r\n"
<< "Content-Length: 0\r\n\r\n";
belleSipUtils.sendRawRequest(requestMatchingFilter.str());
CoreAssert asserter{server, belleSipUtilsFallback, belleSipUtils};
asserter
.wait([&responseReceived, &requestReceived]() {
// ... so the fallback route MUST have received the request...
FAIL_IF(!requestReceived);
// ... and the sender MUST have received the "200 Ok" from the fallback route.
FAIL_IF(!responseReceived);
return ASSERTION_PASSED();
})
.assert_passed();
responseReceived = false;
requestReceived = false;
BellesipUtils belleSipUtilsBis{
"0.0.0.0",
BELLE_SIP_LISTENING_POINT_RANDOM_PORT,
"UDP",
[&responseReceived](int status) {
if (status != 100) {
BC_ASSERT_CPP_EQUAL(status, 404);
responseReceived = true;
}
},
nullptr,
};
// This time we send a request not matching the filter...
stringstream requestNotMatchingFilter{};
requestNotMatchingFilter << "INVITE sip:participant1@127.0.0.1:" << server.getFirstPort() << " SIP/2.0\r\n"
<< "Via: SIP/2.0/UDP 10.10.10.10:5060;branch=z9hG4bK1439638806\r\n"
<< "From: <sip:anthony@127.0.0.1>;tag=465687829\r\n"
<< "To: <sip:participant1@127.0.0.1>\r\n"
<< "CSeq: 1 INVITE\r\n"
<< "Call-ID: 1053183493\r\n"
<< "Contact: <sip:jehan-mac@192.168.1.8:5062>\r\n"
<< "Max-Forwards: 70\r\n"
<< "User-Agent: BelleSipUtils\r\n"
<< "Content-Length: 0\r\n\r\n";
belleSipUtilsBis.sendRawRequest(requestNotMatchingFilter.str());
asserter.registerSteppable(belleSipUtilsBis);
asserter
.wait([&responseReceived, &requestReceived]() {
// ... so the fallback route MUST NOT have received the request...
FAIL_IF(requestReceived);
// ... and the sender MUST have received the "404 Not Found" from flexisip (no user in the registrar db).
FAIL_IF(!responseReceived);
return ASSERTION_PASSED();
})
.assert_passed();
}
/**
* Verify that RouterModule removes route to itself.
*
* In this test we want to verify that every request that enter the module::Router
* with a "Route:" header pointing to itself are actually resolved by using the
* registrar DB and goes out the module::Router with the "Route:" header removed.
*/
void selfRouteHeaderRemoving() {
SLOGD << "Step 1: Setup";
Server server{{
{"global/aliases", "test.flexisip.org"},
{"module::DoSProtection/enabled", "false"},
{"module::Registrar/reg-domains", "test.flexisip.org"},
}};
server.start();
bool isRequestReceived = false;
BellesipUtils belleSipUtilsReceiver{
"0.0.0.0",
BELLE_SIP_LISTENING_POINT_RANDOM_PORT,
"TCP",
static_cast<BellesipUtils::ProcessResponseStatusCb>(nullptr),
[&isRequestReceived](const belle_sip_request_event_t* event) {
isRequestReceived = true;
if (!BC_ASSERT_PTR_NOT_NULL(belle_sip_request_event_get_request(event))) {
return;
}
auto request = belle_sip_request_event_get_request(event);
auto message = BELLE_SIP_MESSAGE(request);
auto routes = belle_sip_message_get_headers(message, "Route");
if (routes != nullptr) {
BC_FAIL("Route was not removed");
}
},
};
bool isRequestAccepted = false;
BellesipUtils belleSipUtilsSender{
"0.0.0.0",
BELLE_SIP_LISTENING_POINT_RANDOM_PORT,
"TCP",
[&isRequestAccepted](int status) {
if (status != 100) {
BC_ASSERT_CPP_EQUAL(status, 200);
isRequestAccepted = true;
}
},
nullptr,
};
ContactInserter inserter{*server.getRegistrarDb()};
inserter.setAor("sip:provencal_le_gaulois@test.flexisip.org")
.setExpire(30s)
.insert({"sip:provencal_le_gaulois@127.0.0.1:" + to_string(belleSipUtilsReceiver.getListeningPort()) +
";transport=tcp"});
CoreAssert asserter{server, belleSipUtilsReceiver, belleSipUtilsSender};
asserter.wait([&inserter] { return LOOP_ASSERTION(inserter.finished()); }).assert_passed();
SLOGD << "Step 2: Send message";
const string body{"C'est pas faux \r\n\r\n"};
stringstream request{};
request << "MESSAGE sip:provencal_le_gaulois@test.flexisip.org SIP/2.0\r\n"
<< "Via: SIP/2.0/TCP 127.0.0.1:" << belleSipUtilsSender.getListeningPort() << ";branch=z9hG4bK.PAWTmC\r\n"
<< "From: <sip:kijou@sip.linphone.org;gr=8aabdb1c>;tag=l3qXxwsO~\r\n"
<< "To: <sip:provencal_le_gaulois@test.flexisip.org>\r\n"
<< "CSeq: 20 MESSAGE\r\n"
<< "Call-ID: Tvw6USHXYv\r\n"
<< "Max-Forwards: 70\r\n"
<< "Route: <sip:127.0.0.1:" << server.getFirstPort() << ";transport=tcp;lr>\r\n"
<< "Supported: replaces, outbound, gruu\r\n"
<< "Date: Fri, 01 Apr 2022 11:18:26 GMT\r\n"
<< "Content-Type: text/plain\r\n"
<< "Content-Length: " << body.size() << "\r\n\r\n";
belleSipUtilsSender.sendRawRequest(request.str(), body);
SLOGD << "Step 3: Assert that request received an answer (200) and is received";
asserter
.wait([&isRequestAccepted, &isRequestReceived]() {
return LOOP_ASSERTION(isRequestAccepted && isRequestReceived);
})
.assert_passed();
}
/**
* Check that module router don't remove route to others.
*
* In this test the message contains two "Route:" headers :
* - One pointing to itself
* - One pointing to another proxy
* We want to assert that the header pointing to itself is removed.
* We want to assure that the module::Router is skipped (no contact is resolved) and
* the request directly forwarded to the other proxy, with the second route header preserved.
*/
void otherRouteHeaderNotRemoved() {
SLOGD << "Step 1: Setup";
Server server{{
{"global/aliases", "test.flexisip.org"},
{"module::DoSProtection/enabled", "false"},
{"module::Registrar/reg-domains", "test.flexisip.org"},
}};
server.start();
bool isRequestReceived = false;
auto belleSipUtilsReceiverPort = "0"s;
BellesipUtils belleSipUtilsReceiver{
"0.0.0.0",
BELLE_SIP_LISTENING_POINT_RANDOM_PORT,
"TCP",
static_cast<BellesipUtils::ProcessResponseStatusCb>(nullptr),
[&isRequestReceived, &belleSipUtilsReceiverPort](const belle_sip_request_event_t* event) {
isRequestReceived = true;
const auto* request = belle_sip_request_event_get_request(event);
BC_HARD_ASSERT_NOT_NULL(request);
const auto* message = BELLE_SIP_MESSAGE(request);
BC_HARD_ASSERT_NOT_NULL(message);
const auto* routes = belle_sip_message_get_headers(message, BELLE_SIP_ROUTE);
BC_HARD_ASSERT_NOT_NULL(routes);
if (bctbx_list_last_elem(routes) != bctbx_list_first_elem(routes)) {
BC_FAIL("Both routes were preserved");
} else {
const auto* routeActual =
reinterpret_cast<belle_sip_header_route_t*>(bctbx_list_first_elem(routes)->data);
const auto* routeExpected = belle_sip_header_route_parse(
string{"Route: <sip:127.0.0.1:" + belleSipUtilsReceiverPort + ";transport=tcp;lr>"}.c_str());
BC_ASSERT_TRUE(belle_sip_header_route_equals(routeActual, routeExpected) == 0);
}
},
};
belleSipUtilsReceiverPort = to_string(belleSipUtilsReceiver.getListeningPort());
bool isRequestAccepted = false;
BellesipUtils belleSipUtilsSender{
"0.0.0.0",
BELLE_SIP_LISTENING_POINT_RANDOM_PORT,
"TCP",
[&isRequestAccepted](int status) {
if (status != 100) {
BC_ASSERT_CPP_EQUAL(status, 200);
isRequestAccepted = true;
}
},
nullptr,
};
// Because we want to assert that module::Router is skipped and that no user is resolved we insert
// a contact pointing to nowhere.
ContactInserter inserter{*server.getRegistrarDb()};
inserter.setAor("sip:provencal_le_gaulois@test.flexisip.org")
.setExpire(30s)
.insert({"sip:provencal_le_gaulois@127.0.0.1:0;transport=tcp"});
CoreAssert asserter{server, belleSipUtilsReceiver, belleSipUtilsSender};
asserter.wait([&inserter] { return LOOP_ASSERTION(inserter.finished()); }).assert_passed();
SLOGD << "Step 2: Send message";
const string body{"C'est pas faux \r\n\r\n"};
stringstream request{};
request << "MESSAGE sip:provencal_le_gaulois@test.flexisip.org SIP/2.0\r\n"
<< "Via: SIP/2.0/TCP 127.0.0.1:" << belleSipUtilsReceiver.getListeningPort() << ";branch=z9hG4bK.PAWTmC\r\n"
<< "From: <sip:kijou@sip.linphone.org;gr=8aabdb1c>;tag=l3qXxwsO~\r\n"
<< "To: <sip:provencal_le_gaulois@test.flexisip.org>\r\n"
<< "CSeq: 20 MESSAGE\r\n"
<< "Call-ID: Tvw6USHXYv\r\n"
<< "Max-Forwards: 70\r\n"
<< "Route: <sip:127.0.0.1:" << server.getFirstPort() << ";transport=tcp;lr>\r\n"
<< "Route: <sip:127.0.0.1:" << belleSipUtilsReceiverPort << ";transport=tcp;lr>\r\n"
<< "Supported: replaces, outbound, gruu\r\n"
<< "Date: Fri, 01 Apr 2022 11:18:26 GMT\r\n"
<< "Content-Type: text/plain\r\n"
<< "Content-Length: " << body.size() << "\r\n\r\n";
belleSipUtilsSender.sendRawRequest(request.str(), body);
SLOGD << "Step 3: Assert that request received an answer (200) and is received";
asserter
.wait([&isRequestAccepted, &isRequestReceived]() {
return LOOP_ASSERTION(isRequestAccepted && isRequestReceived);
})
.assert_passed();
}
template <typename Database>
void messageExpires() {
Database db{};
Server server{[&db]() {
auto config = db.configAsMap();
config.emplace("global/transports", "sip:127.0.0.1:0;transport=udp");
config.emplace("module::Registrar/reg-domains", "127.0.0.1");
return config;
}()};
server.start();
auto responseCount = 0;
BellesipUtils belleSipUtils{
"0.0.0.0",
0,
"UDP",
[&responseCount](int status) {
if (status != 100) {
++responseCount;
}
},
nullptr,
};
ContactInserter inserter{server.getAgent()->getRegistrarDb()};
inserter.setAor("sip:message_expires@127.0.0.1")
.setExpire(0s)
.setContactParams({"message-expires=1609"})
.insert({"sip:message_expires@127.0.0.1:" + to_string(belleSipUtils.getListeningPort())});
CoreAssert asserter{server, belleSipUtils};
asserter.wait([&inserter] { return LOOP_ASSERTION(inserter.finished()); }).hard_assert_passed();
const auto& routerModule = static_pointer_cast<ModuleRouter>(server.getAgent()->findModule("Router"));
BC_HARD_ASSERT(routerModule != nullptr);
auto* forks = routerModule->mStats.mCountForks->start;
BC_ASSERT_CPP_EQUAL(forks->read(), 0);
stringstream rawRequest{};
rawRequest << "OPTIONS sip:message_expires@127.0.0.1:" << server.getFirstPort() << " SIP/2.0\r\n"
<< "Via: SIP/2.0/TCP 127.0.0.1\r\n"
<< "From: <sip:from@127.0.0.1>;tag=stub-from-tag-1\r\n"
<< "To: <sip:message_expires@127.0.0.1>\r\n"
<< "CSeq: 20 OPTIONS\r\n"
<< "Call-ID: stub-call-id-1\r\n"
<< "Content-Length: 0\r\n\r\n";
belleSipUtils.sendRawRequest(rawRequest.str());
rawRequest = {};
rawRequest << "MESSAGE sip:message_expires@127.0.0.1:" << server.getFirstPort() << " SIP/2.0\r\n"
<< "Via: SIP/2.0/TCP 127.0.0.1\r\n"
<< "From: <sip:from@127.0.0.1>;tag=stub-from-tag-2\r\n"
<< "To: <sip:message_expires@127.0.0.1>\r\n"
<< "CSeq: 20 MESSAGE\r\n"
<< "Call-ID: stub-call-id-2\r\n"
<< "Content-Type: text/plain\r\n"
<< "Content-Length: 0\r\n\r\n";
belleSipUtils.sendRawRequest(rawRequest.str());
asserter.wait([&responseCount] { return LOOP_ASSERTION(responseCount == 2); }).hard_assert_passed();
BC_ASSERT_CPP_EQUAL(forks->read(), 1);
}
struct Contact {
string aor;
string uri;
};
/*
* Test helper for unit tests about routing requests with "module::Router/static-targets" parameter.
*/
struct RoutingWithStaticTargets {
RoutingWithStaticTargets(const vector<Contact>& contacts, const vector<string>& staticTargets)
: mInjectedModule({
.injectAfterModule = {"Router"},
.onRequest =
[this](const shared_ptr<RequestSipEvent>& ev) {
if (ev->getMsgSip()->getSipMethod() != sip_method_invite) return;
mActualTargets.emplace_back(url_as_string(ev->getHome(), ev->getSip()->sip_request->rq_url));
},
}),
mProxy(
{
{"global/aliases", "localhost"},
{"module::NatHelper/enabled", "false"},
{"module::DoSProtection/enabled", "false"},
{"module::Registrar/reg-domains", "localhost"},
{"module::Router/static-targets", StringUtils::join(staticTargets)},
},
&mInjectedModule),
mClient(mProxy.getRoot(), "sip:127.0.0.1:0") {
mProxy.start();
ContactInserter inserter(mProxy.getAgent()->getRegistrarDb());
for (const auto& contact : contacts) {
inserter.setAor(contact.aor).setExpire(1min).insert({contact.uri});
}
mAsserter.wait([&inserter] { return inserter.finished(); }).hard_assert_passed();
}
vector<string> mActualTargets{};
Contact mCaller{"sip:caller@localhost", "sip:caller@voluntarily-unreachable:0"};
InjectedHooks mInjectedModule;
Server mProxy;
sofiasip::NtaAgent mClient;
CoreAssert<kDefaultSleepInterval> mAsserter{mProxy};
};
/*
* Test that INVITE request is both routed to the callee and to the provided static targets.
*/
void requestIsAlsoRoutedToStaticTargets() {
// Set up expected targets without transport and port 0 so the server does not try to send forked INVITE requests.
const auto callee = Contact{"sip:callee@localhost", "sip:callee@127.0.0.1:0"};
const auto sTarget = Contact{"sip:sTarget@localhost", "sip:sTarget@127.0.0.1:0"};
const auto sTargetBis = Contact{"sip:sTargetBis@localhost", "sip:sTargetBis@127.0.0.1:0"};
RoutingWithStaticTargets helper{{callee}, {sTarget.uri, sTargetBis.uri}};
vector<string> expectedTargets = {sTarget.uri, sTargetBis.uri, callee.uri};
const auto routeUri = "sip:127.0.0.1:"s + helper.mProxy.getFirstPort();
ostringstream request;
request << "INVITE " << callee.aor << " SIP/2.0\r\n"
<< "Via: SIP/2.0/TCP 127.0.0.1\r\n"
<< "From: \"Caller\" <" << helper.mCaller.aor << ">;tag=stub-tag\r\n"
<< "To: \"Callee\" <" << callee.aor << ">\r\n"
<< "CSeq: 20 INVITE\r\n"
<< "Call-ID: stub-id\r\n"
<< "Contact: <" << helper.mCaller.aor << ";transport=tcp>\r\n"
<< "User-Agent: NtaAgent\r\n"
<< "Allow: INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, NOTIFY, MESSAGE, SUBSCRIBE, INFO, PRACK, UPDATE\r\n"
<< "Supported: replaces, outbound, gruu\r\n"
<< "Content-Type: application/sdp\r\n"
<< "Content-Length: 0\r\n\r\n";
const auto transaction = helper.mClient.createOutgoingTransaction(request.str(), routeUri);
helper.mAsserter.wait([&transaction]() { return transaction->isCompleted(); }).assert_passed();
BC_HARD_ASSERT_CPP_EQUAL(helper.mActualTargets.size(), expectedTargets.size());
for (auto targetId = 0U; targetId < expectedTargets.size(); ++targetId) {
BC_ASSERT_CPP_EQUAL(helper.mActualTargets[targetId], expectedTargets[targetId]);
}
}
/*
* Test that INVITE request is both routed to the list of targets defined in the "X-Target-Uris" header and to provided
* static targets. In this case, it should not be routed to the callee.
*/
void requestIsRoutedToXTargetUrisAndStaticTargets() {
// Set up expected targets without transport and port 0 so the server does not try to send forked INVITE requests.
const auto callee = Contact{"sip:callee@localhost", "sip:callee@127.0.0.1:0"};
const auto sTarget = Contact{"sip:sTarget@localhost", "sip:sTarget@127.0.0.1:0"};
const auto sTargetBis = Contact{"sip:sTargetBis@localhost", "sip:sTargetBis@127.0.0.1:0"};
const auto xTarget = Contact{"sip:xTarget@localhost", "sip:xTarget@127.0.0.1:0"};
const auto xTargetBis = Contact{"sip:xTargetBis@localhost", "sip:xTargetBis@127.0.0.1:0"};
RoutingWithStaticTargets helper{{xTarget, xTargetBis}, {sTarget.uri, sTargetBis.uri}};
vector<string> expectedTargets = {sTarget.uri, sTargetBis.uri, xTarget.uri, xTargetBis.uri};
const auto routeUri = "sip:127.0.0.1:"s + helper.mProxy.getFirstPort();
ostringstream request;
request << "INVITE " << callee.aor << " SIP/2.0\r\n"
<< "Via: SIP/2.0/TCP 127.0.0.1\r\n"
<< "From: \"Caller\" <" << helper.mCaller.aor << ">;tag=stub-tag\r\n"
<< "To: \"Callee\" <" << callee.aor << ">\r\n"
<< "CSeq: 20 INVITE\r\n"
<< "Call-ID: stub-id\r\n"
<< "Contact: <" << helper.mCaller.aor << ";transport=tcp>\r\n"
<< "X-Target-Uris: <" << xTarget.aor << ">,<" << xTargetBis.aor << ">\r\n"
<< "User-Agent: NtaAgent\r\n"
<< "Allow: INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, NOTIFY, MESSAGE, SUBSCRIBE, INFO, PRACK, UPDATE\r\n"
<< "Supported: replaces, outbound, gruu\r\n"
<< "Content-Type: application/sdp\r\n"
<< "Content-Length: 0\r\n\r\n";
const auto transaction = helper.mClient.createOutgoingTransaction(request.str(), routeUri);
helper.mAsserter.wait([&transaction]() { return transaction->isCompleted(); }).assert_passed();
BC_HARD_ASSERT_CPP_EQUAL(helper.mActualTargets.size(), expectedTargets.size());
for (auto targetId = 0U; targetId < expectedTargets.size(); ++targetId) {
BC_ASSERT_CPP_EQUAL(helper.mActualTargets[targetId], expectedTargets[targetId]);
}
}
TestSuite _("RouterModule",
{
CLASSY_TEST(fallbackRouteFilter),
CLASSY_TEST(selfRouteHeaderRemoving),
CLASSY_TEST(otherRouteHeaderNotRemoved),
CLASSY_TEST(messageExpires<DbImplementation::Internal>),
CLASSY_TEST(messageExpires<DbImplementation::Redis>),
CLASSY_TEST(requestIsAlsoRoutedToStaticTargets),
CLASSY_TEST(requestIsRoutedToXTargetUrisAndStaticTargets),
});
} // namespace
} // namespace flexisip::tester