blob: 7880978a0e43df4a26cc36143fe6fb14469d0960 [file] [log] [blame]
Neels Hofmeyreef45782019-10-21 03:24:04 +02001/* Minimalistic SDP parse/compose implementation, focused on GSM audio codecs */
2/*
3 * (C) 2019 by sysmocom - s.m.f.c. GmbH <info@sysmocom.de>
4 * All Rights Reserved
5 *
6 * SPDX-License-Identifier: AGPL-3.0+
7 *
8 * Author: Neels Hofmeyr
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU Affero General Public License as published by
12 * the Free Software Foundation; either version 3 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU Affero General Public License for more details.
19 *
20 * You should have received a copy of the GNU Affero General Public License
21 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 */
23
24#include <string.h>
25#include <errno.h>
26
27#include <osmocom/core/utils.h>
28#include <osmocom/core/logging.h>
29
30#include <osmocom/msc/debug.h>
31#include <osmocom/msc/sdp_msg.h>
32
33/* Compare name, rate and fmtp, returning typical cmp result: 0 on match, and -1 / 1 on mismatch.
34 * Do *not* compare the payload_type number.
35 * The fmtp is only string-compared -- e.g. if AMR parameters appear in a different order, it amounts to a mismatch even
36 * though all parameters are the same. */
37int sdp_audio_codec_cmp(const struct sdp_audio_codec *a, const struct sdp_audio_codec *b)
38{
39 int rc;
40 if (a == b)
41 return 0;
42 if (!a)
43 return -1;
44 if (!b)
45 return 1;
46 rc = strncmp(a->subtype_name, b->subtype_name, sizeof(a->subtype_name));
47 if (rc)
48 return rc;
49
50 if (a->rate < b->rate)
51 return -1;
52 if (a->rate > b->rate)
53 return 1;
54
55 rc = strncmp(a->fmtp, b->fmtp, sizeof(a->fmtp));
56 if (rc)
57 return rc;
58
59 return 0;
60}
61
62/* Given a predefined fixed payload_type number, add an SDP audio codec entry, if not present yet.
63 * The payload_type must exist in sdp_msg_payload_type_names.
64 * Return the audio codec created or already existing for this payload type number.
65 */
66struct sdp_audio_codec *sdp_audio_codec_add(struct sdp_audio_codecs *ac, unsigned int payload_type,
67 const char *subtype_name, unsigned int rate, const char *fmtp)
68{
69 struct sdp_audio_codec *codec;
70
71 /* Does an entry already exist? */
72 codec = sdp_audio_codec_by_payload_type(ac, payload_type, false);
73 if (codec) {
74 /* Already exists, sanity check */
75 if (!codec->subtype_name[0])
76 OSMO_STRLCPY_ARRAY(codec->subtype_name, subtype_name);
77 else if (strcmp(codec->subtype_name, subtype_name)) {
78 /* There already is an entry with this payload_type number but a mismatching subtype_name. That is
79 * weird, rather abort. */
80 return NULL;
81 }
82 if (codec->rate != rate
83 || (fmtp && strcmp(fmtp, codec->fmtp))) {
84 /* Mismatching details. Rather abort */
85 return NULL;
86 }
87 return codec;
88 }
89
90 /* None exists, create codec entry for this payload type number */
91 codec = sdp_audio_codec_by_payload_type(ac, payload_type, true);
92 /* NULL means unable to add an entry */
93 if (!codec)
94 return NULL;
95
96 OSMO_STRLCPY_ARRAY(codec->subtype_name, subtype_name);
97 if (fmtp)
98 OSMO_STRLCPY_ARRAY(codec->fmtp, fmtp);
99 return codec;
100}
101
102struct sdp_audio_codec *sdp_audio_codec_add_copy(struct sdp_audio_codecs *ac, const struct sdp_audio_codec *codec)
103{
104 return sdp_audio_codec_add(ac, codec->payload_type, codec->subtype_name, codec->rate,
105 codec->fmtp[0] ? codec->fmtp : NULL);
106}
107
108struct sdp_audio_codec *sdp_audio_codec_by_payload_type(struct sdp_audio_codecs *ac, unsigned int payload_type,
109 bool create)
110{
111 struct sdp_audio_codec *codec;
112 foreach_sdp_audio_codec(codec, ac) {
113 if (codec->payload_type == payload_type)
114 return codec;
115 }
116
117 if (!create)
118 return NULL;
119
120 /* Not found; codec points after the last entry now. */
121 if ((codec - ac->codec) >= ARRAY_SIZE(ac->codec))
122 return NULL;
123
124 *codec = (struct sdp_audio_codec){
125 .payload_type = payload_type,
126 .rate = 8000,
127 };
128
129 ac->count = (codec - ac->codec) + 1;
130 return codec;
131}
132
133/* Return a given sdp_msg's codec entry that matches the subtype_name, rate and fmtp of the given codec, or NULL if no
134 * match is found. Comparison is made by sdp_audio_codec_cmp(). */
135struct sdp_audio_codec *sdp_audio_codec_by_descr(struct sdp_audio_codecs *ac, const struct sdp_audio_codec *codec)
136{
137 struct sdp_audio_codec *i;
138 foreach_sdp_audio_codec(i, ac) {
139 if (!sdp_audio_codec_cmp(i, codec))
140 return i;
141 }
142 return NULL;
143}
144
145/* Remove the codec entry pointed at by 'codec'. 'codec' must point at an entry of 'sdp' (to use an external codec
146 * instance, use sdp_audio_codec_by_descr()).
147 * Return 0 on success, -ENOENT if codec does not point at the sdp->codec array. */
148int sdp_audio_codec_remove(struct sdp_audio_codecs *ac, const struct sdp_audio_codec *codec)
149{
150 struct sdp_audio_codec *i;
151 if ((codec < ac->codec)
152 || ((codec - ac->codec) >= OSMO_MIN(ac->count, ARRAY_SIZE(ac->codec))))
153 return -ENOENT;
154
155 /* Move all following entries one up */
156 ac->count--;
157 foreach_sdp_audio_codec(i, ac) {
158 if (i < codec)
159 continue;
160 *i = *(i+1);
161 }
162 return 0;
163}
164
165/* Convert struct sdp_msg to the actual SDP protocol representation */
166int sdp_msg_to_str(char *dst, size_t dst_size, const struct sdp_msg *sdp)
167{
168 const struct sdp_audio_codec *codec;
169 struct osmo_strbuf sb = { .buf = dst, .len = dst_size };
170 const char *ip = sdp->rtp.ip[0] ? sdp->rtp.ip : "0.0.0.0";
171
172 OSMO_STRBUF_PRINTF(sb,
173 "v=0\r\n"
174 "o=OsmoMSC 0 0 IN IP4 %s\r\n"
175 "s=GSM Call\r\n"
176 "c=IN IP4 %s\r\n"
177 "t=0 0\r\n"
178 "m=audio %d RTP/AVP",
179 ip, ip,
180 sdp->rtp.port);
181
182 /* Append all payload type numbers to 'm=audio <port> RTP/AVP 3 4 112' line */
183 foreach_sdp_audio_codec(codec, &sdp->audio_codecs)
184 OSMO_STRBUF_PRINTF(sb, " %d", codec->payload_type);
185 OSMO_STRBUF_PRINTF(sb, "\r\n");
186
187 /* Add details for all codecs */
188 foreach_sdp_audio_codec(codec, &sdp->audio_codecs) {
189 if (codec->subtype_name[0]) {
190 OSMO_STRBUF_PRINTF(sb, "a=rtpmap:%d %s/%d\r\n", codec->payload_type, codec->subtype_name,
191 codec->rate > 0? codec->rate : 8000);
192 }
193
194 if (codec->fmtp[0])
195 OSMO_STRBUF_PRINTF(sb, "a=fmtp:%d %s\r\n", codec->payload_type, codec->fmtp);
196 }
197
198 OSMO_STRBUF_PRINTF(sb, "a=ptime:%d\r\n", sdp->ptime > 0? sdp->ptime : 20);
199
200 return sb.chars_needed;
201}
202
203/* Return the first line ending (or the end of the string) at or after the given string position. */
204const char *sdp_msg_line_end(const char *src)
205{
206 const char *line_end = strchr(src, '\r');
207 if (!line_end)
208 line_end = strchr(src, '\n');
209 if (!line_end)
210 line_end = src + strlen(src);
211 return line_end;
212}
213
214/* parse a line like 'a=rtpmap:0 PCMU/8000', 'a=fmtp:112 octet-align=1; mode-set=4', 'a=ptime:20'.
215 * The src should point at the character after 'a=', e.g. at the start of 'rtpmap', 'fmtp', 'ptime'
216 */
217int sdp_parse_attrib(struct sdp_msg *sdp, const char *src)
218{
219 unsigned int payload_type;
220 struct sdp_audio_codec *codec;
221#define A_RTPMAP "rtpmap:"
222#define A_FMTP "fmtp:"
223#define A_PTIME "ptime:"
224#define A_RTCP "rtcp:"
225#define A_SENDRECV "sendrecv"
226#define A_SENDONLY "sendonly"
227#define A_RECVONLY "recvonly"
228
229 if (osmo_str_startswith(src, A_RTPMAP)) {
230 char *audio_name;
231 unsigned int channels = 1;
232 if (sscanf(src, A_RTPMAP "%u", &payload_type) != 1)
233 return -EINVAL;
234
235 audio_name = strchr(src, ' ');
236 if (!audio_name || audio_name >= sdp_msg_line_end(src))
237 return -EINVAL;
238
239 codec = sdp_audio_codec_by_payload_type(&sdp->audio_codecs, payload_type, true);
240 if (!codec)
241 return -ENOSPC;
242
243 if (sscanf(audio_name, " %31[^/]/%u/%u", codec->subtype_name, &codec->rate, &channels) < 1)
244 return -EINVAL;
245
246 if (channels != 1)
247 return -ENOTSUP;
248 }
249
250 else if (osmo_str_startswith(src, A_FMTP)) {
251 char *fmtp_str;
252 const char *line_end = sdp_msg_line_end(src);
253 if (sscanf(src, A_FMTP "%u", &payload_type) != 1)
254 return -EINVAL;
255
256 fmtp_str = strchr(src, ' ');
257 if (!fmtp_str)
258 return -EINVAL;
259 fmtp_str++;
260 if (fmtp_str >= line_end)
261 return -EINVAL;
262
263 codec = sdp_audio_codec_by_payload_type(&sdp->audio_codecs, payload_type, true);
264 if (!codec)
265 return -ENOSPC;
266
267 /* (+1 because osmo_strlcpy() interprets it as size including the '\0') */
268 osmo_strlcpy(codec->fmtp, fmtp_str, line_end - fmtp_str + 1);
269 }
270
271 else if (osmo_str_startswith(src, A_PTIME)) {
272 if (sscanf(src, A_PTIME "%u", &sdp->ptime) != 1)
273 return -EINVAL;
274
275 }
276
277 else if (osmo_str_startswith(src, A_RTCP)) {
278 /* TODO? */
279 }
280
281 else if (osmo_str_startswith(src, A_SENDRECV)) {
282 /* TODO? */
283 }
284
285 else if (osmo_str_startswith(src, A_SENDONLY)) {
286 /* TODO? */
287 }
288
289 else if (osmo_str_startswith(src, A_RECVONLY)) {
290 /* TODO? */
291 }
292
293 return 0;
294}
295
296const struct value_string sdp_msg_payload_type_names[] = {
297 { 0, "PCMU" },
298 { 3, "GSM" },
299 { 8, "PCMA" },
300 { 18, "G729" },
301 { 110, "GSM-EFR" },
302 { 111, "GSM-HR-08" },
303 { 112, "AMR" },
304 { 113, "AMR-WB" },
305 {}
306};
307
308/* Return payload type number matching given string ("AMR", "GSM", ...) or negative if not found. */
309int sdp_subtype_name_to_payload_type(const char *subtype_name)
310{
311 return get_string_value(sdp_msg_payload_type_names, subtype_name);
312}
313
314/* Parse a line like 'm=audio 16398 RTP/AVP 0 3 8 96 112', starting after the '=' */
315static int sdp_parse_media_description(struct sdp_msg *sdp, const char *src)
316{
317 unsigned int port;
318 int i;
319 const char *payload_type_str;
320 const char *line_end = sdp_msg_line_end(src);
321 if (sscanf(src, "audio %u RTP/AVP", &port) < 1)
322 return -ENOTSUP;
323
324 if (port < 0 || port > 0xffff)
325 return -EINVAL;
326
327 sdp->rtp.port = port;
328
329 /* skip "audio 12345 RTP/AVP ", i.e. 3 spaces on */
330 payload_type_str = src;
331 for (i = 0; i < 3; i++) {
332 payload_type_str = strchr(payload_type_str, ' ');
333 if (!payload_type_str)
334 return -EINVAL;
335 while (*payload_type_str == ' ')
336 payload_type_str++;
337 if (payload_type_str >= line_end)
338 return -EINVAL;
339 }
340
341 /* Parse listing of payload type numbers after "RTP/AVP" */
342 while (payload_type_str < line_end) {
343 unsigned int payload_type;
344 struct sdp_audio_codec *codec;
345 const char *subtype_name;
346 if (sscanf(payload_type_str, "%u", &payload_type) < 1)
347 return -EINVAL;
348
349 codec = sdp_audio_codec_by_payload_type(&sdp->audio_codecs, payload_type, true);
350 if (!codec)
351 return -ENOSPC;
352
353 /* Fill in subtype name for fixed payload types */
354 subtype_name = get_value_string_or_null(sdp_msg_payload_type_names, codec->payload_type);
355 if (subtype_name)
356 OSMO_STRLCPY_ARRAY(codec->subtype_name, subtype_name);
357
358 payload_type_str = strchr(payload_type_str, ' ');
359 if (!payload_type_str)
360 payload_type_str = line_end;
361 while (*payload_type_str == ' ')
362 payload_type_str++;
363 }
364
365 return 0;
366}
367
368/* parse a line like 'c=IN IP4 192.168.11.151' starting after the '=' */
369static int sdp_parse_connection_info(struct sdp_msg *sdp, const char *src)
370{
371 char ipv[10];
372 char addr_str[INET6_ADDRSTRLEN];
373 if (sscanf(src, "IN %s %s", ipv, addr_str) < 2)
374 return -EINVAL;
375
376 /* supporting only IPv4 */
377 if (strcmp(ipv, "IP4"))
378 return -ENOTSUP;
379
380 osmo_sockaddr_str_from_str(&sdp->rtp, addr_str, sdp->rtp.port);
381 return 0;
382}
383
384/* Parse SDP string into struct sdp_msg. Return 0 on success, negative on error. */
385int sdp_msg_from_str(struct sdp_msg *sdp, const char *src)
386{
387 const char *pos;
388 *sdp = (struct sdp_msg){};
389
390 for (pos = src; pos && *pos; pos++) {
391 char attrib;
392 int rc = 0;
393
394 if (*pos == '\r' || *pos == '\n')
395 continue;
396
397 /* Expecting only lines starting with 'X='. Not being too strict about it is probably alright. */
398 if (pos[1] != '=')
399 goto next_line;
400
401 attrib = *pos;
402 pos += 2;
403 switch (attrib) {
404 /* a=... */
405 case 'a':
406 rc = sdp_parse_attrib(sdp, pos);
407 break;
408 case 'm':
409 rc = sdp_parse_media_description(sdp, pos);
410 break;
411 case 'c':
412 rc = sdp_parse_connection_info(sdp, pos);
413 break;
414 default:
415 /* ignore any other parameters */
416 break;
417 }
418
419 if (rc) {
420 size_t line_len;
421 const char *line_end = sdp_msg_line_end(pos);
422 pos -= 2;
423 line_len = line_end - pos;
424 switch (rc) {
425 case -EINVAL:
426 LOGP(DMNCC, LOGL_ERROR,
427 "Failed to parse SDP: invalid line: %s\n", osmo_quote_str(pos, line_len));
428 break;
429 case -ENOSPC:
430 LOGP(DMNCC, LOGL_ERROR,
431 "Failed to parse SDP: no more space for: %s\n", osmo_quote_str(pos, line_len));
432 break;
433 case -ENOTSUP:
434 LOGP(DMNCC, LOGL_ERROR,
435 "Failed to parse SDP: not supported: %s\n", osmo_quote_str(pos, line_len));
436 break;
437 default:
438 LOGP(DMNCC, LOGL_ERROR,
439 "Failed to parse SDP: %s\n", osmo_quote_str(pos, line_len));
440 break;
441 }
442 return rc;
443 }
444next_line:
445 pos = strstr(pos, "\r\n");
446 if (!pos)
447 break;
448 }
449
450 return 0;
451}
452
453/* Leave only those codecs in 'ac_dest' that are also present in 'ac_other'.
454 * The matching is made by sdp_audio_codec_cmp(), i.e. payload_type numbers are not compared and fmtp parameters are
455 * compared 1:1 as plain strings.
456 * If translate_payload_type_numbers has an effect if ac_dest and ac_other have mismatching payload_type numbers for the
457 * same SDP codec descriptions. If translate_payload_type_numbers is true, take the payload_type numbers from ac_other.
458 * If false, keep payload_type numbers in ac_dest unchanged. */
459void sdp_audio_codecs_intersection(struct sdp_audio_codecs *ac_dest, const struct sdp_audio_codecs *ac_other,
460 bool translate_payload_type_numbers)
461{
462 int i;
463 for (i = 0; i < ac_dest->count; i++) {
464 struct sdp_audio_codec *codec = &ac_dest->codec[i];
465 struct sdp_audio_codec *other;
466 OSMO_ASSERT(i < ARRAY_SIZE(ac_dest->codec));
467
468 other = sdp_audio_codec_by_descr((struct sdp_audio_codecs*)ac_other, codec);
469
470 if (!other) {
471 OSMO_ASSERT(sdp_audio_codec_remove(ac_dest, codec) == 0);
472 i--;
473 continue;
474 }
475
476 /* Doing payload_type number translation of part of the intersection because it makes the algorithm
477 * simpler: we already know ac_dest is a subset of ac_other, and there is no need to resolve payload
478 * type number conflicts. */
479 if (translate_payload_type_numbers)
480 codec->payload_type = other->payload_type;
481 }
482}
483
484/* Make sure the given codec is listed as the first codec. 'codec' must be an actual codec entry of the given audio
485 * codecs list. */
486void sdp_audio_codecs_select(struct sdp_audio_codecs *ac, struct sdp_audio_codec *codec)
487{
488 struct sdp_audio_codec tmp;
489 struct sdp_audio_codec *pos;
490 OSMO_ASSERT((codec >= ac->codec)
491 && ((codec - ac->codec) < OSMO_MIN(ac->count, ARRAY_SIZE(ac->codec))));
492
493 /* Already the first? */
494 if (codec == ac->codec)
495 return;
496
497 tmp = *codec;
498 for (pos = codec - 1; pos >= ac->codec; pos--)
499 pos[1] = pos[0];
500
501 ac->codec[0] = tmp;
502 return;
503}
504
505/* Short single-line representation of an SDP audio codec, convenient for logging.
506 * Like "AMR/8000:octet-align=1#122" */
507int sdp_audio_codec_name_buf(char *buf, size_t buflen, const struct sdp_audio_codec *codec)
508{
509 struct osmo_strbuf sb = { .buf = buf, .len = buflen };
510 OSMO_STRBUF_PRINTF(sb, "%s", codec->subtype_name);
511 if (codec->fmtp[0])
512 OSMO_STRBUF_PRINTF(sb, ":%s", codec->fmtp);
513 return sb.chars_needed;
514}
515
516char *sdp_audio_codec_name_c(void *ctx, const struct sdp_audio_codec *codec)
517{
518 OSMO_NAME_C_IMPL(ctx, 32, "sdp_audio_codec_name_c-ERROR", sdp_audio_codec_name_buf, codec)
519}
520
521const char *sdp_audio_codec_name(const struct sdp_audio_codec *codec)
522{
523 return sdp_audio_codec_name_c(OTC_SELECT, codec);
524}
525
526/* Short single-line representation of a list of SDP audio codecs, convenient for logging */
527int sdp_audio_codecs_name_buf(char *buf, size_t buflen, const struct sdp_audio_codecs *ac)
528{
529 struct osmo_strbuf sb = { .buf = buf, .len = buflen };
530 const struct sdp_audio_codec *codec;
531 if (!ac->count)
532 OSMO_STRBUF_PRINTF(sb, "(no-codecs)");
533 foreach_sdp_audio_codec(codec, ac) {
534 bool first = (codec == ac->codec);
535 if (!first)
536 OSMO_STRBUF_PRINTF(sb, ",");
537 OSMO_STRBUF_APPEND(sb, sdp_audio_codec_name_buf, codec);
538 }
539 return sb.chars_needed;
540}
541
542char *sdp_audio_codecs_name_c(void *ctx, const struct sdp_audio_codecs *ac)
543{
544 OSMO_NAME_C_IMPL(ctx, 128, "sdp_audio_codecs_name_c-ERROR", sdp_audio_codecs_name_buf, ac)
545}
546
547const char *sdp_audio_codecs_name(const struct sdp_audio_codecs *ac)
548{
549 return sdp_audio_codecs_name_c(OTC_SELECT, ac);
550}
551
552/* Short single-line representation of an SDP message, convenient for logging */
553int sdp_msg_name_buf(char *buf, size_t buflen, const struct sdp_msg *sdp)
554{
555 struct osmo_strbuf sb = { .buf = buf, .len = buflen };
556 if (!sdp) {
557 OSMO_STRBUF_PRINTF(sb, "NULL");
558 return sb.chars_needed;
559 }
560
561 OSMO_STRBUF_PRINTF(sb, OSMO_SOCKADDR_STR_FMT, OSMO_SOCKADDR_STR_FMT_ARGS(&sdp->rtp));
562 OSMO_STRBUF_PRINTF(sb, "{");
563 OSMO_STRBUF_APPEND(sb, sdp_audio_codecs_name_buf, &sdp->audio_codecs);
564 OSMO_STRBUF_PRINTF(sb, "}");
565 return sb.chars_needed;
566}
567
568char *sdp_msg_name_c(void *ctx, const struct sdp_msg *sdp)
569{
570 OSMO_NAME_C_IMPL(ctx, 128, "sdp_msg_name_c-ERROR", sdp_msg_name_buf, sdp)
571}
572
573const char *sdp_msg_name(const struct sdp_msg *sdp)
574{
575 return sdp_msg_name_c(OTC_SELECT, sdp);
576}