| /* |
| * libjingle |
| * Copyright 2004--2005, Google Inc. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright notice, |
| * this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright notice, |
| * this list of conditions and the following disclaimer in the documentation |
| * and/or other materials provided with the distribution. |
| * 3. The name of the author may not be used to endorse or promote products |
| * derived from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED |
| * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO |
| * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; |
| * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, |
| * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
| * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "talk/session/phone/mediasession.h" |
| |
| #include "talk/base/helpers.h" |
| #include "talk/base/logging.h" |
| #include "talk/base/scoped_ptr.h" |
| #include "talk/p2p/base/constants.h" |
| #include "talk/session/phone/channelmanager.h" |
| #include "talk/session/phone/cryptoparams.h" |
| #include "talk/session/phone/srtpfilter.h" |
| #include "talk/xmpp/constants.h" |
| |
| namespace { |
| const char kInline[] = "inline:"; |
| } |
| |
| namespace cricket { |
| |
| using talk_base::scoped_ptr; |
| |
| static bool CreateCryptoParams(int tag, const std::string& cipher, |
| CryptoParams *out) { |
| std::string key; |
| key.reserve(SRTP_MASTER_KEY_BASE64_LEN); |
| |
| if (!talk_base::CreateRandomString(SRTP_MASTER_KEY_BASE64_LEN, &key)) { |
| return false; |
| } |
| out->tag = tag; |
| out->cipher_suite = cipher; |
| out->key_params = kInline; |
| out->key_params += key; |
| return true; |
| } |
| |
| #ifdef HAVE_SRTP |
| static bool AddCryptoParams(const std::string& cipher_suite, |
| CryptoParamsVec *out) { |
| int size = out->size(); |
| |
| out->resize(size + 1); |
| return CreateCryptoParams(size, cipher_suite, &out->at(size)); |
| } |
| #endif |
| |
| // For audio, HMAC 32 is prefered because of the low overhead. |
| static bool GetSupportedAudioCryptos(CryptoParamsVec* cryptos) { |
| #ifdef HAVE_SRTP |
| return AddCryptoParams(CS_AES_CM_128_HMAC_SHA1_32, cryptos) && |
| AddCryptoParams(CS_AES_CM_128_HMAC_SHA1_80, cryptos); |
| #else |
| return false; |
| #endif |
| } |
| |
| static bool GetSupportedVideoCryptos(CryptoParamsVec* cryptos) { |
| #ifdef HAVE_SRTP |
| return AddCryptoParams(CS_AES_CM_128_HMAC_SHA1_80, cryptos); |
| #else |
| return false; |
| #endif |
| } |
| |
| // For video support only 80-bit SHA1 HMAC. For audio 32-bit HMAC is |
| // tolerated because it is low overhead. Pick the crypto in the list |
| // that is supported. |
| static bool SelectCrypto(const MediaContentDescription* offer, |
| CryptoParams *crypto) { |
| bool audio = offer->type() == MEDIA_TYPE_AUDIO; |
| const CryptoParamsVec& cryptos = offer->cryptos(); |
| |
| for (CryptoParamsVec::const_iterator i = cryptos.begin(); |
| i != cryptos.end(); ++i) { |
| if (CS_AES_CM_128_HMAC_SHA1_80 == i->cipher_suite || |
| (CS_AES_CM_128_HMAC_SHA1_32 == i->cipher_suite && audio)) { |
| return CreateCryptoParams(i->tag, i->cipher_suite, crypto); |
| } |
| } |
| return false; |
| } |
| |
| static const StreamParams* FindStreamParamsByName( |
| const StreamParamsVec& params_vec, |
| const std::string& name) { |
| for (StreamParamsVec::const_iterator it = params_vec.begin(); |
| it != params_vec.end(); ++it) { |
| if (it->name == name) |
| return &*it; |
| } |
| return NULL; |
| } |
| |
| static const StreamParams* FindFirstStreamParamsByCname( |
| const StreamParamsVec& params_vec, |
| const std::string& cname) { |
| for (StreamParamsVec::const_iterator it = params_vec.begin(); |
| it != params_vec.end(); ++it) { |
| if (cname == it->cname) |
| return &*it; |
| } |
| return NULL; |
| } |
| |
| static const StreamParams* FindStreamParamsBySsrc( |
| const StreamParamsVec& params_vec, |
| uint32 ssrc) { |
| for (StreamParamsVec::const_iterator stream_it = params_vec.begin(); |
| stream_it != params_vec.end(); ++stream_it) { |
| const std::vector<uint32>& ssrcs = stream_it->ssrcs; |
| for (std::vector<uint32>::const_iterator ssrc_it = ssrcs.begin(); |
| ssrc_it != ssrcs.end(); ++ssrc_it) { |
| if (ssrc == *ssrc_it) |
| return &*stream_it; |
| } |
| } |
| return NULL; |
| } |
| |
| // Generates a new CNAME or the CNAME of an already existing StreamParams |
| // if a StreamParams exist for another Stream in streams with sync_label |
| // sync_label. |
| static bool GenerateCname(const StreamParamsVec& params_vec, |
| const MediaSessionOptions::Streams& streams, |
| const std::string& synch_label, |
| std::string* cname) { |
| ASSERT(cname != NULL); |
| if (!cname) |
| return false; |
| |
| // Check if a CNAME exist for any of the other synched streams. |
| for (MediaSessionOptions::Streams::const_iterator stream_it = streams.begin(); |
| stream_it != streams.end() ; ++stream_it) { |
| if (synch_label != stream_it->sync_label) |
| continue; |
| const StreamParams* param = FindStreamParamsByName(params_vec, |
| stream_it->name); |
| if (param) { |
| *cname = param->cname; |
| return true; |
| } |
| } |
| // No other stream seems to exist that we should sync with. |
| // Generate a random string for the RTCP CNAME, as stated in RFC 6222. |
| // This string is only used for synchronization, and therefore is opaque. |
| do { |
| if (!talk_base::CreateRandomString(16, cname)) { |
| ASSERT(false); |
| return false; |
| } |
| } while (FindFirstStreamParamsByCname(params_vec, *cname)); |
| |
| return true; |
| } |
| |
| // Generate a new SSRC and make sure it does not exist in params_vec. |
| static uint32 GenerateSsrc(const StreamParamsVec& params_vec) { |
| uint32 ssrc = 0; |
| do { |
| ssrc = talk_base::CreateRandomNonZeroId(); |
| } while (FindStreamParamsBySsrc(params_vec, ssrc)); |
| return ssrc; |
| } |
| |
| // Finds all StreamParams of all media types and attach them to stream_params. |
| static void GetCurrentStreamParams(const SessionDescription* sdesc, |
| StreamParamsVec* stream_params) { |
| if (!sdesc) |
| return; |
| |
| const ContentInfos& contents = sdesc->contents(); |
| for (ContentInfos::const_iterator content = contents.begin(); |
| content != contents.end(); content++) { |
| if (!IsAudioContent(&*content) && !IsVideoContent(&*content)) |
| continue; |
| const MediaContentDescription* media = |
| static_cast<const MediaContentDescription*>( |
| content->description); |
| const StreamParamsVec& streams = media->streams(); |
| for (StreamParamsVec::const_iterator it = streams.begin(); |
| it != streams.end(); ++it) { |
| stream_params->push_back(*it); |
| } |
| } |
| } |
| |
| // Adds a StreamParams for each Stream in Streams with media type |
| // media_type to content_description. |
| // current_parms - All currently known StreamParams of any media type. |
| static bool AddStreamParams( |
| MediaType media_type, |
| const MediaSessionOptions::Streams& streams, |
| StreamParamsVec* current_params, |
| MediaContentDescription* content_description) { |
| for (MediaSessionOptions::Streams::const_iterator stream_it = streams.begin(); |
| stream_it != streams.end(); ++stream_it) { |
| if (stream_it->type != media_type) |
| continue; // Wrong media type. |
| const StreamParams* params = FindStreamParamsByName(*current_params, |
| stream_it->name); |
| if (!params) { |
| // This is a new stream. |
| // Get a CNAME. Either new or same as one of the other synched streams. |
| std::string cname; |
| if (!GenerateCname(*current_params, streams, stream_it->sync_label, |
| &cname)) { |
| return false; |
| } |
| uint32 ssrc = GenerateSsrc(*current_params); |
| // TODO: Generate the more complex types of stream_params. |
| |
| StreamParams stream_param; |
| stream_param.name = stream_it->name; |
| stream_param.ssrcs.push_back(ssrc); |
| stream_param.cname = cname; |
| stream_param.sync_label = stream_it->sync_label; |
| content_description->AddStream(stream_param); |
| |
| // Store the new StreamParams in current_params. |
| // This is necessary so that we can use the CNAME for other media types. |
| current_params->push_back(stream_param); |
| } else { |
| content_description->AddStream(*params); |
| } |
| } |
| return true; |
| } |
| |
| void MediaSessionOptions::AddStream(MediaType type, |
| const std::string& name, |
| const std::string& sync_label) { |
| streams.push_back(Stream(type, name, sync_label)); |
| |
| if (type == MEDIA_TYPE_VIDEO) |
| has_video = true; |
| else if (type == MEDIA_TYPE_AUDIO) |
| has_audio = true; |
| } |
| |
| void MediaSessionOptions::RemoveStream(MediaType type, |
| const std::string& name) { |
| Streams::iterator stream_it = streams.begin(); |
| for (; stream_it != streams.end(); ++stream_it) { |
| if (stream_it->type == type && stream_it->name == name) { |
| streams.erase(stream_it); |
| return; |
| } |
| } |
| ASSERT(false); |
| } |
| |
| MediaSessionDescriptionFactory::MediaSessionDescriptionFactory() |
| : secure_(SEC_DISABLED) { |
| } |
| |
| MediaSessionDescriptionFactory::MediaSessionDescriptionFactory( |
| ChannelManager* channel_manager) |
| : secure_(SEC_DISABLED) { |
| channel_manager->GetSupportedAudioCodecs(&audio_codecs_); |
| channel_manager->GetSupportedVideoCodecs(&video_codecs_); |
| } |
| |
| SessionDescription* MediaSessionDescriptionFactory::CreateOffer( |
| const MediaSessionOptions& options, |
| const SessionDescription* current_description) { |
| scoped_ptr<SessionDescription> offer(new SessionDescription()); |
| |
| StreamParamsVec current_params; |
| GetCurrentStreamParams(current_description, ¤t_params); |
| |
| if (options.has_audio) { |
| scoped_ptr<AudioContentDescription> audio(new AudioContentDescription()); |
| for (AudioCodecs::const_iterator codec = audio_codecs_.begin(); |
| codec != audio_codecs_.end(); ++codec) { |
| audio->AddCodec(*codec); |
| } |
| audio->SortCodecs(); |
| if (!AddStreamParams(MEDIA_TYPE_AUDIO, options.streams, ¤t_params, |
| audio.get())) { |
| return NULL; // Abort, something went seriously wrong. |
| } |
| |
| if (options.streams.empty()) { |
| // TODO: Remove this legacy stream when all apps use StreamParams. |
| audio->AddLegacyStream(talk_base::CreateRandomNonZeroId()); |
| } |
| audio->set_rtcp_mux(options.rtcp_mux_enabled); |
| audio->set_lang(lang_); |
| |
| if (secure() != SEC_DISABLED) { |
| CryptoParamsVec audio_cryptos; |
| if (current_description) { |
| // Copy crypto parameters from the previous offer. |
| const ContentInfo* info = |
| GetFirstAudioContent(current_description); |
| if (info) { |
| const AudioContentDescription* desc = |
| static_cast<const AudioContentDescription*>(info->description); |
| audio_cryptos = desc->cryptos(); |
| } |
| } |
| if (audio_cryptos.empty()) |
| GetSupportedAudioCryptos(&audio_cryptos); // Generate new cryptos. |
| |
| for (CryptoParamsVec::const_iterator crypto = audio_cryptos.begin(); |
| crypto != audio_cryptos.end(); ++crypto) { |
| audio->AddCrypto(*crypto); |
| } |
| |
| if (secure() == SEC_REQUIRED) { |
| if (audio->cryptos().empty()) { |
| return NULL; // Abort, crypto required but none found. |
| } |
| audio->set_crypto_required(true); |
| } |
| } |
| |
| offer->AddContent(CN_AUDIO, NS_JINGLE_RTP, audio.release()); |
| } |
| |
| // add video codecs, if this is a video call |
| if (options.has_video) { |
| scoped_ptr<VideoContentDescription> video(new VideoContentDescription()); |
| for (VideoCodecs::const_iterator codec = video_codecs_.begin(); |
| codec != video_codecs_.end(); ++codec) { |
| video->AddCodec(*codec); |
| } |
| |
| video->SortCodecs(); |
| if (!AddStreamParams(MEDIA_TYPE_VIDEO, options.streams, ¤t_params, |
| video.get())) { |
| return NULL; // Abort, something went seriously wrong. |
| } |
| |
| if (options.streams.empty()) { |
| // TODO: Remove this legacy stream when all apps use StreamParams. |
| video->AddLegacyStream(talk_base::CreateRandomNonZeroId()); |
| } |
| video->set_bandwidth(options.video_bandwidth); |
| video->set_rtcp_mux(options.rtcp_mux_enabled); |
| |
| if (secure() != SEC_DISABLED) { |
| CryptoParamsVec video_cryptos; |
| if (current_description) { |
| // Copy crypto parameters from the previous offer. |
| const ContentInfo* info = |
| GetFirstVideoContent(current_description); |
| if (info) { |
| const VideoContentDescription* desc = |
| static_cast<const VideoContentDescription*>(info->description); |
| video_cryptos = desc->cryptos(); |
| } |
| } |
| if (video_cryptos.empty()) |
| GetSupportedVideoCryptos(&video_cryptos); // Generate new crypto. |
| for (CryptoParamsVec::const_iterator crypto = video_cryptos.begin(); |
| crypto != video_cryptos.end(); ++crypto) { |
| video->AddCrypto(*crypto); |
| } |
| if (secure() == SEC_REQUIRED) { |
| if (video->cryptos().empty()) { |
| return NULL; // Abort, crypto required but none found. |
| } |
| video->set_crypto_required(true); |
| } |
| } |
| |
| offer->AddContent(CN_VIDEO, NS_JINGLE_RTP, video.release()); |
| } |
| |
| return offer.release(); |
| } |
| |
| SessionDescription* MediaSessionDescriptionFactory::CreateAnswer( |
| const SessionDescription* offer, const MediaSessionOptions& options, |
| const SessionDescription* current_description) { |
| // The answer contains the intersection of the codecs in the offer with the |
| // codecs we support, ordered by our local preference. As indicated by |
| // XEP-0167, we retain the same payload ids from the offer in the answer. |
| scoped_ptr<SessionDescription> accept(new SessionDescription()); |
| |
| StreamParamsVec current_params; |
| GetCurrentStreamParams(current_description, ¤t_params); |
| |
| const ContentInfo* audio_content = GetFirstAudioContent(offer); |
| if (audio_content && options.has_audio) { |
| const AudioContentDescription* audio_offer = |
| static_cast<const AudioContentDescription*>(audio_content->description); |
| scoped_ptr<AudioContentDescription> audio_accept( |
| new AudioContentDescription()); |
| for (AudioCodecs::const_iterator ours = audio_codecs_.begin(); |
| ours != audio_codecs_.end(); ++ours) { |
| for (AudioCodecs::const_iterator theirs = audio_offer->codecs().begin(); |
| theirs != audio_offer->codecs().end(); ++theirs) { |
| if (ours->Matches(*theirs)) { |
| AudioCodec negotiated(*ours); |
| negotiated.id = theirs->id; |
| audio_accept->AddCodec(negotiated); |
| } |
| } |
| } |
| |
| audio_accept->SortCodecs(); |
| if (!AddStreamParams(MEDIA_TYPE_AUDIO, options.streams, ¤t_params, |
| audio_accept.get())) { |
| return NULL; // Abort, something went seriously wrong. |
| } |
| |
| if (options.streams.empty()) { |
| // TODO: Remove this legacy stream when all apps use StreamParams. |
| audio_accept->AddLegacyStream(talk_base::CreateRandomNonZeroId()); |
| } |
| audio_accept->set_rtcp_mux( |
| options.rtcp_mux_enabled && audio_offer->rtcp_mux()); |
| |
| if (secure() != SEC_DISABLED) { |
| CryptoParams crypto; |
| |
| if (SelectCrypto(audio_offer, &crypto)) { |
| if (current_description) { |
| // Check if this crypto already exist in the previous |
| // session description. Use it in that case. |
| const ContentInfo* info = |
| GetFirstAudioContent(current_description); |
| if (info) { |
| const AudioContentDescription* desc = |
| static_cast<const AudioContentDescription*>(info->description); |
| const CryptoParamsVec& cryptos = desc->cryptos(); |
| for (CryptoParamsVec::const_iterator it = cryptos.begin(); |
| it != cryptos.end(); ++it) { |
| if (crypto.Matches(*it)) { |
| crypto = *it; |
| break; |
| } |
| } |
| } |
| } |
| audio_accept->AddCrypto(crypto); |
| } |
| } |
| |
| if (audio_accept->cryptos().empty() && |
| (audio_offer->crypto_required() || secure() == SEC_REQUIRED)) { |
| return NULL; // Fails the session setup. |
| } |
| accept->AddContent(audio_content->name, audio_content->type, |
| audio_accept.release()); |
| } else { |
| LOG(LS_INFO) << "Audio is not supported in answer"; |
| } |
| |
| const ContentInfo* video_content = GetFirstVideoContent(offer); |
| if (video_content && options.has_video) { |
| const VideoContentDescription* video_offer = |
| static_cast<const VideoContentDescription*>(video_content->description); |
| scoped_ptr<VideoContentDescription> video_accept( |
| new VideoContentDescription()); |
| for (VideoCodecs::const_iterator ours = video_codecs_.begin(); |
| ours != video_codecs_.end(); ++ours) { |
| for (VideoCodecs::const_iterator theirs = video_offer->codecs().begin(); |
| theirs != video_offer->codecs().end(); ++theirs) { |
| if (ours->Matches(*theirs)) { |
| VideoCodec negotiated(*ours); |
| negotiated.id = theirs->id; |
| video_accept->AddCodec(negotiated); |
| } |
| } |
| } |
| if (!AddStreamParams(MEDIA_TYPE_VIDEO, options.streams, ¤t_params, |
| video_accept.get())) { |
| return NULL; // Abort, something went seriously wrong. |
| } |
| |
| if (options.streams.empty()) { |
| // TODO: Remove this legacy stream when all apps use StreamParams. |
| video_accept->AddLegacyStream(talk_base::CreateRandomNonZeroId()); |
| } |
| video_accept->set_bandwidth(options.video_bandwidth); |
| video_accept->set_rtcp_mux( |
| options.rtcp_mux_enabled && video_offer->rtcp_mux()); |
| video_accept->SortCodecs(); |
| |
| if (secure() != SEC_DISABLED) { |
| CryptoParams crypto; |
| |
| if (SelectCrypto(video_offer, &crypto)) { |
| if (current_description) { |
| // Check if this crypto already exist in the previous |
| // session description. Use it in that case. |
| const ContentInfo* info = GetFirstVideoContent(current_description); |
| if (info) { |
| const VideoContentDescription* desc = |
| static_cast<const VideoContentDescription*>(info->description); |
| const CryptoParamsVec& cryptos = desc->cryptos(); |
| for (CryptoParamsVec::const_iterator it = cryptos.begin(); |
| it != cryptos.end(); ++it) { |
| if (crypto.Matches(*it)) { |
| crypto = *it; |
| break; |
| } |
| } |
| } |
| } |
| video_accept->AddCrypto(crypto); |
| } |
| } |
| |
| if (video_accept->cryptos().empty() && |
| (video_offer->crypto_required() || secure() == SEC_REQUIRED)) { |
| return NULL; // Fails the session setup. |
| } |
| accept->AddContent(video_content->name, video_content->type, |
| video_accept.release()); |
| } else { |
| LOG(LS_INFO) << "Video is not supported in answer"; |
| } |
| return accept.release(); |
| } |
| |
| static bool IsMediaContent(const ContentInfo* content, MediaType media_type) { |
| if (content == NULL || content->type != NS_JINGLE_RTP) { |
| return false; |
| } |
| |
| const MediaContentDescription* media = |
| static_cast<const MediaContentDescription*>(content->description); |
| return media->type() == media_type; |
| } |
| |
| bool IsAudioContent(const ContentInfo* content) { |
| return IsMediaContent(content, MEDIA_TYPE_AUDIO); |
| } |
| |
| bool IsVideoContent(const ContentInfo* content) { |
| return IsMediaContent(content, MEDIA_TYPE_VIDEO); |
| } |
| |
| static const ContentInfo* GetFirstMediaContent(const SessionDescription* sdesc, |
| MediaType media_type) { |
| if (sdesc == NULL) |
| return NULL; |
| |
| const ContentInfos& contents = sdesc->contents(); |
| for (ContentInfos::const_iterator content = contents.begin(); |
| content != contents.end(); content++) { |
| if (IsMediaContent(&*content, media_type)) { |
| return &*content; |
| } |
| } |
| return NULL; |
| } |
| |
| const ContentInfo* GetFirstAudioContent(const SessionDescription* sdesc) { |
| return GetFirstMediaContent(sdesc, MEDIA_TYPE_AUDIO); |
| } |
| |
| const ContentInfo* GetFirstVideoContent(const SessionDescription* sdesc) { |
| return GetFirstMediaContent(sdesc, MEDIA_TYPE_VIDEO); |
| } |
| |
| } // namespace cricket |