authdb-soci.cc 13.6 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 "authdb.hh"
20
#include "soci/mysql/soci-mysql.h"
21
#include "soci-helper.hh"
22 23 24
#include <thread>

using namespace soci;
25 26 27

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

37 38
void SociAuthDB::declareConfig(GenericStruct *mc) {
	// ODBC-specific configuration keys
39 40 41
	ConfigItemDescriptor items[] = {

		{String, "soci-password-request",
42 43 44 45 46 47 48 49 50 51 52 53 54
			"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"},
55

56
		{String, "soci-user-with-phone-request",
57 58 59 60 61
			"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 ",
			""},
62

63
		{String, "soci-users-with-phones-request",
64 65 66 67 68 69
			"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)",
			""},
70 71

		{Integer, "soci-poolsize",
72 73 74 75 76 77
			"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"},
78 79

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

		{String, "soci-connection-string", "The configuration parameters of the Soci backend.\n"
85 86 87 88 89
			"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"},
90

91
		{Integer, "soci-max-queue-size",
92 93 94 95 96 97
			"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"},
98

99
		config_item_end};
100 101 102 103

	mc->addChildrenValues(items);
}

104
SociAuthDB::SociAuthDB() {
105 106
	GenericStruct *cr = GenericManager::get()->getRoot();
	GenericStruct *ma = cr->get<GenericStruct>("module::Authentication");
107
	GenericStruct *mp = cr->get<GenericStruct>("module::Presence");
108

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

119 120
	conn_pool.reset(new connection_pool(poolSize));
	thread_pool.reset(new ThreadPool(poolSize, max_queue_size));
121

122
	LOGD("[SOCI] Authentication provider for backend %s created. Pooled for %zu connections", backend.c_str(), poolSize);
123

124 125 126 127
	try {
		for (size_t i = 0; i < poolSize; i++) {
			conn_pool->at(i).open(backend, connection_string);
		}
128
	} catch (const soci::mysql_soci_error &e) {
129
		SLOGE << "[SOCI] connection pool open MySQL error: " << e.err_num_ << " " << e.what() << endl;
130 131 132 133 134 135 136
	}
	/*
	   std::runtime_error includes all soci exceptions. Furthermore, std::exception isn't catch here to avoid
	   catching std::logical_error because this kind of exceptons are for programing errors and should make the application
	   aborts.
	*/
	catch (const runtime_error &e) {
137
		SLOGE << "[SOCI] connection pool open error: " << e.what() << endl;
138
	}
139 140
}

141 142
void SociAuthDB::getPasswordWithPool(const string &id, const string &domain,
									const string &authid, AuthDbListener *listener, AuthDbListener *listener_ref) {
143
	vector<passwd_algo_t> passwd;
144
	string unescapedIdStr = urlUnescape(id);
145

146 147 148
	SociHelper sociHelper(*conn_pool);
	
	try{
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
		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);
						pass.pass = syncMd5(input.c_str(), 16);
					}
				} 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;

							pass.pass = syncMd5(input.c_str(), 16);
							pass.algo = "MD5";
							passwd.push_back(pass);

							pass.pass = syncSha256(input.c_str(), 32);
							pass.algo = "SHA-256";
							passwd.push_back(pass);

							break;
						}
					} else {
189 190
						pass.algo = algo;
						pass.pass = password;
191
					}
192
				}
193
				passwd.push_back(pass);
194
			}
195
		});
196

197
		
198 199 200 201 202

		if (listener_ref) listener_ref->finishVerifyAlgos(passwd);
		if (!passwd.empty()) cachePassword(createPasswordKey(id, authid), domain, passwd, mCacheExpire);
		if (listener){
			listener->onResult(passwd.empty() ? PASSWORD_NOT_FOUND : PASSWORD_FOUND, passwd);
203
		}
204 205
	}catch(SociHelper::DatabaseException &e){
		if (listener) listener->onResult(AUTH_ERROR, passwd);
206 207 208
	}
}

209 210
void SociAuthDB::getUserWithPhoneWithPool(const string &phone, const string &domain, AuthDbListener *listener) {
	string user;
211 212

	try {
213 214
		SociHelper sociHelper(*conn_pool);
		
215
		if(get_user_with_phone_request != "") {
216
			sociHelper.execute([&](session &sql){
217 218
				sql << get_user_with_phone_request, into(user), use(phone, "phone");
			});
219
		} else {
Benjamin REIS's avatar
Benjamin REIS committed
220 221 222 223 224 225
			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");
			}
226 227 228 229 230 231
			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);
				}
232
			});
233
			
234
		}
235 236 237 238 239 240
		if (!user.empty())  {
			cacheUserWithPhone(phone, domain, user);
		}
		if (listener){
			listener->onResult(user.empty() ? PASSWORD_NOT_FOUND : PASSWORD_FOUND, user);
		}
241
	} catch (SociHelper::DatabaseException &e) {
242 243 244 245
		if (listener) listener->onResult(PASSWORD_NOT_FOUND, user);
	}
}

246
void SociAuthDB::getUsersWithPhonesWithPool(list<tuple<string, string,AuthDbListener*>> &creds) {
247
	set<pair<string, string>> presences;
248 249 250
	ostringstream in;
	list<string> phones;
	list<string> domains;
Benjamin REIS's avatar
Benjamin REIS committed
251
	bool first = true;
252
	
253 254 255
	for(const auto &cred : creds) {
		const auto &phone = std::get<0>(cred);
		phones.push_back(phone);
256
		domains.push_back(std::get<1>(cred));
257
		if(first) {
Benjamin REIS's avatar
Benjamin REIS committed
258
			first = false;
259
			in << "'" << phone << "'";
260
		} else {
261
			in << ",'" << phone << "'";
262 263
		}
	}
264

Benjamin REIS's avatar
Benjamin REIS committed
265 266 267 268 269 270 271
	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");
	}

272
	try {
273
		SociHelper sociHelper(*conn_pool);
274 275
		sociHelper.execute([&](session &sql){
			rowset<row> ret = (sql.prepare << s);
276

277 278 279 280 281
			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) : "";
282

283 284 285 286
				bool domain_match = false;
				if (check_domain_in_presence_results) {
					domain_match = find(domains.begin(), domains.end(), domain) != domains.end();
				}
287

288 289 290 291 292 293 294
				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));
					}
295
				}
Benjamin REIS's avatar
Benjamin REIS committed
296
			}
297
		});
298
		notifyAllListeners(creds, presences);
299
	} catch (SociHelper::DatabaseException &e) {
Benjamin REIS's avatar
Benjamin REIS committed
300
		SLOGE << "[SOCI] MySQL request causing the error was : " << s;
301
		presences.clear();
302
		notifyAllListeners(creds, presences);
303 304
	}
}
305

306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
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);
		}
	}
}

322
#ifdef __clang__
323
#pragma mark - Inherited virtuals
324
#endif
325

326 327
void SociAuthDB::getPasswordFromBackend(const string &id, const string &domain,
										const string &authid, AuthDbListener *listener, AuthDbListener *listener_ref) {
328 329

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

332
	bool success = thread_pool->run(func);
333
	if (success == FALSE) {
334
		// Enqueue() can fail when the queue is full, so we have to act on that
335
		SLOGE << "[SOCI] Auth queue is full, cannot fullfil password request for " << id << " / " << domain << " / "
Mickaël Turnel's avatar
Mickaël Turnel committed
336
			<< authid;
337
		if (listener) listener->onResult(AUTH_ERROR, "");
338
	}
339 340
}

341
void SociAuthDB::getUserWithPhoneFromBackend(const string &phone, const string &domain, AuthDbListener *listener) {
342 343

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

346
	bool success = thread_pool->run(func);
347 348 349 350 351
	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, "");
	}
352
}
353

354
void SociAuthDB::getUsersWithPhonesFromBackend(list<tuple<string, string, AuthDbListener*>> &creds) {
355

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

359
	bool success = thread_pool->run(func);
360 361 362
	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;
363 364 365 366
		for (const auto &cred : creds) {
			AuthDbListener *listener = std::get<2>(cred);
			if (listener) listener->onResult(AUTH_ERROR, "");
		}
367 368
	}
}