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