mirror of
https://github.com/RangeNetworks/openbts.git
synced 2025-10-25 00:53:56 +00:00
1698 lines
66 KiB
C++
1698 lines
66 KiB
C++
/*
|
|
* Copyright 2013, 2014 Range Networks, Inc.
|
|
*
|
|
* This software is distributed under multiple licenses;
|
|
* see the COPYING file in the main directory for licensing
|
|
* information for this specific distribution.
|
|
*
|
|
* This use of this software may be subject to additional restrictions.
|
|
* See the LEGAL file in the main directory for details.
|
|
|
|
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.
|
|
|
|
*/
|
|
// Written by Pat Thompson.
|
|
|
|
|
|
#define LOG_GROUP LogGroup::SIP // Can set Log.Level.SIP for debugging
|
|
|
|
#include "SIPBase.h"
|
|
#include "SIPDialog.h"
|
|
#include "SIP2Interface.h"
|
|
#include "SIPTransaction.h"
|
|
#include <Reporting.h> // For gReports.
|
|
#include <L3TranEntry.h>
|
|
#include <L3TermCause.h>
|
|
#include <L3StateMachine.h>
|
|
#include <GSML3MMElements.h> // for L3CMServiceType
|
|
#include <GSML3CCElements.h> // for L3Cause
|
|
#include <algorithm>
|
|
|
|
namespace SIP {
|
|
using namespace Control;
|
|
SipDialog *gRegisterDialog = NULL;
|
|
|
|
// Only the SipMTInviteServerTransactionLayer and SipMOInviteClientTransactionLayer are allowed to call
|
|
// the underlying sipWrite method directly for the invite transactions.
|
|
void SipDialogBase::sipWrite(SipMessage *sipmsg)
|
|
{
|
|
if (!mProxy.mipValid) {
|
|
LOG(ERR) << "Attempt to write to invalid proxy ignored, address:"<<mProxy.mipName;
|
|
return;
|
|
}
|
|
gSipInterface.siWrite(&mProxy.mipSockAddr,sipmsg);
|
|
}
|
|
|
|
SipDialog *SipDialogBase::dgGetDialog()
|
|
{
|
|
return dynamic_cast<SipDialog*>(this);
|
|
}
|
|
|
|
SipState SipDialogBase::MODSendCANCEL(TermCause cause)
|
|
{
|
|
LOG(INFO) << "SIP term info MODSendCANCEL cause: " << cause;
|
|
LOG(INFO) << sbText();
|
|
setSipState(MODCanceling); // (pat) MOD sent a cancel.
|
|
SipMOCancelTU *cancelTU = new SipMOCancelTU(dynamic_cast<SipDialog*>(this),cause.tcGetSipReasonHeader());
|
|
//cancelTU->mstOutRequest.addCallTerminationReasonSM(CallTerminationCause::eQ850, cause.tcGetCCCause(), ""); // MODSendCANCEL
|
|
cancelTU->sctStart();
|
|
return getSipState();
|
|
}
|
|
|
|
void SipDialogBase::initRTP()
|
|
{
|
|
SdpInfo sdp;
|
|
sdp.sdpParse(getSdpRemote().c_str());
|
|
initRTP1(sdp.sdpHost.c_str(),sdp.sdpRtpPort,mDialogId);
|
|
}
|
|
|
|
|
|
string SipDialogBase::makeSDPOffer()
|
|
{
|
|
SdpInfo sdp;
|
|
sdp.sdpInitOffer(this);
|
|
return sdp.sdpValue();
|
|
//return makeSDP("0","0");
|
|
}
|
|
|
|
// mCodec is an implicit parameter, consisting of the chosen codec.
|
|
string SipDialogBase::makeSDPAnswer()
|
|
{
|
|
SdpInfo answer;
|
|
answer.sdpInitOffer(this);
|
|
mSdpAnswer = answer.sdpValue();
|
|
return mSdpAnswer;
|
|
}
|
|
|
|
void SipDialogBase::MTCInitRTP()
|
|
{
|
|
initRTP();
|
|
}
|
|
|
|
void SipDialogBase::MOCInitRTP()
|
|
{
|
|
initRTP();
|
|
}
|
|
|
|
|
|
void SipDialogBase::sdbText(std::ostringstream&os, bool verbose) const
|
|
{
|
|
sbText(os);
|
|
|
|
if (verbose || IS_LOG_LEVEL(DEBUG)) {
|
|
rtpText(os);
|
|
//os << "proxy=(" << mProxy.ipToText() << ")";
|
|
}
|
|
}
|
|
|
|
string SipDialogBase::sdbText() const
|
|
{
|
|
std::ostringstream ss;
|
|
sdbText(ss);
|
|
return ss.str();
|
|
}
|
|
|
|
void SipMOInviteClientTransactionLayer::MOUssdSendINVITE(string ussd, const L3LogicalChannel *chan)
|
|
{
|
|
static const char* xmlUssdTemplate =
|
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
|
"<ussd-data>\n"
|
|
" <language>en</language>\n"
|
|
" <ussd-string>%s</ussd-string>\n"
|
|
"</ussd-data>\n";
|
|
|
|
LOG(INFO) << "user " << sipLocalUsername() << " state " << getSipState() <<sdbText();
|
|
|
|
static const string cInviteStr("INVITE");
|
|
SipMessage *invite = makeInitialRequest(cInviteStr);
|
|
// This is dumber than snot. We have to put in a dummy sdp with port 0.
|
|
mRTPPort = 0;
|
|
invite->smAddBody(string("application/sdp"),makeSDPOffer());
|
|
|
|
// Add RFC-4119 geolocation XML to content area, if available.
|
|
// TODO: This makes it a multipart message, needs to be tested.
|
|
string xml = format(xmlUssdTemplate,ussd);
|
|
invite->smAddBody(string("application/vnd.3gpp.ussd+xml"),xml);
|
|
|
|
SipCallbacks::writePrivateHeaders(invite,chan);
|
|
moWriteLowSide(invite);
|
|
LOG(DEBUG) <<LOGVAR(invite);
|
|
delete invite;
|
|
setSipState(Starting);
|
|
}
|
|
|
|
//old args: const char * calledUser, const char * calledDomain, short rtpPort, Control::CodecSet codec,
|
|
void SipMOInviteClientTransactionLayer::MOCSendINVITE(const L3LogicalChannel *chan)
|
|
{
|
|
static const char* xmlGeoprivTemplate =
|
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
|
"<presence xmlns=\"urn:ietf:params:xml:ns:pidf\"\n"
|
|
"xmlns:gp=\"urn:ietf:params:xml:ns:pidf:geopriv10\"\n"
|
|
"xmlns:gml=\"urn:opengis:specification:gml:schema-xsd:feature:v3.0\"\n"
|
|
"entity=\"pres:%s@%s\">\n"
|
|
"<tuple id=\"1\">\n"
|
|
"<status>\n"
|
|
"<gp:geopriv>\n"
|
|
"<gp:location-info>\n"
|
|
"<gml:location>\n"
|
|
"<gml:Point gml:id=\"point1\" srsName=\"epsg:4326\">\n"
|
|
"<gml:coordinates>%s</gml:coordinates>\n"
|
|
"</gml:Point>\n"
|
|
"</gml:location>\n"
|
|
"</gp:location-info>\n"
|
|
"<gp:usage-rules>\n"
|
|
"<gp:retransmission-allowed>no</gp:retransmission-allowed>\n"
|
|
"</gp:usage-rules>\n"
|
|
"</gp:geopriv>\n"
|
|
"</status>\n"
|
|
"</tuple>\n"
|
|
"</presence>\n";
|
|
|
|
LOG(INFO) << "user " << sipLocalUsername() << " state " << getSipState() <<sdbText();
|
|
#if PAT_TEST_SIP_DIRECT
|
|
// (pat 7-23-2013): This code has eroded beyond recoverability...
|
|
//bool directBtsConnection = false;
|
|
// exten => _X.,1,Set(Name=${ODBC_SQL(select dial from dialdata_table where exten=\"${EXTEN}\")})
|
|
// exten => _X.,n,GotoIf($["${Name}"=""] ?other-lines,${EXTEN},1)
|
|
// exten => _X.,n,Set(IPAddr=${ODBC_SQL(select ipaddr from sip_buddies where username=\"${Name}\")})
|
|
// exten => _X.,n,GotoIf($["${IPAddr}"=""] ?other-lines,${EXTEN},1)
|
|
// exten => _X.,n,Set(Port=${ODBC_SQL(select port from sip_buddies where username=\"${Name}\")})
|
|
// exten => _X.,n,GotoIf($["${Port}"!=""] ?dialNum)
|
|
// exten => _X.,n,Set(Port=5062) ; Port was not set, so set to default. Gets around bug in subscriberRegistry
|
|
// exten => _X.,n(dialNum),Dial(SIP/${Name}@${IPAddr}:${Port})
|
|
if (gConfig.getStr("SIP.Proxy.Mode") == string("direct")) {
|
|
// Is this IMSI registered directly on a BTS?
|
|
string remoteIMSI = gSubscriberRegistry.getIMSI(wCalledUsername);
|
|
string remoteIPStr, remotePortStr;
|
|
if (remoteIMSI != "") {
|
|
remoteIPStr = gSubscriberRegistry.imsiGet(remoteIMSI,"ipaddr");
|
|
remotePortStr = gSubscriberRegistry.imsiGet(remoteIMSI,"port");
|
|
unsigned remotePort = (remotePortStr != "") ? atoi(remotePortStr.c_str()) : 0;
|
|
LOG(DEBUG) << "BTS direct test: "<<wCalledUsername
|
|
<< format(" -> SIP/%s@%s:%s",remoteIMSI.c_str(),remoteIPStr.c_str(),remotePortStr.c_str()) <<sdbText();
|
|
if (remoteIPStr != "" && remotePort) {
|
|
//directBTSConnection = true;
|
|
mRemoteUsername = remoteIMSI;
|
|
mProxyIP = remoteIPStr;
|
|
mProxyPort = remotePort;
|
|
LOG(INFO) << "Calling BTS direct: "<<wCalledUsername
|
|
<< format(" -> SIP/%s@%s:%u",mRemoteUsername.c_str(),mProxyIP.c_str(),mProxyPort) <<sdbText();
|
|
}
|
|
}
|
|
}
|
|
//mRemoteUsername = "IMSI001690000000002"; // pats iphone
|
|
#endif
|
|
|
|
LOG(DEBUG) <<sdbText();
|
|
|
|
static const string cInviteStr("INVITE");
|
|
SipMessage *invite = makeInitialRequest(cInviteStr);
|
|
invite->smAddBody(string("application/sdp"),makeSDPOffer());
|
|
|
|
|
|
string username = sipLocalUsername();
|
|
WATCH("MOC imsi="<<username);
|
|
|
|
if (this->dsPAssociatedUri.size()) {
|
|
invite->smAddHeader("P-Associated-URI",this->dsPAssociatedUri);
|
|
}
|
|
if (this->dsPAssertedIdentity.size()) {
|
|
invite->smAddHeader("P-Asserted-Identity",this->dsPAssertedIdentity);
|
|
}
|
|
|
|
SipCallbacks::writePrivateHeaders(invite,chan);
|
|
moWriteLowSide(invite);
|
|
delete invite;
|
|
setSipState(Starting);
|
|
};
|
|
|
|
|
|
void SipMOInviteClientTransactionLayer::handleSMSResponse(SipMessage *sipmsg)
|
|
{
|
|
// There are only three cases, and here they are:
|
|
stopTimers();
|
|
int code = sipmsg->smGetCode();
|
|
if (code < 200) {
|
|
// (pat) 11-26 This was wrong: mTimerBF.set(64*T1);
|
|
return; // Ignore 100 Trying.
|
|
}
|
|
|
|
dialogPushState((code/100)*100 == 200 ? Cleared : SSFail,code);
|
|
mTimerK.setOnce(T4); // 17.1.2.2: Timer K Controls when the dialog is destroyed.
|
|
}
|
|
|
|
void SipInviteServerTransactionLayerBase::SipMTCancel(SipMessage *sipmsg)
|
|
{
|
|
assert(sipmsg->isCANCEL());
|
|
SipMessageReply cancelok(sipmsg,200,string("OK"),this);
|
|
sipWrite(&cancelok);
|
|
if (dsPeer()->ipIsReliableTransport()) {
|
|
// It no longer matters whether we use Canceled or MTDCanceling state, and we should get rid of some states.
|
|
dialogPushState(Canceled,0);
|
|
} else {
|
|
dialogPushState(MTDCanceling,0);
|
|
setTimerJ();
|
|
}
|
|
}
|
|
|
|
void SipInviteServerTransactionLayerBase::SipMTBye(SipMessage *sipmsg)
|
|
{
|
|
assert(sipmsg->isBYE());
|
|
gReports.incr("OpenBTS.SIP.BYE.In");
|
|
SipMessageReply byeok(sipmsg,200,string("OK"),this);
|
|
sipWrite(&byeok);
|
|
if (dsPeer()->ipIsReliableTransport()) {
|
|
dialogPushState(Cleared,0);
|
|
} else {
|
|
dialogPushState(MTDClearing,0);
|
|
setTimerJ();
|
|
}
|
|
}
|
|
|
|
// Incoming message from SIPInterface.
|
|
void SipMOInviteClientTransactionLayer::MOWriteHighSide(SipMessage *sipmsg)
|
|
{
|
|
int code = sipmsg->smGetCode();
|
|
LOG(DEBUG) <<LOGVAR(code) <<LOGVAR(dgIsInvite());
|
|
if (code == 0) { // It is a SIP Request. Switch based on the method.
|
|
if (sipmsg->isCANCEL()) {
|
|
mTimerBF.stop();
|
|
// I dont think the peer is supposed to do this, but lets cancel it:
|
|
SipMTCancel(sipmsg);
|
|
} else if (sipmsg->isBYE()) {
|
|
// A BYE should not be sent by the peer until dialog established, and we are supposed to send 405 error.
|
|
// but instead we are going to quietly terminate the dialog anyway.
|
|
SipMTBye(sipmsg);
|
|
} else {
|
|
// Not expecting any others.
|
|
// Must send 405 error.
|
|
LOG(WARNING)<<"SIP Message ignored:"<<sipmsg;
|
|
SipMessageReply oops(sipmsg,405,string("Method Not Allowed"),this);
|
|
sipWrite(&oops);
|
|
}
|
|
} else { // It is a SIP reply.
|
|
saveMOResponse(sipmsg); LOG(DEBUG)<<"saveResponse"<<sipmsg;
|
|
if (dgIsInvite()) { // It is INVITE
|
|
stopTimers(); // Yes we stop the timers for every possible case. TimerBF will be restarted when handleInviteResponse sends the reply.
|
|
handleInviteResponse(code,true);
|
|
} else { // It is a MESSAGE
|
|
handleSMSResponse(sipmsg);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Outgoing message.
|
|
void SipMOInviteClientTransactionLayer::moWriteLowSide(SipMessage *sipmsg)
|
|
{
|
|
if (sipmsg->isINVITE() || sipmsg->isMESSAGE()) { // It cant be anything else.
|
|
if (!dsPeer()->ipIsReliableTransport()) { mTimerAE.set(2*T1); }
|
|
mTimerBF.setOnce(64*T1); // RFC3261 17.1.2.2.2 Timer F
|
|
// This assert is not true in the weird case where we resend an SMS message.
|
|
// We should not be using this code for MESSAGE in the first place - it should be a TU.
|
|
//assert(mInvite == 0);
|
|
saveInviteOrMessage(sipmsg,true);
|
|
}
|
|
sipWrite(sipmsg);
|
|
}
|
|
|
|
// (pat) Counter-intuitively, the "ACK" is a SIP Request, not a SIP Response.
|
|
// Therefore its first line includes a Request-URI, and the request-uri is also placed in the "To" field.
|
|
// RFC2234 13.1: "The procedure for sending this ACK depends on the type of response.
|
|
// For final responses between 300 and 699, the ACK processing is done in the transaction layer and follows
|
|
// one set of rules (See Section 17). For 2xx responses, the ACK is generated by the UAC core. (section 13)"
|
|
// 17.1.1.3: "The ACK request constructed by the client transaction MUST contain
|
|
// values for the Call-ID, From, and Request-URI that are equal to the
|
|
// values of those header fields in the request passed to the transport
|
|
// by the client transaction (call this the "original request"). The To
|
|
// header field in the ACK MUST equal the To header field in the
|
|
// response being acknowledged, and therefore will usually differ from
|
|
// the To header field in the original request by the addition of the
|
|
// tag parameter. The ACK MUST contain a single Via header field, and
|
|
// this MUST be equal to the top Via header field of the original
|
|
// request. The CSeq header field in the ACK MUST contain the same
|
|
// value for the sequence number as was present in the original request,
|
|
// but the method parameter MUST be equal to "ACK".
|
|
//
|
|
void SipMOInviteClientTransactionLayer::MOCSendACK()
|
|
{
|
|
assert(! mTimerAE.isActive() && ! mTimerBF.isActive());
|
|
LOG(INFO) << sdbText();
|
|
//LOG(INFO) << "user " << mSipUsername << " state " << getSipState() <<sdbText();
|
|
|
|
static const string cAckstr("ACK");
|
|
SipMessageAckOrCancel ack(cAckstr,mInvite);
|
|
ack.msmTo = *dsRequestToHeader(); // Must get the updated to-tag.
|
|
sipWrite(&ack);
|
|
// we dont care mTimerD.set(T4);
|
|
}
|
|
|
|
void SipMOInviteClientTransactionLayer::MOSMSSendMESSAGE(const string &messageText, const string &contentType)
|
|
{
|
|
LOG(INFO) << "SIP send to " << dsRequestToHeader() <<" MESSAGE " << messageText <<sdbText();
|
|
assert(mDialogType == SIPDTMOSMS);
|
|
gReports.incr("OpenBTS.SIP.MESSAGE.Out");
|
|
|
|
static const string cMessagestr("MESSAGE");
|
|
SipMessage *msg = makeInitialRequest(cMessagestr);
|
|
msg->smAddBody(contentType,messageText);
|
|
moWriteLowSide(msg);
|
|
delete msg;
|
|
setSipState(MOSMSSubmit);
|
|
}
|
|
|
|
|
|
// Return TRUE to remove the dialog.
|
|
bool SipMOInviteClientTransactionLayer::moPeriodicService()
|
|
{
|
|
if (mTimerAE.expired()) { // Resend timer.
|
|
// The HANDOVER is inbound, but the invite is outbound like MOC.
|
|
if (getSipState() == Starting || getSipState() == HandoverInbound || getSipState() == MOSMSSubmit) {
|
|
sipWrite(getInvite());
|
|
mTimerAE.setDouble();
|
|
} else {
|
|
mTimerAE.stop();
|
|
}
|
|
} else if (mTimerBF.expired() || mTimerD.expired()) { // Dialog killer timer.
|
|
// Whoops. No response from peer.
|
|
stopTimers();
|
|
dialogPushState(SSFail,0);
|
|
return true;
|
|
} else if (mTimerK.expired()) { // Normal exit delay to absorb resends.
|
|
stopTimers();
|
|
if (! dgIsInvite()) { return true; } // It is a SIP MESSAGE.
|
|
} else if (sipIsFinished()) {
|
|
// If one of the kill timers is active, wait for it to expire, otherwise kill now.
|
|
return (mTimerBF.isActive() || mTimerD.isActive() || mTimerK.isActive()) ? false : true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
string SipMOInviteClientTransactionLayer::motlText() const
|
|
{
|
|
ostringstream os;
|
|
os <<LOGVAR2("Timers: AE",mTimerAE) <<LOGVAR2("BF",mTimerBF) <<LOGVAR2("K",mTimerK) <<LOGVAR2("D",mTimerD);
|
|
return os.str();
|
|
}
|
|
|
|
void SipMTInviteServerTransactionLayer::MTCSendTrying()
|
|
{
|
|
SipMessage *invite = getInvite();
|
|
if (invite==NULL) {
|
|
setSipState(SSFail);
|
|
gReports.incr("OpenBTS.SIP.Failed.Local");
|
|
}
|
|
LOG(INFO) << sdbText();
|
|
if (getSipState()==SSFail) return;
|
|
SipMessageReply trying(invite,100,string("Trying"),this);
|
|
mtWriteLowSide(&trying);
|
|
setSipState(Proceeding);
|
|
}
|
|
|
|
void SipMTInviteServerTransactionLayer::MTSMSSendTrying()
|
|
{
|
|
MTCSendTrying();
|
|
}
|
|
|
|
|
|
void SipMTInviteServerTransactionLayer::MTCSendRinging()
|
|
{
|
|
if (getSipState()==SSFail) return;
|
|
SipMessage *invite = getInvite();
|
|
LOG(INFO) <<sdbText();
|
|
assert(invite);
|
|
LOG(DEBUG) << "send ringing" <<sdbText();
|
|
SipMessageReply ringing(invite,180,string("Ringing"),this);
|
|
mtWriteLowSide(&ringing);
|
|
setSipState(Proceeding);
|
|
}
|
|
|
|
void SipMTInviteServerTransactionLayer::MTCSendOK(CodecSet wCodec, const L3LogicalChannel *chan)
|
|
{
|
|
if (getSipState()==SSFail) { devassert(0); }
|
|
SipMessage *invite = getInvite();
|
|
gReports.incr("OpenBTS.SIP.INVITE-OK.Out");
|
|
mRTPPort = allocateRTPPorts();
|
|
mCodec = wCodec;
|
|
LOG(INFO) <<sdbText();
|
|
SipMessageReply ok(invite,200,string("OK"),this);
|
|
ok.smAddBody(string("application/sdp"),makeSDPAnswer()); // TODO: This should be a reply to the originating SDP offer.
|
|
// (pat) Chan is NULL when in a weird special handled in dialogCancel.
|
|
if (chan) SipCallbacks::writePrivateHeaders(&ok,chan);
|
|
mtWriteLowSide(&ok);
|
|
setSipState(Connecting);
|
|
// In RFC-3261 the Transaction Layer no longer handles timers after the OK is sent.
|
|
// The Transport Layer alone is not capabable of sending the 200 OK reliably because then the
|
|
// INVITE server transaction ends, and the INVITE client transaction no longer resends INVITEs after
|
|
// receiving a provisional response. Rather, the way that would end up being handled is by starting a new
|
|
// INVITE transaction, which is totally not what we want to do. So we will push out the 2xx OK
|
|
// until we get the ACK. Doesnt matter for reliable transports.
|
|
if (dgIsInvite()) { setTimerG(); }
|
|
setTimerH();
|
|
}
|
|
|
|
string SipMTInviteServerTransactionLayer::mttlText() const
|
|
{
|
|
ostringstream ss;
|
|
ss << LOGVAR2("Timers: G",mTimerG) <<LOGVAR2("H",mTimerH) <<LOGVAR2("J",mTimerJ);
|
|
return ss.str();
|
|
}
|
|
|
|
// Doesnt seem like messages need the private headers.
|
|
void SipMTInviteServerTransactionLayer::MTSMSReply(int code, const char *explanation) // , const L3LogicalChannel *chan)
|
|
{
|
|
LOG(INFO) <<sdbText();
|
|
// If this operation was initiated from the CLI, there was no MESSAGE
|
|
if (mInvite) { // It is a MESSAGE in this case, not an INVITE
|
|
//2-2014: the reply to MESSAGE must include the to-field, so we pass the dialog to SIpMessageReply
|
|
SipMessageReply reply(mInvite,code,string(explanation),this); // previous: NULL);
|
|
sipWrite(&reply);
|
|
} else {
|
|
LOG(INFO) << "clearing CLI-generated transaction" <<sdbText();
|
|
}
|
|
setSipState(code == 200 ? Cleared : SSFail);
|
|
}
|
|
|
|
// This can only be used for early errors before we get the ACK.
|
|
void SipMTInviteServerTransactionLayer::MTCEarlyError(TermCause cause) // The message must be 300-699.
|
|
{
|
|
LOG(DEBUG) << LOGVAR(cause);
|
|
string reason;
|
|
int sipcode = cause.tcGetSipCodeAndReason(reason);
|
|
devassert(sipcode);
|
|
// Double check cause validity.
|
|
if (sipcode == 0) { // This should never happen; this is just a last resort in case of bugs.
|
|
sipcode = 486; reason = "No_User_Responding";
|
|
}
|
|
// TODO: What if we were already ACKed?
|
|
setSipState(MODError);
|
|
SipMessageReply reply(getInvite(),sipcode,reason,this); // The message must be 300-699.
|
|
reply.msmReasonHeader = cause.tcGetSipReasonHeader(); // Returns a SIP "Reason:" header string.
|
|
mtWriteLowSide(&reply);
|
|
if (dgIsInvite()) { setTimerG(); }
|
|
setTimerH();
|
|
}
|
|
|
|
// This is called for the second and subsequent received INVITEs as well as the ACK.
|
|
// We send the current response, whatever it is.
|
|
void SipMTInviteServerTransactionLayer::MTWriteHighSide(SipMessage *sipmsg) { // Incoming message from SIPInterface.
|
|
LOG(DEBUG);
|
|
SipState state = getSipState();
|
|
if (sipmsg->smGetCode() == 0) {
|
|
if (sipmsg->isINVITE()) {
|
|
if (mtLastResponse.smIsEmpty()) { MTCSendTrying(); }
|
|
else { sipWrite(&mtLastResponse); }
|
|
} else if (sipmsg->isACK()) {
|
|
if (state == SSNullState || state == Proceeding || state == Connecting) {
|
|
dialogPushState(Active,0);
|
|
gSipInterface.dmAddLocalTag(dgGetDialog());
|
|
} else {
|
|
// We could be failed or canceled, so ignore the ACK.
|
|
}
|
|
stopTimers(); // happiness
|
|
// The spec shows a short Timer I being started here, but all it does is specify a time
|
|
// when the dialog will stop absorbing additional ACKS, thus suppressing error messages.
|
|
// Well, how about if we just never throw an error for that? Done.
|
|
} else if (sipmsg->isCANCEL()) {
|
|
SipMTCancel(sipmsg);
|
|
} else if (sipmsg->isBYE()) { // TODO: This should not happen.
|
|
SipMTBye(sipmsg);
|
|
} else if (sipmsg->isMESSAGE()) {
|
|
// It is a duplicate MESSAGE. Resend the current response.
|
|
if (mtLastResponse.smIsEmpty()) {
|
|
if (! gConfig.getBool("SIP.RFC3428.NoTrying")) { MTSMSSendTrying(); } // Otherwise just ignore the duplicate MESSAGE.
|
|
} else {
|
|
sipWrite(&mtLastResponse);
|
|
}
|
|
} else {
|
|
// Not expecting any others. Must send 405 error.
|
|
LOG(WARNING)<<"SIP Message ignored:"<<sipmsg;
|
|
SipMessageReply oops(sipmsg,405,string("Method Not Allowed"),this);
|
|
sipWrite(&oops);
|
|
}
|
|
} else {
|
|
LOG(WARNING)<<"SIP Message ignored:"<<sipmsg;
|
|
}
|
|
}
|
|
|
|
// Return TRUE to remove the dialog.
|
|
bool SipMTInviteServerTransactionLayer::mtPeriodicService()
|
|
{
|
|
if (mTimerG.expired()) { // Resend timer.
|
|
if (getSipState() == SSFail || getSipState() == Active) {
|
|
sipWrite(mLastResponse);
|
|
mTimerG.setDouble(T2); // Will send again later.
|
|
} else {
|
|
// This could happen if a CANCEL started before the ACK was received.
|
|
// Not sure what to do - I think we will let the CANCEL take precedence, so stop sending this response.
|
|
mTimerG.stop();
|
|
}
|
|
} else if (mTimerJ.expired() || mTimerH.expired()) { // Dialog killer timers.
|
|
stopTimers(); // probably redundant.
|
|
// Time to destroy the Dialog.
|
|
if (dgIsInvite()) {
|
|
// Whoops. No ACK received. Notify L3 and remove the dialog
|
|
dialogPushState(SSFail,0);
|
|
} else {
|
|
// No need to notify, just remove the dialog.
|
|
}
|
|
return true; // Stop the dialog now. It will be deleted by the periodic service loop after the associated L3 transaction ends.
|
|
} else if (sipIsFinished()) {
|
|
// If one of the kill timers is active, wait for it to expire, otherwise kill now.
|
|
return (mTimerJ.isActive() || mTimerH.isActive()) ? false : true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
SipDialog *getRegistrar()
|
|
{
|
|
if (gRegisterDialog == NULL) {
|
|
gRegisterDialog = SipDialog::newSipDialogRegister1();
|
|
} else {
|
|
// This allows the user to change SIP.Proxy.Registration from the CLI.
|
|
gRegisterDialog->updateProxy("SIP.Proxy.Registration");
|
|
}
|
|
return gRegisterDialog;
|
|
}
|
|
|
|
|
|
// We wrap our REGISTER messages inside a dialog object, even though it is technically not a dialog.
|
|
SipMessage *SipDialog::makeRegisterMsg(DialogType wMethod, const L3LogicalChannel* chan, string RAND, const FullMobileId &msid, const char *SRES)
|
|
{
|
|
// TODO: We need to make a transaction here...
|
|
static const string registerStr("REGISTER");
|
|
// The request URI is special for REGISTER;
|
|
string reqUri = string("sip:") + proxyIP();
|
|
// We formerly allocated a new Dialog for each REGISTER message so the IMSI was stashed in the dialog, and localSipUri() worked.
|
|
//SipPreposition myUri(localSipUri());
|
|
string username = msid.fmidUsername();
|
|
// RFC3261 is somewhat contradictory on the From-tag and To-tag.
|
|
// The documentation for 'from' says the from-tag is always included.
|
|
// The examples in 24.1 show a From-tag but no To-tag.
|
|
// The To-tag includes the optional <>, and Paul at null team incorrectly thought the <> were required,
|
|
// so we will include them as that appears to be common practice.
|
|
|
|
string myUriString;
|
|
string authUri;
|
|
string authUsername;
|
|
string realm = gConfig.getStr("SIP.Realm");
|
|
if (realm.length() > 0) {
|
|
authUri = string("sip:") + realm;
|
|
authUsername = string("IMSI") + msid.mImsi;
|
|
myUriString = makeUri(username,realm,0);
|
|
} else {
|
|
myUriString = makeUri(username,dsPeer()->mipName,0); // The port, if any, is already in mipName.
|
|
}
|
|
|
|
//string fromUriString = makeUriWithTag(username,dsPeer()->mipName,make_tag()); // The port, if any, is already in mipName.
|
|
SipPreposition toHeader("",myUriString,"");
|
|
SipPreposition fromHeader("",myUriString,make_tag());
|
|
dsNextCSeq(); // Advance CSeqNum.
|
|
SipMessage *msg = makeRequest(registerStr,reqUri,username,&toHeader,&fromHeader,make_branch());
|
|
|
|
// Replace the Contact header so we can set the expires option. What a botched up spec.
|
|
// Replace the P-Preferred-Identity
|
|
unsigned expires;
|
|
if (wMethod == SIPDTRegister ) {
|
|
expires = 60*gConfig.getNum("SIP.RegistrationPeriod");
|
|
if (SRES && strlen(SRES)) {
|
|
if (realm.length() > 0) {
|
|
string response = makeResponse(authUsername, realm, SRES, registerStr, authUri, RAND);
|
|
msg->msmAuthorizationValue = format("Digest realm=\"%s\", username=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\", algorithm=MD5, qop=\"auth\" ",
|
|
realm.c_str(), authUsername.c_str(), RAND.c_str(), authUri.c_str(), response.c_str());
|
|
} else {
|
|
// (pat 9-2014) These fields are all supposed to be quoted. It was a mistake not to quote them.
|
|
// The above code that quotes the fields was used for kazoo, so the unquoted strings are only use for sipauthserve.
|
|
// Also remove the extraneous leading comma.
|
|
// RFC3261 says the fields are quoted.
|
|
//msg->msmAuthorizationValue = format("Digest, nonce=%s, uri=%s, response=%s",RAND.c_str(),msid.mImsi.c_str(),SRES);
|
|
msg->msmAuthorizationValue = format("Digest nonce=\"%s\", uri=\"%s\", response=\"%s\"",RAND.c_str(),msid.mImsi.c_str(),SRES);
|
|
}
|
|
}
|
|
} else if (wMethod == SIPDTUnregister ) {
|
|
expires = 0;
|
|
} else { assert(0); }
|
|
// We use myURI instead of localContact because the SIPDialog for REGISTER is shared by all REGISTER
|
|
// users and does not contain the personal info for this user.
|
|
//msg->msmContactValue = format("<%s>;expires=%u",myUriString.c_str(),expires);
|
|
msg->msmContactValue = localContact(username,expires);
|
|
SipCallbacks::writePrivateHeaders(msg,chan);
|
|
return msg;
|
|
}
|
|
|
|
void SipDialog::dgReset()
|
|
{
|
|
mPrevDialogState = DialogState::dialogUndefined; sipStopTimers();
|
|
//mDownlinkFifo.clear();
|
|
}
|
|
|
|
|
|
void SipDialog::MODSendBYE(TermCause cause)
|
|
{
|
|
LOG(INFO) <<sdbText();
|
|
LOG(INFO) << "SIP term info MODSendBYE cause: " << cause; // SVGDBG
|
|
|
|
setSipState(MODClearing);
|
|
SipMOByeTU *byeTU = new SipMOByeTU(this,cause.tcGetSipReasonHeader());
|
|
//byeTU->mstOutRequest.addCallTerminationReasonSM(CallTerminationCause::eQ850, cause.tcGetCCCause(), ""); // MODSendBYE
|
|
byeTU->sctStart();
|
|
}
|
|
|
|
void SipDialog::sendInfoDtmf(unsigned bcdkey)
|
|
{
|
|
// Has a previous DTMF not finished yet?
|
|
|
|
// Start a new Sip INFO Transaction to send the key off.
|
|
SipDtmfTU *dtmfTU = new SipDtmfTU(this,bcdkey);
|
|
dtmfTU->sctStart();
|
|
}
|
|
|
|
// (pat) This is the post-l3-rewrite way, most initialization during construction.
|
|
SipDialog *SipDialog::newSipDialogMT(DialogType dtype, SipMessage *req)
|
|
{
|
|
LOG(DEBUG);
|
|
assert(dtype == SIPDTMTC || dtype == SIPDTMTSMS);
|
|
string proxy = req->smGetProxy(); // Get it from the top via.
|
|
if (proxy.empty()) {
|
|
LOG(ERR) << "Missing proxy (from top via) in MT SIP message:"<<req;
|
|
// Guess at a proxy and try to keep going.
|
|
proxy = gConfig.getStr(dtype == SIPDTMTSMS ? "SIP.Proxy.SMS" : "SIP.Proxy.Speech");
|
|
}
|
|
SipDialog *dialog = new SipDialog(dtype,proxy,"INVITE or MESSAGE via");
|
|
// 2-2014: RFC 3267 8.2.6.2 says the UAS (sip server) MUST add a "to" tag to a response, and MAY add a "to" tag to a provisional (100) response.
|
|
// The reason is in case the request is forked, the client could distinguish responses from multiple servers, a case that would not happen for us.
|
|
dialog->dsSetLocalHeaderMT(&req->msmTo,dtype == SIPDTMTC);
|
|
dialog->dsSetRemoteHeader(&req->msmFrom);
|
|
//dialog->mSipUsername = req->smUriUsername(); // IMSI/TMSI is in both the URI and the To: header.
|
|
// TODO: Validate username - must be valid IMSI or TMSI.
|
|
ScopedLock lock(dialog->mDialogLock,__FILE__,__LINE__); // probably unnecessary.
|
|
dialog->dsSetCallId(req->msmCallId);
|
|
dialog->mSdpOffer = req->msmBody; // Only useful for MTC, a no-op for MTSMS.
|
|
dialog->saveInviteOrMessage(req,false);
|
|
gSipInterface.dmAddCallDialog(dialog);
|
|
return dialog;
|
|
}
|
|
|
|
// There is just one SipDialog that handles all REGISTER requests.
|
|
SipDialog *SipDialog::newSipDialogRegister1() // caller imsi
|
|
{
|
|
LOG(DEBUG);
|
|
SipDialog *dialog = new SipDialog(SIPDTRegister,gConfig.getStr("SIP.Proxy.Registration"),"SIP.Proxy.Registration");
|
|
// RFC3261 10.2: REGISTER fields are different from normal requests.
|
|
// The Request URL is the IP address (only) of the Registrar.
|
|
// The To: is the 'address of record' formatted as a SIP URI.
|
|
// The From: is the 'responsible party' and is equal to To: unless it is a third-party registration.
|
|
// What about tags? I dont think it needs them because it is not a dialog creating request, but we add them
|
|
// anyway and it hasn't hurt anything.
|
|
dialog->dsSetCallId(globallyUniqueId(""));
|
|
gSipInterface.dmAddCallDialog(dialog);
|
|
return dialog;
|
|
}
|
|
|
|
// Open an MOSMS [Mobile Originated Short Message Service] SIP Transaction and send the invite.
|
|
// We use a dialog for this even though it is just a message because it was easier to interface
|
|
// to the Control directory without changing anything.
|
|
SipDialog *SipDialog::newSipDialogMOSMS(
|
|
TranEntryId tranid,
|
|
const FullMobileId &fromMsId, // caller imsi
|
|
const string &calledDigits, // number being called, or it may be config option SIP.SMSC
|
|
const string &body,
|
|
const string &contentType)
|
|
{
|
|
LOG(DEBUG) <<LOGVAR(fromMsId)<<LOGVAR2("called",calledDigits); //<<LOGVAR2("tranid",wTranId);
|
|
// This is weird - use the local IP address as the domain of the remote user?
|
|
SipDialog *dialog = new SipDialog(SIPDTMOSMS,gConfig.getStr("SIP.Proxy.SMS"),"SIP.Proxy.SMS");
|
|
dialog->dsSetLocalMO(fromMsId, true);
|
|
string calledDomain = dialog->localIP();
|
|
dialog->dsSetRemoteUri(makeUri(calledDigits,calledDomain));
|
|
dialog->smsBody = body; // Temporary until smqueue is fixed.
|
|
dialog->smsContentType = contentType; // Temporary until smqueue is fixed.
|
|
|
|
// Must lock once we do dmAddCallDialog to prevent the SIPInterface threads from accessing this dialog
|
|
// while we finish construction.
|
|
ScopedLock lock(dialog->mDialogLock,__FILE__,__LINE__);
|
|
gSipInterface.dmAddCallDialog(dialog);
|
|
//dialog->MOSMSSendMESSAGE(calledDigits,calledDomain,body,contentType);
|
|
gNewTransactionTable.ttSetDialog(tranid,dialog); // Must do this before the dialog receives any messages.
|
|
dialog->MOSMSSendMESSAGE(body,contentType);
|
|
return dialog;
|
|
}
|
|
|
|
|
|
// SIP-URI = "sip:" [ userinfo ] hostport
|
|
// userinfo = ( user / telephone-subscriber ) [ ":" password ] "@"
|
|
// user = 1*( unreserved / escaped / user-unreserved )
|
|
// unreserved = alphanum / mark
|
|
// mark = "-" / "_" / "." / "!" / "~" / "*" / "'" / "(" / ")"
|
|
// escaped = "%" HEXDIG HEXDIG
|
|
// user-unreserved = "&" / "=" / "+" / "$" / "," / ";" / "?" / "/"
|
|
// Any other character needs to be escaped. RFC 2396.
|
|
//
|
|
// In general, URI encoding (RFC2396) is kind of complicated - for example, we need a-priori knowledge if ";" in the string is
|
|
// marking a URI parameter, in which case it needs to be left alone, or if it is part of the URI component, in which case it must be escaped.
|
|
// RFC3267 talks about URBNF the URI string may contain:
|
|
// This handles the USSD special case. A USSD request can contain only digits, letters * and #,
|
|
// and of these the only special one is "#" turns into "%23".
|
|
string escapeUssdUri(string ss)
|
|
{
|
|
size_t pos = 0;
|
|
while (1) {
|
|
pos = ss.find(pos,'#');
|
|
if (pos == string::npos) return ss;
|
|
ss.replace(pos,1,"%23"); // it is ok not to advance pos because replacement "%23" does not include search "#".
|
|
}
|
|
return ss;
|
|
}
|
|
|
|
SipDialog *SipDialog::newSipDialogMOUssd(
|
|
TranEntryId tranid,
|
|
const FullMobileId &fromMsId, // caller imsi
|
|
const string &wUssd, // USSD string entered by user to send to network.
|
|
L3LogicalChannel *chan
|
|
)
|
|
{
|
|
LOG(DEBUG) << "MOUssd (INVITE)"<<LOGVAR(fromMsId)<<LOGVARM(wUssd);
|
|
// TODO: The SIPEngine constructor calls sipSetUser. FIX IT. Maybe I just need to replace SIPEngine.
|
|
const char *proxyOption = "SIP.Proxy.USSD";
|
|
string proxy = gConfig.getStr(proxyOption);
|
|
LOG(DEBUG) << LOGVAR(proxyOption) <<LOGVAR(proxy);
|
|
if (proxy.length() > 259) { // TODO: This should be in the config checker, if anywhere.
|
|
LOG(ALERT) << "Configured " <<proxyOption <<" hostname is greater than 253 bytes!";
|
|
}
|
|
SipDialog *dialog = new SipDialog(SIPDTMOUssd,proxy,proxyOption);
|
|
dialog->dsSetLocalMO(fromMsId,true);
|
|
gReports.incr("OpenBTS.SIP.INVITE.Out");
|
|
// Must lock once we do dmAddCallDialog to prevent the SIPInterface threads from accessing this dialog
|
|
// while we finish construction.
|
|
ScopedLock lock(dialog->mDialogLock,__FILE__,__LINE__); // Must lock before dmAddCallDialog.
|
|
|
|
if (proxy == "testmode") {
|
|
gNewTransactionTable.ttSetDialog(tranid,dialog); // Must do this before the dialog receives any messages.
|
|
DialogUssdMessage *dmsg = new DialogUssdMessage(tranid,DialogState::dialogBye,0);
|
|
dmsg->dmMsgPayload = "Hello from OpenBTS. You entered:"+wUssd;
|
|
LOG(DEBUG) << "USSD test mode"<<LOGVAR(chan)<<LOGVAR(tranid)<<LOGVAR(fromMsId)<<dmsg->dmMsgPayload;
|
|
dialog->dialogQueueMessage(dmsg);
|
|
return dialog;
|
|
}
|
|
|
|
// Yes, you really do put the USSD request in the To: of the SIP as per 3GPP 24.090 Appendix A, but we need to
|
|
// escape the "#" character. We also add a "dialstring" tag.
|
|
// Like this: INVITE sip:*135%23;phone-context=home1.net;user=dialstring SIP/2.0
|
|
// The obvious way to pass a USSD string is as a "tel:" domain, but evidently that was too easy, so we
|
|
// must add the user=dialstring tag as per RFC 4967, and then THAT requires a "context" as per RFC3966 because
|
|
// this is not a "global" tel number, and therefore by definition it must be a "local" tel number, which requires
|
|
// a context, but we learn from RFC 4967 that we can stick in anything we want for the context, since it is meaningless
|
|
// when used for a USSD string. This spec is not exactly an "A-team" effort.
|
|
|
|
string ussdEscaped = escapeUssdUri(wUssd);
|
|
string ussdPerSpec = ussdEscaped + ";phone-context=irrelevant.net;user=dialstring";
|
|
dialog->dsSetRemoteUri(makeUri(ussdPerSpec,dialog->localIP()));
|
|
// TODO: What about codecs? The example in 24.390 annex A has them.
|
|
|
|
gSipInterface.dmAddCallDialog(dialog);
|
|
gNewTransactionTable.ttSetDialog(tranid,dialog); // Must do this before the dialog receives any messages.
|
|
dialog->MOUssdSendINVITE(wUssd,chan);
|
|
return dialog;
|
|
}
|
|
|
|
// Open an MOC [Mobile Originated Call] dialog and send the invite.
|
|
SipDialog *SipDialog::newSipDialogMOC(
|
|
TranEntryId tranid,
|
|
const FullMobileId &fromMsId, // caller imsi
|
|
const string &wCalledDigits, // number being called, or empty for an emergency call.
|
|
CodecSet wCodecs, // phone capabilities
|
|
L3LogicalChannel *chan
|
|
)
|
|
{
|
|
|
|
LOG(DEBUG) << "MOC SIP (INVITE)"<<LOGVAR(fromMsId)<<LOGVAR2("called",wCalledDigits);
|
|
// TODO: The SIPEngine constructor calls sipSetUser. FIX IT. Maybe I just need to replace SIPEngine.
|
|
const char *proxyOption = "SIP.Proxy.Speech";
|
|
|
|
string proxy = gConfig.getStr(proxyOption);
|
|
LOG(DEBUG) << LOGVAR(proxyOption) <<LOGVAR(proxy);
|
|
if (proxy.length() > 259) { // TODO: This should be in the config checker, if anywhere.
|
|
LOG(ALERT) << "Configured " <<proxyOption <<" hostname is greater than 253 bytes!";
|
|
}
|
|
|
|
SipDialog *dialog = new SipDialog(SIPDTMOC,proxy,proxyOption);
|
|
dialog->dsSetLocalMO(fromMsId,true);
|
|
|
|
{
|
|
gReports.incr("OpenBTS.SIP.INVITE.Out");
|
|
dialog->dsSetRemoteUri(makeUri(wCalledDigits,dialog->localIP()));
|
|
}
|
|
|
|
string username = fromMsId.fmidUsername();
|
|
if (username.length()) {
|
|
devassert(username.substr(0,4) == "IMSI");
|
|
string pAssociatedUri, pAssertedIdentity;
|
|
gTMSITable.getSipIdentities(username.substr(4),pAssociatedUri,pAssertedIdentity); // They may be empty.
|
|
dialog->dsPAssociatedUri = pAssociatedUri;
|
|
dialog->dsPAssertedIdentity = pAssertedIdentity;
|
|
WATCH("MOC"<<LOGVAR(username)<<LOGVAR(pAssociatedUri)<<LOGVAR(pAssertedIdentity));
|
|
//if (pAssociatedUri.size()) { invite->smAddHeader("P-Associated-URI",pAssociatedUri); }
|
|
//if (pAssertedIdentity.size()) { invite->smAddHeader("P-Asserted-Identity",pAssertedIdentity); }
|
|
}
|
|
|
|
dialog->mRTPPort = Control::allocateRTPPorts();
|
|
dialog->mCodec = wCodecs;
|
|
|
|
// Must lock once we do dmAddCallDialog to prevent the SIPInterface threads from accessing this dialog
|
|
// while we finish construction.
|
|
ScopedLock lock(dialog->mDialogLock,__FILE__,__LINE__); // Must lock before dmAddCallDialog.
|
|
gSipInterface.dmAddCallDialog(dialog);
|
|
gNewTransactionTable.ttSetDialog(tranid,dialog); // Must do this before the dialog receives any messages.
|
|
dialog->MOCSendINVITE(chan);
|
|
return dialog;
|
|
}
|
|
|
|
// This is called in BS2 after a handover complete is received. It is an inbound handover, but an outoing MO re-INVITE.
|
|
// We take the SIP REFER message created by BS1 and send it to the SIP server as a re-INVITE.
|
|
// Note that the MS may go from BS1 to BS2 and back to BS1, in which case there may
|
|
// already be an existing dialog in some non-Active state.
|
|
SipDialog *SipDialog::newSipDialogHandover(TranEntry *tran, string sipReferStr)
|
|
{
|
|
LOG(DEBUG)<<LOGVAR(tran) <<LOGVAR(sipReferStr);
|
|
static const string inviteStr("INVITE");
|
|
|
|
// Init the Dialog State from the SIP REFER message.
|
|
SipMessage *msg = sipParseBuffer(sipReferStr.c_str());
|
|
if (msg == NULL) { return NULL; } // Message already printed.
|
|
SipUri referto(msg->msmHeaders.paramFind("Refer-To"));
|
|
string proxy = referto.uriHostAndPort();
|
|
// 7-23 wrong: SipDialog *dialog = new SipDialog(SIPDTMTC,proxy);
|
|
SipDialog *dialog = new SipDialog(SIPDTMOC,proxy,"REFER message");
|
|
dialog->mIsHandover = true;
|
|
dialog->dsSetRemoteHeader(&msg->msmTo);
|
|
dialog->dsSetLocalHeader(&msg->msmFrom);
|
|
dialog->dsSetCallId(msg->msmCallId);
|
|
// TODO: If any other intervening messages were sent by BTS1 between the REFER and now the CSeqNum will not be correct.
|
|
dialog->mLocalCSeq = msg->msmCSeqNum + 1;
|
|
// We copied the peer SDP we got from the SIP server into the handover message passed from BS1 to BS2;
|
|
// I dont think we need to save sdpResponse here - we are going to use it for the last time immediately below.
|
|
dialog->mCodec = tran->getCodecs(); // TODO: We need to renegotiate this, or set it from SDP. There is no point even setting this here.
|
|
|
|
// Get remote RTP from SIP REFER message, init RTP, create new SDP offer from previous SDP response.
|
|
// The incoming SDP has the codec previously negotiated, so it should still be ok.
|
|
dialog->mRTPPort = Control::allocateRTPPorts();
|
|
SdpInfo sdpRemote;
|
|
sdpRemote.sdpParse(msg->msmBody);
|
|
SdpInfo sdpLocal = sdpRemote; // In particular, we are copying the sessionId and versionId.
|
|
// Send our local RTP port to the SIP server.
|
|
sdpLocal.sdpRtpPort = dialog->mRTPPort;
|
|
sdpLocal.sdpHost = dialog->localIP();
|
|
dialog->mSdpOffer = sdpLocal.sdpValue();
|
|
|
|
// Make the re-INVITE
|
|
SipMessage *invite = dialog->makeInitialRequest(inviteStr);
|
|
invite->smAddBody(string("application/sdp"),dialog->mSdpOffer);
|
|
|
|
// Send it off.
|
|
ScopedLock lock(dialog->mDialogLock,__FILE__,__LINE__);
|
|
gSipInterface.dmAddCallDialog(dialog);
|
|
dialog->moWriteLowSide(invite);
|
|
delete invite; // moWriteLowSide saved a copy of this.
|
|
dialog->setSipState(HandoverInbound);
|
|
|
|
return dialog;
|
|
}
|
|
|
|
|
|
SipDialog::~SipDialog()
|
|
{
|
|
// nothing
|
|
}
|
|
|
|
TranEntry *SipDialog::findTranEntry()
|
|
{
|
|
if (this->mTranId == 0) {
|
|
// No attached transaction. Can happen if we jumped the gun (the dialog is created before the transaction
|
|
// and there could be a race with an incoming message) or if we responded with an early error
|
|
// to a dialog and never created a transaction for it, for example, 486 Busy Here.
|
|
return NULL;
|
|
}
|
|
return gNewTransactionTable.ttFindById(this->mTranId);
|
|
}
|
|
|
|
// If it is not an IMSI we think it may be a phone number.
|
|
static bool isPhoneNumber(string thing)
|
|
{
|
|
if (thing.size() == 0) { return false; } // Not a phone number.
|
|
if (0 == strncasecmp(thing.c_str(),"IMSI",4)) { return false; } // It is an IMSI, not a phone number.
|
|
return true; // Well, maybe it is a phone number.
|
|
}
|
|
|
|
static string removeUriFluff(string thing)
|
|
{
|
|
if (unsigned first = thing.find('<') != string::npos) { // Remove the angle brackets.
|
|
thing = thing.substr(first); // chop off initial angle bracket.
|
|
thing = thing.substr(0,thing.find_last_of('>')); // chop off trailing angle bracket, and anything following.
|
|
}
|
|
thing = thing.substr(0,thing.find_last_of('@')); // Chop off the ip address, if any.
|
|
const char *str = thing.c_str();
|
|
// Very clever, that a phone number may be prefixed with either sip: or tel:
|
|
if (0 == strncasecmp(str,"sip:",4) || 0 == strncasecmp(str,"tel:",4)) {
|
|
thing = thing.substr(4);
|
|
} else if (0 == strncasecmp(str,"sips:",5)) { // secure not supported, but always hopeful....
|
|
thing = thing.substr(5);
|
|
}
|
|
LOG(DEBUG) <<LOGVAR(thing);
|
|
return thing; // Hopefully, what is remaining is a phone number.
|
|
}
|
|
|
|
|
|
TranEntry *SipDialog::createMTTransaction(SipMessage *invite)
|
|
{
|
|
TranEntry *tran = NULL;
|
|
string callerId;
|
|
string callerIdSource = gConfig.getStr("GSM.CallerID.Source");
|
|
if (0 == strcasecmp(callerIdSource.c_str(),"auto")) {
|
|
// (pat 6-2014) Added automatic caller id identification.
|
|
// We can do this automatically because we know whether the displayname, etc, are imsi because they are preceded by "IMSI".
|
|
// btw, the P-Asserted-Identity should by an IMSI; the phone number is supposed to be in the P-Associated-URI.
|
|
callerId = sipRemoteDisplayname(); // This is out best guess.
|
|
LOG(DEBUG) << "CallerID=auto: sipRemoteDisplayname="<<callerId;
|
|
if (! isPhoneNumber(callerId)) {
|
|
// Well that wasnt it, try again...
|
|
callerId = removeUriFluff(invite->msmHeaders.paramFind("P-Associated-URI"));
|
|
LOG(DEBUG) << "CallerID=auto: P-Associated-URI="<<callerId;
|
|
}
|
|
if (! isPhoneNumber(callerId)) {
|
|
// Keep trying...
|
|
callerId = removeUriFluff(invite->msmHeaders.paramFind("P-Asserted-Identity"));
|
|
LOG(DEBUG) << "CallerID=auto: P-Asserted-Identity="<<callerId;
|
|
}
|
|
if (! isPhoneNumber(callerId)) {
|
|
// The SIP username is not likely a phone number, but it is our last hope.
|
|
callerId = removeUriFluff(sipRemoteUsername());
|
|
LOG(DEBUG) << "CallerID=auto: sipRemoteUserName="<<callerId;
|
|
}
|
|
if (! isPhoneNumber(callerId)) {
|
|
callerId = string(""); // Did not find a phone number, sigh, zero it out.
|
|
LOG(DEBUG) << "CallerID=auto: giving up";
|
|
}
|
|
} else if (callerIdSource.compare("username") == 0) {
|
|
callerId = sipRemoteUsername();
|
|
LOG(INFO) << "source=username, callerId = " << callerId;
|
|
} else if (callerIdSource.compare("p-asserted-identity") == 0) {
|
|
string tmpcid = invite->msmHeaders.paramFind("P-Asserted-Identity");
|
|
unsigned first = tmpcid.find("<sip:");
|
|
unsigned last = tmpcid.find_last_of("@");
|
|
callerId = tmpcid.substr(first+5, last-first-5);
|
|
LOG(INFO) << "source=p-asserted-identity, callerId = " << callerId;
|
|
} else {
|
|
callerId = sipRemoteDisplayname();
|
|
LOG(INFO) << "source=username, callerId = " << callerId;
|
|
}
|
|
FullMobileId msid;
|
|
msid.mImsi = invite->smGetInviteImsi();
|
|
if (invite->isINVITE()) {
|
|
tran = TranEntry::newMTC(this,msid,GSM::L3CMServiceType::MobileTerminatedCall,callerId);
|
|
// Tell the sender we are trying.
|
|
this->MTCSendTrying();
|
|
} else {
|
|
devassert(0);
|
|
}
|
|
return tran;
|
|
}
|
|
|
|
// If the cause is handoverOutbound, kill the dialog now: dont send a BYE, dont wait for any other incoming messsages.
|
|
// Used for outbound handover, where the SIP session was transferred to another BTS.
|
|
// cause == This is the reason a Transaction (TranEntry) was cancelled
|
|
// Layer3 may call this multiple times just to be safe and make sure the dialog is truly cancelled;
|
|
// generally the first call includes the real causes, then the second call is made when the transaction
|
|
// is destroyed and the cause is some bogus cause.
|
|
void SipDialog::dialogCancel(TermCause cause)
|
|
{
|
|
SIP::SipState state = this->getSipState();
|
|
//bool bTerminationAdded = false;
|
|
|
|
LOG(INFO) << "SIP term info dialogCancel begin cancel cause: " << cause; // SVGDBG
|
|
|
|
WATCH("dialogCancel"<<LOGVAR(state)<<LOGVAR(cause) )
|
|
ScopedLock lock(mDialogLock,__FILE__,__LINE__);
|
|
|
|
LOG(DEBUG) << dialogText(); // "SIP state " << state; //SVGDBG switch to LOG(INFO)
|
|
|
|
if (cause.tcGetValue() == L3Cause::Handover_Outbound) {
|
|
// Terminate the dialog instantly. Dont send anything on the SIP interface.
|
|
sipStopTimers();
|
|
// We need to remove the callid of the terminated outbound dialog queue from SIPInterface in case
|
|
// the same call is handerovered back, it would then be a duplicate.
|
|
gSipInterface.dmRemoveDialog(this);
|
|
LOG(INFO) << "SIP term info dialogCancel dmRemoveDialog return";
|
|
return;
|
|
}
|
|
|
|
// why aren't we checking for failed here? -kurtis ; we are now. -david
|
|
if (this->sipIsFinished()) {
|
|
// No bye or cancel message will be sent.
|
|
LOG(INFO) << "SIP term info dialogCancel sipIsFinished return SIP state: " << this->getSipState();
|
|
return;
|
|
}
|
|
switch (mDialogType) {
|
|
case SIPDTRegister:
|
|
case SIPDTUnregister:
|
|
// The Register is not a full dialog so we dont send anything when we cancel.
|
|
break;
|
|
case SIPDTMOSMS:
|
|
case SIPDTMTSMS:
|
|
case SIPDTMOUssd:
|
|
setSipState(Cleared);
|
|
break;
|
|
case SIPDTMTC:
|
|
case SIPDTMOC:
|
|
switch (state) {
|
|
case SSTimeout:
|
|
case MOSMSSubmit: // Should never see a message state in an INVITE dialog.
|
|
LOG(ERR) "Unexpected SIP State:"<<state;
|
|
break;
|
|
case Active: // (pat) MOC received OK; MTC sent ACK
|
|
caseActive:
|
|
//Changes state to clearing
|
|
this->MODSendBYE(cause);
|
|
//bTerminationAdded = true;
|
|
//then cleared
|
|
sipStartTimers(); // formerly: MODWaitForBYEOK();
|
|
break;
|
|
case SSNullState: // (pat) MTC initial state - nothing sent yet. MOC not used because sends INVITE on construction.
|
|
case Starting: // (pat) MOC or MOSMS or inboundHandover sent INVITE; MTC not used.
|
|
case Proceeding: // (pat) MOC received Trying, Queued, BeingForwarded; MTC sent Trying
|
|
case Ringing: // (pat) MOC received Ringing, notably not used for MTC sent Ringing.
|
|
case MOCBusy: // (pat) MOC received Busy; MTC not used.
|
|
case Connecting: // (pat) MTC sent OK.
|
|
case HandoverInbound:
|
|
if (mDialogType == SIPDTMOC) {
|
|
// To cancel the invite before the ACK is received we must send CANCEL instead of BYE.
|
|
this->MODSendCANCEL(cause); //Changes state to MODCanceling
|
|
//bTerminationAdded = true;
|
|
} else {
|
|
// We are the INVITE recipient server and have not received the ACK yet, so we must send an error response.
|
|
// Way back in version 3 this was used for MTC also.
|
|
// RFC3261 (SIP) is internally inconsistent describing the error codes - the 4xxx and 5xx generic
|
|
// descriptions are contradicted by specific error code descriptions.
|
|
// This is from Paul Chitescu at Null Team:
|
|
// "A 504 Server Timeout seems the most adequate response [to MS not responding to page.]
|
|
// 408 is reserved for SIP protocol timeouts (no answer to SIP message)
|
|
// 504 indicates some other timeout beyond SIP (interworking)
|
|
// 480 indicates some temporary form of resource unavailability or congestion but
|
|
// resource is accessible and can be checked"
|
|
// 486 "Busy Here" implies that we found the MS but it really is busy.
|
|
// 503 indicates the service is unavailable but does not imply for how long
|
|
// TODO: We should probably send different codes for different reasons.
|
|
// Note: We previously sent 480.
|
|
//this->MTCEarlyError(480, "Temporarily Unavailable"); // The message must be 300-699.
|
|
|
|
if (cause.tcGetCCCause() == L3Cause::Normal_Call_Clearing) {
|
|
// (pat 7-2014) The handset MTC hung up normally but the SIP Dialog here is not in the Active state.
|
|
// This weird case occurs if the handset sends a disconnect before it sends a connect,
|
|
// which can happen on some if the user answers and hangs up immediately.
|
|
// We need to send a 200 OK and then BYE instead of sending an early error.
|
|
// We dont care about codecs because we are clearing immediately so just regurgitate the codecs from the INVITE.
|
|
LOG(DEBUG) << "SIP SPECIAL CASE: Disconnect before connect";
|
|
this->MTCSendOK(this->vGetCodecs(), NULL);
|
|
goto caseActive;
|
|
} else {
|
|
this->MTCEarlyError(cause);
|
|
}
|
|
#if PREVIOUS_CODE
|
|
// (pat) Keeping this old code here for a while to show what SIP responses were returned by the version 4 code.
|
|
// Now SIP responses are formulated from the TermCause by tcGetSipCodeAndReason()
|
|
int sipcode = 486; const char *reason = "No answer";
|
|
switch (cause) {
|
|
case CancelCauseHandoverOutbound:
|
|
case CancelCauseSipInternalError:
|
|
assert(0); // handled above
|
|
case CancelCauseNormalDisconnect: // 0 Loss of contact with MS or an error.
|
|
case CancelCauseBusy: // MS is here and unavailable.
|
|
sipcode = 486; reason = "Busy Here";
|
|
break;
|
|
|
|
case CancelCauseUnknown: // 0 Loss of contact with MS or an error.
|
|
if (l3Cause == L3Cause::SwitchingEquipmentCongestion) {
|
|
sipcode = 503; reason = "Normal circuit congestion";
|
|
}
|
|
else if (l3Cause == L3Cause::NoUserResponding) {
|
|
sipcode = 408; reason = "No user responding";
|
|
}
|
|
else if (l3Cause == L3Cause::CallRejected) {
|
|
sipcode = 603; reason = "Call rejected";
|
|
}
|
|
|
|
break;
|
|
|
|
case CancelCauseCongestion: // This reason is never used MS is here but no channel avail or other congestion.
|
|
sipcode = 503; reason = "Normal circuit congestion";
|
|
break;
|
|
|
|
case CancelCauseNoAnswerToPage: // Not used We dont have any clue if the MS is in this area or not.
|
|
// The MS is not here or turned off.
|
|
sipcode = 408; reason = "No user responding";
|
|
break;
|
|
case CancelCauseOperatorIntervention:
|
|
sipcode = 487; reason = "Request Terminated Operator Intervention";
|
|
break;
|
|
}
|
|
this->MTCEarlyError(sipcode,reason); // The message must be 300-699.
|
|
#endif
|
|
//bTerminationAdded = true;
|
|
}
|
|
break;
|
|
case MODClearing: // (pat) MOD sent BYE
|
|
case MODCanceling: // (pat) MOD sent a cancel.
|
|
case MODError: // (pat) MOD sent an error response.
|
|
case MTDClearing: // (pat) MTD received BYE.
|
|
case MTDCanceling: // (pat) MTD received CANCEL
|
|
case Canceled: // (pat) received OK to CANCEL.
|
|
case Cleared: // (pat) MTD sent OK to BYE, or MTD internal error loss of FIFO, or MOSMS received OK, or MTSMS sent OK.
|
|
case SSFail:
|
|
// Some kind of clearing already in progress. Do not repeat.
|
|
break;
|
|
case HandoverOutbound: // We never used this state.
|
|
// Not sure what to do with these.
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
assert(0);
|
|
break;
|
|
}
|
|
|
|
LOG(INFO) << "SIP term info end dialogCancel, cancel cause: " << cause << " mDialogType: " << mDialogType <<LOGVAR(state)<<LOGVAR(cause);
|
|
// << " bTerminationAdded: " << bTerminationAdded;
|
|
|
|
// (pat) The TermCause has the complete termination reason. The cause was sent in the error response, CANCEL message, or BYE message;
|
|
// we dont need to save it in the dialog.
|
|
// Save termination reason
|
|
//if (!bTerminationAdded) {
|
|
// if (! cause.tcIsEmpty()) { // Don't log unknown reason codes
|
|
// LOG(INFO) << "SIP term info add message from dialogCancel"<<LOGVAR(cause); // SVGDBG
|
|
// addCallTerminationReasonDlg(CallTerminationCause::eQ850, cause.tcGetCCCause(), ""); // dialCancel
|
|
// }
|
|
//}
|
|
|
|
} // dialogCancel
|
|
|
|
|
|
void SipEngine::dialogQueueMessage(DialogMessage *dmsg)
|
|
{
|
|
// This was used when there was just one layer3 thread:
|
|
// TODO: We may still use this for UMTS.
|
|
//Control::gCSL3StateMachine.csl3Write(new Control::GenericL3Msg(dmsg,callID()));
|
|
// Now we enqueue dialog messages in a queue in their dialog, and let L3 fish it out from there.
|
|
// We dont enqueue on the GSM LogicalChannel because that may change from, eg, SDCCH to FACCH before this message is processed.
|
|
LOG(DEBUG) << "sending DialogMessage to L3 " /*<<dialogText()*/ <<LOGVAR(dmsg);
|
|
//mDownlinkFifo.write(dmsg);
|
|
if (mTranId == 0) {
|
|
// pat 5-2014: There will not be a transaction if the dialog was cancelled in handleInvite because the
|
|
// called user is already busy with no transaction slot available. This case will go away when
|
|
// we support call-hold/call-wait.
|
|
LOG(DEBUG) << "Warning: dialog with no attached transaction; DialogMessage ignored.";
|
|
delete dmsg;
|
|
return;
|
|
}
|
|
gNewTransactionTable.ttAddMessage(mTranId,dmsg);
|
|
}
|
|
|
|
bool SipDialog::permittedTransition(DialogState::msgState oldState, DialogState::msgState newState)
|
|
{
|
|
if (newState > oldState) { return true; } // That was easy!
|
|
if (newState == oldState) {
|
|
// Allow multiple proceeding/ringing notifications:
|
|
if (newState == DialogState::dialogProceeding || newState == DialogState::dialogRinging) { return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void SipDialog::dialogPushState(
|
|
SipState newSipState, // The new sip state.
|
|
int code, // The SIP message code that caused the state change, or 0 for ACK or total failures.
|
|
char timer)
|
|
{
|
|
SipState oldSipState = getSipState();
|
|
DialogState::msgState oldDialogState = getDialogState();
|
|
setSipState(newSipState);
|
|
|
|
// If it is a new state, inform L3.
|
|
DialogState::msgState nextDialogState = getDialogState(); // based on the newSipState we just set.
|
|
LOG(DEBUG) <<LOGVAR(oldSipState)<<LOGVAR(newSipState)<<LOGVAR(getSipState())<<LOGVAR(mPrevDialogState)<<LOGVAR(oldDialogState)<<LOGVAR(nextDialogState)<<dialogText();
|
|
if (nextDialogState == DialogState::dialogStarted) {
|
|
// This state is used for MO transactions just to indicate the dialog is active,
|
|
// but the MO state machine already knows that since it created the dialog,
|
|
// so we dont return this state as a notification.
|
|
return;
|
|
}
|
|
if (permittedTransition(mPrevDialogState,nextDialogState)) {
|
|
DialogMessage *dmsg = new DialogMessage(mTranId,nextDialogState,code);
|
|
dialogQueueMessage(dmsg);
|
|
} else {
|
|
LOG(DEBUG) << "no dialog state change";
|
|
}
|
|
mPrevDialogState = nextDialogState;
|
|
|
|
// A timer may be specified if the SIP state is one indicating failure.
|
|
// The timer letter corresponds to one of those specified in RFC3261, and specifies the dialog
|
|
// should not be destroyed until the timer expires.
|
|
switch (timer) {
|
|
case 0:
|
|
break; // default, no timer specified.
|
|
case 'D':
|
|
// RFC3261 17.1.1.2 says set Timer D to 32s instead of 64*T1. Whatever.
|
|
if (dsPeer()->ipIsReliableTransport()) {
|
|
mTimerD.setOnce(32000);
|
|
}
|
|
break;
|
|
case 'K':
|
|
mTimerK.setOnce(T4);
|
|
break;
|
|
default: assert(0);
|
|
}
|
|
}
|
|
|
|
|
|
void SipDialog::dialogChangeState(
|
|
SipMessage *sipmsg) // The message that caused the state change, or NULL for total failures.
|
|
{
|
|
dialogPushState(getSipState(),sipmsg?sipmsg->smGetCode():0);
|
|
//LOG(DEBUG) <<dialogText();
|
|
//// If it is a new state, inform L3.
|
|
//DialogState::msgState nextDialogState = getDialogState();
|
|
//if (nextDialogState == DialogState::dialogStarted) {
|
|
// // This state is used for MO transactions just to indicate the dialog is active,
|
|
// // but the MO state machine already knows that since it created the dialog,
|
|
// // so we dont return this state as a notification.
|
|
// return;
|
|
//}
|
|
//if (permittedTransition(mPrevDialogState,nextDialogState)) {
|
|
// unsigned code = sipmsg ? sipmsg->smGetCode() : 0;
|
|
// DialogMessage *dmsg = new DialogMessage(mTranId,nextDialogState,code);
|
|
// // done by the register TU
|
|
// dialogQueueMessage(dmsg);
|
|
//} else {
|
|
// LOG(DEBUG) << "no dialog state change";
|
|
//}
|
|
//mPrevDialogState = nextDialogState;
|
|
}
|
|
|
|
// Only a small subset of SIP states are passed to the L3 Control layer as dialog states.
|
|
DialogState::msgState SipDialog::getDialogState() const
|
|
{
|
|
// Do not add a default case so that if someone adds a new SipState they will get a warning here.
|
|
// Therefore we define every state including the impossible ones.
|
|
switch (getSipState()) {
|
|
case SSNullState:
|
|
return DialogState::dialogUndefined;
|
|
case Starting: // (pat) MOC or MOSMS or inboundHandover sent INVITE; MTC not used.
|
|
return DialogState::dialogStarted;
|
|
case Proceeding: // (pat) MOC received Trying, Queued, BeingForwarded; MTC sent Trying
|
|
case Connecting: // (pat) MTC sent OK.
|
|
// TODO: Is this correct for MTC Connecting?
|
|
return DialogState::dialogProceeding;
|
|
case Ringing: // (pat) MOC received Ringing, notably not used for MTC sent Ringing, which is probably a bug of no import.
|
|
return DialogState::dialogRinging;
|
|
case Active: // (pat) MOC received OK; MTC sent ACK
|
|
return DialogState::dialogActive;
|
|
|
|
case MODClearing: // (pat) MOD sent BYE
|
|
case MODCanceling: // (pat) MOD sent a cancel.
|
|
case MTDClearing: // (pat) MTD received BYE.
|
|
case MTDCanceling: // (pat) received CANCEL
|
|
case Canceled: // (pat) received OK to CANCEL.
|
|
case Cleared: // (pat) MTD sent OK to BYE, or MTD internal error loss of FIFO, or MOSMS received OK, or MTSMS sent OK.
|
|
return DialogState::dialogBye;
|
|
|
|
case MOCBusy: // (pat) MOC received Busy; MTC not used.
|
|
case SSTimeout:
|
|
case MODError: // (pat) MOD sent a cancel.
|
|
case SSFail:
|
|
return DialogState::dialogFail;
|
|
|
|
//case SipRegister: // (pat) This SIPEngine is being used for registration, none of the other stuff applies.
|
|
//case SipUnregister: // (pat) This SIPEngine is being used for registration, none of the other stuff applies.
|
|
case MOSMSSubmit: // (pat) SMS message submitted, "MESSAGE" method. Set but never used.
|
|
case HandoverInbound:
|
|
case HandoverOutbound:
|
|
return DialogState::dialogUndefined;
|
|
}
|
|
devassert(0);
|
|
return DialogState::dialogUndefined;
|
|
}
|
|
|
|
|
|
// Handle response to INVITE or MESSAGE.
|
|
// Only responses (>=200) to INVITE get an ACK. Specifically, not MESSAGE.
|
|
void SipDialog::handleInviteResponse(int status,
|
|
bool sendAck) // TRUE if transaction is INVITE. We used to use this for MESSAGE also, in which case it was false.
|
|
{
|
|
LOG(DEBUG) <<LOGVAR(status) <<LOGVAR(sendAck);
|
|
switch (status) {
|
|
// class 1XX: Provisional messages
|
|
case 100: // Trying
|
|
case 181: // Call Is Being Forwarded
|
|
case 182: // Queued
|
|
case 183: // Session Progress FIXME we need to setup the sound channel (early media)
|
|
dialogPushState(Proceeding,status);
|
|
break;
|
|
case 180: // Ringing
|
|
mReceived180 = true;
|
|
dialogPushState(Ringing,status);
|
|
break;
|
|
|
|
// class 2XX: Success
|
|
case 200: // OK
|
|
// Save the response and update the state,
|
|
// but the ACK doesn't happen until the call connects.
|
|
dialogPushState(Active,status);
|
|
break;
|
|
|
|
// class 3xx: Redirection
|
|
case 300: // Multiple Choices
|
|
case 301: // Moved Permanently
|
|
case 302: // Moved Temporarily
|
|
case 305: // Use Proxy
|
|
case 380: // Alternative Service
|
|
LOG(NOTICE) << "redirection not supported code " << status <<sdbText();
|
|
dialogPushState(SSFail,status, 'D');
|
|
gReports.incr("OpenBTS.SIP.Failed.Remote.3xx");
|
|
// TODO: What if it is not MOC?
|
|
if (sendAck) MOCSendACK();
|
|
break;
|
|
// Anything 400 or above terminates the call, so we ACK.
|
|
// FIXME -- It would be nice to save more information about the
|
|
// specific failure cause.
|
|
|
|
// class 4XX: Request failures
|
|
case 405: // Method Not Allowed
|
|
// We must not ACK to "405 Method Not Allowed" or you could have an infinite loop. Saw this with smqueue.
|
|
dialogPushState(SSFail,status, 'D');
|
|
gReports.incr("OpenBTS.SIP.Failed.Remote.4xx");
|
|
break;
|
|
|
|
case 400: // Bad Request
|
|
case 401: // Unauthorized: Used only by registrars. Proxys should use proxy authorization 407
|
|
case 402: // Payment Required (Reserved for future use)
|
|
case 403: // Forbidden
|
|
case 404: // Not Found: User not found
|
|
case 406: // Not Acceptable
|
|
case 407: // Proxy Authentication Required
|
|
case 408: // Request Timeout: Couldn't find the user in time
|
|
case 409: // Conflict
|
|
case 410: // Gone: The user existed once, but is not available here any more.
|
|
case 413: // Request Entity Too Large
|
|
case 414: // Request-URI Too Long
|
|
case 415: // Unsupported Media Type
|
|
case 416: // Unsupported URI Scheme
|
|
case 420: // Bad Extension: Bad SIP Protocol Extension used, not understood by the server
|
|
case 421: // Extension Required
|
|
case 422: // Session Interval Too Small
|
|
case 423: // Interval Too Brief
|
|
case 480: // Temporarily Unavailable
|
|
case 481: // Call/Transaction Does Not Exist
|
|
case 482: // Loop Detected
|
|
case 483: // Too Many Hops
|
|
case 484: // Address Incomplete
|
|
case 485: // Ambiguous
|
|
LOG(NOTICE) << "request failure code " << status <<sdbText();
|
|
dialogPushState(SSFail,status, 'D');
|
|
gReports.incr("OpenBTS.SIP.Failed.Remote.4xx");
|
|
if (sendAck) MOCSendACK();
|
|
break;
|
|
|
|
case 486: // Busy Here
|
|
LOG(NOTICE) << "remote end busy code " << status <<sdbText();
|
|
dialogPushState(MOCBusy,status,'D');
|
|
// TODO: What if it is not MOC?
|
|
if (sendAck) MOCSendACK();
|
|
break;
|
|
case 487: // Request Terminated
|
|
case 488: // Not Acceptable Here
|
|
case 491: // Request Pending
|
|
case 493: // Undecipherable: Could not decrypt S/MIME body part
|
|
LOG(NOTICE) << "request failure code " << status <<sdbText();
|
|
dialogPushState(SSFail,status,'D');
|
|
gReports.incr("OpenBTS.SIP.Failed.Remote.4xx");
|
|
if (sendAck) MOCSendACK();
|
|
break;
|
|
|
|
// class 5XX: Server failures
|
|
case 500: // Server Internal Error
|
|
case 501: // Not Implemented: The SIP request method is not implemented here
|
|
case 502: // Bad Gateway
|
|
case 503: // Service Unavailable
|
|
case 504: // Server Time-out
|
|
case 505: // Version Not Supported: The server does not support this version of the SIP protocol
|
|
case 513: // Message Too Large
|
|
LOG(NOTICE) << "server failure code " << status <<sdbText();
|
|
dialogPushState(SSFail,status,'D');
|
|
gReports.incr("OpenBTS.SIP.Failed.Remote.5xx");
|
|
// TODO: What if it is not MOC?
|
|
if (sendAck) MOCSendACK();
|
|
break;
|
|
|
|
// class 6XX: Global failures
|
|
case 600: // Busy Everywhere
|
|
case 603: // Decline
|
|
dialogPushState(MOCBusy,status,'D');
|
|
if (sendAck) MOCSendACK();
|
|
break;
|
|
case 604: // Does Not Exist Anywhere
|
|
case 606: // Not Acceptable
|
|
LOG(NOTICE) << "global failure code " << status <<sdbText();
|
|
dialogPushState(SSFail,status,'D');
|
|
gReports.incr("OpenBTS.SIP.Failed.Remote.6xx");
|
|
if (sendAck) MOCSendACK();
|
|
default:
|
|
LOG(NOTICE) << "unhandled status code " << status <<sdbText();
|
|
dialogPushState(SSFail,status,'D');
|
|
gReports.incr("OpenBTS.SIP.Failed.Remote.xxx");
|
|
if (sendAck) MOCSendACK();
|
|
}
|
|
}
|
|
|
|
// Look for <tag>blah</tag> in xmlin and return "blah".
|
|
static string xmlFind(const char *xmlin, const char *tag)
|
|
{
|
|
char tagbuf[56];
|
|
assert(strlen(tag) < 50);
|
|
sprintf(tagbuf,"<%s>",tag);
|
|
const char *start = strstr(xmlin,tagbuf);
|
|
if (!start) return string("");
|
|
const char *result = start + strlen(tagbuf);
|
|
sprintf(tagbuf,"</%s>",tag);
|
|
const char *end = strstr(start,tagbuf);
|
|
if (!start) return string("");
|
|
return string(result,end-result);
|
|
}
|
|
|
|
// The incoming USSD BYE message could have a payload to be sent to the MS.
|
|
void SipDialog::handleUssdBye(SipMessage *msg)
|
|
{
|
|
// There could be multiple BYE messages, hopefully all identical, but we only want to send one DialogMessage.
|
|
if (getSipState() == Cleared) return;
|
|
DialogUssdMessage *dmsg = new DialogUssdMessage(mTranId,DialogState::dialogBye,0);
|
|
// Is it is ok for there to be no response string?
|
|
// We have to send something to the MS so in that case return an empty string.
|
|
if (msg->smGetMessageContentType().find("application/vnd.3gpp.ussd+xml") == string::npos) {
|
|
LOG(INFO) << "UUSD response does not contain correct body type";
|
|
} else {
|
|
dmsg->dmMsgPayload = xmlFind(msg->smGetMessageBody().c_str(),"ussd-string");
|
|
if (dmsg->dmMsgPayload == "") {
|
|
// This is ok.
|
|
LOG(INFO) << "Missing UUSD response does not contain correct body type";
|
|
}
|
|
}
|
|
dialogQueueMessage(dmsg);
|
|
if (dsPeer()->ipIsReliableTransport()) {
|
|
dialogPushState(Cleared,0);
|
|
} else {
|
|
dialogPushState(MTDClearing,0);
|
|
setTimerJ();
|
|
}
|
|
}
|
|
|
|
|
|
// The SIPInterface sends this to us based on mCallId.
|
|
// We will process the message and possibly send replies or DialogMessages to the L3 State machine.
|
|
// Blah, this should be handled by Dialog sub-classes.
|
|
void SipDialog::dialogWriteDownlink(SipMessage *msg)
|
|
{
|
|
LOG(DEBUG) <<"received SIP" /*<<LOGVAR2("SIP.state",sipState())*/ <<" msg:"<<msg->text() <<dialogText();
|
|
sipStopTimers();
|
|
ScopedLock lock(mDialogLock,__FILE__,__LINE__);
|
|
|
|
unsigned code = msg->smGetCode();
|
|
|
|
//if (code == 200) { saveResponse(msg); }
|
|
//if (code >= 400) { mFailCode = code; }
|
|
|
|
//SipDialog::msgState nextDialogState = sipMessage2DialogState(msg);
|
|
|
|
switch (mDialogType) {
|
|
case SIPDTRegister:
|
|
case SIPDTUnregister:
|
|
LOG(ERR) << "REGISTER transaction received unexpected message:"<<msg;
|
|
break;
|
|
case SIPDTMOUssd:
|
|
LOG(DEBUG);
|
|
if (code == 0 && msg->isBYE()) { // It is a SIP Request. Switch based on the method.
|
|
// Grab any xml ussd response from the BYE message.
|
|
handleUssdBye(msg);
|
|
}
|
|
goto otherwise;
|
|
case SIPDTMOC: // This is a MOC transaction.
|
|
case SIPDTMTC: // This is a MTC transaction. Could be an inbound handover.
|
|
LOG(DEBUG);
|
|
otherwise:
|
|
if (code == 0) { // It is a SIP Request. Switch based on the method.
|
|
if (msg->isBYE()) {
|
|
SipMTBye(msg);
|
|
} else if (msg->isCANCEL()) {
|
|
// This is an error since we have already passed the ACK stage, but lets cancel the dialog anyway.
|
|
SipMTCancel(msg);
|
|
} else {
|
|
// Not expecting any others. Must send 405 error.
|
|
LOG(ALERT)<<"SIP Message ignored:"<<msg; // TEMPORARY: Make this show up.
|
|
LOG(WARNING)<<"SIP Message ignored:"<<msg;
|
|
SipMessageReply oops(msg,405,string("Method Not Allowed"),this);
|
|
sipWrite(&oops);
|
|
}
|
|
} else {
|
|
// This should have matched a Transaction somewhere.
|
|
// We cant send an error back for an unrecognized response or we get in an infinite loop.
|
|
LOG(ALERT) << "SIP response not handled:"<<msg;
|
|
}
|
|
break;
|
|
case SIPDTMOSMS:
|
|
case SIPDTMTSMS:
|
|
LOG(ERR) << "MESSAGE transaction received unexpected message:"<<msg;
|
|
break;
|
|
default:
|
|
assert(0);
|
|
}
|
|
dialogChangeState(msg);
|
|
delete msg;
|
|
}
|
|
|
|
// This is only called after the dialog has already been removed from the active dialogs,
|
|
// so we dont have to check the dialog state, all we have to check is there is nothing pointing to it
|
|
// that would cause a crash if genuinely deleted.
|
|
bool SipDialog::dgIsDeletable() const
|
|
{
|
|
ScopedLock lock(mDialogLock,__FILE__,__LINE__);
|
|
switch (mDialogType) {
|
|
case SIPDTMOC:
|
|
case SIPDTMTC:
|
|
case SIPDTMOSMS:
|
|
case SIPDTMTSMS:
|
|
case SIPDTMOUssd:
|
|
return gNewTransactionTable.ttIsDialogReleased(this->mTranId);
|
|
// We never expire the dialog associated with REGISTER.
|
|
case SIPDTRegister:
|
|
case SIPDTUnregister:
|
|
case SIPDTUndefined:
|
|
return false; // We never delete the Register dialog.
|
|
default:
|
|
assert(0);
|
|
}
|
|
}
|
|
|
|
// Called periodicially to check for SIP timer expiration.
|
|
bool SipDialog::dialogPeriodicService()
|
|
{
|
|
// Take care. This is a potential deadlock if somone tries to add a locked SipDialog into the DialogMap,
|
|
// because the kicker code locks the whole DialogMap against modification.
|
|
ScopedLock lock(mDialogLock,__FILE__,__LINE__);
|
|
// Now we use TransactionUsers for client transactions, so this code handles only server transactions.
|
|
// The in-dialog server transactions are trivial - the transaction-layer simply resends the final
|
|
// response each time the request is received.
|
|
switch (mDialogType) {
|
|
case SIPDTUndefined:
|
|
case SIPDTRegister:
|
|
case SIPDTUnregister:
|
|
// FIXME: I dont think we delete these, ever.
|
|
break;
|
|
case SIPDTMTSMS:
|
|
case SIPDTMTC:
|
|
return mtPeriodicService();
|
|
break;
|
|
case SIPDTMOSMS:
|
|
case SIPDTMOC:
|
|
case SIPDTMOUssd:
|
|
return moPeriodicService();
|
|
break;
|
|
//default: break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
const char *DialogState::msgStateString(DialogState::msgState dstate)
|
|
{
|
|
switch (dstate) {
|
|
case DialogState::dialogUndefined: return "undefined";
|
|
case DialogState::dialogStarted: return "Started";
|
|
case DialogState::dialogProceeding: return "Proceeding";
|
|
case DialogState::dialogRinging: return "Ringing";
|
|
case DialogState::dialogActive: return "Active";
|
|
case DialogState::dialogBye: return "Bye";
|
|
case DialogState::dialogFail: return "Fail";
|
|
case DialogState::dialogDtmf: return "DTMF";
|
|
};
|
|
return "unknown_DialogState";
|
|
}
|
|
|
|
string SipDialog::dialogText(bool verbose) const
|
|
{
|
|
std::ostringstream ss;
|
|
ss << " SipDialog("<<LOGVARM(mTranId) ;
|
|
ss << LOGVAR2("state",getDialogState()) <<LOGVARM(mPrevDialogState);
|
|
//ss << LOGVAR2("fifo",mDownlinkFifo.size());
|
|
sdbText(ss,verbose);
|
|
// The C++ virtual inheritance is so broken we cant use it. Gag me.
|
|
switch (mDialogType) {
|
|
case SIPDTMTC: case SIPDTMTSMS:
|
|
ss << mttlText();
|
|
break;
|
|
case SIPDTMOC: case SIPDTMOSMS: case SIPDTMOUssd:
|
|
ss << motlText();
|
|
break;
|
|
default: ss << "."; break;
|
|
}
|
|
ss <<")";
|
|
return ss.str();
|
|
}
|
|
|
|
std::ostream& operator<<(std::ostream& os, const SipDialog*dg) {
|
|
if (dg) os << dg->dialogText(); else os << "(null SipDialog)";
|
|
return os;
|
|
}
|
|
std::ostream& operator<<(std::ostream& os, const SipDialog&dg) { os << dg.dialogText(); return os; } // stupid language
|
|
std::ostream& operator<<(std::ostream& os, const SipDialogRef&dr) {
|
|
SipDialog *dg = dr.self();
|
|
if (dg) os << dg->dialogText(); else os << "(null SipDialog)";
|
|
return os;
|
|
}
|
|
|
|
std::ostream& operator<<(std::ostream& os, const DialogState::msgState dstate)
|
|
{
|
|
os << DialogState::msgStateString(dstate);
|
|
return os;
|
|
}
|
|
|
|
std::ostream& operator<<(std::ostream& os, const DialogMessage*dmsg)
|
|
{
|
|
if (dmsg) {
|
|
os <<"DialogMessage("<<LOGVAR2("MsgState",DialogState::msgStateString(dmsg->mMsgState)) <<LOGVAR2("StatusCode",dmsg->mSipStatusCode)<<")";
|
|
} else {
|
|
os << "(null DialogMessage)";
|
|
}
|
|
return os;
|
|
}
|
|
|
|
std::ostream& operator<<(std::ostream& os, const DialogMessage&dmsg) { os << &dmsg; return os; } // stupid language
|
|
|
|
|
|
}; // namespace
|