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 : #include "instant_messaging.h"
18 :
19 : #include "logger.h"
20 : #include "connectivity/sip_utils.h"
21 : #include "string_utils.h"
22 :
23 : #include <pjsip_ua.h>
24 : #include <pjsip.h>
25 :
26 : namespace jami {
27 :
28 : using sip_utils::CONST_PJ_STR;
29 :
30 : /**
31 : * the pair<string, string> we receive is expected to be in the format <MIME type, payload>
32 : * the MIME type is in the format "type/subtype"
33 : * in the header it will be presented as "Content-Type: type/subtype"
34 : * following the RFC spec, this header line can also contain other parameters in the format:
35 : * Content-Type: type/subtype; arg=value; arg=value; …
36 : * thus we also accept the key of the map to be in such a format:
37 : * type/subtype; arg=value; arg=value; …
38 : */
39 : static void
40 239 : createMessageBody(pj_pool_t* pool, const std::pair<std::string, std::string>& payload, pjsip_msg_body** body_p)
41 : {
42 : /* parse the key:
43 : * 1. split by ';'
44 : * 2. parse the first result by spliting by '/' into a type and subtype
45 : * 3. parse any following strings into arg=value by splitting by '='
46 : */
47 239 : std::string_view mimeType, parameters;
48 239 : auto sep = payload.first.find(';');
49 238 : if (std::string::npos == sep) {
50 238 : mimeType = payload.first;
51 : } else {
52 0 : mimeType = std::string_view(payload.first).substr(0, sep);
53 0 : parameters = std::string_view(payload.first).substr(sep + 1);
54 : }
55 :
56 : // split MIME type to type and subtype
57 238 : sep = mimeType.find('/');
58 238 : if (std::string::npos == sep) {
59 0 : JAMI_WARNING("Bad MIME type: '{}'", mimeType);
60 0 : throw im::InstantMessageException("Invalid MIME type");
61 : }
62 :
63 238 : auto type = sip_utils::CONST_PJ_STR(mimeType.substr(0, sep));
64 239 : auto subtype = sip_utils::CONST_PJ_STR(mimeType.substr(sep + 1));
65 237 : auto message = sip_utils::CONST_PJ_STR(payload.second);
66 :
67 : // create part
68 238 : *body_p = pjsip_msg_body_create(pool, &type, &subtype, &message);
69 :
70 239 : if (not parameters.size())
71 239 : return;
72 :
73 : // now attempt to add parameters one by one
74 : do {
75 0 : sep = parameters.find(';');
76 0 : auto paramPair = parameters.substr(0, sep);
77 0 : if (paramPair.empty())
78 0 : break;
79 :
80 : // split paramPair into arg and value by '='
81 0 : auto paramSplit = paramPair.find('=');
82 0 : if (std::string::npos == paramSplit) {
83 0 : JAMI_WARNING("Bad parameter: '{}'", paramPair);
84 0 : throw im::InstantMessageException("Invalid parameter");
85 : }
86 :
87 0 : auto arg = sip_utils::CONST_PJ_STR(paramPair.substr(0, paramSplit));
88 0 : auto value = sip_utils::CONST_PJ_STR(paramPair.substr(paramSplit + 1));
89 0 : pj_strtrim(&arg);
90 0 : pj_strtrim(&value);
91 : pj_str_t arg_pj, value_pj;
92 0 : pjsip_param* param = PJ_POOL_ALLOC_T(pool, pjsip_param);
93 0 : param->name = *pj_strdup(pool, &arg_pj, &arg);
94 0 : param->value = *pj_strdup(pool, &value_pj, &value);
95 0 : pj_list_push_back(&(*body_p)->content_type.param, param);
96 :
97 : // next parameter?
98 0 : if (std::string::npos != sep)
99 0 : parameters = parameters.substr(sep + 1);
100 0 : } while (std::string::npos != sep);
101 : }
102 :
103 : void
104 238 : im::fillPJSIPMessageBody(pjsip_tx_data& tdata, const std::map<std::string, std::string>& payloads)
105 : {
106 : // multi-part body?
107 238 : if (payloads.size() == 1) {
108 238 : createMessageBody(tdata.pool, *payloads.begin(), &tdata.msg->body);
109 238 : return;
110 : }
111 :
112 : /* if Ctype is not specified "multipart/mixed" will be used
113 : * if the boundary is not specified, a random one will be generateAudioPort
114 : * FIXME: generate boundary and check that none of the message parts contain it before
115 : * calling this function; however the probability of this happenings if quite low as
116 : * the randomly generated string is fairly long
117 : */
118 0 : tdata.msg->body = pjsip_multipart_create(tdata.pool, nullptr, nullptr);
119 :
120 0 : for (const auto& pair : payloads) {
121 0 : auto* part = pjsip_multipart_create_part(tdata.pool);
122 0 : if (not part) {
123 0 : JAMI_ERR("pjsip_multipart_create_part failed: not enough memory");
124 0 : throw InstantMessageException("Internal SIP error");
125 : }
126 :
127 0 : createMessageBody(tdata.pool, pair, &part->body);
128 :
129 0 : auto status = pjsip_multipart_add_part(tdata.pool, tdata.msg->body, part);
130 0 : if (status != PJ_SUCCESS) {
131 0 : JAMI_ERR("pjsip_multipart_add_part failed: %s", sip_utils::sip_strerror(status).c_str());
132 0 : throw InstantMessageException("Internal SIP error");
133 : }
134 : }
135 : }
136 :
137 : void
138 239 : im::sendSipMessage(pjsip_inv_session* session, const std::map<std::string, std::string>& payloads)
139 : {
140 239 : if (payloads.empty()) {
141 0 : JAMI_WARN("The payloads argument is empty; ignoring message");
142 0 : return;
143 : }
144 :
145 238 : constexpr pjsip_method msg_method = {PJSIP_OTHER_METHOD, CONST_PJ_STR(sip_utils::SIP_METHODS::MESSAGE)};
146 :
147 : {
148 238 : auto* dialog = session->dlg;
149 238 : sip_utils::PJDialogLock dialog_lock {dialog};
150 :
151 239 : pjsip_tx_data* tdata = nullptr;
152 239 : auto status = pjsip_dlg_create_request(dialog, &msg_method, -1, &tdata);
153 239 : if (status != PJ_SUCCESS) {
154 0 : JAMI_ERR("pjsip_dlg_create_request failed: %s", sip_utils::sip_strerror(status).c_str());
155 0 : throw InstantMessageException("Internal SIP error");
156 : }
157 :
158 239 : fillPJSIPMessageBody(*tdata, payloads);
159 :
160 238 : status = pjsip_dlg_send_request(dialog, tdata, -1, nullptr);
161 239 : if (status != PJ_SUCCESS) {
162 0 : JAMI_ERR("pjsip_dlg_send_request failed: %s", sip_utils::sip_strerror(status).c_str());
163 0 : throw InstantMessageException("Internal SIP error");
164 : }
165 239 : }
166 : }
167 :
168 : /**
169 : * Creates std::pair with the Content-Type header contents as the first value and the message
170 : * payload as the second value.
171 : *
172 : * The format of the first value will be:
173 : * type/subtype[; *[; arg=value]]
174 : * eg: "text/plain;id=1234;part=2;of=1001"
175 : */
176 : static std::pair<std::string, std::string>
177 238 : parseMessageBody(const pjsip_msg_body* body)
178 : {
179 476 : std::string header = sip_utils::as_view(body->content_type.type) + "/"
180 475 : + sip_utils::as_view(body->content_type.subtype);
181 :
182 : // iterate over parameters
183 238 : auto* param = body->content_type.param.next;
184 238 : while (param != &body->content_type.param) {
185 0 : header += ";" + sip_utils::as_view(param->name) + "=" + sip_utils::as_view(param->value);
186 0 : param = param->next;
187 : }
188 :
189 : // get the payload, assume we can interpret it as chars
190 476 : return {std::move(header), std::string(static_cast<char*>(body->data), (size_t) body->len)};
191 238 : }
192 :
193 : /**
194 : * Parses given SIP message into a map where the key is the contents of the Content-Type header
195 : * (along with any parameters) and the value is the message payload.
196 : *
197 : * @param msg received SIP message
198 : *
199 : * @return map of content types and message payloads
200 : */
201 : std::map<std::string, std::string>
202 238 : im::parseSipMessage(const pjsip_msg* msg)
203 : {
204 238 : std::map<std::string, std::string> ret;
205 :
206 238 : if (!msg->body) {
207 0 : JAMI_WARN("Message body is empty");
208 0 : return ret;
209 : }
210 :
211 : // check if its a multipart message
212 238 : constexpr pj_str_t typeMultipart {CONST_PJ_STR("multipart")};
213 :
214 238 : if (pj_strcmp(&typeMultipart, &msg->body->content_type.type) != 0) {
215 : // treat as single content type message
216 238 : ret.emplace(parseMessageBody(msg->body));
217 : } else {
218 : /* multipart type message, we will treat it as multipart/mixed even if the subtype is
219 : * something else, eg: related
220 : */
221 0 : auto* part = pjsip_multipart_get_first_part(msg->body);
222 0 : while (part != nullptr) {
223 0 : ret.emplace(parseMessageBody(part->body));
224 0 : part = pjsip_multipart_get_next_part(msg->body, part);
225 : }
226 : }
227 :
228 238 : return ret;
229 0 : }
230 :
231 : } // namespace jami
|