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