authdb-soci.cc 14.1 KB
Newer Older
1
/*
2 3
	Flexisip, a flexible SIP proxy server with media capabilities.
	Copyright (C) 2010-2015  Belledonne Communications SARL, All rights reserved.
4

5 6 7 8
	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.
9

10 11 12 13
	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.
14

15 16
	You should have received a copy of the GNU Affero General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 18
*/

19
#include <thread>
20

21 22 23 24 25 26 27
#include <soci/mysql/soci-mysql.h>

#include "soci-helper.hh"
#include "utils/digest.hh"

#include "authdb.hh"

28
using namespace soci;
29 30 31

// The dreaded chrono::steady_clock which is not supported for gcc < 4.7
#include <chrono>
32
using namespace std;
33
using namespace chrono;
34 35 36 37 38
#ifdef USE_MONOTONIC_CLOCK
namespace std {
typedef monotonic_clock steady_clock;
}
#endif
39
using namespace flexisip;
40

41 42
void SociAuthDB::declareConfig(GenericStruct *mc) {
	// ODBC-specific configuration keys
43 44 45
	ConfigItemDescriptor items[] = {

		{String, "soci-password-request",
46 47 48 49 50 51 52 53 54 55 56 57 58
			"Soci SQL request to execute to obtain the password and algorithm.\n"
			"Named parameters are:\n -':id' : the user found in the from header,\n -':domain' : the authorization realm, "
			"and\n -':authid' : the authorization username.\n"
			"The use of the :id parameter is mandatory.\n"
			"The output of this request MUST contain two columns in this order:\n"
			"\t- the password column\n"
			"\t- the algorithm associated column: it can be a column in the database or an explicitly specified value among these ('CLRTXT', 'MD5', 'SHA-256')\n"
			"Examples: \n"
			" - the password and algorithm are both available in the database\n"
			"\tselect password, algorithm from accounts where login = :id and domain = :domain\n"
			" - all the passwords from the database are MD5\n"
			"\t select password, 'MD5' from accounts where login = :id and domain = :domain",
			"select password, 'MD5' from accounts where login = :id and domain = :domain"},
59

60
		{String, "soci-user-with-phone-request",
61 62 63 64 65
			"Soci SQL request to execute to obtain the username associated with a phone alias.\n"
			"Named parameters are:\n -':phone' : the phone number to search for.\n"
			"The use of the :phone parameter is mandatory.\n"
			"Example : select login from accounts where phone = :phone ",
			""},
66

67
		{String, "soci-users-with-phones-request",
68 69 70 71 72 73
			"Soci SQL request to execute to obtain the usernames associated with phones aliases.\n"
			"Named parameters are:\n -':phones' : the phones to search for.\n"
			"The use of the :phones parameter is mandatory.\n"
			"If you use phone number linked accounts you'll need to select login, domain, phone in your request for flexisip to work."
			"Example : select login, domain, phone from accounts where phone in (:phones)",
			""},
74 75

		{Integer, "soci-poolsize",
76 77 78 79 80 81
			"Size of the pool of connections that Soci will use. We open a thread for each DB query, and this pool will "
			"allow each thread to get a connection.\n"
			"The threads are blocked until a connection is released back to the pool, so increasing the pool size will "
			"allow more connections to occur simultaneously.\n"
			"On the other hand, you should not keep too many open connections to your DB at the same time.",
			"100"},
82 83

		{String, "soci-backend", "Choose the type of backend that Soci will use for the connection.\n"
84 85 86
			"Depending on your Soci package and the modules you installed, this could be 'mysql', "
			"'oracle', 'postgresql' or something else.",
			"mysql"},
87 88

		{String, "soci-connection-string", "The configuration parameters of the Soci backend.\n"
89 90 91 92 93
			"The basic format is \"key=value key2=value2\". For a mysql backend, this "
			"is a valid config: \"db=mydb user=user password='pass' host=myhost.com\".\n"
			"Please refer to the Soci documentation of your backend, for intance: "
			"http://soci.sourceforge.net/doc/3.2/backends/mysql.html",
			"db=mydb user=myuser password='mypass' host=myhost.com"},
94

95
		{Integer, "soci-max-queue-size",
96 97 98 99 100 101
			"Amount of queries that will be allowed to be queued before bailing password "
			"requests.\n This value should be chosen accordingly with 'soci-poolsize', so "
			"that you have a coherent behavior.\n This limit is here mainly as a safeguard "
			"against out-of-control growth of the queue in the event of a flood or big "
			"delays in the database backend.",
			"1000"},
102

103
		config_item_end};
104 105 106 107

	mc->addChildrenValues(items);
}

108
SociAuthDB::SociAuthDB() {
109 110
	GenericStruct *cr = GenericManager::get()->getRoot();
	GenericStruct *ma = cr->get<GenericStruct>("module::Authentication");
111
	GenericStruct *mp = cr->get<GenericStruct>("module::Presence");
112

113 114 115
	poolSize = ma->get<ConfigInt>("soci-poolsize")->read();
	connection_string = ma->get<ConfigString>("soci-connection-string")->read();
	backend = ma->get<ConfigString>("soci-backend")->read();
116
	get_password_request = ma->get<ConfigString>("soci-password-request")->read();
117
	get_user_with_phone_request = ma->get<ConfigString>("soci-user-with-phone-request")->read();
118
	get_users_with_phones_request = ma->get<ConfigString>("soci-users-with-phones-request")->read();
119
	unsigned int max_queue_size = (unsigned int)ma->get<ConfigInt>("soci-max-queue-size")->read();
120
	hashed_passwd = ma->get<ConfigBoolean>("hashed-passwords")->read();
121
	check_domain_in_presence_results = mp->get<ConfigBoolean>("check-domain-in-presence-results")->read();
122

123 124
	conn_pool.reset(new connection_pool(poolSize));
	thread_pool.reset(new ThreadPool(poolSize, max_queue_size));
125

126
	LOGD("[SOCI] Authentication provider for backend %s created. Pooled for %zu connections", backend.c_str(), poolSize);
127 128
	connectDatabase();
}
129

130 131
void SociAuthDB::connectDatabase() {
	SLOGD << "[SOCI] Connecting to database (" << poolSize << " pooled connections)";
132 133 134 135
	try {
		for (size_t i = 0; i < poolSize; i++) {
			conn_pool->at(i).open(backend, connection_string);
		}
136
		_connected = true;
137
	} catch (const soci::mysql_soci_error &e) {
138
		SLOGE << "[SOCI] connection pool open MySQL error: " << e.err_num_ << " " << e.what() << endl;
139 140
		closeOpenedSessions();
	} catch (const runtime_error &e) { // std::runtime_error includes all soci exceptions
141
		SLOGE << "[SOCI] connection pool open error: " << e.what() << endl;
142
		closeOpenedSessions();
143
	}
144 145
}

146 147 148 149 150 151 152 153 154 155
void SociAuthDB::closeOpenedSessions() {
	for (size_t i = 0; i < poolSize; i++) {
		soci::session &conn = conn_pool->at(i);
		if (conn.get_backend()) { // if the session is open
			conn.close();
		}
	}
	_connected = false;
}

156
void SociAuthDB::getPasswordWithPool(const string &id, const string &domain,
157
									const string &authid, AuthDbListener *listener) {
158
	vector<passwd_algo_t> passwd;
159
	string unescapedIdStr = urlUnescape(id);
160

161 162 163
	SociHelper sociHelper(*conn_pool);
	
	try{
164 165 166 167 168 169 170 171 172 173 174 175 176 177
		sociHelper.execute([&](session &sql){
			rowset<row> results =  (sql.prepare << get_password_request, use(unescapedIdStr, "id"), use(domain, "domain"), use(authid, "authid"));
			for (rowset<row>::const_iterator it = results.begin(); it != results.end(); it++) {
				row const& r = *it;
				passwd_algo_t pass;

				/* If size == 1 then we only have the password so we assume MD5 */
				if (r.size() == 1) {
					pass.algo = "MD5";

					if (hashed_passwd) {
						pass.pass = r.get<string>(0);
					} else {
						string input = unescapedIdStr + ":" + domain + ":" + r.get<string>(0);
178
						pass.pass = Md5().compute<string>(input);
179 180 181 182 183 184 185 186 187 188 189 190 191 192
					}
				} else if (r.size() > 1) {
					string password = r.get<string>(0);
					string algo = r.get<string>(1);

					if (algo == "CLRTXT") {
						if (passwd.empty()) {
							pass.algo = algo;
							pass.pass = password;
							passwd.push_back(pass);

							string input;
							input = unescapedIdStr + ":" + domain + ":" + password;

193
							pass.pass = Md5().compute<string>(input);
194 195 196
							pass.algo = "MD5";
							passwd.push_back(pass);

197
							pass.pass = Sha256().compute<string>(input);
198 199 200 201 202 203
							pass.algo = "SHA-256";
							passwd.push_back(pass);

							break;
						}
					} else {
204 205
						pass.algo = algo;
						pass.pass = password;
206
					}
207
				}
208
				passwd.push_back(pass);
209
			}
210
		});
211

212 213 214
		if (!passwd.empty()) cachePassword(createPasswordKey(id, authid), domain, passwd, mCacheExpire);
		if (listener){
			listener->onResult(passwd.empty() ? PASSWORD_NOT_FOUND : PASSWORD_FOUND, passwd);
215
		}
216 217
	}catch(SociHelper::DatabaseException &e){
		if (listener) listener->onResult(AUTH_ERROR, passwd);
218 219 220
	}
}

221 222
void SociAuthDB::getUserWithPhoneWithPool(const string &phone, const string &domain, AuthDbListener *listener) {
	string user;
223 224

	try {
225 226
		SociHelper sociHelper(*conn_pool);
		
227
		if(get_user_with_phone_request != "") {
228
			sociHelper.execute([&](session &sql){
229 230
				sql << get_user_with_phone_request, into(user), use(phone, "phone");
			});
231
		} else {
Benjamin REIS's avatar
Benjamin REIS committed
232 233 234 235 236 237
			string s = get_users_with_phones_request;
			int index = s.find(":phones");
			while(index > -1) {
				s = s.replace(index, 7, phone);
				index = s.find(":phones");
			}
238 239 240 241 242 243
			sociHelper.execute([&](session &sql){
				rowset<row> ret = (sql.prepare << s);
				for (rowset<row>::const_iterator it = ret.begin(); it != ret.end(); ++it) {
					row const& row = *it;
					user = row.get<string>(0);
				}
244
			});
245
			
246
		}
247 248 249 250 251 252
		if (!user.empty())  {
			cacheUserWithPhone(phone, domain, user);
		}
		if (listener){
			listener->onResult(user.empty() ? PASSWORD_NOT_FOUND : PASSWORD_FOUND, user);
		}
253
	} catch (SociHelper::DatabaseException &e) {
254 255 256 257
		if (listener) listener->onResult(PASSWORD_NOT_FOUND, user);
	}
}

258
void SociAuthDB::getUsersWithPhonesWithPool(list<tuple<string, string,AuthDbListener*>> &creds) {
259
	set<pair<string, string>> presences;
260 261 262
	ostringstream in;
	list<string> phones;
	list<string> domains;
Benjamin REIS's avatar
Benjamin REIS committed
263
	bool first = true;
264
	
265 266 267
	for(const auto &cred : creds) {
		const auto &phone = std::get<0>(cred);
		phones.push_back(phone);
268
		domains.push_back(std::get<1>(cred));
269
		if(first) {
Benjamin REIS's avatar
Benjamin REIS committed
270
			first = false;
271
			in << "'" << phone << "'";
272
		} else {
273
			in << ",'" << phone << "'";
274 275
		}
	}
276

Benjamin REIS's avatar
Benjamin REIS committed
277 278 279 280 281 282 283
	string s = get_users_with_phones_request;
	int index = s.find(":phones");
	while(index > -1) {
		s = s.replace(index, 7, in.str());
		index = s.find(":phones");
	}

284
	try {
285
		SociHelper sociHelper(*conn_pool);
286 287
		sociHelper.execute([&](session &sql){
			rowset<row> ret = (sql.prepare << s);
288

289 290 291 292 293
			for (rowset<row>::const_iterator it = ret.begin(); it != ret.end(); ++it) {
				row const& row = *it;
				string user = row.get<string>(0);
				string domain = row.get<string>(1);
				string phone = (row.size() > 2) ? row.get<string>(2) : "";
294

295 296 297 298
				bool domain_match = false;
				if (check_domain_in_presence_results) {
					domain_match = find(domains.begin(), domains.end(), domain) != domains.end();
				}
299

300 301 302 303 304 305 306
				if (!check_domain_in_presence_results || domain_match) {
					if (!phone.empty()) {
						cacheUserWithPhone(phone, domain, user);
						presences.insert(make_pair(user, phone));
					} else {
						presences.insert(make_pair(user, user));
					}
307
				}
Benjamin REIS's avatar
Benjamin REIS committed
308
			}
309
		});
310
		notifyAllListeners(creds, presences);
311
	} catch (SociHelper::DatabaseException &e) {
Benjamin REIS's avatar
Benjamin REIS committed
312
		SLOGE << "[SOCI] MySQL request causing the error was : " << s;
313
		presences.clear();
314
		notifyAllListeners(creds, presences);
315 316
	}
}
317

318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
void SociAuthDB::notifyAllListeners(std::list<std::tuple<std::string, std::string, AuthDbListener *>> &creds, const std::set<std::pair<std::string, std::string>> &presences) {
	for(const auto &cred : creds) {
		const string &phone = std::get<0>(cred);
		AuthDbListener *listener = std::get<2>(cred);
		auto presence = find_if(presences.cbegin(), presences.cend(),
							 [&phone](const pair<string, string> &p){return p.second == phone;}
		);
		if (presence != presences.cend()) {
			// 				mDInfo[presence->first] = mDInfo[phone];
			if (listener) listener->onResult(PASSWORD_FOUND, presence->first);
		} else {
			if (listener) listener->onResult(PASSWORD_NOT_FOUND, phone);
		}
	}
}

334
#ifdef __clang__
335
#pragma mark - Inherited virtuals
336
#endif
337

338
void SociAuthDB::getPasswordFromBackend(const string &id, const string &domain,
339
										const string &authid, AuthDbListener *listener) {
340

341 342
	if (!_connected) connectDatabase();
	if (!_connected) {
343
		if (listener) listener->onResult(AUTH_ERROR , PwList());
344 345 346
		return;
	}

347
	// create a thread to grab a pool connection and use it to retrieve the auth information
348
	auto func = bind(&SociAuthDB::getPasswordWithPool, this, id, domain, authid, listener);
349

350
	bool success = thread_pool->run(func);
351
	if (!success) {
352
		// Enqueue() can fail when the queue is full, so we have to act on that
353
		SLOGE << "[SOCI] Auth queue is full, cannot fullfil password request for " << id << " / " << domain << " / "
Mickaël Turnel's avatar
Mickaël Turnel committed
354
			<< authid;
355
		if (listener) listener->onResult(AUTH_ERROR, PwList());
356
	}
357 358
}

359
void SociAuthDB::getUserWithPhoneFromBackend(const string &phone, const string &domain, AuthDbListener *listener) {
360 361 362 363 364
	if (!_connected) connectDatabase();
	if (!_connected) {
		if (listener) listener->onResult(AUTH_ERROR , "");
		return;
	}
365 366

	// create a thread to grab a pool connection and use it to retrieve the auth information
367
	auto func = bind(&SociAuthDB::getUserWithPhoneWithPool, this, phone, domain, listener);
368

369
	bool success = thread_pool->run(func);
370 371 372 373 374
	if (success == FALSE) {
		// Enqueue() can fail when the queue is full, so we have to act on that
		SLOGE << "[SOCI] Auth queue is full, cannot fullfil user request for " << phone;
		if (listener) listener->onResult(AUTH_ERROR, "");
	}
375
}
376

377
void SociAuthDB::getUsersWithPhonesFromBackend(list<tuple<string, string, AuthDbListener*>> &creds) {
378 379 380 381 382 383 384 385
	if (!_connected) connectDatabase();
	if (!_connected) {
		for (const auto &cred : creds) {
			AuthDbListener *listener = std::get<2>(cred);
			if (listener) listener->onResult(AUTH_ERROR, "");
		}
		return;
	}
386

387
	// create a thread to grab a pool connection and use it to retrieve the auth information
388
	auto func = bind(&SociAuthDB::getUsersWithPhonesWithPool, this, creds);
389

390
	bool success = thread_pool->run(func);
391 392 393
	if (success == FALSE) {
		// Enqueue() can fail when the queue is full, so we have to act on that
		SLOGE << "[SOCI] Auth queue is full, cannot fullfil user request for " << &creds;
394 395 396 397
		for (const auto &cred : creds) {
			AuthDbListener *listener = std::get<2>(cred);
			if (listener) listener->onResult(AUTH_ERROR, "");
		}
398 399
	}
}