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