Commit 1d798e05 authored by Mohamed Chibani's avatar Mohamed Chibani
Browse files

sip: handle empty SIP INVITE

Handle and test generation and processing of SIP INVITEs without
media offers (no SDP)
Also add basic call scenario for SIP accounts

Gitlab: #556

Change-Id: I4967a821936e7c33cf4d7e7bdd7ee96718db285e
parent 0a11520e
......@@ -227,6 +227,7 @@ public:
bool isUsable() const noexcept { return enabled_ and active_; }
void enableVideo(bool enable) { videoEnabled_ = enable; }
bool isVideoEnabled() const noexcept { return videoEnabled_; }
/**
......@@ -357,6 +358,10 @@ public:
bool isIceForMediaEnabled() const { return iceForMediaEnabled_; }
void enableIceForMedia(bool enable) { iceForMediaEnabled_ = enable; }
// Enable/disable generation of empty offers
bool isEmptyOffersEnabled() const { return emptyOffersEnabled_; }
void enableEmptyOffers(bool enable) { emptyOffersEnabled_ = enable; }
// Enable/disable multi-stream feature.
// Multi-stream feature changes the callflow of the re-invite process. All
// clients must support this feature before it can be enabled by default.
......@@ -586,6 +591,12 @@ protected:
bool iceForMediaEnabled_ {true};
bool iceCompIdRfc5245Compliant_ {false};
/**
* Allow generating empty offers. Mainly to validate proper
* handling of incoming empty offers.
*/
bool emptyOffersEnabled_ {false};
/**
* private account codec searching functions
*/
......
......@@ -878,6 +878,16 @@ IceTransport::Impl::addServerReflexiveCandidates(
std::vector<std::pair<IpAddr, IpAddr>>
IceTransport::Impl::setupGenericReflexiveCandidates()
{
if (not accountLocalAddr_) {
JAMI_WARN("[ice:%p]: Local address needed for reflexive candidates!", this);
return {};
}
if (not accountPublicAddr_) {
JAMI_WARN("[ice:%p]: Public address needed for reflexive candidates!", this);
return {};
}
std::vector<std::pair<IpAddr, IpAddr>> addrList;
auto isTcp = isTcpEnabled();
......@@ -887,25 +897,23 @@ IceTransport::Impl::setupGenericReflexiveCandidates()
// For TCP transport, the connection type is set to passive for UPNP
// candidates and set to active otherwise.
if (accountLocalAddr_ and accountPublicAddr_) {
addrList.reserve(compCount_);
for (unsigned id = 1; id <= compCount_; id++) {
// For TCP, the type is set to active, because most likely the incoming
// connection will be blocked by the NAT.
// For UDP use random port number.
uint16_t port = isTcp ? 9
: upnp::Controller::generateRandomPort(isTcp ? PortType::TCP
: PortType::UDP);
accountLocalAddr_.setPort(port);
accountPublicAddr_.setPort(port);
addrList.emplace_back(accountLocalAddr_, accountPublicAddr_);
JAMI_DBG("[ice:%p]: Add generic local reflexive candidates [%s : %s]",
this,
accountLocalAddr_.toString(true).c_str(),
accountPublicAddr_.toString(true).c_str());
}
addrList.reserve(compCount_);
for (unsigned id = 1; id <= compCount_; id++) {
// For TCP, the type is set to active, because most likely the incoming
// connection will be blocked by the NAT.
// For UDP use random port number.
uint16_t port = isTcp ? 9
: upnp::Controller::generateRandomPort(isTcp ? PortType::TCP
: PortType::UDP);
accountLocalAddr_.setPort(port);
accountPublicAddr_.setPort(port);
addrList.emplace_back(accountLocalAddr_, accountPublicAddr_);
JAMI_DBG("[ice:%p]: Add generic local reflexive candidates [%s : %s]",
this,
accountLocalAddr_.toString(true).c_str(),
accountPublicAddr_.toString(true).c_str());
}
return addrList;
......
......@@ -3133,6 +3133,17 @@ Manager::getCallDetails(const std::string& callID) const
}
}
std::vector<MediaAttribute>
Manager::getMediaAttributeList(const std::string& callID) const
{
if (auto call = getCallFromCallID(callID)) {
return call->getMediaAttributeList();
} else {
JAMI_ERR("Call is NULL");
return {};
}
}
std::vector<std::string>
Manager::getCallList() const
{
......
......@@ -506,11 +506,18 @@ public:
/**
* Retrieve details about a given call
* @param callID The account identifier
* @param callID The call identifier
* @return std::map< std::string, std::string > The call details
*/
std::map<std::string, std::string> getCallDetails(const std::string& callID) const;
/**
* Get the attribute list of current media
* @param callID The call identifier
* @return A vector of media attributes
*/
std::vector<MediaAttribute> getMediaAttributeList(const std::string& callID) const;
/**
* Get list of calls (internal subcalls are filter-out)
* @return std::vector<std::string> A list of call IDs (without subcalls)
......
......@@ -185,6 +185,8 @@ MediaAttribute::toString(bool full) const
descr << "[" << this << "] ";
descr << "type " << (type_ == MediaType::MEDIA_AUDIO ? "[AUDIO]" : "[VIDEO]");
descr << " ";
descr << "enabled " << (enabled_ ? "[YES]" : "[NO]");
descr << " ";
descr << "muted " << (muted_ ? "[YES]" : "[NO]");
descr << " ";
descr << "label [" << label_ << "]";
......
......@@ -45,7 +45,6 @@ public:
, sourceUri_(source)
, label_(label)
, onHold_(onHold)
{}
MediaAttribute(const DRing::MediaMap& mediaMap);
......@@ -90,16 +89,17 @@ public:
// NOTE: the hold and mute attributes are related but not
// tightly coupled. A hold/un-hold operation should always
// trigger a new re-invite to change the change the media
// attributes. For instance, on an active call, the hold
// action would change media direction from "sendrecv" to
// "sendonly".
// In contrast, the mute attribute describe the presence
// (or absence) of media signal in the stream. In other
// words, the mute action can be performed without requiring
// a media direction change. For instance, muting the audio
// can be done by disabling the audio input (capture) of the
// encoding session, resulting in sending an RTP stream without
// actual audio (silence).
// trigger a new re-invite to notify the change in media
// direction.For instance, on an active call, the hold action
// would change the media direction attribute from "sendrecv"
// to "sendonly". A new SDP with the new media direction will
// be generated and sent to the peer in the re-invite.
// In contrast, the mute attribute is a local attribute, and
// describes the presence (or absence) of the media signal in
// the stream. In other words, the mute action can be performed
// with or without a media direction change (no re-invite).
// For instance, muting the audio can be done by disabling the
// audio input (capture) of the encoding session, resulting in
// sending an RTP packets without actual audio (silence).
};
} // namespace jami
......@@ -249,12 +249,12 @@ Sdp::addMediaDescription(const MediaAttribute& mediaAttr)
switch (type) {
case MediaType::MEDIA_AUDIO:
med->desc.media = sip_utils::CONST_PJ_STR("audio");
med->desc.port = localAudioDataPort_;
med->desc.port = mediaAttr.enabled_ ? localAudioDataPort_ : 0;
med->desc.fmt_count = audio_codec_list_.size();
break;
case MediaType::MEDIA_VIDEO:
med->desc.media = sip_utils::CONST_PJ_STR("video");
med->desc.port = localVideoDataPort_;
med->desc.port = mediaAttr.enabled_ ? localVideoDataPort_ : 0;
med->desc.fmt_count = video_codec_list_.size();
break;
default:
......@@ -441,14 +441,10 @@ Sdp::setLocalMediaCapabilities(MediaType type,
const char*
Sdp::getSdpDirectionStr(SdpDirection direction)
{
if (direction == SdpDirection::LOCAL_OFFER)
return "LOCAL_OFFER";
if (direction == SdpDirection::LOCAL_ANSWER)
return "LOCAL_ANSWER";
if (direction == SdpDirection::REMOTE_OFFER)
return "REMOTE_OFFER";
if (direction == SdpDirection::REMOTE_ANSWER)
return "REMOTE_ANSWER";
if (direction == SdpDirection::OFFER)
return "OFFER";
if (direction == SdpDirection::ANSWER)
return "ANSWER";
return "NONE";
}
......@@ -536,9 +532,9 @@ Sdp::createOffer(const std::vector<MediaAttribute>& mediaList)
throw SdpException("Media list size exceeds SDP media maximum size");
}
JAMI_DBG("Creating SDP offer with %lu medias", mediaList.size());
JAMI_DBG("Creating SDP offer with %lu media", mediaList.size());
createLocalSession(SdpDirection::LOCAL_OFFER);
createLocalSession(SdpDirection::OFFER);
if (validateSession() != PJ_SUCCESS) {
JAMI_ERR("Failed to create initial offer");
......@@ -548,7 +544,9 @@ Sdp::createOffer(const std::vector<MediaAttribute>& mediaList)
localSession_->media_count = 0;
for (auto const& media : mediaList) {
localSession_->media[localSession_->media_count++] = addMediaDescription(media);
if (media.enabled_) {
localSession_->media[localSession_->media_count++] = addMediaDescription(media);
}
}
if (validateSession() != PJ_SUCCESS) {
......@@ -562,7 +560,7 @@ Sdp::createOffer(const std::vector<MediaAttribute>& mediaList)
return false;
}
Sdp::printSession(localSession_, "Local session (initial):", sdpDirection_);
printSession(localSession_, "Local session (initial):", sdpDirection_);
return true;
}
......@@ -580,18 +578,19 @@ Sdp::setReceivedOffer(const pjmedia_sdp_session* remote)
bool
Sdp::processIncomingOffer(const std::vector<MediaAttribute>& mediaList)
{
assert(remoteSession_);
if (not remoteSession_)
return false;
JAMI_DBG("Processing received offer for [%s] with %lu media",
sessionName_.c_str(),
mediaList.size());
printSession(remoteSession_, "Remote session:", SdpDirection::REMOTE_OFFER);
printSession(remoteSession_, "Remote session:", SdpDirection::OFFER);
if (not localSession_) {
createLocalSession(SdpDirection::LOCAL_ANSWER);
createLocalSession(SdpDirection::ANSWER);
if (validateSession() != PJ_SUCCESS) {
JAMI_ERR("Failed to create initial offer");
JAMI_ERR("Failed to create local session");
return false;
}
}
......@@ -599,7 +598,9 @@ Sdp::processIncomingOffer(const std::vector<MediaAttribute>& mediaList)
localSession_->media_count = 0;
for (auto const& media : mediaList) {
localSession_->media[localSession_->media_count++] = addMediaDescription(media);
if (media.enabled_) {
localSession_->media[localSession_->media_count++] = addMediaDescription(media);
}
}
printSession(localSession_, "Local session:\n", sdpDirection_);
......@@ -650,7 +651,7 @@ Sdp::startNegotiation()
setActiveLocalSdpSession(active_local);
if (active_local != nullptr) {
Sdp::printSession(active_local, "Local active session:", sdpDirection_);
printSession(active_local, "Local active session:", sdpDirection_);
}
if (pjmedia_sdp_neg_get_active_remote(negotiator_, &active_remote) != PJ_SUCCESS
......@@ -661,7 +662,7 @@ Sdp::startNegotiation()
setActiveRemoteSdpSession(active_remote);
Sdp::printSession(active_remote, "Remote active session:", sdpDirection_);
printSession(active_remote, "Remote active session:", sdpDirection_);
return true;
}
......@@ -1019,10 +1020,14 @@ Sdp::getMediaAttributeListFromSdp(const pjmedia_sdp_session* sdpSession)
mediaAttr.type_ = MediaType::MEDIA_VIDEO;
else {
JAMI_WARN("Media#%u only 'audio' and 'video' types are supported!", idx);
// Keep the media in the list but dont bother parsing the attributes
// Disable the media. No need to parse the attributes.
mediaAttr.enabled_ = false;
continue;
}
// Set enabled flag
mediaAttr.enabled_ = media->desc.port > 0;
// Get mute state.
auto direction = getMediaDirection(media);
mediaAttr.muted_ = direction != MediaDirection::SENDRECV
......
......@@ -61,7 +61,7 @@ public:
{}
};
enum class SdpDirection { LOCAL_OFFER, LOCAL_ANSWER, REMOTE_OFFER, REMOTE_ANSWER, NONE };
enum class SdpDirection { OFFER, ANSWER, NONE };
class Sdp
{
......
......@@ -323,9 +323,13 @@ SIPAccount::newOutgoingCall(std::string_view toUrl, const std::vector<MediaAttri
}
auto toUri = getToUri(to);
if (call->isIceEnabled()) {
// Do not init ICE yet if the the media list is empty. This may occur
// if we are sending an invite with no SDP offer.
if (call->isIceEnabled() and not mediaAttrList.empty()) {
call->initIceMediaTransport(true);
}
call->setPeerNumber(toUri);
call->setPeerUri(toUri);
......@@ -468,7 +472,8 @@ SIPAccount::SIPStartCall(std::shared_ptr<SIPCall>& call)
from.c_str(),
toUri.c_str());
auto local_sdp = call->getSDP().getLocalSdpSession();
auto local_sdp = isEmptyOffersEnabled() ? nullptr : call->getSDP().getLocalSdpSession();
pjsip_dialog* dialog {nullptr};
pjsip_inv_session* inv {nullptr};
if (!CreateClientDialogAndInvite(&pjFrom, &pjContact, &pjTo, nullptr, local_sdp, &dialog, &inv))
......
......@@ -482,6 +482,7 @@ public:
IpAddr createBindingAddress();
void setActiveCodecs(const std::vector<unsigned>& list) override;
bool isSrtpEnabled() const override { return srtpKeyExchange_ != KeyExchangeProtocol::NONE; }
private:
void doRegister1_();
......@@ -514,8 +515,6 @@ private:
bool hostnameMatch(std::string_view hostname) const;
bool proxyMatch(std::string_view hostname) const;
bool isSrtpEnabled() const override { return srtpKeyExchange_ != KeyExchangeProtocol::NONE; }
/**
* Callback called by the transport layer when the registration
* transport state changes.
......
......@@ -745,13 +745,13 @@ SIPAccountBase::createDefaultMediaList(bool addVideo, bool onHold)
bool secure = isSrtpEnabled();
// Add audio and DTMF events
mediaList.emplace_back(
MediaAttribute(MediaType::MEDIA_AUDIO, false, secure, false, "", "main audio", onHold));
MediaAttribute(MediaType::MEDIA_AUDIO, false, secure, true, "", "audio_0", onHold));
#ifdef ENABLE_VIDEO
// Add video if allowed.
if (isVideoEnabled() and addVideo) {
mediaList.emplace_back(
MediaAttribute(MediaType::MEDIA_VIDEO, false, secure, false, "", "main video", onHold));
MediaAttribute(MediaType::MEDIA_VIDEO, false, secure, true, "", "video_0", onHold));
}
#endif
return mediaList;
......
......@@ -25,10 +25,10 @@
#include "call_factory.h"
#include "sipcall.h"
#include "sipaccount.h" // for SIPAccount::ACCOUNT_TYPE
#include "sipaccount.h"
#include "sipaccountbase.h"
#include "sipvoiplink.h"
#include "logger.h" // for _debug
#include "logger.h"
#include "sdp.h"
#include "manager.h"
#include "string_utils.h"
......@@ -144,14 +144,13 @@ SIPCall::SIPCall(const std::shared_ptr<SIPAccountBase>& account,
mediaList = mediaAttrList;
} else if (type_ == Call::CallType::INCOMING) {
// Handle incoming call without media offer.
JAMI_WARN("[call:%s] No media offered in the incoming invite. Will provide an offer in the "
"answer",
JAMI_WARN("[call:%s] No media offered in the incoming invite. An offer will be provided in "
"the answer",
getCallId().c_str());
mediaList = getSIPAccount()->createDefaultMediaList(getSIPAccount()->isVideoEnabled(),
getState() == CallState::HOLD);
} else {
JAMI_ERR("[call:%s] Media list can not be empty for outgoing calls", getCallId().c_str());
return;
JAMI_WARN("[call:%s] Creating an outgoing call with empty offer", getCallId().c_str());
}
JAMI_DBG("[call:%s] Create a new [%s] SIP call with %lu media",
......@@ -391,6 +390,9 @@ SIPCall::generateMediaPorts()
return;
}
// TODO. Setting specfic range for RTP ports is obsolete, in
// particular in the context of ICE.
// Reference: http://www.cs.columbia.edu/~hgs/rtp/faq.html#ports
// We only want to set ports to new values if they haven't been set
const unsigned callLocalAudioPort = account->generateAudioPort();
......@@ -421,7 +423,7 @@ void
SIPCall::setTransport(const std::shared_ptr<SipTransport>& t)
{
if (isSecure() and t and not t->isSecure()) {
JAMI_ERR("Can't set unsecure transport to secure call.");
JAMI_ERR("Can't set un-secure transport to secure call.");
return;
}
......@@ -660,12 +662,6 @@ SIPCall::setInviteSession(pjsip_inv_session* inviteSession)
inviteSession_.reset(inviteSession);
}
void
SIPCall::updateSDPFromSTUN()
{
JAMI_WARN("[call:%s] SIPCall::updateSDPFromSTUN() not implemented", getCallId().c_str());
}
void
SIPCall::terminateSipSession(int status)
{
......@@ -723,11 +719,8 @@ SIPCall::answer()
if (!inviteSession_->neg) {
JAMI_WARN("[call:%s] Negotiator is NULL, we've received an INVITE without an SDP",
getCallId().c_str());
pjmedia_sdp_session* dummy = 0;
Manager::instance().sipVoIPLink().createSDPOffer(inviteSession_.get(), &dummy);
if (account->isStunEnabled())
updateSDPFromSTUN();
Manager::instance().sipVoIPLink().createSDPOffer(inviteSession_.get());
}
pj_str_t contact(account->getContactHeader(transport_ ? transport_->get() : nullptr));
......@@ -775,13 +768,35 @@ SIPCall::answer(const std::vector<MediaAttribute>& mediaAttrList)
JAMI_ERR("No account detected");
return;
}
if (mediaAttrList.empty()) {
JAMI_DBG("[call:%s] Media list must not be empty!", getCallId().c_str());
return;
}
if (not inviteSession_)
JAMI_DBG("[call:%s] No invite session for this call", getCallId().c_str());
JAMI_DBG("[call:%s] Answering incoming call with %lu media:",
getCallId().c_str(),
mediaAttrList.size());
if (mediaAttrList.size() != rtpStreams_.size()) {
JAMI_ERR("Media list size %lu in answer does not match. Expected %lu",
JAMI_ERR("[call:%s] Media list size %lu in answer does not match. Expected %lu",
getCallId().c_str(),
mediaAttrList.size(),
rtpStreams_.size());
return;
}
for (size_t idx = 0; idx < mediaAttrList.size(); idx++) {
auto const& mediaAttr = mediaAttrList.at(idx);
JAMI_DBG("[call:%s] Media @%lu: %s",
getCallId().c_str(),
idx,
mediaAttr.toString(true).c_str());
}
// Apply the media attributes provided by the user.
for (size_t idx = 0; idx < mediaAttrList.size(); idx++) {
rtpStreams_[idx].mediaAttribute_ = std::make_shared<MediaAttribute>(mediaAttrList[idx]);
......@@ -792,13 +807,53 @@ SIPCall::answer(const std::vector<MediaAttribute>& mediaAttrList)
+ "] answer: no invite session for this call");
if (not inviteSession_->neg) {
JAMI_WARN("[call:%s] Negotiator is NULL, we've received an INVITE without an SDP",
// We are answering to an INVITE that did not include a media offer (SDP).
// The SIP specification (RFCs 3261/6337) requires that if a UA wishes to
// proceed with the call, it must provide a media offer (SDP) if the initial
// INVITE did not offer one. In this case, the SDP offer will be included in
// the SIP OK (200) answer. The peer UA will then include its SDP answer in
// the SIP ACK message.
// TODO. This code should be unified with the code used by accounts to create
// SDP offers.
JAMI_WARN("[call:%s] No negotiator session, peer sent an empty INVITE (without SDP)",
getCallId().c_str());
pjmedia_sdp_session* dummy = 0;
Manager::instance().sipVoIPLink().createSDPOffer(inviteSession_.get(), &dummy);
if (account->isStunEnabled())
updateSDPFromSTUN();
Manager::instance().sipVoIPLink().createSDPOffer(inviteSession_.get());
generateMediaPorts();
// Setup and create ICE offer
if (account->isIceForMediaEnabled()) {
sdp_->clearIce();
auto opts = account->getIceOptions();
auto publicAddr = account->getPublishedIpAddress();
if (not publicAddr) {
// If the published address is unknown, just use the local address. Not
// optimal, but may work just fine if both endpoints are in the same
// local network.
publicAddr = ip_utils::getInterfaceAddr(account->getLocalInterface(), pj_AF_INET());
}
if (publicAddr) {
opts.accountPublicAddr = publicAddr;
if (auto interfaceAddr = ip_utils::getInterfaceAddr(account->getLocalInterface(),
publicAddr.getFamily())) {
opts.accountLocalAddr = interfaceAddr;
initIceMediaTransport(true, std::move(opts));
addLocalIceAttributes();
} else {
JAMI_WARN("[call:%s] Cant init ICE transport, missing local address",
getCallId().c_str());
}
} else {
JAMI_WARN("[call:%s] Cant init ICE transport, missing public address",
getCallId().c_str());
}
}
}
pj_str_t contact(account->getContactHeader(transport_ ? transport_->get() : nullptr));
......@@ -807,14 +862,9 @@ SIPCall::answer(const std::vector<MediaAttribute>& mediaAttrList)
if (!inviteSession_->last_answer)
throw std::runtime_error("Should only be called for initial answer");
// TODO. We need a test for this scenario.
// How to Check if this use-case is not broken by the changes.
// Answer with an SDP offer if the initial invite was empty.
// SIP protocol allows a UA to send a call invite without SDP.
// In this case, if the callee wants to accept the call, it must
// provide an SDP offer in the answer. The caller will then send
// its SDP answer in the SIP OK (200) message.
// Answer with an SDP offer if the initial invite was empty,
// otherwise, set the local_sdp session to null to use the
// current SDP session.
pjsip_tx_data* tdata;
if (pjsip_inv_answer(inviteSession_.get(),
PJSIP_SC_OK,
......@@ -896,7 +946,7 @@ SIPCall::answerMediaChangeRequest(const std::vector<MediaAttribute>& mediaAttrLi
}
if (isIceEnabled())
setupLocalIce();
setupIceResponse();
if (not sdp_->startNegotiation()) {
JAMI_ERR("[call:%s] Could not start media negotiation for a re-invite request",
......@@ -1222,11 +1272,7 @@ SIPCall::unhold()
bool success = false;
try {
if (account->isStunEnabled())
success = internalOffHold([&] { updateSDPFromSTUN(); });
else
success = internalOffHold([] {});
success = internalOffHold([] {});
} catch (const SdpException& e) {
JAMI_ERR("[call:%s] %s", getCallId().c_str(), e.what());
throw VoipLinkException("SDP issue in offhold");
......@@ -1577,6 +1623,14 @@ SIPCall::addLocalIceAttributes()
if (account->isIceCompIdRfc5245Compliant()) {
unsigned streamIdx = 0;
for (auto const& stream : rtpStreams_) {
if (not stream.mediaAttribute_->enabled_) {
// Dont add ICE candidates if the media is disabled
JAMI_DBG("[call:%s] media [%s] @ %u is disabled, dont add local candidates",
getCallId().c_str(),
stream.mediaAttribute_->toString().c_str(),
streamIdx);
continue;
}
JAMI_DBG("[call:%s] add ICE local candidates for media [%s] @ %u",
getCallId().c_str(),
stream.mediaAttribute_->toString().c_str(),
......@@ -1594,6 +1648,10 @@ SIPCall::addLocalIceAttributes()
unsigned idx = 0;
unsigned compId = 1;
for (auto const& stream : rtpStreams_) {
if (not stream.mediaAttribute_->enabled_) {
// Skipping local ICE candidates if the media is disabled
continue;
}
JAMI_DBG("[call:%s] add ICE local candidates for media [%s] @ %u",
getCallId().c_str(),
stream.mediaAttribute_->toString().c_str(),
......@@ -1740,6 +1798,20 @@ SIPCall::isVideoMuted() const
#endif
}
bool
SIPCall::isMediaTypeEnabled(MediaType type) const
{
#ifdef ENABLE_VIDEO
std::function<bool(const RtpStream& stream)> enabledCheck = [&type](auto const& stream) {
return (stream.mediaAttribute_->type_ == type and stream.mediaAttribute_->enabled_);
};