/**@file TransactionTable and related classes. */ /* * Copyright 2008, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Process, Inc. * Copyright 2011, 2012, 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. */ #define LOG_GROUP LogGroup::Control // Can set Log.Level.Control for debugging #include #include #include "ControlCommon.h" #include "L3TranEntry.h" #include "L3MMLayer.h" #include #include #include #include #include #include #include #include #include #include #include //#include //#include #include //#include #include #include #undef WARNING // This is in the global namespace. Control::NewTransactionTable gNewTransactionTable; int gCountTranEntry = 0; namespace Control { using namespace std; using namespace GSM; using namespace SIP; using namespace Peering; // for sockaddr2string - remove me CdrService gCdrService; #if EXTERNAL_TRANSACTION_TABLE // (pat) This external transaction table is obsolete and we will not support it any more. // The code implementing it has eroded and would not work if enabled. // It is retained here until we release version 4 in the remote off-chance that some important customer // has built legacy applications that use this, so we can help migrate that customer to something different. // This is extremely unlikely, since we have no customers. static const char* createNewTransactionTable = { "CREATE TABLE IF NOT EXISTS TRANSACTION_TABLE (" "ID INTEGER PRIMARY KEY, " // internal transaction ID "CHANNEL TEXT DEFAULT NULL," // channel description string (cross-refs CHANNEL_TABLE) "CREATED INTEGER NOT NULL, " // Unix time of record creation "CHANGED INTEGER NOT NULL, " // time of last state change "TYPE TEXT, " // transaction type "SUBSCRIBER TEXT, " // IMSI, if known "L3TI INTEGER, " // GSM L3 transaction ID, +0x08 if generated by MS "SIP_CALLID TEXT, " // SIP-side call id tag "SIP_PROXY TEXT, " // SIP proxy IP "CALLED TEXT, " // called party number "CALLING TEXT, " // calling party number "GSMSTATE TEXT, " // GSM/Q.931 state "SIPSTATE TEXT " // SIP state ")" }; #endif HandoverEntry::HandoverEntry(const TranEntry *tran) : mMyTranID(tran->tranID()), mHandoverOtherBSTransactionID(0) { memset(&mInboundPeer,0,sizeof(mInboundPeer)); memset(&mOutboundPeer,0,sizeof(mOutboundPeer)); }; HandoverEntry *TranEntry::getHandoverEntry(bool create) const // It is not const, but we want C++ to be a happy compiler. { if (!mHandover && create) { mHandover = new HandoverEntry(this); } return mHandover; } // class base initialization goes here. void TranEntry::TranEntryInit() { mID = gNewTransactionTable.ttNewID(); mL3TI = cL3TIInvalid; // Until we know better. mDialog = 0; mHandover = NULL; //mGSMState = CCState::NullState; moved to TranEntryProtected mNumSQLTries = gConfig.getNum("Control.NumSQLTries"); // will be increased later by the SOS constructor. mContext = NULL; //mChannel = NULL; //mNextChannel = NULL; mMMData = NULL; //initTimers(); } //#include TranEntry::TranEntry( SipDialog *wDialog, //const L3MobileIdentity& wSubscriber, const L3CMServiceType& wService) { gCountTranEntry++; TranEntryInit(); if (wDialog) setDialog(wDialog); //mSubscriber = wSubscriber; mService = wService; mTerminationRequested.value = 0; // redundant; it inits itself to 0 /***** if (0) { const int elements = 100; void *buffer[elements]; int nptrs = backtrace(buffer, elements); char **strings = backtrace_symbols(buffer, nptrs); if (strings == NULL) { perror("backtrace_symbols"); } else { for (int j = 0; j < nptrs; j++) printf("%s\n", strings[j]); free(strings); } } ***/ mStartTime = time(NULL); mConnectTime = 0; // Means never connected. //mEndTime = 0; //gNewTransactionTable.ttAdd(this); } // For MO the channel is always known. TranEntry *TranEntry::newMO(MMContext *wChan, const GSM::L3CMServiceType& wService) { //L3MobileIdentity unknownId; //TranEntry *result = new TranEntry(proxy,unknownId,wChannel,wService,CCState::NullState); TranEntry *result = new TranEntry(NULL,wService); // No SipDialog yet for MO transactions. LOG(DEBUG); wChan->mmConnectTran(result); gNewTransactionTable.ttAdd(result); return result; } void TranEntry::setDialog(SIP::SipDialog *dialog) { mDialog = dialog; dialog->setTranId(mID); } void TranEntry::txFrame(SIP::AudioFrame* frame, unsigned numFlushed) { getDialog()->txFrame(frame,numFlushed); } SIP::AudioFrame *TranEntry::rxFrame() { return getDialog()->rxFrame(); } // Crashes if rtp not established. unsigned TranEntry::getRTPPort() const { if (SipDialog *dialog = getDialog()) { return dialog->RTPPort(); } return 0; } TranEntry *TranEntry::newMOSSD(MMContext* wChannel) { return newMO(wChannel,L3CMServiceType::SupplementaryService); } TranEntry *TranEntry::newMOC(MMContext* wChannel, CMServiceTypeCode serviceType) { devassert(serviceType == L3CMServiceType::MobileOriginatedCall); return newMO(wChannel,serviceType); } TranEntry *TranEntry::newMOSMS(MMContext* wChannel) { return newMO(wChannel,L3CMServiceType::ShortMessage); } TranEntry *TranEntry::newMOMM(MMContext* wChannel) { return newMO(wChannel,L3CMServiceType::LocationUpdateRequest); } // The transaction is created without an assigned channel. TranEntry *TranEntry::newMTC( SipDialog *dialog, const FullMobileId& msid, const GSM::L3CMServiceType& wService, // MobileTerminatedCall or UndefinedType for generic page from CLI. const string wCallerId) //const L3CallingPartyBCDNumber& wCalling) { //L3MobileIdentity subscriber(toImsiDigits.c_str()); //TranEntry *result = new TranEntry(dialog,subscriber, wService); TranEntry *result = new TranEntry(dialog, wService); result->mSubscriber = msid; result->mCalling = GSM::L3CallingPartyBCDNumber(wCallerId.c_str()); LOG(DEBUG) <mCalling.digits()); gNewTransactionTable.ttAdd(result); return result; } // post-l3-rewrite TranEntry *TranEntry::newMTSMS( SipDialog *dialog, const FullMobileId& msid, const L3CallingPartyBCDNumber& wCalling, string smsBody, // (pat) The recommendation for C++11 is to pass-by-value parameters that will be copied. string smsContentType) { //TranEntry *result = new TranEntry(dialog,subscriber,GSM::L3CMServiceType::MobileTerminatedShortMessage); TranEntry *result = new TranEntry(dialog,GSM::L3CMServiceType::MobileTerminatedShortMessage); result->mSubscriber = msid; // The the L3TI is assigned when the transaction starts running. If ever. result->mCalling = wCalling; result->mMessage = smsBody; result->mContentType = smsContentType; gNewTransactionTable.ttAdd(result); return result; } // Form for inbound handovers. TranEntry *TranEntry::newHandover( const struct sockaddr_in* peer, unsigned wInboundHandoverReference, SimpleKeyValue ¶ms, L3LogicalChannel *wChannel, unsigned wHandoverOtherBSTransactionID) { MMContext *mmchan = wChannel->chanGetContext(true); // This is where we create the MMContext for a handover. //TranEntry *result = new TranEntry(proxy,imsi,wChannel,GSM::L3CMServiceType::HandoverCall,CCState::HandoverInbound); // We dont want to open the dialog before receiving the handover. // The proxy is not used until the dialog is created so it is no longer a parameter. TranEntry *result = newMO(mmchan, GSM::L3CMServiceType::HandoverCall); result->setGSMState(CCState::HandoverInbound); const char* IMSI = params.get("IMSI"); if (IMSI) result->mSubscriber = FullMobileId(IMSI); const char* called = params.get("called"); if (called) { // TODO: Do we need to call setCalled() which will update sql? result->mCalled = GSM::L3CalledPartyBCDNumber(called); result->mService = GSM::L3CMServiceType::MobileOriginatedCall; } const char* calling = params.get("calling"); if (calling) { result->mCalling = GSM::L3CallingPartyBCDNumber(calling); result->mService = GSM::L3CMServiceType::MobileTerminatedCall; } const char* L3TI = params.get("L3TI"); if (L3TI) { result->mL3TI = strtol(L3TI,NULL,10); } else { // TODO: And what should l3ti be otherwise? result->mL3TI = 7; // (pat) Not sure what this should be if not in inbound handover parameters. } const char* codec = params.get("codec"); // TODO: Is this an RTP codec number or a CodecSet number? // Assuming this information came from a peer OpenBTS unit it is our internal CodecSet number. if (codec) result->mCodecs = CodecSet((CodecType)atoi(codec)); // Set the SIP state. //result->mSIP->setSipState(SIP::HandoverInbound); //const char * callId = params.get("CallID"); //result->mSIP->setCallId(callId); // This is used for inbound handovers. // We are "BS2" in the handover ladder diagram. // The message string was formed by the handoverString method. result->getHandoverEntry(true)->initHandoverEntry(peer,wInboundHandoverReference,wHandoverOtherBSTransactionID,params); return result; } void HandoverEntry::initHandoverEntry( const struct sockaddr_in* peer, unsigned wInboundHandoverReference, unsigned wHandoverOtherBSTransactionID, SimpleKeyValue ¶ms) { // FIXME: This is also in the params. Which do we want to use? (pat) This one. mInboundReference = wInboundHandoverReference; mHandoverOtherBSTransactionID = wHandoverOtherBSTransactionID; // Save the peer address. memcpy(&mInboundPeer,peer,sizeof(mInboundPeer)); const char* refer = params.get("REFER"); if (refer) { // We changed spaces to tabs to get the REFER message through the peering interface. // Since we are sending it through the SIP parser, it does not matter very much, // however the tabs are preserved in a few places, especially the SDP strings, // so change all the tabs back to spaces to be safe. const char *inp; char *outp, *outbuf = (char*)alloca(strlen(refer)+1); for (inp = refer, outp = outbuf; *inp; inp++, outp++) { *outp = (*inp == '\t') ? ' ' : *inp; } *outp = 0; mSipReferStr = string(outbuf); } } TranEntry::~TranEntry() { gCountTranEntry--; // This lock should go out of scope before the object is actually destroyed. //ScopedLock lock(mLock,__FILE__,__LINE__); // This is the l3-rewrite stack of procedures running for this transaction. while (mProcStack.size()) { MachineBase *pb = mProcStack.back(); mProcStack.pop_back(); delete pb; } // Remove any FIFO from the gPeerInterface. gPeerInterface.removeFIFO(tranID()); if (mMMData) { delete mMMData; } if (mHandover) { delete mHandover; } #if EXTERNAL_TRANSACTION_TABLE // Delete the SQL table entry. (pat) There wont be any for LocationUpdating procedure, or transactions that did not run until they got an IMSI. char query[100]; sprintf(query,"DELETE FROM TRANSACTION_TABLE WHERE ID=%u",tranID()); runQuery(query); #endif } bool TranEntryProtected::clearingGSM() const { //ScopedLock lock(mLock,__FILE__,__LINE__); return (mGSMState==CCState::ReleaseRequest) || (mGSMState==CCState::DisconnectIndication); } bool TranEntryProtected::isStuckOrRemoved() const { unsigned age = mStateTimer.elapsed(); // 180-second tests if (age < 180*1000) return false; // Dead if someone requested removal >3 min ago. // (pat) Post-l3-rewrite we dont need to wait to delete TranEntrys, // because nothing points back to them permanently, only currently running functions, for example, // Peering gets a TranEntry pointer and immediately modifies it. One second would be over-kill. // But having TranEntrys stick around a while may still be useful for debugging to see them in the CLI, // so I did not change this. // Any GSM state other than Active for >3 min? if (getGSMState() !=CCState::Active) { return true; } // Any SIP stte other than active for >3 min? //if (lSIPState !=SIP::Active) return true; return false; } bool TranEntry::deadOrRemoved() const { //ScopedLock lock(mLock,__FILE__,__LINE__); if (isStuckOrRemoved()) { LOG(NOTICE)<<"Transaction in state "<3 minutes; "<<*this; return true; } SipDialog *dialog = getDialog(); if (dialog && dialog->sipIsStuck()) return true; return false; // still going } //SIP::SipState TranEntry::getSipState() const //{ // if (mDialog) { return mDialog->getSipState(); } // post-l3-rewrite // return SIP::NullState; //} #if UNUSED bool TranEntry::teDead() const { // Get the state information and release the locks. // If it's locked, we assume someone has locked it, // so it's not dead. // And if someone locked in permanently, // the resulting deadlock would spread through the whole system. //if (!mLock.trylock()) return false; if (mDialog && mDialog->sipIsStuck()) return true; //mLock.unlock(); #if 0 // (pat) You cannot check the sip state here based on the transaction state-age because the state // age is not updated for sip-side state changes. // 30-second tests if (age < 30*1000) return false; // Failed? if (lSIPState==SIP::Fail) return true; // Bad handover? if (lSIPState==SIP::HandoverInbound) return true; // SIP Null state? if (lSIPState==SIP::NullState) return true; // SIP stuck in proceeding? if (lSIPState==SIP::Proceeding) return true; // SIP cancelled? if (lSIPState==SIP::Canceled) return true; // SIP Cleared? if (lSIPState==SIP::Cleared) return true; #endif // If we got here, the state-vs-timer relationship // appears to be valid. return false; } #endif void TranEntryProtected::stateText(ostream &os) const { os << " GSMState=" << mGSMState; // Dont call getGSMState(), it asserts 0 if the transaction has been removed; if (isStuckOrRemoved()) os << " [defunct]"; } void TranEntry::text(ostream& os) const { //ScopedLock lock(mLock,__FILE__,__LINE__); os << " TranEntry("; os <proxyPort(); // os << " SIPState=" << mSIP->sipState(); //} os << LOGVARM(mService); if (mCalled.digits()[0]) os << " to=" << mCalled.digits(); if (mCalling.digits()[0]) os << " from=" << mCalling.digits(); os << " stateAge=(" << (stateAge()+500)/1000 << " sec)"; if (currentProcedure()) { os << " stack=("; for (list::const_iterator it = mProcStack.begin(); it != mProcStack.end(); it++) { (*it)->machText(os); } os << ")"; } L3TimerList::text(os); if (mMessage.size()) os << " message=\"" << mMessage << "\""; } os << ")"; } string TranEntry::text() const { ostringstream os; text(os); return os.str(); } ostream& operator<<(ostream& os, const TranEntry& entry) { entry.text(os); return os; } ostream& operator<<(ostream& os, const TranEntry* entry) { if (entry == NULL) { os << "(null TranEntry)"; return os; } entry->text(os); return os; } #if EXTERNAL_TRANSACTION_TABLE void TranEntry::runQuery(const char* query) const { // Caller should hold mLock and should have already checked isRemoved().. for (unsigned i=0; igetSipState(); // sipStateSS << mPrevSipState; // // string subscriber = mSubscriber.fmidUsername(); // // const char* stateString = CCState::callStateString(getGSMState()); // assert(stateString); // // // FIXME -- This should be done in a single SQL transaction. // // char query[500]; // unsigned now = (unsigned)time(NULL); // sprintf(query,"INSERT INTO TRANSACTION_TABLE " // "(ID,CREATED,CHANGED,TYPE,SUBSCRIBER,L3TI,CALLED,CALLING,GSMSTATE,SIPSTATE,SIP_CALLID,SIP_PROXY) " // "VALUES (%u,%u, %u, '%s','%s', %u,'%s', '%s', '%s', '%s', '%s', '%s')", // tranID(),now,now, // serviceTypeSS.str().c_str(), // subscriber.c_str(), // mL3TI, // mCalled.digits(), // mCalling.digits(), // stateString, // sipStateSS.str().c_str(), // mDialog->callId().c_str(), // mDialog->proxyIP().c_str() // ); // // runQuery(query); // // if (!channel()) return; // sprintf(query,"UPDATE TRANSACTION_TABLE SET CHANNEL='%s' WHERE ID=%u", // channel()->descriptiveString(), tranID()); // runQuery(query); //} #if UNUSED void TranEntry::setChannel(L3LogicalChannel* wChannel) { //ScopedLock lock(mLock,__FILE__,__LINE__); mChannel = wChannel; char query[500]; if (mChannel) { sprintf(query,"UPDATE TRANSACTION_TABLE SET CHANGED=%u,CHANNEL='%s' WHERE ID=%u", (unsigned)time(NULL), mChannel->descriptiveString(), tranID()); } else { sprintf(query,"UPDATE TRANSACTION_TABLE SET CHANGED=%u,CHANNEL=NULL WHERE ID=%u", (unsigned)time(NULL), tranID()); } runQuery(query); } #endif void TranEntry::setSubscriberImsi(string imsi, bool andAttach) { mSubscriber.mImsi = imsi; // Now that we have an imsi we can hook up the MMUser. if (andAttach) { gMMLayer.mmAttachByImsi(channel(),imsi); } } L3LogicalChannel* TranEntry::channel() { MMContext *ts = teGetContext(); return ts ? ts->tsChannel() : NULL; } const L3LogicalChannel* TranEntry::channel() const { MMContext *ts = Unconst(this)->teGetContext(); // gotta love it. return ts ? ts->tsChannel() : NULL; } //bool TranEntry::isChannelMatch(const L3LogicalChannel *lch) //{ // // The void* compares pointers even if someone defines operator== on L3LogicalChannel. // return ((void*)this->channel() == (void*)lch || (void*)this->getL2Channel()->SACCH() == (void*)lch || // (this->mNextChannel && ((void*)this->mNextChannel == (void*)lch || (void*)this->mNextChannel->getL2Channel()->SACCH() == (void*)lch))); //} L2LogicalChannel* TranEntry::getL2Channel() const { L3LogicalChannel *chan = Unconst(channel()); // what a pathetic language return chan ? dynamic_cast(chan) : NULL; } // This is used after the channel() is changed from SDCCH to to TCHFACCH just to be safe. L3LogicalChannel* TranEntry::getTCHFACCH() { devassert(channel()->chtype()==FACCHType); // This is the type returned by the TCHFACCHLogicalChannel, even though it is TCH too. return channel(); } unsigned TranEntry::getL3TI() const { return mL3TI; } CallState TranEntryProtected::getGSMState() const { //ScopedLock lock(mLock,__FILE__,__LINE__); // redundant return mGSMState; } void TranEntryProtected::setGSMState(CallState wState) { //ScopedLock lock(mLock,__FILE__,__LINE__); mStateTimer.now(); mGSMState = wState; #if UNUSED // We are removing the transaction table, so I'm just taking this out. const char* stateString = CCState::callStateString(wState); assert(stateString); unsigned now = mStateTimer.sec(); char query[150]; sprintf(query, "UPDATE TRANSACTION_TABLE SET GSMSTATE='%s',CHANGED=%u WHERE ID=%u", stateString,now, tranID()); runQuery(query); #endif } SIP::SipState TranEntry::echoSipState(SIP::SipState state) const { // Caller should hold mLock. if (mPrevSipState==state) return state; mPrevSipState = state; const char* stateString = SIP::SipStateString(state); devassert(stateString); #if EXTERNAL_TRANSACTION_TABLE unsigned now = time(NULL); char query[150]; sprintf(query, "UPDATE TRANSACTION_TABLE SET SIPSTATE='%s',CHANGED=%u WHERE ID=%u", stateString,now,tranID()); runQuery(query); #endif return state; } void TranEntry::setCalled(const L3CalledPartyBCDNumber& wCalled) { //ScopedLock lock(mLock,__FILE__,__LINE__); mCalled = wCalled; #if EXTERNAL_TRANSACTION_TABLE char query[151]; snprintf(query,150, "UPDATE TRANSACTION_TABLE SET CALLED='%s' WHERE ID=%u", mCalled.digits(), tranID()); runQuery(query); #endif } // Does this ti reported by the MS match this transaction? bool TranEntry::matchL3TI(unsigned ti, bool fromMS) { // Old incorrect way: //return l3TISigBits(mL3TI) == l3TISigBits(ti); if (fromMS) { // If the ti argument came from the MS flip the TI flag. if (ti & 0x8) { ti &= ~0x8; } else { ti |= 0x8; } } return mL3TI == ti; } void TranEntry::setL3TI(unsigned wL3TI) { //ScopedLock lock(mLock,__FILE__,__LINE__); mL3TI = wL3TI; #if EXTERNAL_TRANSACTION_TABLE char query[151]; snprintf(query,150, "UPDATE TRANSACTION_TABLE SET L3TI=%u WHERE ID=%u", mL3TI, tranID()); runQuery(query); #endif } L3Cause::AnyCause TranEntry::terminationRequested() { ScopedLock lock(mAnotherLock,__FILE__,__LINE__); L3Cause::AnyCause retVal = mTerminationRequested; mTerminationRequested.value = 0; return retVal; } // The handover is from BS1 to BS2. // This is run in BS1 to create the handover string to send to BS2. // The string must contain everything about the SIP side of the session. // Everything needed to be known about the radio side of the session was transferred as an L3 HandoverCommand. string TranEntry::handoverString(string peer) const { // This string is a set of key-value pairs. // It needs to carry all of the information of the GSM Abis Handover Request message, // as well as all of the information of the SIP REFER message. // We call this as "BS1" in the handover ladder diagram. // It is decoded at the other end by a TransactionEnty constructor. //ScopedLock lock(mLock,__FILE__,__LINE__); ostringstream os; os << tranID(); os << " IMSI=" << mSubscriber.mImsi; // We dont need these. //HandoverEntry *handover = getHandoverEntry(true); //if (getGSMState()==CCState::HandoverInbound) os << " inbound-ref=" << handover->mInboundReference; //if (getGSMState()==CCState::Handover_Outbound) os << " outbound-ref=" << handover->mOutboundReference.value(); os << " L3TI=" << mL3TI; if (mCalled.digits()[0]) os << " called=" << mCalled.digits(); if (mCalling.digits()[0]) os << " calling=" << mCalling.digits(); const SipBase *sip = Unconst(this)->getDialog(); os << " REFER=" << sip->dsHandoverMessage(peer); // remote ip and port (pat) This is where we send the re-INVITE, but subsequent messages // are sent to our proxy IP. This is wrong, but our SIP response routing for other messages is wrong too. // RFC3261 section 4 page 16 describes routing as follows: // 1. The INVITE is necessarily sent via proxies, which add their own "via" headers. // 2. The reply to the INVITE must include the "via" headers so it can get back. // 3. Subsequently, if there is a Contact field, all messages bypass the proxies and are sent directly to the Contact. // 4. But the proxies might want to see the messages too, so they can add a "required-route" parameter which trumps // the "contact" header and specifies that messages are sent there instead. This is called "Loose Routing." What a mess. // And I quote: "These procedures separate the destination of the request (present in the Request-URI) from // the set of proxies that need to be visited along the way (present in the Route header field)." // In contrast, A Strict Router "follows the Route processing rules of RFC 2543 and many prior work in // progress versions of this RFC. That rule caused proxies to destroy the contents of the Request-URI // when a Route header field was present." // 8.1.1.1: Normally the request-URI is equal to the To: field. But if there is a configured proxy (our case) // this is called a "pre-existing route set" and we must follow 12.2.1.1 using the request-URI as the // remote target URI(???) // 12.2: The route-set is immutably defined by the initial INVITE. You can change the remote-URI in a re-INVITE // (aka target-refresh-request) but not the route-set. // Remote-URI: Intial request remote-URI must == To: field. // Functional but unused. See comments in SIP::inboundHandoverSendINVITE() //os << " RTPState=" << // sip->RTPSession()->rtp.snd_time_offset << "," << // sip->RTPSession()->rtp.snd_ts_offset << "," << // sip->RTPSession()->rtp.snd_rand_offset << "," << // sip->RTPSession()->rtp.snd_last_ts << "," << // sip->RTPSession()->rtp.rcv_time_offset << "," << // sip->RTPSession()->rtp.rcv_ts_offset << "," << // sip->RTPSession()->rtp.rcv_query_ts_offset << "," << // sip->RTPSession()->rtp.rcv_last_ts << "," << // sip->RTPSession()->rtp.rcv_last_app_ts << "," << // sip->RTPSession()->rtp.rcv_last_ret_ts << "," << // sip->RTPSession()->rtp.hwrcv_extseq << "," << // sip->RTPSession()->rtp.hwrcv_seq_at_last_SR << "," << // sip->RTPSession()->rtp.hwrcv_since_last_SR << "," << // sip->RTPSession()->rtp.last_rcv_SR_ts << "," << // sip->RTPSession()->rtp.last_rcv_SR_time.tv_sec << "," << sip->RTPSession()->rtp.last_rcv_SR_time.tv_usec << "," << // sip->RTPSession()->rtp.snd_seq << "," << // sip->RTPSession()->rtp.last_rtcp_report_snt_r << "," << // sip->RTPSession()->rtp.last_rtcp_report_snt_s << "," << // sip->RTPSession()->rtp.rtcp_report_snt_interval << "," << // sip->RTPSession()->rtp.last_rtcp_packet_count << "," << // sip->RTPSession()->rtp.sent_payload_bytes; return os.str(); } void NewTransactionTable::ttInit() { //if (! l3rewrite()) return; // Only one of TransactionTable::init or NewTransactionTable::ttInit LOG(DEBUG); // This assumes the main application uses sdevrandom. //mIDCounter = random(); mIDCounter = 100; // pat changed. 0 is reserved. Start it high enough so it cannot possibly be confused with an L3TI. #if EXTERNAL_TRANSACTION_TABLE // Connect to the database. const char *path = gConfig.getStr("Control.Reporting.TransactionTable").c_str(); int rc = sqlite3_open(path,&mDB); if (rc) { LOG(ALERT) << "Cannot open Transaction Table database at " << path << ": " << sqlite3_errmsg(mDB); sqlite3_close(mDB); mDB = NULL; return; } // Create a new table, if needed. if (!sqlite3_command(mDB,createNewTransactionTable)) { LOG(ALERT) << "Cannot create Transaction Table"; } // Clear any previous entires. if (!sqlite3_command(gNewTransactionTable.getDB(),"DELETE FROM TRANSACTION_TABLE")) LOG(WARNING) << "cannot clear previous transaction table"; #endif } #if EXTERNAL_TRANSACTION_TABLE NewTransactionTable::~NewTransactionTable() { // Don't bother disposing of the memory, // since this is only invoked when the application exits. if (mDB) sqlite3_close(mDB); } #endif unsigned NewTransactionTable::ttNewID() { ScopedLock lock(mttLock,__FILE__,__LINE__); return mIDCounter++; } void NewTransactionTable::ttAdd(TranEntry* value) { LOG(DEBUG); LOG(INFO) << "new transaction " << *value; ScopedLock lock(mttLock,__FILE__,__LINE__); //clearDeadEntries(); // This the only call to clearDeadEntries that really matters. mTable[value->tranID()]=value; } bool TranEntry::teIsTalking() { LOG(DEBUG) << LOGVAR2("GSMState",this->getGSMState()); if (this->getGSMState() == CCState::Active) { //WATCHINFO(LOGVAR2("GSMState",this->getGSMState()) <channel()->getL2Channel()->getSACCH()->measurementResults())); if (L3LogicalChannel *l3chan = this->channel()) { L2LogicalChannel *l2chan = l3chan->getL2Channel(); GSM::L3MeasurementResults meas = l2chan->getSACCH()->measurementResults(); if (meas.isServingCellValid()) { return true; } } } return false; } bool NewTransactionTable::ttIsTalking(TranEntryId tranid) { bool result = false; ScopedLock lock(mttLock,__FILE__,__LINE__); if (TranEntry *tran = ttFindById(tranid)) { result = tran->teIsTalking(); } return result; } TranEntry* NewTransactionTable::ttFindById(TranEntryId key) { // Since this is a log-time operation, we don't screw that up by calling clearDeadEntries. // ID==0 is a non-valid special case. LOG(DEBUG) << "by key: " << key; devassert(key); ScopedLock lock(mttLock,__FILE__,__LINE__); NewTransactionMap::iterator itr = mTable.find(key); if (itr==mTable.end()) return NULL; if (itr->second->deadOrRemoved()) return NULL; return (itr->second); } // In l3-rewrite this is called ONLY from teRemove. bool NewTransactionTable::ttRemove(TranEntryId key) { LOG(DEBUG) <second; ScopedLock lock2(tran->mAnotherLock,__FILE__,__LINE__); tran->mTerminationRequested = cause; return true; } // Does the TranEntry referenced by this id still pointer to its SipDialog? // We use the TranEntryId so we can delete the TranEntry completely separately from the SipDialog. // However, the TranEntry has a pointer to the SipDialog, so we dont delete that until its gone. bool NewTransactionTable::ttIsDialogReleased(TranEntryId tid) { ScopedLock lock(mttLock,__FILE__,__LINE__); NewTransactionMap::iterator itr = mTable.find(tid); if (itr==mTable.end()) { return true; } // TranEntry no longer exists. return itr->second->mDialog == 0; } bool NewTransactionTable::ttSetDialog(TranEntryId tid, SipDialog *dialog) { ScopedLock lock(mttLock,__FILE__,__LINE__); NewTransactionMap::iterator itr = mTable.find(tid); if (itr==mTable.end()) { return false; } // TranEntry no longer exists. itr->second->setDialog(dialog); return true; } #if UNUSED TranEntry* NewTransactionTable::ttFindByTypeAndOffset(GSM::TypeAndOffset desc) { LOG(DEBUG) << "by type and offset: " << desc; ScopedLock lock(mttLock,__FILE__,__LINE__); // Yes, it's linear time. // Since clearDeadEntries is also linear, do that here, too. clearDeadEntries(); // Brute force search. for (NewTransactionMap::iterator itr = mTable.begin(); itr!=mTable.end(); ++itr) { if (itr->second->deadOrRemoved()) continue; const L3LogicalChannel* thisChan = itr->second->channel(); if (thisChan->typeAndOffset()!=desc) continue; return itr->second; } //LOG(DEBUG) << "no match for " << *chan << " (" << chan << ")"; return NULL; } #endif #if UNUSED TranEntry* NewTransactionTable::ttFindByMobileIDState(const L3MobileIdentity& mobileID, CallState state) { LOG(DEBUG) << "by ID and state: " << mobileID << " in " << state; ScopedLock lock(mttLock,__FILE__,__LINE__); // Yes, it's linear time. // Since clearDeadEntries is also linear, do that here, too. clearDeadEntries(); // Brute force search. for (NewTransactionMap::iterator itr = mTable.begin(); itr!=mTable.end(); ++itr) { if (itr->second->deadOrRemoved()) continue; if (itr->second->getGSMState() != state) continue; if (itr->second->subscriber() != mobileID) continue; return itr->second; } return NULL; } #endif #if UNUSED bool NewTransactionTable::isBusy(const L3MobileIdentity& mobileID) { LOG(DEBUG) << "id: " << mobileID << "?"; ScopedLock lock(mttLock,__FILE__,__LINE__); // Yes, it's linear time. // Since clearDeadEntries is also linear, do that here, too. clearDeadEntries(); // Brute force search. for (NewTransactionMap::iterator itr = mTable.begin(); itr!=mTable.end(); ++itr) { if (itr->second->deadOrRemoved()) continue; if (itr->second->subscriber() != mobileID) continue; GSM::L3CMServiceType::TypeCode service = itr->second->servicetype(); bool speech = service==GSM::L3CMServiceType::EmergencyCall || service==GSM::L3CMServiceType::MobileOriginatedCall || service==GSM::L3CMServiceType::MobileTerminatedCall; if (!speech) continue; // OK, so we found a transaction for this call. bool inCall = CCState::isInCall(itr->second->getGSMState()); if (inCall) return true; } return false; } #endif #if UNUSED // Find the TranEntry that wants to receive this l3msg, if any. // Look at the PD and the TI. // TODO: Fix this. When we start a MOC there is no TI yet so the Setup message TI will not match the TranEntry. // If no TranEntry matches the TI, we should call, um, we cant just call the default TranEntry // because the message may be for an old dead TranEntry. Maybe should just special-case Setup. // Maybe the TranEntry should expectCC(Setup). // Another way to fix might be to add a default TranEntry for each L3PD, but again that would get dead TIs. // What we really want is a separate MM manager to route the messages. TranEntry *NewTransactionTable::ttFindByL3Msg(GSM::L3Message *l3msg, L3LogicalChannel *lch) { GSM::L3PD pd = l3msg->PD(); ScopedLock lock(gNewTransactionTable.mttLock,__FILE__,__LINE__); for (NewTransactionMap::iterator itr = gNewTransactionTable.mTable.begin(); itr!=gNewTransactionTable.mTable.end(); ++itr) { TranEntry *tran = itr->second; if (tran->deadOrRemoved()) continue; if (! tran->isChannelMatch(lch)) continue; GSM::L3CMServiceType service = tran->service(); switch (pd) { case L3CallControlPD: return tran; // Only one for now. //if (service.isCC() && tran->getL3TI() == dynamic_cast(l3msg)->TI()) { return tran; } continue; case L3SMSPD: return tran; // Only one for now. //if (service.isSMS() && tran->getL3TI() == dynamic_cast(l3msg)->TI()) { return tran; } continue; case L3MobilityManagementPD: case L3RadioResourcePD: // We dont yet have a separate MobilityManagement layer, so MM and RR messages are handled by the primary TranEntry, // which is either the LocationUpdateRequest or the in-progress CC TranEntry, which needs RR messages // to modify the channel for the voice call. if (service.isMM()) { return tran; } if (service.isSMS()) continue; // For now, just assume there is only one transaction, so this must be it. return tran; default: LOG(ERR) << "unrecognized L3"<mTranInbox.write(dmsg); } else { // This is ok - the SIP dialog and L3 transaction side are completely decoupled so it is quite // possible that the transaction was deleted (for example, MS signal failure) while // a SIP dialog is still running. LOG(DEBUG) << "info: SIP Dialog message to non-existent"<getGSMState() : CCState::NullState; return result; } // This is an external interface so we dont have to include L3TranEntry.h just to access this function. void NewTransactionTable_ttAddMessage(TranEntryId tranid,SIP::DialogMessage *dmsg) { gNewTransactionTable.ttAddMessage(tranid,dmsg); } TranEntry* NewTransactionTable::ttFindHandoverOther(const L3MobileIdentity& mobileID, unsigned otherBS1TranId) { LOG(DEBUG) <second; LOG(DEBUG) << "comparing "<mHandover?tran->mHandover->mHandoverOtherBSTransactionID:-1)); if (tran->deadOrRemoved()) continue; if (!tran->mHandover) { LOG(DEBUG) "no match, no handover"<mHandover->mHandoverOtherBSTransactionID != otherBS1TranId) { LOG(DEBUG) "no match "<mHandover->mHandoverOtherBSTransactionID<<"!="<subscriber())) { LOG(DEBUG) "no match"<subscriber()) <second->deadOrRemoved()) continue; if (! itr->second->subscriber().fmidMatch(mobileID)) continue; L3LogicalChannel* chan = itr->second->channel(); if (!chan) continue; if (chan->chtype() == FACCHType) return chan; if (chan->chtype() == SDCCHType) return chan; // (pat) What other channel type could there be? The SACCH are not returned by channel(). assert(0); // Alert pat if you get this assertion. } return NULL; } #endif #if UNUSED unsigned NewTransactionTable::countChan(const L3LogicalChannel* chan) { ScopedLock lock(mttLock,__FILE__,__LINE__); clearDeadEntries(); unsigned count = 0; for (NewTransactionMap::iterator itr = mTable.begin(); itr!=mTable.end(); ++itr) { if (itr->second->deadOrRemoved()) continue; if (itr->second->channel() == chan) count++; } return count; } #endif size_t NewTransactionTable::dump(ostream& os, bool showAll) const { ScopedLock lock(mttLock,__FILE__,__LINE__); size_t sz = 0; for (NewTransactionMap::const_iterator itr = mTable.begin(); itr!=mTable.end(); ++itr) { if ((!showAll) && itr->second->deadOrRemoved()) continue; sz++; os << *(itr->second) << endl; } return sz; } TranEntryId NewTransactionTable::findLongestCall() { ScopedLock lock(mttLock,__FILE__,__LINE__); //clearDeadEntries(); long longTime = 0; NewTransactionMap::iterator longCall = mTable.end(); for (NewTransactionMap::iterator itr = mTable.begin(); itr!=mTable.end(); ++itr) { if (itr->second->deadOrRemoved()) continue; if (!(itr->second->channel())) continue; if (itr->second->getGSMState() != CCState::Active) continue; long runTime = itr->second->stateAge(); if (runTime > longTime) { runTime = longTime; longCall = itr; } } if (longCall == mTable.end()) return 0; return longCall->second->tranID(); } /** Return an even UDP port number for the RTP even/odd pair. */ unsigned allocateRTPPorts() { const unsigned base = gConfig.getNum("RTP.Start"); const unsigned range = gConfig.getNum("RTP.Range"); const unsigned top = base+range; static Mutex lock; // Pick a random starting point. (pat) Why? Because there is a bug and we are trying to avoid it? static unsigned port = base + 2*(random()%(range/2)); unsigned retVal; lock.lock(); //This is a little hacky as RTPAvail is O(n) do { retVal = port; port += 2; if (port>=top) port=base; } while (!gNewTransactionTable.RTPAvailable(retVal)); lock.unlock(); return retVal; } /* linear, we should move the actual search into this structure */ // (pat) Speed entirely irrelevant; this is done once per call. bool NewTransactionTable::RTPAvailable(unsigned rtpPort) { ScopedLock lock(mttLock,__FILE__,__LINE__); //clearDeadEntries(); bool avail = true; for (NewTransactionMap::iterator itr = mTable.begin(); itr!=mTable.end(); ++itr) { if (itr->second->deadOrRemoved()) continue; if (itr->second->getRTPPort() == rtpPort){ avail = false; break; } } return avail; } #if 0 bool NewTransactionTable::outboundReferenceUsed(unsigned ref) { // Called is expected to hold mttLock. for (NewTransactionMap::iterator itr = mTable.begin(); itr!=mTable.end(); ++itr) { if (itr->second->deadOrRemoved()) continue; if (itr->second->getGSMState() != GSM::Handover_Outbound) continue; if (itr->second->handoverReference() == ref) return true; } return false; } unsigned NewTransactionTable::generateHandoverReference(TranEntry *transaction) { ScopedLock lock(mttLock,__FILE__,__LINE__); clearDeadEntries(); unsigned ref = random() % 256; while (outboundReferenceUsed(ref)) { ref = (ref+1) % 256; } transaction->handoverReference(ref); return ref; } #endif MachineBase *TranEntry::tePopMachine() { if (mProcStack.size() == 0) { return NULL; } MachineBase *top = mProcStack.back(); mProcStack.pop_back(); return top; } void TranEntry::tePushProcedure(MachineBase *it) { mProcStack.push_back(it); } // Replace the current procedure with that specified. // We dont delete the current procedure when switching between sub-procedures of an over-all procedure, for example, // when switching from LUIdentication to LUAuthentication with L3ProcedureLocationUpdate. // Update: the above case does not exist any more. void TranEntry::teSetProcedure(MachineBase *wProc, bool wDeleteCurrent) { if (currentProcedure() == wProc) { return; } MachineBase *old = tePopMachine(); wDeleteCurrent = true; // 9-24-2013: We always delete except when pusing into a procedure and that is handled by tePushProcedure. if (wDeleteCurrent && old) { delete old; } tePushProcedure(wProc); } // Note: handleRecursion returns a MachineStatus and is meant to be used when within a state machine. // handleMachineStatus returns a bool and is the final status-handler called when all state machines have processed the current state to completion. MachineStatus TranEntry::handleRecursion(MachineStatus status) { while (status == MachineStatusPopMachine) { // return to previous procedure on stack // Special case: we do not return, we immediately invoke the popped-to method. delete tran()->tePopMachine(); LOG(DEBUG) "popped to "<debugName()<<" at state "<mPopState; status = currentProcedure()->machineRunState(currentProcedure()->mPopState); } return status; } // Return TRUE if the status indicates the message or whatever had a message handler, regardless of the success/fail result. bool TranEntry::handleMachineStatus(MachineStatus status) { //MMContext *set = teGetContext(); OBJLOG(DEBUG) <isReleased()) { // If the caller did not already call this, we dont know what the heck happened, so do a RELEASE instead of HARDRELEASE. channel()->chanRelease(L3_RELEASE_REQUEST,status.msCause); } return true; case MachineStatus::MachineCodeQuitTran: // aka MachineStatusQuitTran // Pop all procedures from stack and remove the transaction. Procedure already sent messages. // This is the normal exit from a completed procedure. // In all cases the caller was supposed to already send termination messages toward both layer2 (the handset) // and SIP Dialog (the network) however, we will perform additional termination to make sure. teRemove(status.msCause); // Danger will robinson!!!! Deletes the Transaction we are running. return true; case MachineStatus::MachineCodeUnexpectedState: // aka MachineStatusUnexpectedState return false; // The message or state was unrecognized by this state machine. //default: //return true; // All others; Message was handled by the current Procedure. } #if 0 switch (status) { case MachineStatusUnexpectedState: // Invalid procRun argument; very unlikely internal error. LOG(ERR) << "unexpected state"; return false; // unhandled. Should we keep going anyway? probably not. case MachineStatusUnexpectedMessage: // error message printed by caller. LOG(ERR) << "unsupported message"; return false; // unhandled but keep going. case MachineStatusQuit: while (currentProcedure()) { delete tran()->tePopMachine(); } teClose(); // Danger will robinson!!!! return true; case MachineStatusUnexpectedPrimitive: LOG(ERR) << "unexpected primitive"; // Dont think this MachineStatus is used anywhere. return false; default: return true; // All others; Message was handled by the current Procedure. } #endif return true; // unnecessary but makes gcc happy. } // The 'lockAnd...' methods are used to initially start or restart a Procedure. // Update: This locking is no longer needed or relevant. // (pat 5-13-2014) Update update: Yes the locks appear to be used - I got the "waiting more than one second" emssage from lockAndInvokeFrame; // the situation was starting then cancelling a call on the same handset. This may have been in the midst of a reassignment procedure // so messages were arriving simultaneously on both channels. Looks like the CC Disconnect was sent on the SDCCH right before the // reassign procedure, then a dialog cancel came in too. // The phone misbehaved - it looked like it stayed connected after pressing disconnect? check again. // When jumping between procedures we dont use these, although it would not matter since the locks can be recursive. // Start a procedure by calling stateStart: // If no proc is specified here, assume that teSetProcedure was called previously and start the currentProcedure. bool TranEntry::lockAndStart(MachineBase *wProc) { ScopedLock lock(mL3RewriteLock,__FILE__,__LINE__); if (wProc) { teSetProcedure(wProc,false); devassert(wProc == currentProcedure()); } else { wProc = currentProcedure(); devassert(wProc); // Someone set the currentProcedure before calling this method. } return handleMachineStatus(wProc->callMachStart(wProc)); } // Start a procedure by passing it this L3 message: bool TranEntry::lockAndStart(MachineBase *wProc, GSM::L3Message *l3msg) { ScopedLock lock(mL3RewriteLock,__FILE__,__LINE__); teSetProcedure(wProc,false); devassert(wProc == currentProcedure()); return handleMachineStatus(wProc->dispatchL3Msg(l3msg)); } #if UNUSED // Send a message to the current Procedure, either l3msg or lch. // lch is the channel this message arrived on. It is information we have, but I dont think it is useful. // I wonder if there are any cases where lch may not be the L3LogicalChannel that initiated the Procedure? // It probably doesnt matter - we use the L3LogicalChannel to send return messages to the MS, // and the initial channel that created the Procedure is probably the correct one. // For example if lch is FACCH, we cannot send anything downstrem on that. bool TranEntry::lockAndInvokeL3Msg(const GSM::L3Message *l3msg /*, const L3LogicalChannel *lch*/) { LOG(DEBUG); ScopedLock lock(mL3RewriteLock,__FILE__,__LINE__); #if ORIGINAL // Look up the message in the Procedure message table. int state = mCurrentProcedure->findMsgMap(l3msg->PD(),l3msg->MTI()); if (state == -1) { // If state is -1, message is not mapped and is ignored by us. return false; // Message was not wanted by this Procedure. } teProcInvoke(state,l3msg,NULL); #endif if (MachineBase *proc = currentProcedure()) { LOG(DEBUG) <<"sending l3msg to"<dispatchL3Msg(l3msg)); } return false; } #endif // l3msg may be NULL for primitives or unparseable messages. bool TranEntry::lockAndInvokeFrame(const L3Frame *frame, const L3Message *l3msg) { LOG(DEBUG) << l3msg; ScopedLock lock(mL3RewriteLock,__FILE__,__LINE__); if (MachineBase *proc = currentProcedure()) { LOG(DEBUG) <<"sending frame to"<dispatchFrame(frame,l3msg)); } LOG(ERR) <<"Received message for transaction with no state machine. "<mSipHandlerState; if (state >= 0) { return teProcInvoke(state,NULL,sipmsg); } return MachineStatusUnexpectedMessage; #endif if (MachineBase *proc = currentProcedure()) { return handleMachineStatus(proc->dispatchSipDialogMsg(sipmsg)); } return false; } bool TranEntry::lockAndInvokeSipMsgs() { // SIP Message processing is blocked during the AssignTCHF procedure. // Now that is handled by checking for sip state changes when the AssignTCHF procedure is finished. //if (mSipDialogMessagesBlocked) { return false; } if (DialogMessage*dmsg = this->mTranInbox.readNoBlock()) { lockAndInvokeSipMsg(dmsg); delete dmsg; // Since the message can result in the transaction being killed, only process one message // then we return to let the caller invoke us again if the transaction is still active. return true; } return false; } bool TranEntry::lockAndInvokeTimeout(L3Timer *timer) { LOG(DEBUG) << LOGVAR2("timer",timer->tName()) << this; ScopedLock lock(mL3RewriteLock,__FILE__,__LINE__); if (MachineBase *proc = currentProcedure()) { return handleMachineStatus(proc->dispatchTimeout(timer)); } return false; } void TranEntry::terminateHook() { LOG(INFO) "SIP term info terminateHook"; if (MachineBase *proc = currentProcedure()) { proc->handleTerminationRequest(); } } void TranEntry::teCloseDialog(TermCause cause) { CallState state = getGSMState(); LOG(INFO) << "SIP term info teCloseDialog cancel cause: " << cause /*<< " l3Cause : " << l3Cause*/ ; // SVGDBG // An MO transaction may not have a dialog yet. // The dialog can also be NULL because the phone will send a DISCONNECT first thing if a previous call did not close correctly. SipDialog *dialog = getDialog(); if (dialog) { // For the special case of outbound handover we must destroy the dialog immediately // in case a new handover comes back to us in the reverse direction. // just drop the dialog, dont send a BYE. if (state == CCState::HandoverOutbound) { cause = TermCause::Local(L3Cause::Handover_Outbound); } LOG(INFO) << "SIP term info dialogCancel called in teCloseDialog"; dialog->dialogCancel(cause); // Does nothing if dialog not yet started. } } // Used by MMLayer to immediately remove the transaction, without notifying MM layer. // An assumption is that the dialog pointer is valid as long as the transaction exists, // so we dont zero out the dialog pointer until we kill the dialog. void TranEntry::teRemove(TermCause cause) { if (mFinalDisposition.tcIsEmpty()) { mFinalDisposition = cause; } SipDialog *dialog = getDialog(); CallState state = getGSMState(); if (dialog) { if (state == CCState::HandoverOutbound) { // FIXME: Sigh, it could be NoUserResponding from the default timer handler. devassert(cause.tcGetValue() == L3Cause::Handover_Outbound || cause.tcIsEmpty()); cause = TermCause::Local(L3Cause::Handover_Outbound); } if (cause.tcIsEmpty() && dialog->getLastResponseCode()) { // Both transaction and dialog are already cancelled, so this cause is for reporting purposes. cause = dialog2TermCause(dialog); } LOG(INFO) << "SIP term info dialogCancel called in teRemove cancel cause: " << cause; dialog->dialogCancel(cause); // Does nothing if dialog not yet started. } //GSM::CCCause l3Cause = GSM::L3Cause::UnknownL3Cause; //if (dialog) { // int SIPerror = dialog->getLastResponseCode(); // // For the special case of outbound handover we must destroy the dialog immediately // // in case a new handover comes back to us in the reverse direction. // // just drop the dialog, dont send a BYE. // if (state == CCState::Handover_Outbound) { // devassert(cause == TermCauseHandoverOutbound); // cause = TermCauseHandoverOutbound; // } // if (cause == TermCauseNoAnswerToPage) // l3Cause = GSM::L3Cause::NoUserResponding; // else if (cause == TermCauseBusy) // l3Cause = GSM::L3Cause::UserBusy; // else if (cause == TermCauseCongestion) // l3Cause = GSM::L3Cause::SwitchingEquipmentCongestion; // if (l3Cause == GSM::L3Cause::UnknownL3Cause) { // // Translate SIP error to L3Cause // switch (SIPerror) { // case 408: l3Cause = L3Cause::NoUserResponding; break; // case 480: l3Cause = L3Cause::UserAlertingNoAnswer; break; // case 404: l3Cause = L3Cause::UnassignedNumber; break; // default: break; // } // } // LOG(INFO) << "SIP term info dialogCancel called in teRemove cancel cause: " << cause << " L3Cause: " << l3Cause << " SIPerror: " << SIPerror; // dialog->dialogCancel(cause, l3Cause); // Does nothing if dialog not yet started. //} if (L3CDR *cdr = this->createCDR(false,cause)) { gCdrService.cdrServiceStart(); gCdrService.cdrAdd(cdr); } // It is important to make this transaction no longer point at the dialog, because the dialog // will not destroy itself while a transaction still points at it. Taking the transaction // out of the TransactionTable prevents the dialog from finding the transaction any longer. // However to prevent a race we must do this after using the dialog, which we did above. setGSMState(CCState::NullState); // redundant, transaction is being deleted. gNewTransactionTable.ttRemove(this->tranID()); while (currentProcedure()) { delete tran()->tePopMachine(); } if (mContext) { mContext->mmDisconnectTran(this); } // DANGER: this deletes the transaction as a side effect. } // Send closure messages for a transaction that is known to be a CS transaction, using the specified CC cause. // Must only call from the thread running the channel. // To close all transactions on a channel, see L3LogicalChannel::chanClose() void TranEntry::teCloseCallNow(TermCause cause, bool sendCause) { LOG(INFO) << "SIP term info closeCallNow cause: " << cause; // SVGDBG WATCHINFO("CloseCallNow"<descriptiveString()); LOG(DEBUG) <teCloseDialog(cause); // Redundant with teRemove, but I want to be sure we terminate the dialog regardless of bugs. if (isL3TIValid() && tran()->getGSMState() != CCState::NullState && tran()->getGSMState() != CCState::ReleaseRequest) { unsigned l3ti = getL3TI(); // 24.008 5.4.2: Permitted method to close call immediately. if (sendCause) { channel()->l3sendm(GSM::L3ReleaseComplete(l3ti,cause.tcGetCCCause())); // This is a CC message that releases this Transaction immediately. } else { channel()->l3sendm(GSM::L3ReleaseComplete(l3ti)); // This is a reply that confirms transaction was released. } } setGSMState(CCState::NullState); // redundant, we are deleting this transaction. } void writePrivateHeaders(SipMessage *msg, const L3LogicalChannel *l3chan) { // P-PHY-Info // This is a non-standard private header in OpenBTS. // TODO: If we add the MSC params to this, especially L3TI, the SIP message will completely encapsulate handover. // TA= TE= UpRSSI= TxPwr= DnRSSIdBm= // Get the values. if (l3chan) { char phy_info[200]; // (pat) TODO: This is really cheating. const GSM::L2LogicalChannel *chan = l3chan->getL2Channel(); MSPhysReportInfo *phys = chan->getPhysInfo(); // (pat 5-2014) Adding the imsi to the info, since it is sometimes not possible to know which of the // two handsets involved in the dialog initiated the SIP message. string imsi = l3chan->chanGetImsi(true); snprintf(phy_info,200,"OpenBTS; IMSI=%s TA=%d TE=%f UpRSSI=%f TxPwr=%d DnRSSIdBm=%d time=%9.3lf", imsi.c_str(), phys->actualMSTiming(), phys->timingError(), phys->getRSSI(), phys->actualMSPower(), chan->measurementResults().RXLEV_FULL_SERVING_CELL_dBm(), phys->timestamp()); static const string cPhyInfoString("P-PHY-Info"); msg->smAddHeader(cPhyInfoString,phy_info); } // P-Access-Network-Info // See 3GPP 24.229 7.2. This is a genuine specified header. char cgi_3gpp[256]; snprintf(cgi_3gpp,256,"3GPP-GERAN; cgi-3gpp=%s%s%04x%04x", gConfig.getStr("GSM.Identity.MCC").c_str(),gConfig.getStr("GSM.Identity.MNC").c_str(), (unsigned)gConfig.getNum("GSM.Identity.LAC"),(unsigned)gConfig.getNum("GSM.Identity.CI")); static const string cAccessNetworkInfoString("P-Access-Network-Info"); msg->smAddHeader(cAccessNetworkInfoString, cgi_3gpp); // FIXME -- Use the subscriber registry to look up the E.164 // and make a second P-Preferred-Identity header. } void TranInit() { SipCallbacks::setcallback_ttAddMessage( & NewTransactionTable_ttAddMessage); SipCallbacks::setcallback_writePrivateHeaders( & writePrivateHeaders); } // Look up the phone number, if any, passed to us from the Registrar when this imsi registered. static string lookupPhoneNumber(string imsi) { string number, unused; gTMSITable.getSipIdentities(imsi,number,unused); int len = number.size(); if (len >= 2 && number[0] == '<') { number = number.substr(1,len-2); } if (0 == strncasecmp(number.c_str(),"tel:",4)) { number = number.substr(4); } return number; } L3CDR *TranEntry::createCDR(bool makeCMR, TermCause cause) // If true, make a CMR instead of a CDR. CMRs have more info. { L3CDR *cdrp = new L3CDR(), &cdr = *cdrp; // Gotta love this language. cdr.cdrToNumber = string(this->called().digits()); cdr.cdrFromNumber = string(this->calling().digits()); switch (this->service().type()) { case L3CMServiceType::MobileOriginatedCall: cdr.cdrType = "MOC"; labelmoc: if (mConnectTime == 0 && ! makeCMR) { goto labelIgnore; } cdr.cdrFromImsi = this->subscriber().mImsi; if (SipDialog *dialog = this->getDialog()) { // MOC do not know the IMSI of the phone number they are calling. //cdr.cdrToImsi = dialog->sipRemoteUsername(); // This is the phone number, not the imsi. if (dialog->dsPeer()) cdr.cdrPeer = dialog->dsPeer()->mipName; } if (cdr.cdrFromNumber.empty()) { cdr.cdrFromNumber = lookupPhoneNumber(cdr.cdrFromImsi); } break; case L3CMServiceType::EmergencyCall: cdr.cdrType = "Emergency"; goto labelmoc; break; case L3CMServiceType::ShortMessage: if (this->mMessage.size() == 0 && ! makeCMR) { goto labelIgnore; } cdr.cdrType = "MOSMS"; goto labelmoc; break; case L3CMServiceType::MobileTerminatedShortMessage: if (this->mMessage.size() == 0 && ! makeCMR) { goto labelIgnore; } cdr.cdrType = "MTSMS"; goto labelmtc; break; case L3CMServiceType::UndefinedType: case L3CMServiceType::SupplementaryService: case L3CMServiceType::VoiceCallGroup: case L3CMServiceType::VoiceBroadcast: case L3CMServiceType::LocationService: case L3CMServiceType::LocationUpdateRequest: case L3CMServiceType::HandoverCall: // The handover code sets the type to MobileOriginatedCall or MobileTerminatedCall so we should not see this. break; case L3CMServiceType::MobileTerminatedCall: cdr.cdrType = "MTC"; labelmtc: if (mConnectTime == 0 && ! makeCMR) { goto labelIgnore; } cdr.cdrToImsi = this->subscriber().mImsi; if (SipDialog *dialog2 = this->getDialog()) { cdr.cdrFromImsi = dialog2->sipRemoteUsername(); if (0 == strncasecmp(cdr.cdrFromImsi.c_str(),"IMSI",4)) { cdr.cdrFromImsi = cdr.cdrFromImsi.substr(4); } if (dialog2->dsPeer()) cdr.cdrPeer = dialog2->dsPeer()->mipName; } if (cdr.cdrToNumber.empty()) { cdr.cdrToNumber = lookupPhoneNumber(cdr.cdrToImsi); } break; }; if (cdr.cdrType == "") { LOG(ERR) << "Unrecognized service, no CDR generated:"<service(); labelIgnore: delete cdrp; return NULL; } cdr.cdrTid = this->tranID(); cdr.cdrConnectTime = this->mConnectTime; cdr.cdrDuration = cdr.cdrConnectTime ? time(NULL) - cdr.cdrConnectTime : 0; cdr.cdrMessageSize = this->mMessage.size(); cdr.cdrCause = this->mFinalDisposition; if (HandoverEntry *hp = this->getHandoverEntry(false)) { cdr.cdrFromHandover = sockaddr2string(&hp->mInboundPeer,false); cdr.cdrToHandover = sockaddr2string(&hp->mOutboundPeer,false); } return cdrp; } void L3CDR::cdrWriteHeader(FILE *pf) { // Fields must match type and order in cdrWriteEntry() typedef struct {const char *name; const char *type;} Field; static Field fields[] = { {"Type","string"}, {"Transaction-id","integer"}, {"To-IMSI","string"}, // usually empty for MOC. {"From-IMSI","string"}, {"To-number","string"}, {"From-number","string"}, {"Peer","string"}, {"Start-time","integer"}, // standard unix time. // Connect duration, as opposed to total time, in seconds. 0 means call never successfully connected. {"Connect-Duration","integer"}, {"Message-size","integer"}, // Only for SMS. {"To-Handover","string"}, // TODO {"From-Handover","string"}, {"Termination-Side","char"}, // R or L for remote or local side. {"Termination-Cause","integer"}, {NULL,NULL} }; // Print the field names. for (Field *field = fields; field->name; field++) { if (field != fields) fputc(',',pf); fprintf(pf,"%s",field->name); } fputc('\n',pf); // Print the field types. for (Field *field = fields; field->type; field++) { if (field != fields) fputc(',',pf); fprintf(pf,"%s",field->type); } fputc('\n',pf); fflush(pf); } void L3CDR::cdrWriteEntry(FILE *pf) { fprintf(pf,"%s,%u,",cdrType.c_str(),cdrTid); fprintf(pf,"%s,%s,",cdrToImsi.c_str(),cdrFromImsi.c_str()); fprintf(pf,"%s,%s,",cdrToNumber.c_str(),cdrFromNumber.c_str()); fprintf(pf,"%s,",cdrPeer.c_str()); fprintf(pf,"%lu,%lu,",cdrConnectTime,cdrDuration); fprintf(pf,"%d,",cdrMessageSize); fprintf(pf,"%s,%s,",cdrToHandover.c_str(),cdrFromHandover.c_str()); // handover to, from fprintf(pf,"%c",(cdrCause.mtcInstigator == TermCause::SideLocal) ? 'L' : 'R'); // termination side fprintf(pf,"%d",cdrCause.tcGetValue()); // termination cause fprintf(pf,"\n"); fflush(pf); // Write to disk in case OpenBTS crashes. } // Open a new file whenever the date changes. void CdrService::cdrOpenFile() { time_t now = time(NULL); struct tm tm; localtime_r(&now,&tm); if (tm.tm_yday == cdrCurrentDay) { // We already tried to open the file. Either we succeeded or not, but we wont try to open it on every transaction. return; } if (mpf) { fclose(mpf); mpf = NULL; } cdrCurrentDay = tm.tm_yday; string date = format("%04d-%02d-%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); const char *dirname = "/var/log/OpenBTS"; // TODO: make this a config option. mkdir(dirname,0777); // Doesnt hurt to do this even if unnecessary. string btsid = gConfig.getStr("SIP.Local.IP"); // Default bts id to the local IP address. string filename = format("%s/OpenBTS_%s_%s.cdr",dirname,btsid,date); mpf = fopen(filename.c_str(),"a"); // Dont re-write the header if we are appending to an existing file. if (mpf && 0 == ftell(mpf)) { L3CDR::cdrWriteHeader(mpf); } } void*CdrService::cdrServiceLoop(void*arg) { CdrService *self = static_cast(arg); while (!gBTS.btsShutdown()) { L3CDR *cdr = self->mCdrQueue.read(); // Blocking read. self->cdrOpenFile(); // We may have to open a new file if the date changed. if (self->mpf) { cdr->cdrWriteEntry(self->mpf); } delete cdr; } return NULL; } void CdrService::cdrServiceStart() { if (!cdrServiceRunning) { cdrServiceRunning = true; cdrServiceThread.start(cdrServiceLoop,this); } } }; // namespace // vim: ts=4 sw=4