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 24 : , 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_DBG("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_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 0 : if (uri == acc_->getFromUri()) {
186 : // save the status of our own account
187 0 : status_ = status->info[0].basic_open;
188 0 : note_ = note;
189 : }
190 : // report status to client signal
191 0 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(acc_ID,
192 0 : std::string(uri),
193 : status->info[0].basic_open,
194 : note);
195 0 : }
196 :
197 : void
198 0 : SIPPresence::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 0 : for (const auto& c : sub_client_list_) {
212 0 : if (c->getURI() == uri) {
213 : // JAMI_DBG("-PresSubClient:%s exists in the list. Replace it.", uri.c_str());
214 0 : if (flag)
215 0 : c->subscribe();
216 : else
217 0 : c->unsubscribe();
218 0 : return;
219 : }
220 : }
221 :
222 0 : if (sub_client_list_.size() >= MAX_N_SUB_CLIENT) {
223 0 : JAMI_WARN("Unable to add PresSubClient, max number reached.");
224 0 : return;
225 : }
226 :
227 0 : if (flag) {
228 0 : PresSubClient* c = new PresSubClient(uri, this);
229 0 : if (!(c->subscribe())) {
230 0 : JAMI_WARN("Failed send subscribe.");
231 0 : delete c;
232 : }
233 : // the buddy has to be accepted before being added in the list
234 : }
235 : }
236 :
237 : void
238 0 : SIPPresence::addPresSubClient(PresSubClient* c)
239 : {
240 0 : if (sub_client_list_.size() < MAX_N_SUB_CLIENT) {
241 0 : sub_client_list_.push_back(c);
242 0 : JAMI_DBG("New Presence_subscription_client added (list[%zu]).", sub_client_list_.size());
243 : } else {
244 0 : JAMI_WARN("Max Presence_subscription_client is reach.");
245 : // let the client alive //delete c;
246 : }
247 0 : }
248 :
249 : void
250 0 : SIPPresence::removePresSubClient(PresSubClient* c)
251 : {
252 0 : JAMI_DBG("Remove Presence_subscription_client from the buddy list.");
253 0 : sub_client_list_.remove(c);
254 0 : }
255 :
256 : void
257 0 : SIPPresence::approvePresSubServer(const std::string& uri, bool flag)
258 : {
259 0 : for (const auto& s : sub_server_list_) {
260 0 : if (s->matches((char*) uri.c_str())) {
261 0 : s->approve(flag);
262 : // return; // 'return' would prevent multiple-time subscribers from spam
263 : }
264 : }
265 0 : }
266 :
267 : void
268 0 : SIPPresence::addPresSubServer(PresSubServer* s)
269 : {
270 0 : if (sub_server_list_.size() < MAX_N_SUB_SERVER) {
271 0 : sub_server_list_.push_back(s);
272 : } else {
273 0 : JAMI_WARN("Max Presence_subscription_server is reach.");
274 : // let de server alive // delete s;
275 : }
276 0 : }
277 :
278 : void
279 0 : SIPPresence::removePresSubServer(PresSubServer* s)
280 : {
281 0 : sub_server_list_.remove(s);
282 0 : JAMI_DBG("Presence_subscription_server removed");
283 0 : }
284 :
285 : void
286 0 : SIPPresence::notifyPresSubServer()
287 : {
288 0 : JAMI_DBG("Iterating through IP2IP Presence_subscription_server:");
289 :
290 0 : for (const auto& s : sub_server_list_)
291 0 : s->notify();
292 0 : }
293 :
294 : void
295 0 : SIPPresence::lock()
296 : {
297 0 : mutex_.lock();
298 0 : }
299 :
300 : bool
301 0 : SIPPresence::tryLock()
302 : {
303 0 : return mutex_.try_lock();
304 : }
305 :
306 : void
307 0 : SIPPresence::unlock()
308 : {
309 0 : mutex_.unlock();
310 0 : }
311 :
312 : void
313 0 : SIPPresence::fillDoc(pjsip_tx_data* tdata, const pres_msg_data* msg_data)
314 : {
315 0 : if (tdata->msg->type == PJSIP_REQUEST_MSG) {
316 0 : constexpr pj_str_t STR_USER_AGENT = CONST_PJ_STR("User-Agent");
317 0 : std::string useragent(acc_->getUserAgentName());
318 0 : pj_str_t pJuseragent = pj_str((char*) useragent.c_str());
319 0 : pjsip_hdr* h = (pjsip_hdr*) pjsip_generic_string_hdr_create(tdata->pool, &STR_USER_AGENT, &pJuseragent);
320 0 : pjsip_msg_add_hdr(tdata->msg, h);
321 0 : }
322 :
323 0 : if (msg_data == NULL)
324 0 : return;
325 :
326 : const pjsip_hdr* hdr;
327 0 : hdr = msg_data->hdr_list.next;
328 :
329 0 : while (hdr && hdr != &msg_data->hdr_list) {
330 : pjsip_hdr* new_hdr;
331 0 : new_hdr = (pjsip_hdr*) pjsip_hdr_clone(tdata->pool, hdr);
332 0 : JAMI_DBG("adding header %p", new_hdr->name.ptr);
333 0 : pjsip_msg_add_hdr(tdata->msg, new_hdr);
334 0 : hdr = hdr->next;
335 : }
336 :
337 0 : if (msg_data->content_type.slen && msg_data->msg_body.slen) {
338 : pjsip_msg_body* body;
339 0 : constexpr pj_str_t type = CONST_PJ_STR("application");
340 0 : constexpr pj_str_t subtype = CONST_PJ_STR("pidf+xml");
341 0 : body = pjsip_msg_body_create(tdata->pool, &type, &subtype, &msg_data->msg_body);
342 0 : tdata->msg->body = body;
343 : }
344 : }
345 :
346 : static const pjsip_publishc_opt my_publish_opt = {true}; // this is queue_request
347 :
348 : /*
349 : * Client presence publication callback.
350 : */
351 : void
352 0 : SIPPresence::publish_cb(struct pjsip_publishc_cbparam* param)
353 : {
354 0 : SIPPresence* pres = (SIPPresence*) param->token;
355 :
356 0 : if (param->code / 100 != 2 || param->status != PJ_SUCCESS) {
357 0 : pjsip_publishc_destroy(param->pubc);
358 0 : pres->publish_sess_ = NULL;
359 0 : std::string error = fmt::format("{} / {}", param->code, sip_utils::as_view(param->reason));
360 0 : if (param->status != PJ_SUCCESS) {
361 : char errmsg[PJ_ERR_MSG_SIZE];
362 0 : pj_strerror(param->status, errmsg, sizeof(errmsg));
363 0 : JAMI_ERR("Client (PUBLISH) failed, status=%d, msg=%s", param->status, errmsg);
364 0 : emitSignal<libjami::PresenceSignal::ServerError>(pres->getAccount()->getAccountID(), error, errmsg);
365 :
366 0 : } else if (param->code == 412) {
367 : /* 412 (Conditional Request Failed)
368 : * The PUBLISH refresh has failed, retry with new one.
369 : */
370 0 : JAMI_WARN("Publish retry.");
371 0 : publish(pres);
372 0 : } else if ((param->code == PJSIP_SC_BAD_EVENT) || (param->code == PJSIP_SC_NOT_IMPLEMENTED)) { // 489 or 501
373 0 : JAMI_WARN("Client (PUBLISH) failed (%s)", error.c_str());
374 :
375 0 : emitSignal<libjami::PresenceSignal::ServerError>(pres->getAccount()->getAccountID(),
376 : error,
377 : "Publish not supported.");
378 :
379 0 : pres->getAccount()->supportPresence(PRESENCE_FUNCTION_PUBLISH, false);
380 : }
381 :
382 0 : } else {
383 0 : 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 : */
387 0 : pjsip_publishc_destroy(param->pubc);
388 0 : pres->publish_sess_ = NULL;
389 : }
390 :
391 0 : pres->getAccount()->supportPresence(PRESENCE_FUNCTION_PUBLISH, true);
392 : }
393 0 : }
394 :
395 : /*
396 : * Send PUBLISH request.
397 : */
398 : pj_status_t
399 0 : SIPPresence::send_publish(SIPPresence* pres)
400 : {
401 : pjsip_tx_data* tdata;
402 : pj_status_t status;
403 :
404 0 : JAMI_DBG("Send PUBLISH (%s).", pres->getAccount()->getAccountID().c_str());
405 :
406 0 : SIPAccount* acc = pres->getAccount();
407 0 : std::string contactWithAngles = acc->getFromUri();
408 0 : contactWithAngles.erase(contactWithAngles.find('>'));
409 :
410 : /* Create PUBLISH request */
411 : char* bpos;
412 : pj_str_t entity;
413 :
414 0 : status = pjsip_publishc_publish(pres->publish_sess_, PJ_TRUE, &tdata);
415 0 : pj_str_t from = pj_strdup3(pres->pool_, acc->getFromUri().c_str());
416 :
417 0 : if (status != PJ_SUCCESS) {
418 0 : JAMI_ERR("Error creating PUBLISH request %d", status);
419 0 : goto on_error;
420 : }
421 :
422 0 : if ((bpos = pj_strchr(&from, '<')) != NULL) {
423 0 : char* epos = pj_strchr(&from, '>');
424 :
425 0 : if (epos - bpos < 2) {
426 0 : JAMI_ERR("Unexpected invalid URI");
427 0 : status = PJSIP_EINVALIDURI;
428 0 : goto on_error;
429 : }
430 :
431 0 : entity.ptr = bpos + 1;
432 0 : entity.slen = epos - bpos - 1;
433 : } else {
434 0 : entity = from;
435 : }
436 :
437 : /* Create and add PIDF message body */
438 0 : status = pjsip_pres_create_pidf(tdata->pool, pres->getStatus(), &entity, &tdata->msg->body);
439 :
440 : pres_msg_data msg_data;
441 :
442 0 : if (status != PJ_SUCCESS) {
443 0 : JAMI_ERR("Error creating PIDF for PUBLISH request");
444 0 : pjsip_tx_data_dec_ref(tdata);
445 0 : goto on_error;
446 : }
447 :
448 0 : pj_bzero(&msg_data, sizeof(msg_data));
449 0 : pj_list_init(&msg_data.hdr_list);
450 0 : pjsip_media_type_init(&msg_data.multipart_ctype, NULL, NULL);
451 0 : pj_list_init(&msg_data.multipart_parts);
452 :
453 0 : pres->fillDoc(tdata, &msg_data);
454 :
455 : /* Send the PUBLISH request */
456 0 : status = pjsip_publishc_send(pres->publish_sess_, tdata);
457 :
458 0 : if (status == PJ_EPENDING) {
459 0 : JAMI_WARN("Previous request is in progress, ");
460 0 : } else if (status != PJ_SUCCESS) {
461 0 : JAMI_ERR("Error sending PUBLISH request");
462 0 : goto on_error;
463 : }
464 :
465 0 : return PJ_SUCCESS;
466 :
467 0 : on_error:
468 :
469 0 : if (pres->publish_sess_) {
470 0 : pjsip_publishc_destroy(pres->publish_sess_);
471 0 : pres->publish_sess_ = NULL;
472 : }
473 :
474 0 : return status;
475 0 : }
476 :
477 : /* Create client publish session */
478 : pj_status_t
479 0 : SIPPresence::publish(SIPPresence* pres)
480 : {
481 : pj_status_t status;
482 0 : constexpr pj_str_t STR_PRESENCE = CONST_PJ_STR("presence");
483 0 : SIPAccount* acc = pres->getAccount();
484 0 : pjsip_endpoint* endpt = Manager::instance().sipVoIPLink().getEndpoint();
485 :
486 : /* Create and init client publication session */
487 :
488 : /* Create client publication */
489 0 : status = pjsip_publishc_create(endpt, &my_publish_opt, pres, &publish_cb, &pres->publish_sess_);
490 :
491 0 : if (status != PJ_SUCCESS) {
492 0 : pres->publish_sess_ = NULL;
493 0 : JAMI_ERR("Failed to create a publish session.");
494 0 : return status;
495 : }
496 :
497 : /* Initialize client publication */
498 0 : pj_str_t from = pj_strdup3(pres->pool_, acc->getFromUri().c_str());
499 0 : status = pjsip_publishc_init(pres->publish_sess_, &STR_PRESENCE, &from, &from, &from, 0xFFFF);
500 :
501 0 : if (status != PJ_SUCCESS) {
502 0 : JAMI_ERR("Failed to init a publish session");
503 0 : pres->publish_sess_ = NULL;
504 0 : return status;
505 : }
506 :
507 : /* Add credential for authentication */
508 0 : if (acc->hasCredentials()
509 0 : and pjsip_publishc_set_credentials(pres->publish_sess_,
510 0 : static_cast<int>(acc->getCredentialCount()),
511 : acc->getCredInfo())
512 : != PJ_SUCCESS) {
513 0 : JAMI_ERR("Unable to initialize credentials for invite session authentication");
514 0 : return status;
515 : }
516 :
517 : /* Set route-set */
518 : // FIXME: is this really necessary?
519 0 : pjsip_regc* regc = acc->getRegistrationInfo();
520 0 : if (regc and acc->hasServiceRoute())
521 0 : pjsip_regc_set_route_set(regc, sip_utils::createRouteSet(acc->getServiceRoute(), pres->getPool()));
522 :
523 : /* Send initial PUBLISH request */
524 0 : status = send_publish(pres);
525 :
526 0 : if (status != PJ_SUCCESS)
527 0 : return status;
528 :
529 0 : return PJ_SUCCESS;
530 : }
531 :
532 : } // namespace jami
|