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