#include "B2BSession.hxx" #include "Server.hxx" #include "IChatIPPortData.hxx" #include "AppSubsystem.hxx" #include #include #include #include #include #include #include #include #include using namespace gateway; using namespace resip; using namespace std; #define RESIPROCATE_SUBSYSTEM AppSubsystem::GATEWAY #ifdef BRIEF_MSG_LOGGING #define LOG_MSG msg.brief() #define LOG_MSG_WITH_SDP msg.brief() << ", sdp=" << sdp #define LOG_MSGP msg->brief() #else #define LOG_MSG endl << msg #define LOG_MSG_WITH_SDP endl << msg #define LOG_MSGP endl << *msg #endif #define B2BLOG_PREFIX << "B2BSession[" << mHandle << "] " namespace gateway { B2BSession::B2BSession(Server& server, bool hasDialogSet) : AppDialogSet(server.getDialogUsageManager()), mServer(server), mHasDialogSet(hasDialogSet), mDum(server.getDialogUsageManager()), mPeer(0), mUACConnectedDialogId(Data::Empty, Data::Empty, Data::Empty), mWaitingOfferFromPeer(false), mWaitingAnswerFromPeer(false), mWaitingNitAnswerFromPeer(false), mAnchorMedia(false), mMediaRelayPort(0), mIChatEndpoint(false), mIChatWaitingToAccept(false), mIChatWaitingToProceed(false), mIChatWaitingToContinue(false), mIChatSdp(0) { mHandle = mServer.registerB2BSession(this); } B2BSession::~B2BSession() { endPeer(); if(mMediaRelayPort != 0) { mServer.mMediaRelay->destroyRelay(mMediaRelayPort); } if(mIChatSdp) delete mIChatSdp; mServer.unregisterB2BSession(mHandle); } void B2BSession::end() { if(mIChatWaitingToAccept) { rejectIChatCall(); } if (mHasDialogSet) { AppDialogSet::end(); } else { delete this; } } void B2BSession::setPeer(B2BSession* peer) { mPeer = peer; } B2BSession* B2BSession::getPeer() { return mPeer; } void B2BSession::stealPeer(B2BSession* victimSession) { // Assume Peer mapping of victim - and copy some settings setPeer(victimSession->getPeer()); if(mPeer) { mPeer->setPeer(this); } mMediaRelayPort = victimSession->mMediaRelayPort; victimSession->mMediaRelayPort = 0; // clear out so that session will not dealloc media port mAnchorMedia = victimSession->mAnchorMedia; // Clear peer mapping in victim session victimSession->setPeer(0); } class IChatCallTimeout : public resip::DumCommand { public: IChatCallTimeout(const Server& server, const B2BSessionHandle& handle) : mServer(server), mB2BSessionHandle(handle) {} IChatCallTimeout(const IChatCallTimeout& rhs) : mServer(rhs.mServer), mB2BSessionHandle(rhs.mB2BSessionHandle) {} ~IChatCallTimeout() {} void executeCommand() { B2BSession* session = mServer.getB2BSession(mB2BSessionHandle); if(session) { session->timeoutIChatCall(); } } resip::Message* clone() const { return new IChatCallTimeout(*this); } EncodeStream& encode(EncodeStream& strm) const { strm << "IChatCallTimeout: handle=" << mB2BSessionHandle; return strm; } EncodeStream& encodeBrief(EncodeStream& strm) const { return encode(strm); } private: const Server& mServer; B2BSessionHandle mB2BSessionHandle; }; void B2BSession::initiateIChatCallRequest(const std::string& to, const std::string& from) { // Notify jabber connector we are proceeding and what our session handle is mIChatCallToJID = to; mIChatCallFromJID = from; mIChatEndpoint = true; mIChatWaitingToAccept = true; proceedingIChatCall(); bool result=false; try { Uri toUri(Data("xmpp:") + to.c_str()); try { NameAddr fromNameAddr(Data("sip:") + from.c_str()); fromNameAddr.displayName() = fromNameAddr.uri().user(); result = createNewPeer(toUri, fromNameAddr, 0); } catch(resip::BaseException& e) { ErrLog(B2BLOG_PREFIX << "Error creating NameAddr from Jabber from header=" << from << ", error=" << e); } } catch(resip::BaseException& e) { ErrLog(B2BLOG_PREFIX << "Error extracting Uri from Jabber To header=" << to << ", error=" << e); } if(!result) { rejectIChatCall(); end(); } } void B2BSession::startIChatCall(const Uri& destinationUri, const NameAddr& from, const SdpContents *sdp) { resip_assert(destinationUri.scheme() == "xmpp"); mIChatEndpoint = true; mIChatDestination = destinationUri; mIChatFrom = from; if(sdp) { mIChatSdp = new SdpContents(*sdp); } mIChatCallToJID = Data::from(destinationUri).substr(5).c_str(); // Get all of address, except xmpp: part Uri fromUri; // Note: for some reason iChat will send a 603 Decline if the username is more than 10 characters - so we truncate fromUri.user() = from.uri().user().substr(0, from.uri().user().size() > 10 ? 10 : from.uri().user().size()); fromUri.host() = mServer.mJabberComponentName; mIChatCallFromJID = fromUri.getAor().c_str(); initiateIChatCall(); // Start a timer IChatCallTimeout t(mServer, mHandle); mServer.post(t, mServer.mIChatProceedingTimeout); mIChatWaitingToProceed = true; } void B2BSession::startSIPCall(const Uri& destinationUri, const NameAddr& from, const SdpContents *sdp) { // Create a UserProfile for new call SharedPtr userProfile(new UserProfile(mServer.getMasterProfile())); // Set the From address userProfile->setDefaultFrom(from); userProfile->getDefaultFrom().remove(p_tag); // Remove tag (if it exists) // Create the invite message SharedPtr invitemsg = mDum.makeInviteSession( NameAddr(destinationUri), userProfile, sdp, this); // Send the invite message mDum.send(invitemsg); } bool B2BSession::checkIChatCallMatch(const resip::SipMessage& msg) { if(!mHasDialogSet && !mIChatWaitingToAccept) { // Check if URI matches // If we are an iChat endpoint then real To uri is actually in the display name of the To header InfoLog(B2BLOG_PREFIX << "B2BSession::checkIChatCallMatch: checking to: " << msg.header(h_To).displayName() << " = " << mIChatCallToJID); InfoLog(B2BLOG_PREFIX << "B2BSession::checkIChatCallMatch: checking from: " << msg.header(h_From).displayName() << " = " << mIChatCallFromJID); if(msg.header(h_To).displayName() == Data(mIChatCallToJID.c_str()) && msg.header(h_From).displayName() == Data(mIChatCallFromJID.c_str())) { mHasDialogSet = true; // If we match, we are going to get a dialog set associated with us return true; } } return false; } bool B2BSession::createNewPeer(const Uri& destinationUri, const NameAddr& from, const SdpContents *sdp) { const SdpContents *pSdp = sdp; resip_assert(!mPeer); mPeer = new B2BSession(mServer); Data destinationData; if(!mServer.translateAddress(Data::from(destinationUri), destinationData, true /* failIfNoRule */)) { ErrLog(B2BLOG_PREFIX << "B2BSession::createNewPeer: No translation rule for " << destinationUri); delete mPeer; mPeer = 0; return false; } Uri destination; try { destination = Uri(destinationData); } catch(resip::BaseException& e) { ErrLog(B2BLOG_PREFIX << "B2BSession::createNewPeer: Translation of " << destinationUri << " resulted in invalid uri format: " << e); delete mPeer; mPeer = 0; return false; } // Check if media needs to be anchored SdpContents localOffer; if((mServer.mAlwaysRelayIChatMedia && mIChatEndpoint) || destination.scheme() == "xmpp" || !sdp) { mAnchorMedia = true; mPeer->mAnchorMedia = true; // Check if sdp used to create new peer is locally generated - if so, don't allocate a new media relay, use the existing one if(sdp && sdp->session().origin().user() == SDP_ICHATGW_ORIGIN_USER) { if(sdp->session().media().size() >= 1 && sdp->session().media().front().port() != 0) { mMediaRelayPort = sdp->session().media().front().port(); InfoLog(B2BLOG_PREFIX << "B2BSession::createNewPeer: Detected looped back session - sharing media relay (port=" << mMediaRelayPort << ") with originator."); } } // Replace passed in remote SDP with local SDP if(!buildLocalOffer(localOffer)) { ErrLog(B2BLOG_PREFIX << "B2BSession::createNewPeer: Unable to build local offer."); delete mPeer; mPeer = 0; return false; } pSdp = &localOffer; mPeer->mMediaRelayPort = mMediaRelayPort; } // Create B2BCall InfoLog(B2BLOG_PREFIX << "B2BSession::createNewPeer: Routing new call to " << destination); // Map other leg to this one mPeer->setPeer(this); if(destination.scheme() == "xmpp") { mPeer->startIChatCall(destination, from, pSdp); } else { mPeer->startSIPCall(destination, from, pSdp); } return true; } void B2BSession::notifyIChatCallCancelled() { InfoLog(B2BLOG_PREFIX << "notifyIChatCallCancelled"); mIChatWaitingToAccept = false; end(); } void B2BSession::notifyIChatCallProceeding(const std::string& to) { InfoLog(B2BLOG_PREFIX << "notifyIChatCallProceeding: to=" << to); mIChatWaitingToProceed = false; mIChatWaitingToContinue = true; if(mPeer) { if(mPeer->mInviteSessionHandle.isValid()) { // Sending a provisional back to the other end should cause ringback to be played ServerInviteSession* sis = dynamic_cast(mPeer->mInviteSessionHandle.get()); if(sis && !sis->isAccepted()) { sis->provisional(180); } } } } void B2BSession::notifyIChatCallFailed(unsigned int statusCode) { InfoLog(B2BLOG_PREFIX << "notifyIChatCallFailed: Failed IChat call, statusCode=" << statusCode << " - tearing down other leg."); endPeer(); delete this; } void B2BSession::continueIChatCall(const std::string& remoteIPPortListBlob) { resip_assert(!mIChatWaitingToProceed); resip_assert(mIChatWaitingToContinue); mIChatWaitingToContinue = false; // Create a UserProfile for new call SharedPtr userProfile(new UserProfile(mServer.getMasterProfile())); // Doesn't really matter what's in the SIP message from header that goes to iChat - it's never displayed to the client userProfile->setDefaultFrom(mServer.getMasterProfile()->getDefaultFrom()); Tuple destinationIPPort; IChatIPPortData remoteIPPortList(remoteIPPortListBlob); // Take first ipv4 or ipv6 entry - for ipv4 it appears to be a STUN mapped address bool skipFirst = mServer.mSkipFirstIChatAddress; // Note (for testing): skipping the first IPV4 NAT mapped address (on systems with one interface) causes the local address to be used bool findTilda = false; // Note (for testing): Not too sure what the ~ addresses are - they could be media relay servers - you cannot just send RTP to them, so there must be some additional protocol to allocate a relay bool v4found = false; IChatIPPortData::IPPortDataList::const_iterator it = remoteIPPortList.getIPPortDataList().begin(); for(; it != remoteIPPortList.getIPPortDataList().end(); it++) { if(!v4found && it->second.ipVersion() == V4) { if(skipFirst) { skipFirst = false; } else { if(!findTilda || it->first.find("~") != Data::npos) { // return first matching address destinationIPPort = it->second; if(!mServer.mPreferIPv6) break; v4found = true; } } } if(mServer.mPreferIPv6 && it->second.ipVersion() == V6) { if(skipFirst) { skipFirst = false; } else { if(!findTilda || it->first.find("~") != Data::npos) { // return first matching address destinationIPPort = it->second; break; } } } } if(destinationIPPort.getPort() == 0) { // We didn't find an applicable address InfoLog(B2BLOG_PREFIX << "B2BSession::continueIChatCall: no appropriate address found in remoteIPPortList."); notifyIChatCallFailed(603); // Decline return; } Data uriData; if(destinationIPPort.ipVersion() == V6) { uriData = "sip:user@[" + destinationIPPort.presentationFormat() + "]:" + Data(destinationIPPort.getPort()); } else { uriData = "sip:user@" + destinationIPPort.presentationFormat() + ":" + Data(destinationIPPort.getPort()); } try { NameAddr destination(uriData); userProfile->setOutboundProxy(destination.uri()); userProfile->setForceOutboundProxyOnAllRequestsEnabled(true); InfoLog(B2BLOG_PREFIX << "B2BSession::continueIChatCall: to=" << destination << " tuple=" << destinationIPPort); // Create the invite message SharedPtr invitemsg = mDum.makeInviteSession( destination, userProfile, mIChatSdp, this); // Send the invite message mDum.send(invitemsg); // Prime the RTP engine of iChat if using IPv6 // Note: under IPv6 iChat will not send an initial RTP packet until it receives one if(destinationIPPort.ipVersion() == V6 && mMediaRelayPort > 0) { mServer.mMediaRelay->primeNextEndpoint(mMediaRelayPort, destinationIPPort); } } catch(resip::BaseException& e) { ErrLog(B2BLOG_PREFIX << "continueIChatCall: Invalid NameAddr format=" << uriData << ": " << e); notifyIChatCallFailed(500); return; } } void B2BSession::timeoutIChatCall() { if(mIChatWaitingToProceed) { InfoLog(B2BLOG_PREFIX << "timeoutIChatCall: could not find an appropriate iChat endpoint."); cancelIChatCall(); notifyIChatCallFailed(408); return; } } void B2BSession::initiateIChatCall() { IPCMsg msg; msg.addArg("initiateIChatCall"); msg.addArg(mIChatCallToJID.c_str()); msg.addArg(mIChatCallFromJID.c_str()); msg.addArg(mHandle); mServer.mIPCThread->sendIPCMsg(msg); } void B2BSession::cancelIChatCall() { IPCMsg msg; msg.addArg("cancelIChatCall"); msg.addArg(mIChatCallToJID.c_str()); msg.addArg(mIChatCallFromJID.c_str()); mServer.mIPCThread->sendIPCMsg(msg); } void B2BSession::proceedingIChatCall() { IPCMsg msg; msg.addArg("proceedingIChatCall"); msg.addArg(mIChatCallToJID.c_str()); msg.addArg(mIChatCallFromJID.c_str()); msg.addArg(mHandle); mServer.mIPCThread->sendIPCMsg(msg); } void B2BSession::acceptIChatCall() { IPCMsg msg; msg.addArg("acceptIChatCall"); msg.addArg(mIChatCallToJID.c_str()); msg.addArg(mIChatCallFromJID.c_str()); mServer.mIPCThread->sendIPCMsg(msg); mIChatWaitingToAccept = false; } void B2BSession::rejectIChatCall() { IPCMsg msg; msg.addArg("rejectIChatCall"); msg.addArg(mIChatCallToJID.c_str()); msg.addArg(mIChatCallFromJID.c_str()); mServer.mIPCThread->sendIPCMsg(msg); mIChatWaitingToAccept = false; } bool B2BSession::buildLocalOffer(SdpContents& offer) { // Build s=, o=, t=, and c= lines UInt64 currentTime = Timer::getTimeMicroSec(); unsigned int port=0; if(mAnchorMedia) { if(mMediaRelayPort == 0) { // If we don't have an allocated port yet, then create one if(!mServer.mMediaRelay->createRelay(mMediaRelayPort)) { ErrLog(B2BLOG_PREFIX << "Failed to allocate relay port!"); return false; } } port = mMediaRelayPort; } // Note: The outbound decorator will take care of filling in the correct IP address before the message is sent // to the wire. SdpContents::Session::Origin origin(SDP_ICHATGW_ORIGIN_USER, currentTime /* sessionId */, currentTime /* version */, SdpContents::IP4, "0.0.0.0"); // o= SdpContents::Session session(0, origin, "-" /* s= */); session.connection() = SdpContents::Session::Connection(SdpContents::IP4, "0.0.0.0"); // c= session.addTime(SdpContents::Session::Time(0, 0)); // Build Codecs and media offering SdpContents::Session::Medium medium("audio", port, 1, "RTP/AVP"); SdpContents::Session::Codec g711ucodec("PCMU", 8000); g711ucodec.payloadType() = 0; /* RFC3551 */ ; medium.addCodec(g711ucodec); if(mAnchorMedia) { medium.addAttribute("sendrecv"); } else { medium.addAttribute("inactive"); } session.addMedium(medium); offer.session() = session; return true; } bool B2BSession::buildLocalAnswer(SdpContents& answer) { bool valid = false; if(mInviteSessionHandle.isValid() && mInviteSessionHandle->hasProposedRemoteSdp()) { const SdpContents& offer = mInviteSessionHandle->getProposedRemoteSdp(); try { // use our local offer as a starting place - this also allocates a relay port (if needed) if(!buildLocalOffer(answer)) { return false; } // Set sessionid and version for this answer UInt64 currentTime = Timer::getTimeMicroSec(); answer.session().origin().getSessionId() = currentTime; answer.session().origin().getVersion() = currentTime; // Copy t= field from sdp (RFC3264) resip_assert(answer.session().getTimes().size() > 0); if(offer.session().getTimes().size() >= 1) { answer.session().getTimes().clear(); answer.session().addTime(offer.session().getTimes().front()); } // Clear out m= lines in answer then populate below answer.session().media().clear(); // Loop through each offered m= line and provide a response std::list::const_iterator itMediaLine = offer.session().media().begin(); for(; itMediaLine != offer.session().media().end(); itMediaLine++) { const SdpContents::Session::Medium& offerMediaLine = *itMediaLine; // We only process one media stream - so if we already have a valid - just reject the rest if(valid) { SdpContents::Session::Medium rejmedium(offerMediaLine.name(), 0, 1, // Reject medium by specifying port 0 (RFC3264) offerMediaLine.protocol()); answer.session().addMedium(rejmedium); continue; } // Answer Media Line // If this is a valid audio medium then process it if(isEqualNoCase(offerMediaLine.name(), "audio") && isEqualNoCase(offerMediaLine.protocol(), "RTP/AVP") && offerMediaLine.port() != 0) { SdpContents::Session::Medium medium("audio", mMediaRelayPort, 1, "RTP/AVP"); // Iterate through codecs and look for supported codecs (only G711 for now) - tag found ones by storing their payload id std::list::const_iterator itCodec = offerMediaLine.codecs().begin(); for(; itCodec != offerMediaLine.codecs().end(); itCodec++) { const Codec& codec = *itCodec; if(isEqualNoCase(codec.getName(), "pcmu") && codec.getRate() == 8000) { SdpContents::Session::Codec answerCodec(codec); answerCodec.payloadType() = codec.payloadType(); // honour offered payload id - just to be nice :) medium.addCodec(answerCodec); // Consider offer valid if we see any matching codec valid = true; break; } } if(valid) { //medium.addAttribute("ptime", 20); // Check requested direction if(offerMediaLine.exists("inactive")) { medium.addAttribute("inactive"); } else if(offerMediaLine.exists("sendonly")) { medium.addAttribute("recvonly"); } else if(offerMediaLine.exists("recvonly")) { medium.addAttribute("sendonly"); } else { // Note: sendrecv is the default in SDP medium.addAttribute("sendrecv"); } answer.session().addMedium(medium); } else { SdpContents::Session::Medium rejmedium(offerMediaLine.name(), 0, 1, // Reject medium by specifying port 0 (RFC3264) offerMediaLine.protocol()); answer.session().addMedium(rejmedium); } } else { SdpContents::Session::Medium rejmedium(offerMediaLine.name(), 0, 1, // Reject medium by specifying port 0 (RFC3264) offerMediaLine.protocol()); answer.session().addMedium(rejmedium); } } // end loop through m= offers if(!valid) { WarningLog( B2BLOG_PREFIX << "B2BSession::buildLocalAnswer - no matching codecs found in offer"); } } catch(BaseException &e) { WarningLog( B2BLOG_PREFIX << "B2BSession::buildLocalAnswer - exception parsing SDP offer: " << e.getMessage()); valid = false; } catch(...) { WarningLog(B2BLOG_PREFIX << "B2BSession::buildLocalAnswer - unknown exception parsing SDP offer"); valid = false; } //InfoLog( << "B2BSession::buildLocalAnswer - SDPOffer: " << offer); //InfoLog( << "B2BSession::buildLocalAnswer - SDPAnswer: " << answer); } else { ErrLog(B2BLOG_PREFIX << "B2BSession::buildLocalAnswer - unable to build local answer, ishValid=" << mInviteSessionHandle.isValid() << ", hasProposedRemoteSdp=" << mInviteSessionHandle->hasProposedRemoteSdp()); } return valid; } bool B2BSession::provideLocalAnswer() { SdpContents answer; resip_assert(mInviteSessionHandle.isValid()); bool answerOk = buildLocalAnswer(answer); if(answerOk) { mInviteSessionHandle->provideAnswer(answer); } else { mInviteSessionHandle->reject(488); } return answerOk; } void B2BSession::endPeer() { if(mPeer) { mPeer->setPeer(0); if(mPeer->mIChatWaitingToContinue || mPeer->mIChatWaitingToProceed) { mPeer->cancelIChatCall(); delete mPeer; } else { mPeer->end(); // send cancel or bye appropriately } mPeer = 0; } } bool B2BSession::isUACConnected() { return !mUACConnectedDialogId.getCallId().empty(); } bool B2BSession::isStaleFork(const DialogId& dialogId) { return (!mUACConnectedDialogId.getCallId().empty() && dialogId != mUACConnectedDialogId); } void B2BSession::fixupSdp(const SdpContents& origSdp, SdpContents& fixedSdp) { fixedSdp = origSdp; if(fixedSdp.session().media().size() >= 1) { fixedSdp.session().media().front().clearCodecs(); const std::list& origcodecs = origSdp.session().media().front().codecs(); std::list::const_iterator it = origcodecs.begin(); for(; it != origcodecs.end(); it++) { if(mServer.mCodecIdFilterList.find(it->payloadType()) == mServer.mCodecIdFilterList.end()) { fixedSdp.session().media().front().addCodec(*it); } } } } SharedPtr B2BSession::selectUASUserProfile(const SipMessage& msg) { // If this is an iChat endpoint to force all requests in the session to go to the IP address and port // that the initiatial invite was from if(msg.exists(h_UserAgent) && msg.header(h_UserAgent).value().prefix("Viceroy")) { // Create a UserProfile for new call SharedPtr userProfile(new UserProfile(mServer.getMasterProfile())); // Doesn't really matter what's in the SIP message from header that goes to iChat - it's never displayed to the client userProfile->setDefaultFrom(mServer.getMasterProfile()->getDefaultFrom()); // Force endpoint routing Data destinationData; if(msg.getSource().ipVersion() == V6) { destinationData = "sip:[" + msg.getSource().presentationFormat() + "]:" + Data(msg.getSource().getPort()); } else { destinationData = "sip:" + msg.getSource().presentationFormat() + ":" + Data(msg.getSource().getPort()); } Uri destination(destinationData); userProfile->setOutboundProxy(destination); userProfile->setForceOutboundProxyOnAllRequestsEnabled(true); return userProfile; } else { return mServer.getMasterProfile(); } } //////////////////////////////////////////////////////////////////////////////// // InviteSessionHandler /////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// void B2BSession::onNewSession(ClientInviteSessionHandle h, InviteSession::OfferAnswerType oat, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onNewSession(ClientInviteSessionHandle): msg=" << LOG_MSG); mInviteSessionHandle = h->getSessionHandle(); if(msg.exists(h_UserAgent)) { if(msg.header(h_UserAgent).value().prefix("Viceroy")) { mIChatEndpoint = true; } } } void B2BSession::onNewSession(ServerInviteSessionHandle h, InviteSession::OfferAnswerType oat, const SipMessage& msg) { mInviteSessionHandle = h->getSessionHandle(); if(msg.exists(h_UserAgent)) { if(msg.header(h_UserAgent).value().prefix("Viceroy")) { mIChatEndpoint = true; } } // First check if this INVITE is to replace an existing session if(msg.exists(h_Replaces)) { pair presult; presult = mDum.findInviteSession(msg.header(h_Replaces)); if(!(presult.first == InviteSessionHandle::NotValid())) { B2BSession* sessionToReplace = dynamic_cast(presult.first->getAppDialogSet().get()); InfoLog(B2BLOG_PREFIX << "onNewSession(ServerInviteSessionHandle): to replace handle=" << sessionToReplace->getB2BSessionHandle() << ", msg=" << LOG_MSG); // Assume Peer mapping of old call - and copy some settings stealPeer(sessionToReplace); // Session to replace was found - end old session sessionToReplace->end(); return; } } InfoLog(B2BLOG_PREFIX << "onNewSession(ServerInviteSessionHandle): msg=" << LOG_MSG); // Note: remaining inbound call handling is done in onOffer and onOfferRequired } void B2BSession::onFailure(ClientInviteSessionHandle h, const SipMessage& msg) { // Note: Teardown of peer is handled in destructor InfoLog(B2BLOG_PREFIX << "onFailure: msg=" << LOG_MSG); } void B2BSession::onEarlyMedia(ClientInviteSessionHandle h, const SipMessage& msg, const SdpContents& sdp) { InfoLog(B2BLOG_PREFIX << "onEarlyMedia: msg=" << LOG_MSG_WITH_SDP); } void B2BSession::onProvisional(ClientInviteSessionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onProvisional: msg=" << LOG_MSG); if(isStaleFork(h->getDialogId())) return; if(mPeer) { if(mPeer->mInviteSessionHandle.isValid()) { ServerInviteSession* sis = dynamic_cast(mPeer->mInviteSessionHandle.get()); if(sis && !sis->isAccepted()) { sis->provisional(msg.header(h_StatusLine).responseCode()); } } } } void B2BSession::onConnected(ClientInviteSessionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onConnected: msg=" << LOG_MSG); if(!isUACConnected()) { // It is possible in forking scenarios to get multiple 200 responses, if this is // our first 200 response, then this is the leg we accept, store the connected DialogId mUACConnectedDialogId = h->getDialogId(); // Note: each forked leg will update mInviteSessionHandle (in onNewSession call) - need to set mInviteSessionHandle for final answering leg on 200 mInviteSessionHandle = h->getSessionHandle(); } else { // We already have a connected leg - end this one with a BYE h->end(); } } void B2BSession::onConnected(InviteSessionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onConnected: msg=" << LOG_MSG); } void B2BSession::onStaleCallTimeout(ClientInviteSessionHandle h) { InfoLog(B2BLOG_PREFIX << "onStaleCallTimeout:"); endPeer(); } unsigned int B2BSession::mapRejectionCodeForIChat(unsigned int statusCode) { // iChat appears to display the following error codes nicely // 486 - Busy // 487 - Cancelled // 603 - Decline // 606 - Security Error switch(statusCode) { case 486: case 600: return 486; case 487: return 487; default: return 603; } } void B2BSession::onTerminated(InviteSessionHandle h, InviteSessionHandler::TerminatedReason reason, const SipMessage* msg) { Data reasonData; switch(reason) { case InviteSessionHandler::RemoteBye: reasonData = "received a BYE from peer"; break; case InviteSessionHandler::RemoteCancel: reasonData = "received a CANCEL from peer"; break; case InviteSessionHandler::Rejected: reasonData = "received a rejection from peer"; break; case InviteSessionHandler::LocalBye: reasonData = "ended locally via BYE"; break; case InviteSessionHandler::LocalCancel: reasonData = "ended locally via CANCEL"; break; case InviteSessionHandler::Replaced: reasonData = "ended due to being replaced"; break; case InviteSessionHandler::Referred: reasonData = "ended due to being reffered"; break; case InviteSessionHandler::Error: reasonData = "ended due to an error"; break; case InviteSessionHandler::Timeout: reasonData = "ended due to a timeout"; break; default: resip_assert(false); break; } if(msg) { InfoLog(B2BLOG_PREFIX << "onTerminated: reason=" << reasonData << ", msg=" << LOG_MSGP); } else { InfoLog(B2BLOG_PREFIX << "onTerminated: reason=" << reasonData); } unsigned int statusCode = 603; if(msg) { if(msg->isResponse()) { statusCode = msg->header(h_StatusLine).responseCode(); } } // If this is a referred call and the refer is still around - then switch back to referrer (ie. failed transfer recovery) if(mReferringAppDialogSet.isValid() && mPeer) { B2BSession* session = (B2BSession*)mReferringAppDialogSet.get(); session->stealPeer(this); } if(isStaleFork(h->getDialogId())) return; // If we have a peer that hasn't been accepted yet - then pass back terminated code, by calling reject now if(mPeer && mPeer->mInviteSessionHandle.isValid()) { ServerInviteSession* sis = dynamic_cast(mPeer->mInviteSessionHandle.get()); if(sis && !sis->isAccepted()) { if(mPeer->mIChatEndpoint) { statusCode = mapRejectionCodeForIChat(statusCode); } sis->reject(statusCode); } } endPeer(); } void B2BSession::onRedirected(ClientInviteSessionHandle h, const SipMessage& msg) { // We will recurse on redirect requests, so nothing to do here InfoLog(B2BLOG_PREFIX << "onRedirected: msg=" << LOG_MSG); } void B2BSession::onAnswer(InviteSessionHandle h, const SipMessage& msg, const SdpContents& sdp) { InfoLog(B2BLOG_PREFIX << "onAnswer: msg=" << LOG_MSG_WITH_SDP); if(isStaleFork(h->getDialogId())) return; if(mPeer) { if(mPeer->mInviteSessionHandle.isValid()) { if(mPeer->mWaitingAnswerFromPeer) { mPeer->mWaitingAnswerFromPeer = false; if(mAnchorMedia) { // We need to answer with local sdp mPeer->provideLocalAnswer(); } else { mPeer->mInviteSessionHandle->provideAnswer(sdp); } } else if(mPeer->mWaitingOfferFromPeer) { if(mAnchorMedia) { // We need to send a local sdp offer SdpContents localOffer; if(!buildLocalOffer(localOffer)) { ErrLog(<< "B2BSession::onAnswer: unable to build local offer."); endPeer(); end(); return; } mPeer->mInviteSessionHandle->provideOffer(localOffer); } else { mPeer->mInviteSessionHandle->provideOffer(sdp); } mPeer->mWaitingOfferFromPeer = false; } ServerInviteSession* sis = dynamic_cast(mPeer->mInviteSessionHandle.get()); if(sis && !sis->isAccepted()) { sis->accept(); } } else { if(!mPeer->mHasDialogSet && mPeer->mIChatWaitingToAccept) { // We are in a call setup phase for iChat -> SIP - peer is just an empty dialogset mPeer->acceptIChatCall(); } } } } void B2BSession::onOffer(InviteSessionHandle h, const SipMessage& msg, const SdpContents& sdp) { InfoLog(B2BLOG_PREFIX << "onOffer: msg=" << LOG_MSG_WITH_SDP); if(isStaleFork(h->getDialogId())) return; if(mPeer) { if(mAnchorMedia) { // We need to answer with local sdp if(provideLocalAnswer()) { ServerInviteSession* sis = dynamic_cast(mInviteSessionHandle.get()); if(sis && !sis->isAccepted()) { sis->accept(); } } } else if(mPeer->mInviteSessionHandle.isValid()) { if(mPeer->mWaitingOfferFromPeer) { SdpContents fixedUpOffer; fixupSdp(sdp, fixedUpOffer); mPeer->mInviteSessionHandle->provideOffer(fixedUpOffer); mPeer->mWaitingOfferFromPeer = false; mWaitingAnswerFromPeer = true; } else { h->provideAnswer(h->getLocalSdp()); /* Experimental - appears to server no purpose if(sdp.session().media().front().exists("sendonly") || sdp.session().media().front().exists("inactive")) { PlainContents msgBody("VCRemoteMuted:ON"); mPeer->mInviteSessionHandle->message(msgBody); } else { PlainContents msgBody("VCRemoteMuted:OFF"); mPeer->mInviteSessionHandle->message(msgBody); }*/ } } else { resip_assert(false); } } else { // If we don't have a peer yet - we need to place a new call SdpContents fixedUpOffer; fixupSdp(sdp, fixedUpOffer); bool result; if(mIChatEndpoint) { try { // If we are an iChat endpoint then real To/From uri is actually in the display name of the To/From // header. Uri to(Data("xmpp:") + msg.header(h_To).displayName()); if(!msg.header(h_From).displayName().empty()) { try { NameAddr from(Data("sip:" + msg.header(h_From).displayName())); if(!from.uri().user().empty()) { from.displayName() = from.uri().user(); } result = createNewPeer(to, from, &fixedUpOffer); } catch(resip::BaseException&) { result = createNewPeer(to, msg.header(h_From), &fixedUpOffer); } } else { result = createNewPeer(to, msg.header(h_From), &fixedUpOffer); } } catch(resip::BaseException& e) { ErrLog(B2BLOG_PREFIX << "Error extracting real URI from iChat To header display name=" << msg.header(h_To) << ", error=" << e); result = false; } } else { result = createNewPeer(msg.header(h_To).uri(), msg.header(h_From),&fixedUpOffer); } if(!result) { ErrLog(B2BLOG_PREFIX << "Failed to create new peer, rejecting call with 404."); h->reject(404); } else { mWaitingAnswerFromPeer = true; } } } void B2BSession::onOfferRequired(InviteSessionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onOfferRequired: msg=" << LOG_MSG); if(isStaleFork(h->getDialogId())) return; if(mPeer) { if(mAnchorMedia) { // We need to offer with local sdp SdpContents sdp; if(!buildLocalOffer(sdp)) { ErrLog(<< "B2BSession::onOfferRequired: unable to build local offer."); endPeer(); end(); return; } h->provideOffer(sdp); } else if(mPeer->mInviteSessionHandle.isValid()) { mPeer->mInviteSessionHandle->requestOffer(); mWaitingOfferFromPeer = true; } else { resip_assert(false); } } else { // If we don't have a peer yet - we need to place a new call if(!createNewPeer(msg.header(h_To).uri(), msg.header(h_From),0)) { ErrLog(B2BLOG_PREFIX << "Failed to create new peer, rejecting call with 404."); h->reject(404); } else { mWaitingOfferFromPeer = true; } } } void B2BSession::onOfferRejected(InviteSessionHandle h, const SipMessage* msg) { int statusCode = 488; WarningCategory* warning=0; if(isStaleFork(h->getDialogId())) return; if(msg) { InfoLog(B2BLOG_PREFIX << "onOfferRejected: msg=" << LOG_MSGP); if(msg->exists(h_Warnings)) { warning = (WarningCategory*)&msg->header(h_Warnings).back(); } if(msg->isResponse()) { statusCode = msg->header(h_StatusLine).responseCode(); } } else { InfoLog(B2BLOG_PREFIX << "onOfferRejected:"); } if(mPeer) { if(mPeer->mInviteSessionHandle.isValid() && mPeer->mWaitingAnswerFromPeer) { mPeer->mInviteSessionHandle->reject(statusCode, warning); mPeer->mWaitingAnswerFromPeer = false; } } } void B2BSession::onOfferRequestRejected(InviteSessionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onOfferRequestRejected: msg=" << LOG_MSG); // This is called when we are waiting to resend a INVITE with no sdp after a glare condition, and we // instead receive an inbound INVITE or UPDATE if(mPeer) { if(mPeer->mInviteSessionHandle.isValid() && mPeer->mWaitingOfferFromPeer) { // Return glare to peer mPeer->mInviteSessionHandle->reject(491); mPeer->mWaitingOfferFromPeer = false; } } } void B2BSession::onRemoteSdpChanged(InviteSessionHandle h, const SipMessage& msg, const SdpContents& sdp) { /// called when a modified SDP is received in a 2xx response to a /// session-timer reINVITE. Under normal circumstances where the response /// SDP is unchanged from current remote SDP no handler is called /// There is not much we can do about this. If session timers are used then they are managed seperately per leg /// and we have no real mechanism to notify the other peer of new SDP without starting a new offer/answer negotiation InfoLog(B2BLOG_PREFIX << "onRemoteSdpChanged: msg=" << LOG_MSG_WITH_SDP); } void B2BSession::onInfo(InviteSessionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onInfo: msg=" << LOG_MSG); if(mPeer) { if(mPeer->mInviteSessionHandle.isValid() && msg.getContents()) { mPeer->mInviteSessionHandle->info(*msg.getContents()); mWaitingNitAnswerFromPeer = true; } } else { h->acceptNIT(); } } void B2BSession::onInfoSuccess(InviteSessionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onInfoSuccess: msg=" << LOG_MSG); if(mPeer) { if(mPeer->mInviteSessionHandle.isValid() && mPeer->mWaitingNitAnswerFromPeer) { mPeer->mInviteSessionHandle->acceptNIT(msg.header(h_StatusLine).responseCode(), msg.getContents()); } } } void B2BSession::onInfoFailure(InviteSessionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onInfoFailure: msg=" << LOG_MSG); if(mPeer) { if(mPeer->mInviteSessionHandle.isValid() && mPeer->mWaitingNitAnswerFromPeer) { mPeer->mInviteSessionHandle->rejectNIT(msg.header(h_StatusLine).responseCode()); } } } void B2BSession::onRefer(InviteSessionHandle h, ServerSubscriptionHandle ss, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onRefer: msg=" << LOG_MSG); // If no peer (for some reason), then reject request if(!mPeer) { ss->send(ss->reject(403)); return; } // Recurse on the REFER - do not pass to other leg try { // Accept the Refer ss->send(ss->accept(202 /* Refer Accepted */)); B2BSession* newPeer = new B2BSession(mServer); SdpContents *pOffer = 0; SdpContents offer; if(mAnchorMedia) { buildLocalOffer(offer); pOffer = &offer; } else { mPeer->mWaitingOfferFromPeer = true; } // Map other leg to this new one newPeer->stealPeer(this); newPeer->mReferringAppDialogSet = getHandle(); SharedPtr invitemsg = mDum.makeInviteSessionFromRefer(msg, ss->getHandle(), pOffer, newPeer); mDum.send(invitemsg); } catch(BaseException &e) { WarningLog(B2BLOG_PREFIX << "onRefer exception: " << e); } catch(...) { WarningLog(B2BLOG_PREFIX << "onRefer unknown exception"); } } void B2BSession::onReferAccepted(InviteSessionHandle h, ClientSubscriptionHandle csh, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onReferAccepted: msg=" << LOG_MSG); end(); // Click-to-call refer request was accepted - end our call with the initiator } void B2BSession::onReferRejected(InviteSessionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onReferRejected: msg=" << LOG_MSG); // Endpoint doesn't support REFER - Fallback to anchoring /* mPeer = new B2BSession(mServer); mPeer->setPeer(this); mPeer->startClickToCallAnchorLeg(msg.header(h_From), mClickToCallDestination, mMediaRelayPort); if(mMediaRelayPort == 0) { mWaitingOfferFromPeer = true; }*/ } bool B2BSession::doReferNoSub(const SipMessage& msg) { // If no peer (for some reason), then return false if(!mPeer) { return false; } B2BSession* newPeer = new B2BSession(mServer); SdpContents *pOffer = 0; SdpContents offer; if(mAnchorMedia) { buildLocalOffer(offer); pOffer = &offer; } else { mPeer->mWaitingOfferFromPeer = true; } // Map other leg to this new one newPeer->stealPeer(this); newPeer->mReferringAppDialogSet = getHandle(); // Build the Invite SharedPtr invitemsg = mDum.makeInviteSessionFromRefer(msg, getUserProfile(), pOffer, newPeer); mDum.send(invitemsg); return true; } void B2BSession::onReferNoSub(InviteSessionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onReferNoSub: msg=" << LOG_MSG); // If no peer (for some reason), then reject request if(!mPeer) { h->rejectReferNoSub(403); return; } // Accept the Refer h->acceptReferNoSub(202 /* Refer Accepted */); doReferNoSub(msg); } void B2BSession::onMessage(InviteSessionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onMessage: msg=" << LOG_MSG); if(mPeer && !mIChatEndpoint) // Note: If iChat endpoint, then just respond to message, since iChat sends PING MESSAGES periodically and most phones don't respond { if(mPeer->mInviteSessionHandle.isValid() && msg.getContents()) { mPeer->mInviteSessionHandle->message(*msg.getContents()); mWaitingNitAnswerFromPeer = true; } } else { h->acceptNIT(); } } void B2BSession::onMessageSuccess(InviteSessionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onMessageSuccess: msg=" << LOG_MSG); if(mPeer) { if(mPeer->mInviteSessionHandle.isValid() && mPeer->mWaitingNitAnswerFromPeer) { mPeer->mInviteSessionHandle->acceptNIT(msg.header(h_StatusLine).responseCode(), msg.getContents()); } } } void B2BSession::onMessageFailure(InviteSessionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onMessageFailure: msg=" << LOG_MSG); if(mPeer) { if(mPeer->mInviteSessionHandle.isValid() && mPeer->mWaitingNitAnswerFromPeer) { mPeer->mInviteSessionHandle->rejectNIT(msg.header(h_StatusLine).responseCode()); } } } void B2BSession::onForkDestroyed(ClientInviteSessionHandle h) { InfoLog(B2BLOG_PREFIX << "onForkDestroyed:"); } //////////////////////////////////////////////////////////////////////////////// // DialogSetHandler /////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// void B2BSession::onTrying(AppDialogSetHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onTrying: msg=" << LOG_MSG); } void B2BSession::onNonDialogCreatingProvisional(AppDialogSetHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onNonDialogCreatingProvisional: msg=" << LOG_MSG); if(isUACConnected()) return; if(mPeer) { if(mPeer->mInviteSessionHandle.isValid()) { ServerInviteSession* sis = dynamic_cast(mPeer->mInviteSessionHandle.get()); if(sis && !sis->isAccepted()) { sis->provisional(msg.header(h_StatusLine).responseCode()); } } } } //////////////////////////////////////////////////////////////////////////////// // ClientSubscriptionHandler /////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// void B2BSession::onUpdatePending(ClientSubscriptionHandle h, const SipMessage& msg, bool outOfOrder) { InfoLog(B2BLOG_PREFIX << "onUpdatePending(ClientSubscriptionHandle): " << LOG_MSG); if (msg.exists(h_Event) && msg.header(h_Event).value() == "refer") { h->acceptUpdate(); } else { h->rejectUpdate(400, Data("Only notifies for refers are allowed.")); } } void B2BSession::onUpdateActive(ClientSubscriptionHandle h, const SipMessage& msg, bool outOfOrder) { InfoLog(B2BLOG_PREFIX << "onUpdateActive(ClientSubscriptionHandle): " << LOG_MSG); if (msg.exists(h_Event) && msg.header(h_Event).value() == "refer") { h->acceptUpdate(); } else { h->rejectUpdate(400, Data("Only notifies for refers are allowed.")); } } void B2BSession::onUpdateExtension(ClientSubscriptionHandle h, const SipMessage& msg, bool outOfOrder) { InfoLog(B2BLOG_PREFIX << "onUpdateExtension(ClientSubscriptionHandle): " << LOG_MSG); if (msg.exists(h_Event) && msg.header(h_Event).value() == "refer") { h->acceptUpdate(); } else { h->rejectUpdate(400, Data("Only notifies for refers are allowed.")); } } void B2BSession::onTerminated(ClientSubscriptionHandle h, const SipMessage* msg) { if(msg) { InfoLog(B2BLOG_PREFIX << "onTerminated(ClientSubscriptionHandle): " << LOG_MSGP); } else { InfoLog(B2BLOG_PREFIX << "onTerminated(ClientSubscriptionHandle)"); } // Note: Final notify is sometimes only passed in the onTerminated callback //if (notify.isRequest() && notify.exists(h_Event) && notify.header(h_Event).value() == "refer") //{ //} } void B2BSession::onNewSubscription(ClientSubscriptionHandle h, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onNewSubscription(ClientSubscriptionHandle): " << LOG_MSG); } int B2BSession::onRequestRetry(ClientSubscriptionHandle h, int retryMinimum, const SipMessage& msg) { InfoLog(B2BLOG_PREFIX << "onRequestRetry(ClientSubscriptionHandle): " << LOG_MSG); return -1; } } /* ==================================================================== Copyright (c) 2009, SIP Spectrum, Inc. All rights reserved. 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. Neither the name of SIP Spectrum nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR CONTRIBUTORS 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. ==================================================================== */