Line data Source code
1 : /*
2 : * Copyright (C) 2004-2024 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include <cppunit/TestAssert.h>
19 : #include <cppunit/TestFixture.h>
20 : #include <cppunit/extensions/HelperMacros.h>
21 :
22 : #include <condition_variable>
23 : #include <string>
24 :
25 : #include "callmanager_interface.h"
26 : #include "manager.h"
27 : #include "sip/sipaccount.h"
28 : #include "../../test_runner.h"
29 : #include "jami.h"
30 : #include "jami/media_const.h"
31 : #include "call_const.h"
32 : #include "account_const.h"
33 : #include "sip/sipcall.h"
34 : #include "sip/sdp.h"
35 : using namespace libjami::Account;
36 : using namespace libjami::Call;
37 :
38 : namespace jami {
39 : namespace test {
40 :
41 : struct CallData
42 : {
43 : struct Signal
44 : {
45 12 : Signal(const std::string& name, const std::string& event = {})
46 12 : : name_(std::move(name))
47 12 : , event_(std::move(event)) {};
48 :
49 : std::string name_ {};
50 : std::string event_ {};
51 : };
52 :
53 : std::string accountId_ {};
54 : uint16_t listeningPort_ {0};
55 : std::string userName_ {};
56 : std::string alias_ {};
57 : std::string callId_ {};
58 : std::vector<Signal> signals_;
59 : std::condition_variable cv_ {};
60 : std::mutex mtx_;
61 : };
62 :
63 : /**
64 : * Call tests for SIP accounts.
65 : */
66 : class SipSrtpTest : public CppUnit::TestFixture
67 : {
68 : public:
69 1 : SipSrtpTest()
70 1 : {
71 : // Init daemon
72 1 : libjami::init(libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG));
73 1 : if (not Manager::instance().initialized)
74 1 : CPPUNIT_ASSERT(libjami::start("dring-sample.yml"));
75 1 : }
76 2 : ~SipSrtpTest() { libjami::fini(); }
77 :
78 2 : static std::string name() { return "SipSrtpTest"; }
79 : void setUp();
80 : void tearDown();
81 :
82 : private:
83 : // Test cases.
84 : void audio_video_srtp_enabled_test();
85 :
86 2 : CPPUNIT_TEST_SUITE(SipSrtpTest);
87 1 : CPPUNIT_TEST(audio_video_srtp_enabled_test);
88 :
89 4 : CPPUNIT_TEST_SUITE_END();
90 :
91 : // Event/Signal handlers
92 : static void onCallStateChange(const std::string& accountId,
93 : const std::string& callId,
94 : const std::string& state,
95 : CallData& callData);
96 : static void onIncomingCallWithMedia(const std::string& accountId,
97 : const std::string& callId,
98 : const std::vector<libjami::MediaMap> mediaList,
99 : CallData& callData);
100 : static void onMediaNegotiationStatus(const std::string& callId,
101 : const std::string& event,
102 : CallData& callData);
103 :
104 : // Helpers
105 : void audio_video_call(std::vector<MediaAttribute> offer,
106 : std::vector<MediaAttribute> answer,
107 : bool validateMedia = true);
108 : static void configureTest(CallData& bob, CallData& alice);
109 : static std::string getUserAlias(const std::string& callId);
110 : // Wait for a signal from the callbacks. Some signals also report the event that
111 : // triggered the signal a like the StateChange signal.
112 : static bool waitForSignal(CallData& callData,
113 : const std::string& signal,
114 : const std::string& expectedEvent = {});
115 :
116 : private:
117 : CallData aliceData_;
118 : CallData bobData_;
119 : };
120 :
121 : CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(SipSrtpTest, SipSrtpTest::name());
122 :
123 : void
124 1 : SipSrtpTest::setUp()
125 : {
126 1 : aliceData_.listeningPort_ = 5080;
127 2 : std::map<std::string, std::string> details = libjami::getAccountTemplate("SIP");
128 1 : details[ConfProperties::TYPE] = "SIP";
129 1 : details[ConfProperties::DISPLAYNAME] = "ALICE";
130 1 : details[ConfProperties::ALIAS] = "ALICE";
131 1 : details[ConfProperties::LOCAL_PORT] = std::to_string(aliceData_.listeningPort_);
132 1 : details[ConfProperties::UPNP_ENABLED] = "false";
133 1 : details[ConfProperties::SRTP::KEY_EXCHANGE] = "sdes";
134 1 : aliceData_.accountId_ = Manager::instance().addAccount(details);
135 :
136 1 : bobData_.listeningPort_ = 5082;
137 1 : details = libjami::getAccountTemplate("SIP");
138 1 : details[ConfProperties::TYPE] = "SIP";
139 1 : details[ConfProperties::DISPLAYNAME] = "BOB";
140 1 : details[ConfProperties::ALIAS] = "BOB";
141 1 : details[ConfProperties::LOCAL_PORT] = std::to_string(bobData_.listeningPort_);
142 1 : details[ConfProperties::UPNP_ENABLED] = "false";
143 1 : details[ConfProperties::SRTP::KEY_EXCHANGE] = "sdes";
144 1 : bobData_.accountId_ = Manager::instance().addAccount(details);
145 :
146 1 : JAMI_INFO("Initialize accounts ...");
147 1 : auto aliceAccount = Manager::instance().getAccount<SIPAccount>(aliceData_.accountId_);
148 1 : auto bobAccount = Manager::instance().getAccount<SIPAccount>(bobData_.accountId_);
149 1 : }
150 :
151 : void
152 1 : SipSrtpTest::tearDown()
153 : {
154 1 : JAMI_INFO("Remove created accounts...");
155 :
156 1 : std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;
157 1 : std::mutex mtx;
158 1 : std::unique_lock lk {mtx};
159 1 : std::condition_variable cv;
160 1 : auto currentAccSize = Manager::instance().getAccountList().size();
161 1 : std::atomic_bool accountsRemoved {false};
162 1 : confHandlers.insert(
163 2 : libjami::exportable_callback<libjami::ConfigurationSignal::AccountsChanged>([&]() {
164 2 : if (Manager::instance().getAccountList().size() <= currentAccSize - 2) {
165 1 : accountsRemoved = true;
166 1 : cv.notify_one();
167 : }
168 2 : }));
169 1 : libjami::registerSignalHandlers(confHandlers);
170 :
171 1 : Manager::instance().removeAccount(aliceData_.accountId_, true);
172 1 : Manager::instance().removeAccount(bobData_.accountId_, true);
173 : // Because cppunit is not linked with dbus, just poll if removed
174 2 : CPPUNIT_ASSERT(
175 : cv.wait_for(lk, std::chrono::seconds(30), [&] { return accountsRemoved.load(); }));
176 :
177 1 : libjami::unregisterSignalHandlers();
178 1 : }
179 :
180 : std::string
181 14 : SipSrtpTest::getUserAlias(const std::string& callId)
182 : {
183 14 : auto call = Manager::instance().getCallFromCallID(callId);
184 :
185 14 : if (not call) {
186 2 : JAMI_WARN("Call with ID [%s] does not exist anymore!", callId.c_str());
187 2 : return {};
188 : }
189 :
190 12 : auto const& account = call->getAccount().lock();
191 12 : if (not account) {
192 0 : return {};
193 : }
194 :
195 12 : return account->getAccountDetails()[ConfProperties::ALIAS];
196 14 : }
197 :
198 : void
199 1 : SipSrtpTest::onIncomingCallWithMedia(const std::string& accountId,
200 : const std::string& callId,
201 : const std::vector<libjami::MediaMap> mediaList,
202 : CallData& callData)
203 : {
204 1 : CPPUNIT_ASSERT_EQUAL(callData.accountId_, accountId);
205 :
206 1 : JAMI_INFO("Signal [%s] - user [%s] - call [%s] - media count [%lu]",
207 : libjami::CallSignal::IncomingCallWithMedia::name,
208 : callData.alias_.c_str(),
209 : callId.c_str(),
210 : mediaList.size());
211 :
212 : // NOTE.
213 : // We shouldn't access shared_ptr<Call> as this event is supposed to mimic
214 : // the client, and the client have no access to this type. But here, we only
215 : // needed to check if the call exists. This is the most straightforward and
216 : // reliable way to do it until we add a new API (like hasCall(id)).
217 1 : if (not Manager::instance().getCallFromCallID(callId)) {
218 0 : JAMI_WARN("Call with ID [%s] does not exist!", callId.c_str());
219 0 : callData.callId_ = {};
220 0 : return;
221 : }
222 :
223 1 : std::unique_lock lock {callData.mtx_};
224 1 : callData.callId_ = callId;
225 1 : callData.signals_.emplace_back(CallData::Signal(libjami::CallSignal::IncomingCallWithMedia::name));
226 :
227 1 : callData.cv_.notify_one();
228 1 : }
229 :
230 : void
231 9 : SipSrtpTest::onCallStateChange(const std::string&,
232 : const std::string& callId,
233 : const std::string& state,
234 : CallData& callData)
235 : {
236 9 : auto call = Manager::instance().getCallFromCallID(callId);
237 9 : if (not call) {
238 0 : JAMI_WARN("Call with ID [%s] does not exist anymore!", callId.c_str());
239 0 : return;
240 : }
241 :
242 9 : auto account = call->getAccount().lock();
243 9 : if (not account) {
244 0 : JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str());
245 0 : return;
246 : }
247 :
248 9 : JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]",
249 : libjami::CallSignal::StateChange::name,
250 : callData.alias_.c_str(),
251 : callId.c_str(),
252 : state.c_str());
253 :
254 9 : if (account->getAccountID() != callData.accountId_)
255 0 : return;
256 :
257 : {
258 9 : std::unique_lock lock {callData.mtx_};
259 9 : callData.signals_.emplace_back(
260 18 : CallData::Signal(libjami::CallSignal::StateChange::name, state));
261 9 : }
262 : // NOTE. Only states that we are interested on will notify the CV. If this
263 : // unit test is modified to process other states, they must be added here.
264 9 : if (state == "CURRENT" or state == "OVER" or state == "HUNGUP" or state == "RINGING") {
265 5 : callData.cv_.notify_one();
266 : }
267 9 : }
268 :
269 : void
270 2 : SipSrtpTest::onMediaNegotiationStatus(const std::string& callId,
271 : const std::string& event,
272 : CallData& callData)
273 : {
274 2 : auto call = Manager::instance().getCallFromCallID(callId);
275 2 : if (not call) {
276 0 : JAMI_WARN("Call with ID [%s] does not exist!", callId.c_str());
277 0 : return;
278 : }
279 :
280 2 : auto account = call->getAccount().lock();
281 2 : if (not account) {
282 0 : JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str());
283 0 : return;
284 : }
285 :
286 2 : JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]",
287 : libjami::CallSignal::MediaNegotiationStatus::name,
288 : account->getAccountDetails()[ConfProperties::ALIAS].c_str(),
289 : call->getCallId().c_str(),
290 : event.c_str());
291 :
292 2 : if (account->getAccountID() != callData.accountId_)
293 0 : return;
294 :
295 : {
296 2 : std::unique_lock lock {callData.mtx_};
297 2 : callData.signals_.emplace_back(
298 4 : CallData::Signal(libjami::CallSignal::MediaNegotiationStatus::name, event));
299 2 : }
300 :
301 2 : callData.cv_.notify_one();
302 2 : }
303 :
304 : bool
305 6 : SipSrtpTest::waitForSignal(CallData& callData,
306 : const std::string& expectedSignal,
307 : const std::string& expectedEvent)
308 : {
309 6 : const std::chrono::seconds TIME_OUT {30};
310 6 : std::unique_lock lock {callData.mtx_};
311 :
312 : // Combined signal + event (if any).
313 6 : std::string sigEvent(expectedSignal);
314 6 : if (not expectedEvent.empty())
315 5 : sigEvent += "::" + expectedEvent;
316 :
317 6 : JAMI_INFO("[%s] is waiting for [%s] signal/event", callData.alias_.c_str(), sigEvent.c_str());
318 :
319 6 : auto res = callData.cv_.wait_for(lock, TIME_OUT, [&] {
320 : // Search for the expected signal in list of received signals.
321 9 : bool pred = false;
322 32 : for (auto it = callData.signals_.begin(); it != callData.signals_.end(); it++) {
323 : // The predicate is true if the signal names match, and if the
324 : // expectedEvent is not empty, the events must also match.
325 29 : if (it->name_ == expectedSignal
326 29 : and (expectedEvent.empty() or it->event_ == expectedEvent)) {
327 6 : pred = true;
328 : // Done with this signal.
329 6 : callData.signals_.erase(it);
330 6 : break;
331 : }
332 : }
333 :
334 9 : return pred;
335 : });
336 :
337 6 : if (not res) {
338 0 : JAMI_ERR("[%s] waiting for signal/event [%s] timed-out!",
339 : callData.alias_.c_str(),
340 : sigEvent.c_str());
341 :
342 0 : JAMI_INFO("[%s] currently has the following signals:", callData.alias_.c_str());
343 :
344 0 : for (auto const& sig : callData.signals_) {
345 0 : JAMI_INFO() << "Signal [" << sig.name_
346 0 : << (sig.event_.empty() ? "" : ("::" + sig.event_)) << "]";
347 : }
348 : }
349 :
350 6 : return res;
351 6 : }
352 :
353 : void
354 1 : SipSrtpTest::configureTest(CallData& aliceData, CallData& bobData)
355 : {
356 : {
357 1 : CPPUNIT_ASSERT(not aliceData.accountId_.empty());
358 1 : auto const& account = Manager::instance().getAccount<SIPAccount>(aliceData.accountId_);
359 1 : aliceData.userName_ = account->getAccountDetails()[ConfProperties::USERNAME];
360 1 : aliceData.alias_ = account->getAccountDetails()[ConfProperties::ALIAS];
361 1 : account->setLocalPort(aliceData.listeningPort_);
362 1 : }
363 :
364 : {
365 1 : CPPUNIT_ASSERT(not bobData.accountId_.empty());
366 1 : auto const& account = Manager::instance().getAccount<SIPAccount>(bobData.accountId_);
367 1 : bobData.userName_ = account->getAccountDetails()[ConfProperties::USERNAME];
368 1 : bobData.alias_ = account->getAccountDetails()[ConfProperties::ALIAS];
369 1 : account->setLocalPort(bobData.listeningPort_);
370 1 : }
371 :
372 1 : std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> signalHandlers;
373 :
374 : // Insert needed signal handlers.
375 1 : signalHandlers.insert(libjami::exportable_callback<libjami::CallSignal::IncomingCallWithMedia>(
376 1 : [&](const std::string& accountId,
377 : const std::string& callId,
378 : const std::string&,
379 : const std::vector<libjami::MediaMap> mediaList) {
380 1 : auto user = getUserAlias(callId);
381 1 : if (not user.empty())
382 1 : onIncomingCallWithMedia(accountId,
383 : callId,
384 : mediaList,
385 1 : user == aliceData.alias_ ? aliceData : bobData);
386 1 : }));
387 :
388 1 : signalHandlers.insert(
389 2 : libjami::exportable_callback<libjami::CallSignal::StateChange>([&](const std::string& accountId,
390 : const std::string& callId,
391 : const std::string& state,
392 : signed) {
393 11 : auto user = getUserAlias(callId);
394 11 : if (not user.empty())
395 9 : onCallStateChange(accountId,
396 : callId,
397 : state,
398 9 : user == aliceData.alias_ ? aliceData : bobData);
399 11 : }));
400 :
401 1 : signalHandlers.insert(libjami::exportable_callback<libjami::CallSignal::MediaNegotiationStatus>(
402 2 : [&](const std::string& callId,
403 : const std::string& event,
404 : const std::vector<std::map<std::string, std::string>>& /* mediaList */) {
405 2 : auto user = getUserAlias(callId);
406 2 : if (not user.empty())
407 2 : onMediaNegotiationStatus(callId,
408 : event,
409 2 : user == aliceData.alias_ ? aliceData : bobData);
410 2 : }));
411 :
412 1 : libjami::registerSignalHandlers(signalHandlers);
413 1 : }
414 :
415 : void
416 1 : SipSrtpTest::audio_video_call(std::vector<MediaAttribute> offer,
417 : std::vector<MediaAttribute> answer,
418 : bool validateMedia)
419 : {
420 1 : JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
421 :
422 1 : configureTest(aliceData_, bobData_);
423 :
424 1 : JAMI_INFO("=== Start a call and validate ===");
425 :
426 1 : std::string bobUri = "127.0.0.1:" + std::to_string(bobData_.listeningPort_);
427 :
428 2 : aliceData_.callId_ = libjami::placeCallWithMedia(aliceData_.accountId_,
429 : bobUri,
430 2 : MediaAttribute::mediaAttributesToMediaMaps(
431 1 : offer));
432 :
433 1 : CPPUNIT_ASSERT(not aliceData_.callId_.empty());
434 :
435 1 : JAMI_INFO("ALICE [%s] started a call with BOB [%s] and wait for answer",
436 : aliceData_.accountId_.c_str(),
437 : bobData_.accountId_.c_str());
438 :
439 : // Give it some time to ring
440 1 : std::this_thread::sleep_for(std::chrono::seconds(2));
441 :
442 : // Wait for call to be processed.
443 1 : CPPUNIT_ASSERT(
444 : waitForSignal(aliceData_, libjami::CallSignal::StateChange::name, StateEvent::RINGING));
445 :
446 : // Wait for incoming call signal.
447 1 : CPPUNIT_ASSERT(waitForSignal(bobData_, libjami::CallSignal::IncomingCallWithMedia::name));
448 :
449 : // Answer the call.
450 1 : libjami::acceptWithMedia(bobData_.accountId_,
451 1 : bobData_.callId_,
452 2 : MediaAttribute::mediaAttributesToMediaMaps(answer));
453 :
454 : // Wait for media negotiation complete signal.
455 1 : CPPUNIT_ASSERT(waitForSignal(bobData_,
456 : libjami::CallSignal::MediaNegotiationStatus::name,
457 : libjami::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS));
458 :
459 : // Wait for the StateChange signal.
460 1 : CPPUNIT_ASSERT(
461 : waitForSignal(bobData_, libjami::CallSignal::StateChange::name, StateEvent::CURRENT));
462 :
463 1 : JAMI_INFO("BOB answered the call [%s]", bobData_.callId_.c_str());
464 :
465 : // Wait for media negotiation complete signal.
466 1 : CPPUNIT_ASSERT(waitForSignal(aliceData_,
467 : libjami::CallSignal::MediaNegotiationStatus::name,
468 : libjami::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS));
469 :
470 : // Validate Alice's media
471 1 : if (validateMedia) {
472 1 : auto call = Manager::instance().getCallFromCallID(aliceData_.callId_);
473 1 : auto activeMediaList = call->getMediaAttributeList();
474 1 : CPPUNIT_ASSERT_EQUAL(offer.size(), activeMediaList.size());
475 : // Audio
476 1 : CPPUNIT_ASSERT_EQUAL(MediaType::MEDIA_AUDIO, activeMediaList[0].type_);
477 1 : CPPUNIT_ASSERT_EQUAL(offer[0].enabled_, activeMediaList[0].enabled_);
478 :
479 : // Video
480 1 : if (offer.size() > 1) {
481 1 : CPPUNIT_ASSERT_EQUAL(MediaType::MEDIA_VIDEO, activeMediaList[1].type_);
482 1 : CPPUNIT_ASSERT_EQUAL(offer[1].enabled_, activeMediaList[1].enabled_);
483 : }
484 1 : }
485 :
486 : // Validate Bob's media
487 1 : if (validateMedia) {
488 1 : auto call = Manager::instance().getCallFromCallID(bobData_.callId_);
489 1 : auto activeMediaList = call->getMediaAttributeList();
490 1 : CPPUNIT_ASSERT_EQUAL(answer.size(), activeMediaList.size());
491 : // Audio
492 1 : CPPUNIT_ASSERT_EQUAL(MediaType::MEDIA_AUDIO, activeMediaList[0].type_);
493 1 : CPPUNIT_ASSERT_EQUAL(answer[0].enabled_, activeMediaList[0].enabled_);
494 :
495 : // Video
496 1 : if (offer.size() > 1) {
497 1 : CPPUNIT_ASSERT_EQUAL(MediaType::MEDIA_VIDEO, activeMediaList[1].type_);
498 1 : CPPUNIT_ASSERT_EQUAL(answer[1].enabled_, activeMediaList[1].enabled_);
499 : }
500 1 : }
501 :
502 : // Give some time to media to start and flow
503 1 : std::this_thread::sleep_for(std::chrono::seconds(3));
504 :
505 : // Bob hang-up.
506 1 : JAMI_INFO("Hang up BOB's call and wait for ALICE to hang up");
507 1 : libjami::hangUp(bobData_.accountId_, bobData_.callId_);
508 :
509 1 : CPPUNIT_ASSERT(
510 : waitForSignal(aliceData_, libjami::CallSignal::StateChange::name, StateEvent::HUNGUP));
511 :
512 1 : JAMI_INFO("Call terminated on both sides");
513 1 : }
514 :
515 : void
516 1 : SipSrtpTest::audio_video_srtp_enabled_test()
517 : {
518 : // Test with video enabled on Alice's side and disabled
519 : // on Bob's side.
520 :
521 1 : auto const aliceAcc = Manager::instance().getAccount<SIPAccount>(aliceData_.accountId_);
522 1 : CPPUNIT_ASSERT(aliceAcc->isSrtpEnabled());
523 :
524 1 : auto const bobAcc = Manager::instance().getAccount<SIPAccount>(bobData_.accountId_);
525 1 : CPPUNIT_ASSERT(bobAcc->isSrtpEnabled());
526 :
527 1 : std::vector<MediaAttribute> offer;
528 1 : std::vector<MediaAttribute> answer;
529 :
530 1 : MediaAttribute audio(MediaType::MEDIA_AUDIO);
531 1 : MediaAttribute video(MediaType::MEDIA_VIDEO);
532 :
533 : // Configure Alice
534 1 : audio.enabled_ = true;
535 1 : audio.label_ = "audio_0";
536 1 : offer.emplace_back(audio);
537 :
538 1 : video.enabled_ = true;
539 1 : video.label_ = "video_0";
540 1 : aliceAcc->enableVideo(true);
541 1 : offer.emplace_back(video);
542 :
543 : // Configure Bob
544 1 : audio.enabled_ = true;
545 1 : audio.label_ = "audio_0";
546 1 : answer.emplace_back(audio);
547 :
548 1 : video.enabled_ = false;
549 1 : video.label_ = "video_0";
550 1 : bobAcc->enableVideo(false);
551 1 : answer.emplace_back(video);
552 :
553 : // Run the scenario
554 1 : audio_video_call(offer, answer);
555 1 : }
556 :
557 : } // namespace test
558 : } // namespace jami
559 :
560 1 : RING_TEST_RUNNER(jami::test::SipSrtpTest::name())
|