/* * Copyright 2011 Kestrel Signal Processing, Inc. * Copyright 2011, 2012, 2013, 2014 Range Networks, Inc. * * This software is distributed under the terms of the GNU Affero Public License. * See the COPYING file in the main directory for details. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 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. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ #include "SubscriberRegistry.h" #include #include #include #include #include #include "sqlite3.h" #include #include #include #include // for sort() #include extern ConfigurationTable gConfig; using namespace std; static const char* createRRLPTable = { "CREATE TABLE IF NOT EXISTS RRLP (" "id INTEGER PRIMARY KEY, " "name VARCHAR(80) not null, " "latitude real not null, " "longitude real not null, " "error real not null, " "time text not null " ")" }; static const char* createDDTable = { "CREATE TABLE IF NOT EXISTS DIALDATA_TABLE (" "id INTEGER PRIMARY KEY, " "exten VARCHAR(40) NOT NULL DEFAULT '', " "dial VARCHAR(128) NOT NULL DEFAULT '' " ")" }; static const char* createRateTable = { "create table if not exists rates (service varchar(30) not null, rate integer not null)" }; static const char* createSBTable = { "CREATE TABLE IF NOT EXISTS SIP_BUDDIES (" "id integer primary key, " "name VARCHAR(80) not null, " "context VARCHAR(80), " "callingpres VARCHAR(30) DEFAULT 'allowed_not_screened', " "deny VARCHAR(95), " "permit VARCHAR(95), " "secret VARCHAR(80), " "md5secret VARCHAR(80), " "remotesecret VARCHAR(250), " "transport VARCHAR(10), " "host VARCHAR(31) default '' not null, " "nat VARCHAR(5) DEFAULT 'no' not null, " "type VARCHAR(10) DEFAULT 'friend' not null, " "accountcode VARCHAR(20), " "amaflags VARCHAR(13), " "callgroup VARCHAR(10), " "callerid VARCHAR(80), " "defaultip VARCHAR(40) DEFAULT '0.0.0.0', " "dtmfmode VARCHAR(7) DEFAULT 'info', " "fromuser VARCHAR(80), " "fromdomain VARCHAR(80), " "insecure VARCHAR(4), " "language CHAR(2), " "mailbox VARCHAR(50), " "pickupgroup VARCHAR(10), " "qualify CHAR(3), " "regexten VARCHAR(80), " "rtptimeout CHAR(3), " "rtpholdtimeout CHAR(3), " "setvar VARCHAR(100), " "disallow VARCHAR(100) DEFAULT 'all', " "allow VARCHAR(100) DEFAULT 'gsm' not null, " "fullcontact VARCHAR(80), " "ipaddr VARCHAR(45), " "port int(5) DEFAULT 5062, " "username VARCHAR(80), " "defaultuser VARCHAR(80), " "subscribecontext VARCHAR(80), " "directmedia VARCHAR(3), " "trustrpid VARCHAR(3), " "sendrpid VARCHAR(3), " "progressinband VARCHAR(5), " "promiscredir VARCHAR(3), " "useclientcode VARCHAR(3), " "callcounter VARCHAR(3), " "busylevel int(11) default 1, " "allowoverlap VARCHAR(3) DEFAULT 'no', " "allowsubscribe VARCHAR(3) DEFAULT 'no', " "allowtransfer VARCHAR(3) DEFAULT 'no', " "ignoresdpversion VARCHAR(3) DEFAULT 'no', " "template VARCHAR(100), " "videosupport VARCHAR(6) DEFAULT 'no', " "maxcallbitrate int(11), " "rfc2833compensate VARCHAR(3) DEFAULT 'yes', " "'session-timers' VARCHAR(10) DEFAULT 'accept', " "'session-expires' int(6) DEFAULT 1800, " "'session-minse' int(6) DEFAULT 90, " "'session-refresher' VARCHAR(3) DEFAULT 'uas', " "t38pt_usertpsource VARCHAR(3), " "outboundproxy VARCHAR(250), " "callbackextension VARCHAR(250), " "registertrying VARCHAR(3) DEFAULT 'yes', " "timert1 int(6) DEFAULT 500, " "timerb int(9), " "qualifyfreq int(6) DEFAULT 120, " "contactpermit VARCHAR(250), " "contactdeny VARCHAR(250), " "lastms int(11) DEFAULT 0 not null, " "regserver VARCHAR(100), " "regseconds int(11) DEFAULT 0 not null, " "useragent VARCHAR(100), " "cancallforward CHAR(3) DEFAULT 'yes' not null, " "canreinvite CHAR(3) DEFAULT 'no' not null, " "mask VARCHAR(95), " "musiconhold VARCHAR(100), " "restrictcid CHAR(3), " "calllimit int(5) default 1, " "WhiteListFlag timestamp not null default '0', " "WhiteListCode varchar(8) not null default '0', " "rand varchar(33) default '', " "sres varchar(33) default '', " "ki varchar(33) default '', " "kc varchar(33) default '', " "prepaid int(1) DEFAULT 0 not null, " // flag to indicate prepaid customer "account_balance int(9) default 0 not null, " // current account, neg is debt, pos is credit "RRLPSupported int(1) default 1 not null, " "hardware VARCHAR(20), " "regTime INTEGER default 0 NOT NULL, " // Unix time of most recent registration "a3_a8 varchar(45) default NULL" ")" }; static const char* createMEMSBTable = { "CREATE TABLE IF NOT EXISTS memcache.mem_sip_buddies (" "username varchar(80) primary key, " "ipaddr varchar(45), " "port int(5) default 5062, " "rand varchar(33) default '', " "sres varchar(33) default ''" ")" }; int SubscriberRegistry::init() { string ldb = gConfig.getStr("SubscriberRegistry.db"); size_t p = ldb.find_last_of('/'); if (p == string::npos) { LOG(EMERG) << "SubscriberRegistry.db not in a directory?"; mDB = NULL; return 1; } string dir = ldb.substr(0, p); struct stat buf; if (stat(dir.c_str(), &buf)) { LOG(EMERG) << dir << " does not exist"; mDB = NULL; return 1; } mNumSQLTries=gConfig.getNum("Control.NumSQLTries"); int rc = sqlite3_open(ldb.c_str(),&mDB); if (rc) { LOG(EMERG) << "Cannot open SubscriberRegistry database: " << ldb << " error: " << sqlite3_errmsg(mDB); sqlite3_close(mDB); mDB = NULL; return 1; } if (!sqlite3_command(mDB,createRRLPTable,mNumSQLTries)) { LOG(EMERG) << "Cannot create RRLP table"; return 1; } if (!sqlite3_command(mDB,createDDTable,mNumSQLTries)) { LOG(EMERG) << "Cannot create DIALDATA_TABLE table"; return 1; } if (!sqlite3_command(mDB,createRateTable,mNumSQLTries)) { LOG(EMERG) << "Cannot create rate table"; return 1; } if (!sqlite3_command(mDB,createSBTable,mNumSQLTries)) { LOG(EMERG) << "Cannot create SIP_BUDDIES table"; return 1; } // Set high-concurrency WAL mode. if (!sqlite3_command(mDB,enableWAL,mNumSQLTries)) { LOG(EMERG) << "Cannot enable WAL mode on database at " << ldb << ", error message: " << sqlite3_errmsg(mDB); } #ifndef SR_API_ONLY // memory based sip_buddies table if (!sqlite3_command(mDB,"attach database ':memory:' as memcache",mNumSQLTries)) { LOG(EMERG) << "Cannot create memcache database"; return 1; } if (!sqlite3_command(mDB,createMEMSBTable,mNumSQLTries)) { LOG(EMERG) << "Cannot create memcache mem_sip_buddies table"; return 1; } if (!sqlite3_command(mDB,"INSERT INTO mem_sip_buddies SELECT username, ipaddr, port, rand, sres FROM sip_buddies",mNumSQLTries)) { LOG(EMERG) << "Cannot populate mem_sip_buddies table with disk contents"; return 1; } if (!sqlite3_command(mDB,"ALTER TABLE mem_sip_buddies ADD dirty INTEGER DEFAULT 0",mNumSQLTries)) { LOG(EMERG) << "Cannot add dirty column to mem_sip_buddies table"; return 1; } // Start the sync thread mSyncer.start((void*(*)(void*))subscriberRegistrySyncer,NULL); #endif return 0; } string SubscriberRegistry::getResultsAsString(string query) { sqlite3_stmt *stmt; if (sqlite3_prepare_statement(mDB, &stmt, query.c_str(), 5)) { std::cout << " - sqlite3_prepare_statement() failed" << std::endl; return ""; } int state; int row = 0; int col; int colMax = sqlite3_column_count(stmt); const char* colName; const char* colText; ostringstream colNames; ostringstream colTexts; while (1) { state = sqlite3_step(stmt); if (state == SQLITE_ROW) { for (col = 0; col < colMax; col++) { colName = (const char*)sqlite3_column_name(stmt, col); colText = (const char*)sqlite3_column_text(stmt, col); if (row == 0) { colNames << colName << " "; } if (colText == NULL) { colTexts << "NULL" << " "; } else { colTexts << colText << " "; } } } else if (state == SQLITE_DONE) { break; // TODO : handle more SQLITE_FAILURECODES here } else { return " - failed in while(1) in } else {"; } colTexts << std::endl; row++; } sqlite3_finalize(stmt); ostringstream s; string title = colNames.str(); if (title.length()) { s << title << std::endl; s << colTexts << std::endl; } else { s << " - none found" << std::endl; } return s.str(); } vector SubscriberRegistry::getTableColumns(string tableName) { vector colNames; stringstream tmp; tmp << "select * from " << tableName << " limit 1"; sqlite3_stmt *stmt; if (sqlite3_prepare_statement(mDB, &stmt, tmp.str().c_str(), 5)) { std::cout << " - getTableColumns() - sqlite3_prepare_statement() failed" << std::endl; return colNames; } tmp.str(""); int state; int col; int colMax = sqlite3_column_count(stmt); while (1) { state = sqlite3_step(stmt); if (state == SQLITE_ROW) { for (col = 0; col < colMax; col++) { tmp << (const char*)sqlite3_column_name(stmt, col); colNames.push_back(tmp.str()); tmp.str(""); } } else if (state == SQLITE_DONE) { break; // TODO : handle more SQLITE_FAILURECODES here } else { std::cout << " - getTableColumns() - failed in while(1) in } else {" << std::endl; return colNames; } } sqlite3_finalize(stmt); return colNames; } #ifndef SR_API_ONLY string SubscriberRegistry::generateSyncToDiskQuery() { vector columns = getTableColumns("sip_buddies"); if (!columns.size()) { LOG(DEBUG) << "sip_buddies is empty"; return ""; } sort(columns.begin(), columns.end()); std::stringstream ss; unsigned i; ss << "replace into sip_buddies (ipaddr, port, rand, sres, "; for (i = 0; i < columns.size(); i++) { if (columns[i].compare("ipaddr") == 0 || columns[i].compare("port") == 0 || columns[i].compare("rand") == 0 || columns[i].compare("sres") == 0) { continue; } if (i != 0) { ss << ", "; } if (columns[i].find("-") != std::string::npos) { ss << "'" << columns[i] << "'"; } else { ss << columns[i]; } } ss << ") select src.ipaddr, src.port, src.rand, src.sres, "; for (i = 0; i < columns.size(); i++) { if (columns[i].compare("ipaddr") == 0 || columns[i].compare("port") == 0 || columns[i].compare("rand") == 0 || columns[i].compare("sres") == 0) { continue; } if (i != 0) { ss << ", "; } ss << "dest."; if (columns[i].find("-") != std::string::npos) { ss << "'" << columns[i] << "'"; } else { ss << columns[i]; } } ss << " from mem_sip_buddies src "; ss << "inner join sip_buddies dest on src.username = dest.username "; ss << "where src.dirty = \"1\""; return ss.str(); } bool SubscriberRegistry::syncMemoryDB() { bool ret = true; string syncToDisk = generateSyncToDiskQuery(); string cleanDirty = "update mem_sip_buddies set dirty = \"0\""; string syncFromDiskAddNewEntries = "INSERT INTO mem_sip_buddies " "(username, ipaddr, port, rand, sres, dirty) " "SELECT sip_buddies.username, sip_buddies.ipaddr, sip_buddies.port, sip_buddies.rand, sip_buddies.sres, '0' FROM sip_buddies " "LEFT OUTER JOIN mem_sip_buddies ON (mem_sip_buddies.username=sip_buddies.username) " "WHERE mem_sip_buddies.username IS NULL"; string syncFromDiskDeleteOldEntries = "DELETE FROM mem_sip_buddies " "WHERE username IN " "(SELECT mem_sip_buddies.username FROM mem_sip_buddies" " LEFT JOIN sip_buddies ON mem_sip_buddies.username=sip_buddies.username" " WHERE sip_buddies.username IS NULL" ")"; Timeval timer; mLock.lock(); // this string is only non-empty if there are entries in sip_buddies if (syncToDisk.length()) { // sync dirty entries to disk if (sqlUpdate(syncToDisk.c_str()) == FAILURE) { LOG(ERR) << "syncToDisk failed"; } else { // if it succeeds, mark these entries as clean again LOG(INFO) << "syncToDisk succeeded"; if (sqlUpdate(cleanDirty.c_str()) == FAILURE) { LOG(ERR) << "cleanDirty failed"; } else { LOG(INFO) << "cleanDirty succeeded"; } } // sync new rows from the disk into memory if (sqlUpdate(syncFromDiskAddNewEntries.c_str()) == FAILURE) { LOG(ERR) << "syncFromDiskAddNewEntries failed"; } else { LOG(INFO) << "syncFromDiskAddNewEntries succeeded"; } } // delete old entries from memory no longer found on the disk if (sqlUpdate(syncFromDiskDeleteOldEntries.c_str()) == FAILURE) { LOG(ERR) << "syncFromDiskDeleteOldEntries failed"; } else { LOG(INFO) << "syncFromDiskDeleteOldEntries succeeded"; } mLock.unlock(); LOG(INFO) << "syncMemoryDB() locked the db for " << timer.elapsed() << "ms"; return ret; } #endif SubscriberRegistry::~SubscriberRegistry() { if (mDB) sqlite3_close(mDB); } SubscriberRegistry::Status SubscriberRegistry::sqlLocal(const char *query, char **resultptr) { LOG(INFO) << query; if (!resultptr) { if (!sqlite3_command(db(), query, mNumSQLTries)) return FAILURE; return SUCCESS; } sqlite3_stmt *stmt; if (sqlite3_prepare_statement(db(), &stmt, query, mNumSQLTries)) { LOG(ERR) << "sqlite3_prepare_statement problem with query \"" << query << "\""; return FAILURE; } int src = sqlite3_run_query(db(), stmt, mNumSQLTries); if (src != SQLITE_ROW) { sqlite3_finalize(stmt); return FAILURE; } char *column = (char*)sqlite3_column_text(stmt, 0); if (!column) { LOG(ERR) << "Subscriber registry returned a NULL column."; sqlite3_finalize(stmt); return FAILURE; } *resultptr = strdup(column); sqlite3_finalize(stmt); return SUCCESS; } char *SubscriberRegistry::sqlQuery(const char *unknownColumn, const char *table, const char *knownColumn, const char *knownValue) { char *result = NULL; SubscriberRegistry::Status st; ostringstream os; os << "select " << unknownColumn << " from " << table << " where " << knownColumn << " = \"" << knownValue << "\""; // try to find locally st = sqlLocal(os.str().c_str(), &result); if ((st == SUCCESS) && result) { // got it. return it. LOG(INFO) << "result = " << result; return result; } // didn't find locally LOG(INFO) << "not found: " << os.str(); return NULL; } /* * Check for two of the same values for a single unknown */ char *SubscriberRegistry::sqlQuery2(const char *unknownColumn, const char *table, const char *knownColumn, const char *knownValue1, const char *knownValue2) { char *result = NULL; SubscriberRegistry::Status st; ostringstream os; // select knownValue from table where knownColumn IN ('knownValue1', 'knownValue2') os << "select " << unknownColumn << " from " << table << " where " << knownColumn << " IN (\'" << knownValue1 << "\', " << "\'" << knownValue2 << "\')"; //LOG(INFO) << "QUERY STRING = " << os.str().c_str(); // try to find locally st = sqlLocal(os.str().c_str(), &result); if ((st == SUCCESS) && result) { // got it. return it. LOG(INFO) << "result = " << result; return result; } // didn't find locally LOG(INFO) << "not found: " << os.str(); return NULL; } SubscriberRegistry::Status SubscriberRegistry::sqlUpdate(const char *stmt) { LOG(INFO) << stmt; return sqlLocal(stmt, NULL); } string SubscriberRegistry::imsiGet(string imsi, string key) { string name = imsi.substr(0,4) == "IMSI" ? imsi : "IMSI" + imsi; char *st = sqlQuery(key.c_str(), "sip_buddies", "username", name.c_str()); if (!st) { LOG(INFO) << "cannot get key " << key << " for username " << name; return ""; } return st; } #ifndef SR_API_ONLY bool SubscriberRegistry::imsiSet(string imsi, string key, string value) { string name = imsi.substr(0,4) == "IMSI" ? imsi : "IMSI" + imsi; ostringstream os; os << "update mem_sip_buddies set dirty = \"1\", " << key << " = \"" << value << "\" where username = \"" << name << "\""; mLock.lock(); SubscriberRegistry::Status ret = sqlUpdate(os.str().c_str()); mLock.unlock(); return ret == FAILURE; } bool SubscriberRegistry::imsiSet(string imsi, string key1, string value1, string key2, string value2) { string name = imsi.substr(0,4) == "IMSI" ? imsi : "IMSI" + imsi; ostringstream os; os << "update mem_sip_buddies set dirty = \"1\", " << key1 << " = \"" << value1 << "\"," << key2 << " = \"" << value2 << "\" where username = \"" << name << "\""; mLock.lock(); SubscriberRegistry::Status ret = sqlUpdate(os.str().c_str()); mLock.unlock(); return ret == FAILURE; } #endif /* * Get IMSI from phone number * Should be able to get rid of this one */ char *SubscriberRegistry::getIMSI(const char *ISDN) { if (!ISDN) { LOG(WARNING) << "SubscriberRegistry::getIMSI attempting lookup of NULL ISDN"; return NULL; } LOG(INFO) << "getIMSI(" << ISDN << ")"; return sqlQuery("dial", "dialdata_table", "exten", ISDN); } /* * This version handle phone number with or without a plus at the beginning * This should work for all uses of getIMSI * */ char *SubscriberRegistry::getIMSI2(const char *ISDN) { if (!ISDN) { LOG(WARNING) << "SubscriberRegistry::getIMSI2 attempting lookup of NULL ISDN"; return NULL; } LOG(INFO) << "getIMSI2(" << ISDN << ")"; char* str2; char localstr[50]; if (ISDN[0] == '+') { str2 = (char *) &ISDN[1]; // ISDN has plus remove plus for str2 } else { // ISDN no plus add plus to str2 strcpy(&localstr[0], "+"); strncat(localstr, ISDN, sizeof(localstr)-1); localstr[sizeof(localstr)-1] = 0; str2 = localstr; } // Two strings one with plus one without return sqlQuery2("dial", "dialdata_table", "exten", ISDN, (const char*) str2); } char *SubscriberRegistry::getCLIDLocal(const char* IMSI) { if (!IMSI) { LOG(WARNING) << "SubscriberRegistry::getCLIDLocal attempting lookup of NULL IMSI"; return NULL; } LOG(INFO) << "getCLIDLocal(" << IMSI << ")"; return sqlQuery("callerid", "sip_buddies", "username", IMSI); } char *SubscriberRegistry::getCLIDGlobal(const char* IMSI) { if (!IMSI) { LOG(WARNING) << "SubscriberRegistry::getCLIDGlobal attempting lookup of NULL IMSI"; return NULL; } LOG(INFO) << "getCLIDGlobal(" << IMSI << ")"; return sqlQuery("callerid", "sip_buddies", "username", IMSI); } char *SubscriberRegistry::getRegistrationIP(const char* IMSI) { if (!IMSI) { LOG(WARNING) << "SubscriberRegistry::getRegistrationIP attempting lookup of NULL IMSI"; return NULL; } LOG(INFO) << "getRegistrationIP(" << IMSI << ")"; return sqlQuery("ipaddr", "sip_buddies", "username", IMSI); } SubscriberRegistry::Status SubscriberRegistry::setRegTime(const char* IMSI) { if (!IMSI) { LOG(WARNING) << "SubscriberRegistry::setRegTime attempting set for NULL IMSI"; return FAILURE; } unsigned now = (unsigned)time(NULL); ostringstream os; os << "update sip_buddies set regTime = " << now << " where username = " << '"' << IMSI << '"'; return sqlUpdate(os.str().c_str()); } SubscriberRegistry::Status SubscriberRegistry::addUser(const char* IMSI, const char* CLID) { if (!IMSI) { LOG(WARNING) << "SubscriberRegistry::addUser attempting add of NULL IMSI"; return FAILURE; } if (!CLID) { LOG(WARNING) << "SubscriberRegistry::addUser attempting add of NULL CLID"; return FAILURE; } LOG(INFO) << "addUser(" << IMSI << "," << CLID << ")"; ostringstream os; os << "insert into sip_buddies (name, username, type, context, host, callerid, canreinvite, allow, dtmfmode, ipaddr, port) values ("; os << "\"" << IMSI << "\""; os << ","; os << "\"" << IMSI << "\""; os << ","; os << "\"" << "friend" << "\""; os << ","; os << "\"" << "phones" << "\""; os << ","; os << "\"" << "dynamic" << "\""; os << ","; os << "\"" << CLID << "\""; os << ","; os << "\"" << "no" << "\""; os << ","; os << "\"" << "gsm" << "\""; os << ","; os << "\"" << "info" << "\""; os << ","; os << "\"" << "127.0.0.1" << "\""; os << ","; os << "\"" << "5062" << "\""; os << ")"; os << ";"; SubscriberRegistry::Status st = sqlUpdate(os.str().c_str()); ostringstream os2; os2 << "insert into dialdata_table (exten, dial) values ("; os2 << "\"" << CLID << "\""; os2 << ","; os2 << "\"" << IMSI << "\""; os2 << ")"; SubscriberRegistry::Status st2 = sqlUpdate(os2.str().c_str()); return st == SUCCESS && st2 == SUCCESS ? SUCCESS : FAILURE; } char *SubscriberRegistry::mapCLIDGlobal(const char *local) { if (!local) { LOG(WARNING) << "SubscriberRegistry::mapCLIDGlobal attempting lookup of NULL local"; return NULL; } LOG(INFO) << "mapCLIDGlobal(" << local << ")"; char *IMSI = getIMSI2(local); if (!IMSI) return NULL; char *global = getCLIDGlobal(IMSI); free(IMSI); return global; } SubscriberRegistry::Status SubscriberRegistry::RRLPUpdate(string name, string lat, string lon, string err){ ostringstream os; os << "insert into RRLP (name, latitude, longitude, error, time) values (" << '"' << name << '"' << "," << lat << "," << lon << "," << err << "," << "datetime('now')" ")"; LOG(INFO) << os.str(); return sqlUpdate(os.str().c_str()); } #ifndef SR_API_ONLY extern SubscriberRegistry gSubscriberRegistry; void* subscriberRegistrySyncer(void*) { while (true) { sleep(15); gSubscriberRegistry.syncMemoryDB(); } return NULL; } #endif // vim: ts=4 sw=4