blob: 6170e8eb40d443b4d0aab14fb42a313e71667286 [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";
Pau Espin Pedroleeda9e12020-09-03 22:11:03 +0200171 char ipv = osmo_ip_str_type(ip) == AF_INET6 ? '6' : '4';
Neels Hofmeyreef45782019-10-21 03:24:04 +0200172
173 OSMO_STRBUF_PRINTF(sb,
174 "v=0\r\n"
Pau Espin Pedroleeda9e12020-09-03 22:11:03 +0200175 "o=OsmoMSC 0 0 IN IP%c %s\r\n"
Neels Hofmeyreef45782019-10-21 03:24:04 +0200176 "s=GSM Call\r\n"
Pau Espin Pedroleeda9e12020-09-03 22:11:03 +0200177 "c=IN IP%c %s\r\n"
Neels Hofmeyreef45782019-10-21 03:24:04 +0200178 "t=0 0\r\n"
179 "m=audio %d RTP/AVP",
Pau Espin Pedroleeda9e12020-09-03 22:11:03 +0200180 ipv, ip, ipv, ip,
Neels Hofmeyreef45782019-10-21 03:24:04 +0200181 sdp->rtp.port);
182
183 /* Append all payload type numbers to 'm=audio <port> RTP/AVP 3 4 112' line */
184 foreach_sdp_audio_codec(codec, &sdp->audio_codecs)
185 OSMO_STRBUF_PRINTF(sb, " %d", codec->payload_type);
186 OSMO_STRBUF_PRINTF(sb, "\r\n");
187
188 /* Add details for all codecs */
189 foreach_sdp_audio_codec(codec, &sdp->audio_codecs) {
190 if (codec->subtype_name[0]) {
191 OSMO_STRBUF_PRINTF(sb, "a=rtpmap:%d %s/%d\r\n", codec->payload_type, codec->subtype_name,
192 codec->rate > 0? codec->rate : 8000);
193 }
194
195 if (codec->fmtp[0])
196 OSMO_STRBUF_PRINTF(sb, "a=fmtp:%d %s\r\n", codec->payload_type, codec->fmtp);
197 }
198
199 OSMO_STRBUF_PRINTF(sb, "a=ptime:%d\r\n", sdp->ptime > 0? sdp->ptime : 20);
200
201 return sb.chars_needed;
202}
203
204/* Return the first line ending (or the end of the string) at or after the given string position. */
205const char *sdp_msg_line_end(const char *src)
206{
207 const char *line_end = strchr(src, '\r');
208 if (!line_end)
209 line_end = strchr(src, '\n');
210 if (!line_end)
211 line_end = src + strlen(src);
212 return line_end;
213}
214
215/* parse a line like 'a=rtpmap:0 PCMU/8000', 'a=fmtp:112 octet-align=1; mode-set=4', 'a=ptime:20'.
216 * The src should point at the character after 'a=', e.g. at the start of 'rtpmap', 'fmtp', 'ptime'
217 */
218int sdp_parse_attrib(struct sdp_msg *sdp, const char *src)
219{
220 unsigned int payload_type;
221 struct sdp_audio_codec *codec;
222#define A_RTPMAP "rtpmap:"
223#define A_FMTP "fmtp:"
224#define A_PTIME "ptime:"
225#define A_RTCP "rtcp:"
226#define A_SENDRECV "sendrecv"
227#define A_SENDONLY "sendonly"
228#define A_RECVONLY "recvonly"
229
230 if (osmo_str_startswith(src, A_RTPMAP)) {
231 char *audio_name;
232 unsigned int channels = 1;
233 if (sscanf(src, A_RTPMAP "%u", &payload_type) != 1)
234 return -EINVAL;
235
236 audio_name = strchr(src, ' ');
237 if (!audio_name || audio_name >= sdp_msg_line_end(src))
238 return -EINVAL;
239
240 codec = sdp_audio_codec_by_payload_type(&sdp->audio_codecs, payload_type, true);
241 if (!codec)
242 return -ENOSPC;
243
244 if (sscanf(audio_name, " %31[^/]/%u/%u", codec->subtype_name, &codec->rate, &channels) < 1)
245 return -EINVAL;
246
247 if (channels != 1)
248 return -ENOTSUP;
249 }
250
251 else if (osmo_str_startswith(src, A_FMTP)) {
252 char *fmtp_str;
253 const char *line_end = sdp_msg_line_end(src);
254 if (sscanf(src, A_FMTP "%u", &payload_type) != 1)
255 return -EINVAL;
256
257 fmtp_str = strchr(src, ' ');
258 if (!fmtp_str)
259 return -EINVAL;
260 fmtp_str++;
261 if (fmtp_str >= line_end)
262 return -EINVAL;
263
264 codec = sdp_audio_codec_by_payload_type(&sdp->audio_codecs, payload_type, true);
265 if (!codec)
266 return -ENOSPC;
267
268 /* (+1 because osmo_strlcpy() interprets it as size including the '\0') */
269 osmo_strlcpy(codec->fmtp, fmtp_str, line_end - fmtp_str + 1);
270 }
271
272 else if (osmo_str_startswith(src, A_PTIME)) {
273 if (sscanf(src, A_PTIME "%u", &sdp->ptime) != 1)
274 return -EINVAL;
275
276 }
277
278 else if (osmo_str_startswith(src, A_RTCP)) {
279 /* TODO? */
280 }
281
282 else if (osmo_str_startswith(src, A_SENDRECV)) {
283 /* TODO? */
284 }
285
286 else if (osmo_str_startswith(src, A_SENDONLY)) {
287 /* TODO? */
288 }
289
290 else if (osmo_str_startswith(src, A_RECVONLY)) {
291 /* TODO? */
292 }
293
294 return 0;
295}
296
297const struct value_string sdp_msg_payload_type_names[] = {
298 { 0, "PCMU" },
299 { 3, "GSM" },
300 { 8, "PCMA" },
301 { 18, "G729" },
302 { 110, "GSM-EFR" },
303 { 111, "GSM-HR-08" },
304 { 112, "AMR" },
305 { 113, "AMR-WB" },
306 {}
307};
308
309/* Return payload type number matching given string ("AMR", "GSM", ...) or negative if not found. */
310int sdp_subtype_name_to_payload_type(const char *subtype_name)
311{
312 return get_string_value(sdp_msg_payload_type_names, subtype_name);
313}
314
315/* Parse a line like 'm=audio 16398 RTP/AVP 0 3 8 96 112', starting after the '=' */
316static int sdp_parse_media_description(struct sdp_msg *sdp, const char *src)
317{
318 unsigned int port;
319 int i;
320 const char *payload_type_str;
321 const char *line_end = sdp_msg_line_end(src);
322 if (sscanf(src, "audio %u RTP/AVP", &port) < 1)
323 return -ENOTSUP;
324
Vadim Yanitskiy40b11c92020-02-09 04:04:54 +0700325 if (port > 0xffff)
Neels Hofmeyreef45782019-10-21 03:24:04 +0200326 return -EINVAL;
327
328 sdp->rtp.port = port;
329
330 /* skip "audio 12345 RTP/AVP ", i.e. 3 spaces on */
331 payload_type_str = src;
332 for (i = 0; i < 3; i++) {
333 payload_type_str = strchr(payload_type_str, ' ');
334 if (!payload_type_str)
335 return -EINVAL;
336 while (*payload_type_str == ' ')
337 payload_type_str++;
338 if (payload_type_str >= line_end)
339 return -EINVAL;
340 }
341
342 /* Parse listing of payload type numbers after "RTP/AVP" */
343 while (payload_type_str < line_end) {
344 unsigned int payload_type;
345 struct sdp_audio_codec *codec;
346 const char *subtype_name;
347 if (sscanf(payload_type_str, "%u", &payload_type) < 1)
348 return -EINVAL;
349
350 codec = sdp_audio_codec_by_payload_type(&sdp->audio_codecs, payload_type, true);
351 if (!codec)
352 return -ENOSPC;
353
354 /* Fill in subtype name for fixed payload types */
355 subtype_name = get_value_string_or_null(sdp_msg_payload_type_names, codec->payload_type);
356 if (subtype_name)
357 OSMO_STRLCPY_ARRAY(codec->subtype_name, subtype_name);
358
359 payload_type_str = strchr(payload_type_str, ' ');
360 if (!payload_type_str)
361 payload_type_str = line_end;
362 while (*payload_type_str == ' ')
363 payload_type_str++;
364 }
365
366 return 0;
367}
368
369/* parse a line like 'c=IN IP4 192.168.11.151' starting after the '=' */
370static int sdp_parse_connection_info(struct sdp_msg *sdp, const char *src)
371{
372 char ipv[10];
373 char addr_str[INET6_ADDRSTRLEN];
374 if (sscanf(src, "IN %s %s", ipv, addr_str) < 2)
375 return -EINVAL;
376
377 /* supporting only IPv4 */
378 if (strcmp(ipv, "IP4"))
379 return -ENOTSUP;
380
381 osmo_sockaddr_str_from_str(&sdp->rtp, addr_str, sdp->rtp.port);
382 return 0;
383}
384
385/* Parse SDP string into struct sdp_msg. Return 0 on success, negative on error. */
386int sdp_msg_from_str(struct sdp_msg *sdp, const char *src)
387{
388 const char *pos;
389 *sdp = (struct sdp_msg){};
390
391 for (pos = src; pos && *pos; pos++) {
392 char attrib;
393 int rc = 0;
394
395 if (*pos == '\r' || *pos == '\n')
396 continue;
397
398 /* Expecting only lines starting with 'X='. Not being too strict about it is probably alright. */
399 if (pos[1] != '=')
400 goto next_line;
401
402 attrib = *pos;
403 pos += 2;
404 switch (attrib) {
405 /* a=... */
406 case 'a':
407 rc = sdp_parse_attrib(sdp, pos);
408 break;
409 case 'm':
410 rc = sdp_parse_media_description(sdp, pos);
411 break;
412 case 'c':
413 rc = sdp_parse_connection_info(sdp, pos);
414 break;
415 default:
416 /* ignore any other parameters */
417 break;
418 }
419
420 if (rc) {
421 size_t line_len;
422 const char *line_end = sdp_msg_line_end(pos);
423 pos -= 2;
424 line_len = line_end - pos;
425 switch (rc) {
426 case -EINVAL:
427 LOGP(DMNCC, LOGL_ERROR,
428 "Failed to parse SDP: invalid line: %s\n", osmo_quote_str(pos, line_len));
429 break;
430 case -ENOSPC:
431 LOGP(DMNCC, LOGL_ERROR,
432 "Failed to parse SDP: no more space for: %s\n", osmo_quote_str(pos, line_len));
433 break;
434 case -ENOTSUP:
435 LOGP(DMNCC, LOGL_ERROR,
436 "Failed to parse SDP: not supported: %s\n", osmo_quote_str(pos, line_len));
437 break;
438 default:
439 LOGP(DMNCC, LOGL_ERROR,
440 "Failed to parse SDP: %s\n", osmo_quote_str(pos, line_len));
441 break;
442 }
443 return rc;
444 }
445next_line:
446 pos = strstr(pos, "\r\n");
447 if (!pos)
448 break;
449 }
450
451 return 0;
452}
453
454/* Leave only those codecs in 'ac_dest' that are also present in 'ac_other'.
455 * The matching is made by sdp_audio_codec_cmp(), i.e. payload_type numbers are not compared and fmtp parameters are
456 * compared 1:1 as plain strings.
457 * If translate_payload_type_numbers has an effect if ac_dest and ac_other have mismatching payload_type numbers for the
458 * same SDP codec descriptions. If translate_payload_type_numbers is true, take the payload_type numbers from ac_other.
459 * If false, keep payload_type numbers in ac_dest unchanged. */
460void sdp_audio_codecs_intersection(struct sdp_audio_codecs *ac_dest, const struct sdp_audio_codecs *ac_other,
461 bool translate_payload_type_numbers)
462{
463 int i;
464 for (i = 0; i < ac_dest->count; i++) {
465 struct sdp_audio_codec *codec = &ac_dest->codec[i];
466 struct sdp_audio_codec *other;
467 OSMO_ASSERT(i < ARRAY_SIZE(ac_dest->codec));
468
469 other = sdp_audio_codec_by_descr((struct sdp_audio_codecs*)ac_other, codec);
470
471 if (!other) {
472 OSMO_ASSERT(sdp_audio_codec_remove(ac_dest, codec) == 0);
473 i--;
474 continue;
475 }
476
477 /* Doing payload_type number translation of part of the intersection because it makes the algorithm
478 * simpler: we already know ac_dest is a subset of ac_other, and there is no need to resolve payload
479 * type number conflicts. */
480 if (translate_payload_type_numbers)
481 codec->payload_type = other->payload_type;
482 }
483}
484
485/* Make sure the given codec is listed as the first codec. 'codec' must be an actual codec entry of the given audio
486 * codecs list. */
487void sdp_audio_codecs_select(struct sdp_audio_codecs *ac, struct sdp_audio_codec *codec)
488{
489 struct sdp_audio_codec tmp;
490 struct sdp_audio_codec *pos;
491 OSMO_ASSERT((codec >= ac->codec)
492 && ((codec - ac->codec) < OSMO_MIN(ac->count, ARRAY_SIZE(ac->codec))));
493
494 /* Already the first? */
495 if (codec == ac->codec)
496 return;
497
498 tmp = *codec;
499 for (pos = codec - 1; pos >= ac->codec; pos--)
500 pos[1] = pos[0];
501
502 ac->codec[0] = tmp;
503 return;
504}
505
506/* Short single-line representation of an SDP audio codec, convenient for logging.
507 * Like "AMR/8000:octet-align=1#122" */
508int sdp_audio_codec_name_buf(char *buf, size_t buflen, const struct sdp_audio_codec *codec)
509{
510 struct osmo_strbuf sb = { .buf = buf, .len = buflen };
511 OSMO_STRBUF_PRINTF(sb, "%s", codec->subtype_name);
512 if (codec->fmtp[0])
513 OSMO_STRBUF_PRINTF(sb, ":%s", codec->fmtp);
514 return sb.chars_needed;
515}
516
517char *sdp_audio_codec_name_c(void *ctx, const struct sdp_audio_codec *codec)
518{
519 OSMO_NAME_C_IMPL(ctx, 32, "sdp_audio_codec_name_c-ERROR", sdp_audio_codec_name_buf, codec)
520}
521
522const char *sdp_audio_codec_name(const struct sdp_audio_codec *codec)
523{
524 return sdp_audio_codec_name_c(OTC_SELECT, codec);
525}
526
527/* Short single-line representation of a list of SDP audio codecs, convenient for logging */
528int sdp_audio_codecs_name_buf(char *buf, size_t buflen, const struct sdp_audio_codecs *ac)
529{
530 struct osmo_strbuf sb = { .buf = buf, .len = buflen };
531 const struct sdp_audio_codec *codec;
532 if (!ac->count)
533 OSMO_STRBUF_PRINTF(sb, "(no-codecs)");
534 foreach_sdp_audio_codec(codec, ac) {
535 bool first = (codec == ac->codec);
536 if (!first)
537 OSMO_STRBUF_PRINTF(sb, ",");
538 OSMO_STRBUF_APPEND(sb, sdp_audio_codec_name_buf, codec);
539 }
540 return sb.chars_needed;
541}
542
543char *sdp_audio_codecs_name_c(void *ctx, const struct sdp_audio_codecs *ac)
544{
545 OSMO_NAME_C_IMPL(ctx, 128, "sdp_audio_codecs_name_c-ERROR", sdp_audio_codecs_name_buf, ac)
546}
547
548const char *sdp_audio_codecs_name(const struct sdp_audio_codecs *ac)
549{
550 return sdp_audio_codecs_name_c(OTC_SELECT, ac);
551}
552
553/* Short single-line representation of an SDP message, convenient for logging */
554int sdp_msg_name_buf(char *buf, size_t buflen, const struct sdp_msg *sdp)
555{
556 struct osmo_strbuf sb = { .buf = buf, .len = buflen };
557 if (!sdp) {
558 OSMO_STRBUF_PRINTF(sb, "NULL");
559 return sb.chars_needed;
560 }
561
562 OSMO_STRBUF_PRINTF(sb, OSMO_SOCKADDR_STR_FMT, OSMO_SOCKADDR_STR_FMT_ARGS(&sdp->rtp));
563 OSMO_STRBUF_PRINTF(sb, "{");
564 OSMO_STRBUF_APPEND(sb, sdp_audio_codecs_name_buf, &sdp->audio_codecs);
565 OSMO_STRBUF_PRINTF(sb, "}");
566 return sb.chars_needed;
567}
568
569char *sdp_msg_name_c(void *ctx, const struct sdp_msg *sdp)
570{
571 OSMO_NAME_C_IMPL(ctx, 128, "sdp_msg_name_c-ERROR", sdp_msg_name_buf, sdp)
572}
573
574const char *sdp_msg_name(const struct sdp_msg *sdp)
575{
576 return sdp_msg_name_c(OTC_SELECT, sdp);
577}