Ring Daemon
Loading...
Searching...
No Matches
sippresence.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2004-2026 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 "sip/sippresence.h"
19
20#include "logger.h"
21#include "manager.h"
22#include "sip/sipaccount.h"
24#include "pres_sub_server.h"
25#include "pres_sub_client.h"
26#include "sip/sipvoiplink.h"
27#include "client/jami_signal.h"
29
30#include <opendht/crypto.h>
31#include <fmt/core.h>
32
33#define MAX_N_SUB_SERVER 50
34#define MAX_N_SUB_CLIENT 50
35
36namespace jami {
37
39
41 : publish_sess_()
42 , status_data_()
43 , enabled_(false)
44 , publish_supported_(false)
45 , subscribe_supported_(false)
46 , status_(false)
47 , note_(" ")
48 , acc_(acc)
49 , sub_server_list_() // IP2IP context
50 , sub_client_list_()
51 , cp_()
52 , pool_()
53{
54 /* init pool */
56 pool_ = pj_pool_create(&cp_.factory, "pres", 1000, 1000, NULL);
57 if (!pool_)
58 throw std::runtime_error("Unable to allocate pool for presence");
59
60 /* init default status */
61 updateStatus(false, " ");
62}
63
65{
66 /* Flush the lists */
67 // FIXME: Unable to destroy/unsubscribe buddies properly.
68 // Is the transport usable when the account is being destroyed?
69 // for (const auto & c : sub_client_list_)
70 // delete(c);
71 sub_client_list_.clear();
72 sub_server_list_.clear();
73
74 pj_pool_release(pool_);
76}
77
80{
81 return acc_;
82}
83
86{
87 return &status_data_;
88}
89
90int
95
98{
99 return pool_;
100}
101
102void
104{
105 enabled_ = enabled;
106}
107
108void
110{
112 publish_supported_ = supported;
114 subscribe_supported_ = supported;
115}
116
117bool
119{
121 return publish_supported_;
123 return subscribe_supported_;
124
125 return false;
126}
127
128void
129SIPPresence::updateStatus(bool status, const std::string& note)
130{
131 // char* pj_note = (char*) pj_pool_alloc(pool_, "50");
132
133 pjrpid_element rpid = {PJRPID_ELEMENT_TYPE_PERSON, CONST_PJ_STR("0"), PJRPID_ACTIVITY_UNKNOWN, CONST_PJ_STR(note)};
134
135 /* fill activity if user not available. */
136 if (note == "away")
137 rpid.activity = PJRPID_ACTIVITY_AWAY;
138 else if (note == "busy")
139 rpid.activity = PJRPID_ACTIVITY_BUSY;
140 /*
141 else // TODO: is there any other possibilities
142 JAMI_DBG("Presence : no activity");
143 */
144
145 pj_bzero(&status_data_, sizeof(status_data_));
146 status_data_.info_cnt = 1;
147 status_data_.info[0].basic_open = status;
148
149 // at most we will have 3 digits + NULL termination
150 char buf[4];
151 pj_utoa(rand() % 1000, buf);
152 status_data_.info[0].id = pj_strdup3(pool_, buf);
153
154 pj_memcpy(&status_data_.info[0].rpid, &rpid, sizeof(pjrpid_element));
155 /* "contact" field is optionnal */
156}
157
158void
159SIPPresence::sendPresence(bool status, const std::string& note)
160{
161 updateStatus(status, note);
162
163 // if ((not publish_supported_) or (not enabled_))
164 // return;
165
166 if (acc_->isIP2IP())
167 notifyPresSubServer(); // to each subscribers
168 else
169 publish(this); // to the PBX server
170}
171
172void
174{
175 /* Update our info. See pjsua_buddy_get_info() for additionnal ideas*/
176 const std::string& acc_ID = acc_->getAccountID();
177 const std::string note(status->info[0].rpid.note.ptr, status->info[0].rpid.note.slen);
178 JAMI_DBG(" Received status of PresSubClient %.*s(acc:%s): status=%s note=%s",
179 (int) uri.size(),
180 uri.data(),
181 acc_ID.c_str(),
182 status->info[0].basic_open ? "open" : "closed",
183 note.c_str());
184
185 if (uri == acc_->getFromUri()) {
186 // save the status of our own account
187 status_ = status->info[0].basic_open;
188 note_ = note;
189 }
190 // report status to client signal
192 std::string(uri),
193 status->info[0].basic_open,
194 note);
195}
196
197void
198SIPPresence::subscribeClient(const std::string& uri, bool flag)
199{
200 /* if an account has a server that doesn't support SUBSCRIBE, it's still possible
201 * to subscribe to someone on another server */
202 /*
203 std::string account_host = std::string(pj_gethostname()->ptr, pj_gethostname()->slen);
204 std::string sub_host = sip_utils::getHostFromUri(uri);
205 if (((not subscribe_supported_) && (account_host == sub_host))
206 or (not enabled_))
207 return;
208 */
209
210 /* Check if the buddy was already subscribed */
211 for (const auto& c : sub_client_list_) {
212 if (c->getURI() == uri) {
213 // JAMI_DBG("-PresSubClient:%s exists in the list. Replace it.", uri.c_str());
214 if (flag)
215 c->subscribe();
216 else
217 c->unsubscribe();
218 return;
219 }
220 }
221
222 if (sub_client_list_.size() >= MAX_N_SUB_CLIENT) {
223 JAMI_WARN("Unable to add PresSubClient, max number reached.");
224 return;
225 }
226
227 if (flag) {
228 PresSubClient* c = new PresSubClient(uri, this);
229 if (!(c->subscribe())) {
230 JAMI_WARN("Failed send subscribe.");
231 delete c;
232 }
233 // the buddy has to be accepted before being added in the list
234 }
235}
236
237void
239{
240 if (sub_client_list_.size() < MAX_N_SUB_CLIENT) {
241 sub_client_list_.push_back(c);
242 JAMI_DBG("New Presence_subscription_client added (list[%zu]).", sub_client_list_.size());
243 } else {
244 JAMI_WARN("Max Presence_subscription_client is reach.");
245 // let the client alive //delete c;
246 }
247}
248
249void
251{
252 JAMI_DBG("Remove Presence_subscription_client from the buddy list.");
253 sub_client_list_.remove(c);
254}
255
256void
257SIPPresence::approvePresSubServer(const std::string& uri, bool flag)
258{
259 for (const auto& s : sub_server_list_) {
260 if (s->matches((char*) uri.c_str())) {
261 s->approve(flag);
262 // return; // 'return' would prevent multiple-time subscribers from spam
263 }
264 }
265}
266
267void
269{
270 if (sub_server_list_.size() < MAX_N_SUB_SERVER) {
271 sub_server_list_.push_back(s);
272 } else {
273 JAMI_WARN("Max Presence_subscription_server is reach.");
274 // let de server alive // delete s;
275 }
276}
277
278void
280{
281 sub_server_list_.remove(s);
282 JAMI_DBG("Presence_subscription_server removed");
283}
284
285void
287{
288 JAMI_DBG("Iterating through IP2IP Presence_subscription_server:");
289
290 for (const auto& s : sub_server_list_)
291 s->notify();
292}
293
294void
296{
297 mutex_.lock();
298}
299
300bool
302{
303 return mutex_.try_lock();
304}
305
306void
308{
309 mutex_.unlock();
310}
311
312void
314{
315 if (tdata->msg->type == PJSIP_REQUEST_MSG) {
316 constexpr pj_str_t STR_USER_AGENT = CONST_PJ_STR("User-Agent");
317 std::string useragent(acc_->getUserAgentName());
318 pj_str_t pJuseragent = pj_str((char*) useragent.c_str());
320 pjsip_msg_add_hdr(tdata->msg, h);
321 }
322
323 if (msg_data == NULL)
324 return;
325
326 const pjsip_hdr* hdr;
327 hdr = msg_data->hdr_list.next;
328
329 while (hdr && hdr != &msg_data->hdr_list) {
332 JAMI_DBG("adding header %p", new_hdr->name.ptr);
334 hdr = hdr->next;
335 }
336
337 if (msg_data->content_type.slen && msg_data->msg_body.slen) {
338 pjsip_msg_body* body;
339 constexpr pj_str_t type = CONST_PJ_STR("application");
340 constexpr pj_str_t subtype = CONST_PJ_STR("pidf+xml");
341 body = pjsip_msg_body_create(tdata->pool, &type, &subtype, &msg_data->msg_body);
342 tdata->msg->body = body;
343 }
344}
345
346static const pjsip_publishc_opt my_publish_opt = {true}; // this is queue_request
347
348/*
349 * Client presence publication callback.
350 */
351void
352SIPPresence::publish_cb(struct pjsip_publishc_cbparam* param)
353{
354 SIPPresence* pres = (SIPPresence*) param->token;
355
356 if (param->code / 100 != 2 || param->status != PJ_SUCCESS) {
358 pres->publish_sess_ = NULL;
359 std::string error = fmt::format("{} / {}", param->code, sip_utils::as_view(param->reason));
360 if (param->status != PJ_SUCCESS) {
362 pj_strerror(param->status, errmsg, sizeof(errmsg));
363 JAMI_ERR("Client (PUBLISH) failed, status=%d, msg=%s", param->status, errmsg);
364 emitSignal<libjami::PresenceSignal::ServerError>(pres->getAccount()->getAccountID(), error, errmsg);
365
366 } else if (param->code == 412) {
367 /* 412 (Conditional Request Failed)
368 * The PUBLISH refresh has failed, retry with new one.
369 */
370 JAMI_WARN("Publish retry.");
371 publish(pres);
372 } else if ((param->code == PJSIP_SC_BAD_EVENT) || (param->code == PJSIP_SC_NOT_IMPLEMENTED)) { // 489 or 501
373 JAMI_WARN("Client (PUBLISH) failed (%s)", error.c_str());
374
375 emitSignal<libjami::PresenceSignal::ServerError>(pres->getAccount()->getAccountID(),
376 error,
377 "Publish not supported.");
378
379 pres->getAccount()->supportPresence(PRESENCE_FUNCTION_PUBLISH, false);
380 }
381
382 } else {
383 if (param->expiration < 1) {
384 /* Could happen if server "forgot" to include Expires header
385 * in the response. We will not renew, so destroy the pubc.
386 */
388 pres->publish_sess_ = NULL;
389 }
390
391 pres->getAccount()->supportPresence(PRESENCE_FUNCTION_PUBLISH, true);
392 }
393}
394
395/*
396 * Send PUBLISH request.
397 */
399SIPPresence::send_publish(SIPPresence* pres)
400{
402 pj_status_t status;
403
404 JAMI_DBG("Send PUBLISH (%s).", pres->getAccount()->getAccountID().c_str());
405
406 SIPAccount* acc = pres->getAccount();
407 std::string contactWithAngles = acc->getFromUri();
408 contactWithAngles.erase(contactWithAngles.find('>'));
409
410 /* Create PUBLISH request */
411 char* bpos;
413
414 status = pjsip_publishc_publish(pres->publish_sess_, PJ_TRUE, &tdata);
415 pj_str_t from = pj_strdup3(pres->pool_, acc->getFromUri().c_str());
416
417 if (status != PJ_SUCCESS) {
418 JAMI_ERR("Error creating PUBLISH request %d", status);
419 goto on_error;
420 }
421
422 if ((bpos = pj_strchr(&from, '<')) != NULL) {
423 char* epos = pj_strchr(&from, '>');
424
425 if (epos - bpos < 2) {
426 JAMI_ERR("Unexpected invalid URI");
427 status = PJSIP_EINVALIDURI;
428 goto on_error;
429 }
430
431 entity.ptr = bpos + 1;
432 entity.slen = epos - bpos - 1;
433 } else {
434 entity = from;
435 }
436
437 /* Create and add PIDF message body */
438 status = pjsip_pres_create_pidf(tdata->pool, pres->getStatus(), &entity, &tdata->msg->body);
439
440 pres_msg_data msg_data;
441
442 if (status != PJ_SUCCESS) {
443 JAMI_ERR("Error creating PIDF for PUBLISH request");
445 goto on_error;
446 }
447
448 pj_bzero(&msg_data, sizeof(msg_data));
449 pj_list_init(&msg_data.hdr_list);
450 pjsip_media_type_init(&msg_data.multipart_ctype, NULL, NULL);
451 pj_list_init(&msg_data.multipart_parts);
452
453 pres->fillDoc(tdata, &msg_data);
454
455 /* Send the PUBLISH request */
456 status = pjsip_publishc_send(pres->publish_sess_, tdata);
457
458 if (status == PJ_EPENDING) {
459 JAMI_WARN("Previous request is in progress, ");
460 } else if (status != PJ_SUCCESS) {
461 JAMI_ERR("Error sending PUBLISH request");
462 goto on_error;
463 }
464
465 return PJ_SUCCESS;
466
468
469 if (pres->publish_sess_) {
470 pjsip_publishc_destroy(pres->publish_sess_);
471 pres->publish_sess_ = NULL;
472 }
473
474 return status;
475}
476
477/* Create client publish session */
479SIPPresence::publish(SIPPresence* pres)
480{
481 pj_status_t status;
482 constexpr pj_str_t STR_PRESENCE = CONST_PJ_STR("presence");
483 SIPAccount* acc = pres->getAccount();
485
486 /* Create and init client publication session */
487
488 /* Create client publication */
489 status = pjsip_publishc_create(endpt, &my_publish_opt, pres, &publish_cb, &pres->publish_sess_);
490
491 if (status != PJ_SUCCESS) {
492 pres->publish_sess_ = NULL;
493 JAMI_ERR("Failed to create a publish session.");
494 return status;
495 }
496
497 /* Initialize client publication */
498 pj_str_t from = pj_strdup3(pres->pool_, acc->getFromUri().c_str());
499 status = pjsip_publishc_init(pres->publish_sess_, &STR_PRESENCE, &from, &from, &from, 0xFFFF);
500
501 if (status != PJ_SUCCESS) {
502 JAMI_ERR("Failed to init a publish session");
503 pres->publish_sess_ = NULL;
504 return status;
505 }
506
507 /* Add credential for authentication */
508 if (acc->hasCredentials()
510 static_cast<int>(acc->getCredentialCount()),
511 acc->getCredInfo())
512 != PJ_SUCCESS) {
513 JAMI_ERR("Unable to initialize credentials for invite session authentication");
514 return status;
515 }
516
517 /* Set route-set */
518 // FIXME: is this really necessary?
519 pjsip_regc* regc = acc->getRegistrationInfo();
520 if (regc and acc->hasServiceRoute())
521 pjsip_regc_set_route_set(regc, sip_utils::createRouteSet(acc->getServiceRoute(), pres->getPool()));
522
523 /* Send initial PUBLISH request */
524 status = send_publish(pres);
525
526 if (status != PJ_SUCCESS)
527 return status;
528
529 return PJ_SUCCESS;
530}
531
532} // namespace jami
const std::string & getAccountID() const
Get the account ID.
Definition account.h:149
const std::string & getUserAgentName()
Get the user-agent.
Definition account.cpp:422
static LIBJAMI_TEST_EXPORT Manager & instance()
Definition manager.cpp:694
SIPVoIPLink & sipVoIPLink() const
Definition manager.cpp:3229
bool subscribe()
Send a SUBCRIBE to the PXB or directly to a pres_client in the IP2IP context.
bool isIP2IP() const override
Returns true if this is the IP2IP account.
std::string getFromUri() const override
int getModId() const
Return presence module ID which is actually the same as the VOIP link.
~SIPPresence()
Destructor.
pjsip_pres_status * getStatus()
Return presence data.
void fillDoc(pjsip_tx_data *tdata, const pres_msg_data *msg_data)
Fill xml document, the header and the body.
void approvePresSubServer(const std::string &uri, bool flag)
IP2IP context.
void reportPresSubClientNotification(std::string_view uri, pjsip_pres_status *status)
Send a signal to the client on DBus.
void updateStatus(bool status, const std::string &note)
Modify the presence data.
SIPPresence(SIPAccount *acc)
Constructor.
void removePresSubServer(PresSubServer *s)
IP2IP context.
void subscribeClient(const std::string &uri, bool flag)
Send a SUBSCRIBE request to PBX/IP2IP.
void removePresSubClient(PresSubClient *b)
Remove a buddy from the list.
bool isSupported(int function)
SIPAccount * getAccount() const
Return associated sipaccount.
void enable(bool enabled)
Activate the module.
void notifyPresSubServer()
IP2IP context.
void addPresSubClient(PresSubClient *b)
Add a buddy in the buddy list.
pj_pool_t * getPool() const
Return a pool for generic functions.
void sendPresence(bool status, const std::string &note)
Send the presence data in a PUBLISH to the PBX or in a NOTIFY to a remote subscriber (IP2IP)
void addPresSubServer(PresSubServer *s)
IP2IP context.
void support(int function, bool enabled)
Support the presence function publish/subscribe.
#define JAMI_ERR(...)
Definition logger.h:230
#define JAMI_DBG(...)
Definition logger.h:228
#define JAMI_WARN(...)
Definition logger.h:229
pjsip_route_hdr * createRouteSet(const std::string &route, pj_pool_t *hdr_pool)
Definition sip_utils.cpp:77
constexpr std::string_view as_view(const pj_str_t &str) noexcept
Definition sip_utils.h:105
constexpr const pj_str_t CONST_PJ_STR(T(&a)[N]) noexcept
Definition sip_utils.h:87
void emitSignal(Args... args)
Definition jami_signal.h:64
static const pjsip_publishc_opt my_publish_opt
A SIP Account specify SIP specific functions and object = SIPCall/SIPVoIPLink)
#define MAX_N_SUB_CLIENT
#define MAX_N_SUB_SERVER
A SIP Presence manages buddy subscription in both PBX and IP2IP contexts.
#define PRESENCE_FUNCTION_SUBSCRIBE
Definition sippresence.h:34
#define PRESENCE_FUNCTION_PUBLISH
Definition sippresence.h:33