/* Minimalistic SDP parse/compose implementation, focused on GSM audio codecs */ /* * (C) 2019 by sysmocom - s.f.m.c. GmbH * All Rights Reserved * * SPDX-License-Identifier: AGPL-3.0+ * * Author: Neels Hofmeyr * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include #include #include #include #include #include bool sdp_audio_codec_is_set(const struct sdp_audio_codec *a) { return a && a->subtype_name[0]; } /* Compare name, rate and fmtp, returning typical cmp result: 0 on match, and -1 / 1 on mismatch. * If cmp_fmtp is false, do *not* compare the fmtp string; if true, compare fmtp 1:1 as strings. * If cmp_payload_type is false, do *not* compare the payload_type number. * The fmtp is only string-compared -- e.g. if AMR parameters appear in a different order, it amounts to a mismatch even * though all parameters are the same. */ int sdp_audio_codec_cmp(const struct sdp_audio_codec *a, const struct sdp_audio_codec *b, bool cmp_fmtp, bool cmp_payload_type) { int cmp; if (a == b) return 0; if (!a) return -1; if (!b) return 1; cmp = strncmp(a->subtype_name, b->subtype_name, sizeof(a->subtype_name)); if (cmp) return cmp; cmp = OSMO_CMP(a->rate, b->rate); if (cmp) return cmp; if (cmp_fmtp) { cmp = strncmp(a->fmtp, b->fmtp, sizeof(a->fmtp)); if (cmp) return cmp; } if (cmp_payload_type) { cmp = OSMO_CMP(a->payload_type, b->payload_type); if (cmp) return cmp; } return 0; } /* Compare two lists of audio codecs, returning typical cmp result: 0 on match, and -1 / 1 on mismatch. * The ordering in the two lists may differ, except that the first codec in 'a' must also be the first codec in 'b'. * This is because the first codec typically expresses the preferred codec to use. * If cmp_fmtp is false, do *not* compare the fmtp strings; if true, compare fmtp 1:1 as strings. * If cmp_payload_type is false, do *not* compare the payload_type numbers. * The fmtp is only string-compared -- e.g. if AMR parameters appear in a different order, it amounts to a mismatch even * though all parameters are the same. */ int sdp_audio_codecs_cmp(const struct sdp_audio_codecs *a, const struct sdp_audio_codecs *b, bool cmp_fmtp, bool cmp_payload_type) { const struct sdp_audio_codec *codec_a; const struct sdp_audio_codec *codec_b; int cmp; if (a == b) return 0; if (!a) return -1; if (!b) return 1; cmp = OSMO_CMP(a->count, b->count); if (cmp) return cmp; if (!a->count) return 0; /* The first codec is the "chosen" codec and should match. The others may appear in different order. */ cmp = sdp_audio_codec_cmp(&a->codec[0], &b->codec[0], cmp_fmtp, cmp_payload_type); if (cmp) return cmp; /* See if each codec in a is also present in b */ sdp_audio_codecs_foreach(codec_a, a) { bool match_found = false; sdp_audio_codecs_foreach(codec_b, b) { if (!sdp_audio_codec_cmp(codec_a, codec_b, cmp_fmtp, cmp_payload_type)) { match_found = true; break; } } if (!match_found) return -1; } return 0; } /* Given a predefined fixed payload_type number, add an SDP audio codec entry, if not present yet. * The payload_type must exist in sdp_msg_payload_type_names. * Return the audio codec created or already existing for this payload type number. */ struct sdp_audio_codec *sdp_audio_codecs_add(struct sdp_audio_codecs *ac, unsigned int payload_type, const char *subtype_name, unsigned int rate, const char *fmtp) { struct sdp_audio_codec *codec; /* Does an entry already exist? */ codec = sdp_audio_codecs_by_payload_type(ac, payload_type, false); if (codec) { /* Already exists, sanity check */ if (!codec->subtype_name[0]) OSMO_STRLCPY_ARRAY(codec->subtype_name, subtype_name); else if (strcmp(codec->subtype_name, subtype_name)) { /* There already is an entry with this payload_type number but a mismatching subtype_name. That is * weird, rather abort. */ return NULL; } if (codec->rate != rate || (fmtp && strcmp(fmtp, codec->fmtp))) { /* Mismatching details. Rather abort */ return NULL; } return codec; } /* None exists, create codec entry for this payload type number */ codec = sdp_audio_codecs_by_payload_type(ac, payload_type, true); /* NULL means unable to add an entry */ if (!codec) return NULL; OSMO_STRLCPY_ARRAY(codec->subtype_name, subtype_name); if (fmtp) OSMO_STRLCPY_ARRAY(codec->fmtp, fmtp); codec->rate = rate; return codec; } struct sdp_audio_codec *sdp_audio_codecs_add_copy(struct sdp_audio_codecs *ac, const struct sdp_audio_codec *codec) { return sdp_audio_codecs_add(ac, codec->payload_type, codec->subtype_name, codec->rate, codec->fmtp[0] ? codec->fmtp : NULL); } /* Find or create an entry for the given payload_type number in the given list of codecs. * If the given payload_type number is already present in ac, return the first matching entry. * If no such payload_type number is present: a) return NULL if create == false; * b) If create == true, add a mostly empty codec entry to the end of ac with the given payload_type number, and return * the created entry. * If create == true, a NULL return value means that there was no unused entry left in ac to add this payload_type. */ struct sdp_audio_codec *sdp_audio_codecs_by_payload_type(struct sdp_audio_codecs *ac, unsigned int payload_type, bool create) { struct sdp_audio_codec *codec; sdp_audio_codecs_foreach(codec, ac) { if (codec->payload_type == payload_type) return codec; } if (!create) return NULL; if (ac->count >= ARRAY_SIZE(ac->codec)) return NULL; codec = &ac->codec[ac->count]; *codec = (struct sdp_audio_codec){ .payload_type = payload_type, .rate = 8000, }; ac->count++; return codec; } /* Return a given sdp_msg's codec entry that matches the subtype_name and rate of the given codec, or NULL if no * match is found. Comparison is made by sdp_audio_codec_cmp(cmp_payload_type=false). */ struct sdp_audio_codec *sdp_audio_codecs_by_descr(struct sdp_audio_codecs *ac, const struct sdp_audio_codec *codec) { struct sdp_audio_codec *i; sdp_audio_codecs_foreach(i, ac) { if (!sdp_audio_codec_cmp(i, codec, false, false)) return i; } return NULL; } /* Remove the codec entry pointed at by 'codec'. 'codec' must point at an entry of 'ac'. * To use any external codec instance, use sdp_audio_codecs_remove(ac, sdp_audio_codecs_by_descr(ac, codec)). * Return 0 on success, -ENOENT if codec does not point at the sdp->codec array. */ int sdp_audio_codecs_remove(struct sdp_audio_codecs *ac, const struct sdp_audio_codec *codec) { struct sdp_audio_codec *i; if ((codec < ac->codec) || ((codec - ac->codec) >= OSMO_MIN(ac->count, ARRAY_SIZE(ac->codec)))) return -ENOENT; /* Move all following entries one up */ ac->count--; sdp_audio_codecs_foreach(i, ac) { if (i < codec) continue; *i = *(i+1); } return 0; } static const char * const sdp_mode_str[] = { [SDP_MODE_UNSET] = "-", [SDP_MODE_SENDONLY] = "sendonly", [SDP_MODE_RECVONLY] = "recvonly", [SDP_MODE_SENDRECV] = "sendrecv", [SDP_MODE_INACTIVE] = "inactive", }; /* Convert struct sdp_msg to the actual SDP protocol representation */ int sdp_msg_to_sdp_str_buf(char *dst, size_t dst_size, const struct sdp_msg *sdp) { const struct sdp_audio_codec *codec; struct osmo_strbuf sb = { .buf = dst, .len = dst_size }; const char *ip; char ipv; if (!sdp) { OSMO_STRBUF_PRINTF(sb, "%s", ""); return sb.chars_needed; } ip = sdp->rtp.ip[0] ? sdp->rtp.ip : "0.0.0.0"; ipv = (osmo_ip_str_type(ip) == AF_INET6) ? '6' : '4'; OSMO_STRBUF_PRINTF(sb, "v=0\r\n" "o=OsmoMSC 0 0 IN IP%c %s\r\n" "s=GSM Call\r\n" "c=IN IP%c %s\r\n" "t=0 0\r\n" "m=audio %d RTP/AVP", ipv, ip, ipv, ip, sdp->rtp.port); /* Append all payload type numbers to 'm=audio RTP/AVP 3 4 112' line */ sdp_audio_codecs_foreach(codec, &sdp->audio_codecs) OSMO_STRBUF_PRINTF(sb, " %d", codec->payload_type); OSMO_STRBUF_PRINTF(sb, "\r\n"); /* Add details for all codecs */ sdp_audio_codecs_foreach(codec, &sdp->audio_codecs) { if (!sdp_audio_codec_is_set(codec)) continue; OSMO_STRBUF_PRINTF(sb, "a=rtpmap:%d %s/%d\r\n", codec->payload_type, codec->subtype_name, codec->rate > 0 ? codec->rate : 8000); if (codec->fmtp[0]) OSMO_STRBUF_PRINTF(sb, "a=fmtp:%d %s\r\n", codec->payload_type, codec->fmtp); } OSMO_STRBUF_PRINTF(sb, "a=ptime:%d\r\n", sdp->ptime > 0? sdp->ptime : 20); if (sdp->mode != SDP_MODE_UNSET && sdp->mode < ARRAY_SIZE(sdp_mode_str)) OSMO_STRBUF_PRINTF(sb, "a=%s\r\n", sdp_mode_str[sdp->mode]); return sb.chars_needed; } /* Return the first line ending (or the end of the string) at or after the given string position. */ const char *sdp_msg_line_end(const char *src) { const char *line_end = strchr(src, '\r'); if (!line_end) line_end = strchr(src, '\n'); if (!line_end) line_end = src + strlen(src); return line_end; } /* parse a line like 'a=rtpmap:0 PCMU/8000', 'a=fmtp:112 octet-align=1; mode-set=4', 'a=ptime:20'. * The src should point at the character after 'a=', e.g. at the start of 'rtpmap', 'fmtp', 'ptime' */ int sdp_parse_attrib(struct sdp_msg *sdp, const char *src) { unsigned int payload_type; struct sdp_audio_codec *codec; #define A_RTPMAP "rtpmap:" #define A_FMTP "fmtp:" #define A_PTIME "ptime:" #define A_RTCP "rtcp:" if (osmo_str_startswith(src, A_RTPMAP)) { /* "a=rtpmap:3 GSM/8000" */ char *audio_name; unsigned int channels = 1; if (sscanf(src, A_RTPMAP "%u", &payload_type) != 1) return -EINVAL; audio_name = strchr(src, ' '); if (!audio_name || audio_name >= sdp_msg_line_end(src)) return -EINVAL; codec = sdp_audio_codecs_by_payload_type(&sdp->audio_codecs, payload_type, true); if (!codec) return -ENOSPC; if (sscanf(audio_name, " %31[^/]/%u/%u", codec->subtype_name, &codec->rate, &channels) < 1) return -EINVAL; if (channels != 1) return -ENOTSUP; } else if (osmo_str_startswith(src, A_FMTP)) { /* "a=fmtp:112 octet-align=1;mode-set=0,1,2,3" */ char *fmtp_str; const char *line_end = sdp_msg_line_end(src); if (sscanf(src, A_FMTP "%u", &payload_type) != 1) return -EINVAL; fmtp_str = strchr(src, ' '); if (!fmtp_str) return -EINVAL; fmtp_str++; if (fmtp_str >= line_end) return -EINVAL; codec = sdp_audio_codecs_by_payload_type(&sdp->audio_codecs, payload_type, true); if (!codec) return -ENOSPC; /* (+1 because osmo_strlcpy() interprets it as size including the '\0') */ osmo_strlcpy(codec->fmtp, fmtp_str, line_end - fmtp_str + 1); } else if (osmo_str_startswith(src, A_PTIME)) { /* "a=ptime:20" */ if (sscanf(src, A_PTIME "%u", &sdp->ptime) != 1) return -EINVAL; } else if (osmo_str_startswith(src, A_RTCP)) { /* TODO? */ } else if (osmo_str_startswith(src, sdp_mode_str[SDP_MODE_SENDRECV])) { /* "a=sendrecv" */ sdp->mode = SDP_MODE_SENDRECV; } else if (osmo_str_startswith(src, sdp_mode_str[SDP_MODE_SENDONLY])) { /* "a=sendonly" */ sdp->mode = SDP_MODE_SENDONLY; } else if (osmo_str_startswith(src, sdp_mode_str[SDP_MODE_RECVONLY])) { /* "a=recvonly" */ sdp->mode = SDP_MODE_RECVONLY; } else if (osmo_str_startswith(src, sdp_mode_str[SDP_MODE_INACTIVE])) { /* "a=inactive" */ sdp->mode = SDP_MODE_INACTIVE; } return 0; } const struct value_string sdp_msg_payload_type_names[] = { { 0, "PCMU" }, { 3, "GSM" }, { 8, "PCMA" }, { 18, "G729" }, { 110, "GSM-EFR" }, { 111, "GSM-HR-08" }, { 112, "AMR" }, { 113, "AMR-WB" }, {} }; /* Return payload type number matching given string ("AMR", "GSM", ...) or negative if not found. */ int sdp_subtype_name_to_payload_type(const char *subtype_name) { return get_string_value(sdp_msg_payload_type_names, subtype_name); } /* Parse a line like 'm=audio 16398 RTP/AVP 0 3 8 96 112', starting after the '=' */ static int sdp_parse_media_description(struct sdp_msg *sdp, const char *src) { unsigned int port; int i; const char *payload_type_str; const char *line_end = sdp_msg_line_end(src); if (sscanf(src, "audio %u RTP/AVP", &port) < 1) return -ENOTSUP; if (port > 0xffff) return -EINVAL; sdp->rtp.port = port; /* skip "audio 12345 RTP/AVP ", i.e. 3 spaces on */ payload_type_str = src; for (i = 0; i < 3; i++) { payload_type_str = strchr(payload_type_str, ' '); if (!payload_type_str) return -EINVAL; while (*payload_type_str == ' ') payload_type_str++; if (payload_type_str >= line_end) return -EINVAL; } /* Parse listing of payload type numbers after "RTP/AVP" */ while (payload_type_str < line_end) { unsigned int payload_type; struct sdp_audio_codec *codec; const char *subtype_name; if (sscanf(payload_type_str, "%u", &payload_type) < 1) return -EINVAL; codec = sdp_audio_codecs_by_payload_type(&sdp->audio_codecs, payload_type, true); if (!codec) return -ENOSPC; /* Fill in subtype name for fixed payload types */ subtype_name = get_value_string_or_null(sdp_msg_payload_type_names, codec->payload_type); if (subtype_name) OSMO_STRLCPY_ARRAY(codec->subtype_name, subtype_name); payload_type_str = strchr(payload_type_str, ' '); if (!payload_type_str) payload_type_str = line_end; while (*payload_type_str == ' ') payload_type_str++; } return 0; } /* parse a line like 'c=IN IP4 192.168.11.151' starting after the '=' */ static int sdp_parse_connection_info(struct sdp_msg *sdp, const char *src) { char ipv[10]; char addr_str[INET6_ADDRSTRLEN]; if (sscanf(src, "IN %s %s", ipv, addr_str) < 2) return -EINVAL; /* supporting only IPv4 */ if (strcmp(ipv, "IP4")) return -ENOTSUP; osmo_sockaddr_str_from_str(&sdp->rtp, addr_str, sdp->rtp.port); return 0; } /* Parse SDP string into struct sdp_msg. Return 0 on success, negative on error. */ int sdp_msg_from_sdp_str(struct sdp_msg *sdp, const char *src) { const char *pos; *sdp = (struct sdp_msg){}; for (pos = src; pos && *pos; pos++) { char attrib; int rc = 0; if (*pos == '\r' || *pos == '\n') continue; /* Expecting only lines starting with 'X='. Not being too strict about it is probably alright. */ if (pos[1] != '=') goto next_line; attrib = *pos; pos += 2; switch (attrib) { /* a=... */ case 'a': rc = sdp_parse_attrib(sdp, pos); break; case 'm': rc = sdp_parse_media_description(sdp, pos); break; case 'c': rc = sdp_parse_connection_info(sdp, pos); break; default: /* ignore any other parameters */ break; } if (rc) { size_t line_len; const char *line_end = sdp_msg_line_end(pos); pos -= 2; line_len = line_end - pos; switch (rc) { case -EINVAL: LOGP(DMNCC, LOGL_ERROR, "Failed to parse SDP: invalid line: %s\n", osmo_quote_str(pos, line_len)); break; case -ENOSPC: LOGP(DMNCC, LOGL_ERROR, "Failed to parse SDP: no more space for: %s\n", osmo_quote_str(pos, line_len)); break; case -ENOTSUP: LOGP(DMNCC, LOGL_ERROR, "Failed to parse SDP: not supported: %s\n", osmo_quote_str(pos, line_len)); break; default: LOGP(DMNCC, LOGL_ERROR, "Failed to parse SDP: %s\n", osmo_quote_str(pos, line_len)); break; } return rc; } next_line: pos = strstr(pos, "\r\n"); if (!pos) break; } return 0; } /* Leave only those codecs in 'ac_dest' that are also present in 'ac_other'. * The matching is made by sdp_audio_codec_cmp(cmp_payload_type=false), i.e. payload_type numbers are not compared and * fmtp parameters are compared 1:1 as plain strings. * If translate_payload_type_numbers has an effect if ac_dest and ac_other have mismatching payload_type numbers for the * same SDP codec descriptions. If translate_payload_type_numbers is true, take the payload_type numbers from ac_other. * If false, keep payload_type numbers in ac_dest unchanged. */ void sdp_audio_codecs_intersection(struct sdp_audio_codecs *ac_dest, const struct sdp_audio_codecs *ac_other, bool translate_payload_type_numbers) { int i; for (i = 0; i < ac_dest->count; i++) { struct sdp_audio_codec *codec = &ac_dest->codec[i]; struct sdp_audio_codec *other; OSMO_ASSERT(i < ARRAY_SIZE(ac_dest->codec)); other = sdp_audio_codecs_by_descr((struct sdp_audio_codecs *)ac_other, codec); if (!other) { OSMO_ASSERT(sdp_audio_codecs_remove(ac_dest, codec) == 0); i--; continue; } /* Doing payload_type number translation of part of the intersection because it makes the algorithm * simpler: we already know ac_dest is a subset of ac_other, and there is no need to resolve payload * type number conflicts. */ if (translate_payload_type_numbers) codec->payload_type = other->payload_type; } } /* Make sure the given codec is listed as the first codec. 'codec' must be an actual codec entry of the given audio * codecs list. */ void sdp_audio_codecs_select(struct sdp_audio_codecs *ac, struct sdp_audio_codec *codec) { struct sdp_audio_codec tmp; struct sdp_audio_codec *pos; OSMO_ASSERT((codec >= ac->codec) && ((codec - ac->codec) < OSMO_MIN(ac->count, ARRAY_SIZE(ac->codec)))); /* Already the first? */ if (codec == ac->codec) return; tmp = *codec; for (pos = codec - 1; pos >= ac->codec; pos--) pos[1] = pos[0]; ac->codec[0] = tmp; return; } /* Short single-line representation of an SDP audio codec, convenient for logging. * Like "AMR/8000:octet-align=1#122" */ int sdp_audio_codec_to_str_buf(char *buf, size_t buflen, const struct sdp_audio_codec *codec) { struct osmo_strbuf sb = { .buf = buf, .len = buflen }; OSMO_STRBUF_PRINTF(sb, "%s", codec->subtype_name); if (codec->rate != 8000) OSMO_STRBUF_PRINTF(sb, "/%u", codec->rate); if (codec->fmtp[0]) OSMO_STRBUF_PRINTF(sb, ":%s", codec->fmtp); OSMO_STRBUF_PRINTF(sb, "#%d", codec->payload_type); return sb.chars_needed; } char *sdp_audio_codec_to_str_c(void *ctx, const struct sdp_audio_codec *codec) { OSMO_NAME_C_IMPL(ctx, 32, "sdp_audio_codec_to_str_c-ERROR", sdp_audio_codec_to_str_buf, codec) } const char *sdp_audio_codec_to_str(const struct sdp_audio_codec *codec) { return sdp_audio_codec_to_str_c(OTC_SELECT, codec); } /* Short single-line representation of a list of SDP audio codecs, convenient for logging */ int sdp_audio_codecs_to_str_buf(char *buf, size_t buflen, const struct sdp_audio_codecs *ac) { struct osmo_strbuf sb = { .buf = buf, .len = buflen }; const struct sdp_audio_codec *codec; if (!ac->count) OSMO_STRBUF_PRINTF(sb, "(no-codecs)"); sdp_audio_codecs_foreach(codec, ac) { bool first = (codec == ac->codec); if (!first) OSMO_STRBUF_PRINTF(sb, ","); OSMO_STRBUF_APPEND(sb, sdp_audio_codec_to_str_buf, codec); } return sb.chars_needed; } char *sdp_audio_codecs_to_str_c(void *ctx, const struct sdp_audio_codecs *ac) { OSMO_NAME_C_IMPL(ctx, 128, "sdp_audio_codecs_to_str_c-ERROR", sdp_audio_codecs_to_str_buf, ac) } const char *sdp_audio_codecs_to_str(const struct sdp_audio_codecs *ac) { return sdp_audio_codecs_to_str_c(OTC_SELECT, ac); } /* Short single-line representation of an SDP message, convenient for logging */ int sdp_msg_to_str_buf(char *buf, size_t buflen, const struct sdp_msg *sdp) { struct osmo_strbuf sb = { .buf = buf, .len = buflen }; if (!sdp) { OSMO_STRBUF_PRINTF(sb, "NULL"); return sb.chars_needed; } OSMO_STRBUF_PRINTF(sb, OSMO_SOCKADDR_STR_FMT, OSMO_SOCKADDR_STR_FMT_ARGS(&sdp->rtp)); OSMO_STRBUF_PRINTF(sb, "{"); OSMO_STRBUF_APPEND(sb, sdp_audio_codecs_to_str_buf, &sdp->audio_codecs); if (sdp->bearer_services.count) { OSMO_STRBUF_PRINTF(sb, ","); OSMO_STRBUF_APPEND(sb, csd_bs_list_to_str_buf, &sdp->bearer_services); } OSMO_STRBUF_PRINTF(sb, "}"); return sb.chars_needed; } char *sdp_msg_to_str_c(void *ctx, const struct sdp_msg *sdp) { OSMO_NAME_C_IMPL(ctx, 128, "sdp_msg_to_str_c-ERROR", sdp_msg_to_str_buf, sdp) } const char *sdp_msg_to_str(const struct sdp_msg *sdp) { return sdp_msg_to_str_c(OTC_SELECT, sdp); } void sdp_audio_codecs_set_csd(struct sdp_audio_codecs *ac) { *ac = (struct sdp_audio_codecs){ .count = 1, .codec = {{ .payload_type = 120, .subtype_name = "CLEARMODE", .rate = 8000, }}, }; }