authdb-odbc.cc 18.2 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
#define SU_MSG_ARG_T void
19

20 21 22
#include "authdb.hh"
#include <vector>
#include <set>
23
#include <chrono>
24

25
using namespace std;
Yann Diorcet's avatar
Yann Diorcet committed
26
using namespace chrono;
27

28 29
#ifdef USE_MONOTONIC_CLOCK
namespace std {
30
typedef monotonic_clock steady_clock;
31 32 33
}
#endif

34 35 36 37 38 39
struct AuthDbTimingsAnalyzer;

struct AuthDbTimings {
	static AuthDbTimingsAnalyzer analyzerFull;
	static AuthDbTimingsAnalyzer analyzerRetr;
	bool error;
40 41
	AuthDbTimings() : error(false) {
	}
42

43 44 45 46
	steady_clock::time_point tStart;
	steady_clock::time_point tGotConnection;
	steady_clock::time_point tGotResult;
	steady_clock::time_point tEnd;
47 48 49 50 51 52 53 54

	void done();
};

// To get 3000 authentications per second
// The average duration should be ~300 microseconds per authentication.
struct AuthDbTimingsAnalyzer {
	static const long maxDurationSlow = 14000; // 700 auth/s
55
	static const long maxDuration = 600;	   // 1500 auth/s
56 57 58 59 60 61 62 63
	static const int steps = 14;
	static const long stepSize = maxDuration / steps;
	static const int LineWidth = 50;

	static mutex tMutex;
	static int displayStatsInterval;
	static int displayStatsAfterCount;

64
	steady_clock::time_point lastDisplay;
65 66 67 68 69 70 71 72 73 74 75
	long errorCount;

	long count;
	long slowCount;
	long slowestCount;

	float average;
	float slowAverage;
	float slowestAverage;

	long maxLineWidth;
76
	float durations[steps + 1];
77 78

	void reset() {
79 80 81 82 83 84 85 86
		lastDisplay = steady_clock::now();
		count = 0;
		errorCount = 0;
		slowCount = 0;
		slowestCount = 0;
		average = 0;
		slowAverage = 0;
		slowestAverage = 0;
87
		memset(durations, 0, sizeof(durations));
88
		maxLineWidth = 0;
89 90
	}

91
	AuthDbTimingsAnalyzer() {
92
		reset();
93
	}
94

95
	void compute(const char *name, steady_clock::time_point &t1, steady_clock::time_point &t2, bool error) {
96 97 98 99 100 101 102
		if (error) {
			tMutex.lock();
			++errorCount;
			tMutex.unlock();
			return;
		}

103 104
		// microseconds duration = t2-t1;
		long ticks = /*duration*/ (t2 - t1).count();
105 106 107

		tMutex.lock();

108
		average = (count * average + ticks) / (count + 1);
109 110
		++count;
		if (ticks > maxDuration) {
111
			// LOGI("bigger max: %f", duration);
112
			if (ticks > maxDurationSlow) {
113
				slowestAverage = (slowestCount * slowestAverage + ticks) / (slowestCount + 1);
114 115
				++slowestCount;
			}
116
			slowAverage = (slowCount * slowAverage + ticks) / (slowCount + 1);
117 118 119
			++slowCount;
			ticks = maxDuration;
		}
120
		long index = (long)(ticks / stepSize);
121
		++(durations[index]);
122 123
		if (durations[index] > maxLineWidth)
			++maxLineWidth;
124 125 126 127 128 129 130

		// Show statistics each 10'000 timings
		if (displayStatsAfterCount && count == 10000) {
			display(name);
			reset();
		}
		// Or every 10 seconds
131
		if (displayStatsInterval && duration_cast<seconds>(t1 - lastDisplay).count() >= displayStatsInterval) {
132 133 134 135 136 137 138
			display(name);
			reset();
		}
		tMutex.unlock();
	}

	void display(const char *name) {
139
		LOGI("%lu [%lu micro] timings (%lu errors) %lu [%lu micro] slow - %lu [%lu millis] slowest", count,
Mickaël Turnel's avatar
Mickaël Turnel committed
140
			(long)average, errorCount, slowCount, (long)slowAverage, slowestCount, ((long)slowestAverage) / 1000);
141 142
		double lDiv = ((double)maxLineWidth) / LineWidth;
		LOGI("Displaying %s, %u steps [%lu - %lu] - max %lu - div %f", name, steps, 0l, maxDuration, maxLineWidth,
Mickaël Turnel's avatar
Mickaël Turnel committed
143
			lDiv);
144
		if (lDiv == 0.f) {
145 146 147 148
			LOGI("Skipping display with no maxcount");
			return;
		}

149 150 151
		for (int i = 0; i < steps; ++i) {
			char line[LineWidth + 1] = {0};
			int lineWidth = (int)(durations[i] / lDiv);
152
			memset(line, '#', lineWidth);
153
			LOGI("[%u-%u] %s", (int)(i * stepSize), (int)((i + 1) * stepSize), line);
154 155 156 157
		}
	}
};
mutex AuthDbTimingsAnalyzer::tMutex;
158
int AuthDbTimingsAnalyzer::displayStatsInterval = 0;   // 0 to disable
159 160 161 162 163
int AuthDbTimingsAnalyzer::displayStatsAfterCount = 0; // 0 to disable
AuthDbTimingsAnalyzer AuthDbTimings::analyzerFull;
AuthDbTimingsAnalyzer AuthDbTimings::analyzerRetr;

void AuthDbTimings::done() {
164
	analyzerFull.compute("full", tStart, tEnd, error);
165 166
	analyzerRetr.compute("pass retrieving", tGotConnection, tGotResult, error);
}
167

168 169 170
static vector<string> parseAndUpdateRequestConfig(string &request) {
	vector<string> found_parameters;
	bool hasIdParameter = false;
171 172 173 174 175 176 177
	static set<string> recognizedParameters = {"id", "authid", "domain"};

	size_t j = 0;
	string pattern(":");
	string space(" ");
	string semicol(";");
	while ((j = request.find(pattern, j)) != string::npos) {
178 179
		string token = request.substr(j + 1, request.length());
		size_t size_token;
180
		if ((size_token = token.find(space)) != string::npos || (size_token = token.find(semicol)) != string::npos)
181 182
			token = token.substr(0, size_token);

183
		if (recognizedParameters.find(token) == recognizedParameters.end()) {
184 185
			LOGF("Unrecognized parameter in SQL request %s", token.c_str());
		}
186

187 188 189 190
		found_parameters.push_back(token);
		if (token == "id") {
			hasIdParameter = true;
		}
191
		request.replace(j, token.length() + 1, "?");
192
	}
193

194 195
	if (!hasIdParameter) {
		LOGF("Couldn't find an :id named parameter in provided request");
196 197
	}

198 199 200
	return found_parameters;
}

201
/**
Mickaël Turnel's avatar
Mickaël Turnel committed
202 203 204
* See documentation on ODBC on Microsoft pages:
* http://msdn.microsoft.com/en-us/library/ms716319%28v=VS.85%29.aspx
*/
205 206 207
OdbcAuthDb::OdbcAuthDb() : mAsynchronousRetrieving(true), env(NULL), execDirect(false) {
	GenericStruct *cr = GenericManager::get()->getRoot();
	GenericStruct *ma = cr->get<GenericStruct>("module::Authentication");
208 209

	string none = "none";
210
	connectionString = ma->get<ConfigString>("datasource")->read();
211 212
	if (connectionString == none)
		LOGF("Authentication is activated but no datasource found");
213
	LOGD("Datasource found: %s", connectionString.c_str());
214

215
	request = ma->get<ConfigString>("request")->read();
216 217
	if (request == none)
		LOGF("Authentication is activated but no request found");
218
	LOGD("request found: %s", request.c_str());
219
	parameters = parseAndUpdateRequestConfig(request);
220 221
	LOGD("request parsed: %s", request.c_str());

Simon Morlat's avatar
Simon Morlat committed
222
	maxPassLength = 256;
223

224 225 226
	AuthDbTimingsAnalyzer::displayStatsInterval = ma->get<ConfigInt>("odbc-display-timings-interval")->read();
	AuthDbTimingsAnalyzer::displayStatsAfterCount = ma->get<ConfigInt>("odbc-display-timings-after-count")->read();

227
	mAsynchronousRetrieving = true;
228 229
	LOGD("%s password retrieving", mAsynchronousRetrieving ? "Asynchronous" : "Synchronous");

230
	asPooling = ma->get<ConfigBoolean>("odbc-pooling")->read();
231

232
	SQLRETURN retcode;
233 234
	// 1. Enable or disable connection pooling.
	// It should be done BEFORE allocating the environment.
235
	// Note: this is useless with the unixODBC implementation:
236
	// the pooling attribute of env is set during allocation
237 238
	// if odbcinst.ini/Pooling=1, and the SQL_ATTR_CONNECTION_POOLING doesn't mean
	// anything to unixODBC. Sob.
239
	unsigned long poolingPtr = asPooling ? SQL_CP_ONE_PER_DRIVER : SQL_CP_OFF;
240
	retcode = SQLSetEnvAttr(NULL, SQL_ATTR_CONNECTION_POOLING, (void *)poolingPtr, 0);
241 242 243
	if (!SQL_SUCCEEDED(retcode)) {
		envError("SQLSetEnvAttr SQL_ATTR_CONNECTION_POOLING=SQL_CP_ONE_PER_DRIVER");
		LOGF("odbc error");
244 245
	}

246
	// 2. Allocate environment
247
	retcode = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
248 249 250
	if (!SQL_SUCCEEDED(retcode)) {
		LOGF("Error allocating ENV");
	}
251 252

	// 3. Use ODBC version 3
253
	retcode = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
254 255 256
	if (!SQL_SUCCEEDED(retcode)) {
		envError("SQLSetEnvAttr ODBCv3");
		LOGF("odbc error");
257
	}
258
/*SM: this follow code is really a crap because it blocks flexisip entirely at startup if the database is not
Mickaël Turnel's avatar
Mickaël Turnel committed
259 260 261
*responding.
*  However it is required because mysql client lib segfaults like a shit when used from a thread for the first.
**/
Simon Morlat's avatar
Simon Morlat committed
262
#if 1
263 264 265
	// Make sure the driver library is loaded.
	AuthDbTimings timings;
	ConnectionCtx ctx;
266
	string init = "init";
267
	getConnection(init, ctx, timings);
268
#endif
269 270
}

271 272 273
void OdbcAuthDb::declareConfig(GenericStruct *mc) {

	// ODBC-specific configuration keys
274
	ConfigItemDescriptor items[] = {
275

276 277 278 279
		{String, "request", "Odbc SQL request to execute to obtain the password \n. "
							"Named parameters are :id (the user found in the from header), :domain (the authorization "
							"realm) and :authid (the authorization username). "
							"The use of the :id parameter is mandatory.",
Mickaël Turnel's avatar
Mickaël Turnel committed
280
		"select password from accounts where id = :id and domain = :domain and authid=:authid"},
281

282
		{Boolean, "odbc-pooling", "Use pooling in ODBC (improves performances). This is not guaranteed to succeed, "
Mickaël Turnel's avatar
Mickaël Turnel committed
283 284 285 286
								"because if you are using unixODBC, it consults the /etc/odbcinst.ini"
								"file in section [ODBC] to check for Pooling=yes/no option. You should make sure "
								"that this flag is set before expecting this option to work.",
		"true"},
287

288
		{Integer, "odbc-display-timings-interval", "Display timing statistics after this count of seconds", "0"},
289

290
		{Integer, "odbc-display-timings-after-count",
Mickaël Turnel's avatar
Mickaël Turnel committed
291
		"Display timing statistics once the number of samples reach this number.", "0"},
292

293
		config_item_end};
294 295 296 297

	mc->addChildrenValues(items);
}

298
void OdbcAuthDb::setExecuteDirect(const bool value) {
299 300
	execDirect = value;
}
301

302
void showCB(SQLLEN cb) {
303
	switch (cb) {
304 305 306 307 308 309 310 311 312
		case SQL_NULL_DATA:
			LOGD("CB : NULL data");
			break;
		case SQL_NO_TOTAL:
			LOGD("CB : NO total");
			break;
		default:
			LOGD("CB : %ld", (long int)cb);
			break;
313 314 315 316 317 318 319 320 321
	}
}

static bool linkFailed(string fn, SQLHANDLE handle, SQLSMALLINT handleType) {
	SQLINTEGER errorNb;
	SQLCHAR sqlState[7];
	SQLCHAR msg[256];
	SQLSMALLINT msgLen;

322
	SQLRETURN ret = SQLGetDiagRec(handleType, handle, 1, sqlState, &errorNb, msg, sizeof(msg), &msgLen);
323 324
	if (SQL_SUCCEEDED(ret) && strcmp((char *)sqlState, "08S01") == 0) {
		LOGE("Odbc link failure while doing %s : (%s) %s", fn.c_str(), (unsigned char *)sqlState, (char *)msg);
325 326 327 328 329 330 331
		return true;
	}

	return false;
}

static void logSqlError(string fn, SQLHANDLE handle, SQLSMALLINT handleType) {
332
	SQLINTEGER i = 0;
333 334 335 336 337 338
	SQLINTEGER errorNb;
	SQLCHAR sqlState[7];
	SQLCHAR msg[256];
	SQLSMALLINT msgLen;
	SQLRETURN ret;

339
	LOGE("Odbc driver errors while doing %s", fn.c_str());
340 341 342
	do {
		ret = SQLGetDiagRec(handleType, handle, ++i, sqlState, &errorNb, msg, sizeof(msg), &msgLen);
		if (SQL_SUCCEEDED(ret))
343 344
			LOGE("%s:%i:%i:%s", (char *)sqlState, (int)i, (int)errorNb, msg);
	} while (ret == SQL_SUCCESS);
345 346
}

347
OdbcAuthDb::~OdbcAuthDb() {
348 349 350
	// Destroy environment
	// All connection should be destroyed already
	LOGD("Disconnecting odbc connector");
351 352
	if (env)
		SQLFreeHandle(SQL_HANDLE_ENV, env);
353 354
}

355
void OdbcAuthDb::envError(const char *doing) {
356 357 358
	logSqlError(doing, env, SQL_HANDLE_ENV);
}

359
void OdbcAuthDb::dbcError(ConnectionCtx &ctx, const char *doing) {
360
	logSqlError(doing, ctx.dbc, SQL_HANDLE_DBC);
361 362
}

363
void OdbcAuthDb::stmtError(ConnectionCtx &ctx, const char *doing) {
364 365
	logSqlError(doing, ctx.stmt, SQL_HANDLE_STMT);
}
366

367
bool OdbcAuthDb::getConnection(const string &id, ConnectionCtx &ctx, AuthDbTimings &timings) {
368
	steady_clock::time_point tp1 = steady_clock::now();
369 370

	// Create a 'wrapper' connection attached to nothing
371 372 373 374 375
	SQLRETURN retcode = SQLAllocHandle(SQL_HANDLE_DBC, env, &ctx.dbc);
	if (!SQL_SUCCEEDED(retcode)) {
		envError("SQLAllocHandle DBC");
		return false;
	}
376
	steady_clock::time_point tp2 = steady_clock::now();
377 378 379 380 381

	// when debug is not active, the compiler complains about tp1 and tp2 not being used.
	(void)tp1;
	(void)tp2;

382
	LOGD("SQLAllocHandle: %s : %lu ms", id.c_str(), (unsigned long)duration_cast<milliseconds>(tp2 - tp1).count());
383

384 385 386 387
	// Either:
	// - reuse an underlying connection from the pool;
	// - establish an underlying connecion;
	// Attach underlying to wrapper.
388
	retcode = SQLDriverConnect(ctx.dbc, NULL, (SQLCHAR *)connectionString.c_str(), SQL_NTS, NULL, 0, NULL,
Mickaël Turnel's avatar
Mickaël Turnel committed
389
							SQL_DRIVER_COMPLETE);
390 391 392 393 394
	if (!SQL_SUCCEEDED(retcode)) {
		dbcError(ctx, "SQLDriverConnect");
		return false;
	}
	LOGD("SQLDriverConnect %s : %lu ms", id.c_str(),
Mickaël Turnel's avatar
Mickaël Turnel committed
395
		(unsigned long)duration_cast<milliseconds>(steady_clock::now() - tp2).count());
396 397

	// Set connection to be read only
398
	SQLSetConnectAttr(ctx.dbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER)SQL_MODE_READ_ONLY, 0);
399 400 401 402
	if (!SQL_SUCCEEDED(retcode)) {
		dbcError(ctx, "SQLSetConnectAttr");
		return false;
	}
403

404
	retcode = SQLAllocHandle(SQL_HANDLE_STMT, ctx.dbc, &ctx.stmt);
405
	if (!SQL_SUCCEEDED(retcode)) {
406
		dbcError(ctx, "SQLAllocHandle STMT");
407
		return false;
408 409
	}

410
	if (!execDirect) {
411
		retcode = SQLPrepare(ctx.stmt, (SQLCHAR *)request.c_str(), SQL_NTS);
412
		if (!SQL_SUCCEEDED(retcode)) {
413
			stmtError(ctx, "SQLPrepare request");
414
			return false;
415 416 417
		}

		// Use isql dsn_name then "help table_name" (without ;) to get information on sql types
418

419
		for (size_t i = 0; i < parameters.size(); i++) {
420 421
			char *fieldBuffer;
			if (parameters[i] == "id") {
422
				fieldBuffer = ctx.idCBuffer;
423
			} else if (parameters[i] == "authid") {
424
				fieldBuffer = ctx.authIdCBuffer;
425
			} else if (parameters[i] == "domain") {
426
				fieldBuffer = ctx.domainCBuffer;
427 428
			} else {
				LOGF("unhandled parameter %s", parameters[i].c_str());
429
			}
430 431
			LOGD("SQLBindParameter %u -> %s", (unsigned int)i, parameters[i].c_str());
			retcode = SQLBindParameter(ctx.stmt, i + 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, (SQLULEN)fieldLength, 0,
Mickaël Turnel's avatar
Mickaël Turnel committed
432
									fieldBuffer, 0, NULL);
433
			if (!SQL_SUCCEEDED(retcode)) {
434
				logSqlError("SQLBindParameter", ctx.stmt, SQL_HANDLE_STMT);
435
				LOGF("couldn't bind parameter");
436
			}
437
		}
438
		LOGD("SQLBindParameter bind OK [%u]", (unsigned int)parameters.size());
439 440 441 442 443 444 445 446 447 448 449
	}

	return true;
}

static void closeCursor(SQLHSTMT &stmt) {
	if (!SQL_SUCCEEDED(SQLCloseCursor(stmt))) {
		logSqlError("SQLCloseCursor", stmt, SQL_HANDLE_STMT);
	}
}

450
void OdbcAuthDb::getPasswordFromBackend(const std::string &id, const std::string &domain,
451
										const std::string &authid, AuthDbListener *listener) {
452

453 454 455 456
	if (mAsynchronousRetrieving) {
		// Asynchronously retrieve password in a new thread.
		// Allocate on the stack and detach. It is lawful since:
		// "When detach() returns, *this no longer represents the possibly continuing thread of execution."
457

458
		thread t = thread(bind(&OdbcAuthDb::doAsyncRetrievePassword, this, id, domain, authid, listener));
459
		t.detach(); // Thread will continue running in detached mode
460
		return;
461 462
	} else {
		AuthDbTimings timings;
463
		string foundPassword;
464
		timings.tStart = steady_clock::now();
465
		ConnectionCtx ctx;
466
		AuthDbResult ret = doRetrievePassword(ctx, id, domain, authid, foundPassword, timings);
467
		timings.tEnd = steady_clock::now();
468 469 470 471
		if (ret == AUTH_ERROR) {
			timings.error = true;
		}
		timings.done();
472
		if (listener) listener->onResult(ret, foundPassword);
473
	}
474 475
}

476
/*
477 478
static unsigned long threadCount=0;
static mutex threadCountMutex;
479
*/
480
void OdbcAuthDb::doAsyncRetrievePassword(string id, string domain, string auth,
Mickaël Turnel's avatar
Mickaël Turnel committed
481
										AuthDbListener *listener) {
482 483 484 485 486
	/*	unsigned long localThreadCountCopy=0;
		threadCountMutex.lock();
		++threadCount;
		localThreadCountCopy=threadCount;
		threadCountMutex.unlock();*/
487
	ConnectionCtx ctx;
488
	string password;
489
	AuthDbTimings timings;
490 491 492
	timings.tStart = steady_clock::now();
	AuthDbResult ret = doRetrievePassword(ctx, id, domain, auth, password, timings);
	timings.tEnd = steady_clock::now();
493 494
	if (ret == AUTH_ERROR) {
		timings.error = true;
495
	}
496 497
	timings.done();

498
	if (listener) listener->onResult(ret, password);
499

500
	/*
501 502
	threadCountMutex.lock();
	--threadCount;
503
	localThreadCountCopy=threadCount;
504
	threadCountMutex.unlock();
505
	*/
506 507
}

508 509
AuthDbResult OdbcAuthDb::doRetrievePassword(ConnectionCtx &ctx, const string &id, const string &domain,
											const string &auth, string &foundPassword, AuthDbTimings &timings) {
510
	if (!getConnection(id, ctx, timings)) {
511
		LOGE("ConnectionCtx creation error");
512 513
		return AUTH_ERROR;
	}
514

515 516
	timings.tGotConnection = steady_clock::now();
	SQLHANDLE stmt = ctx.stmt;
517

518 519 520
	strncpy(ctx.idCBuffer, id.c_str(), fieldLength), ctx.idCBuffer[fieldLength] = 0;
	strncpy(ctx.domainCBuffer, domain.c_str(), fieldLength), ctx.domainCBuffer[fieldLength] = 0;
	strncpy(ctx.authIdCBuffer, auth.c_str(), fieldLength), ctx.authIdCBuffer[fieldLength] = 0;
521 522 523 524

	SQLRETURN retcode;
	if (execDirect) {
		// execute direct
Simon Morlat's avatar
Simon Morlat committed
525
		LOGD("Requesting password of user with id='%s'", ctx.idCBuffer);
526
		retcode = SQLExecDirect(stmt, (SQLCHAR *)request.c_str(), SQL_NTS);
527
		if (!SQL_SUCCEEDED(retcode)) {
528
			stmtError(ctx, "SQLExecDirect");
529 530
			linkFailed("SQLExecDirect", stmt, SQL_HANDLE_STMT);
			return AUTH_ERROR;
531 532 533 534
		}
		LOGD("SQLExecDirect OK");
	} else {
		// Use prepared statement
Simon Morlat's avatar
Simon Morlat committed
535
		LOGD("Requesting password of user with id='%s'", ctx.idCBuffer);
536 537
		retcode = SQLExecute(stmt);
		if (!SQL_SUCCEEDED(retcode)) {
538
			stmtError(ctx, "SQLExecute");
539 540
			linkFailed("SQLExecute", stmt, SQL_HANDLE_STMT);
			return AUTH_ERROR;
541 542
		}
		LOGD("SQLExecute OK");
543 544 545 546 547
	}

	if (retcode != SQL_SUCCESS) {
		LOGE("SQLExecute returned no success");
		closeCursor(stmt);
548
		return AUTH_ERROR;
549 550 551 552 553
	}

	retcode = SQLFetch(stmt);
	if (retcode == SQL_ERROR || retcode == SQL_SUCCESS_WITH_INFO) {
		LOGE("Fetch error or success with info");
554
		stmtError(ctx, "SQLFetch");
555
		closeCursor(stmt);
556
		return AUTH_ERROR;
557 558
	}

559
	if (retcode == SQL_NO_DATA) {
560 561
		LOGD("No data fetched");
		// Seems to be valid
562
		closeCursor(stmt);
563
		timings.tGotResult = steady_clock::now();
564
		return PASSWORD_NOT_FOUND;
565 566 567 568 569
	}

	SQLLEN cbPass;
	SQLCHAR password[maxPassLength + 1];
	retcode = SQLGetData(stmt, 1, SQL_C_CHAR, password, maxPassLength, &cbPass);
570
	if (retcode == SQL_ERROR || retcode == SQL_SUCCESS_WITH_INFO) {
571 572 573 574
		if (retcode == SQL_SUCCESS_WITH_INFO)
			LOGD("SQLGetData success with info");
		else
			LOGD("SQLGetData error or success with info - user not found??");
575
		closeCursor(stmt);
576
		timings.tGotResult = steady_clock::now();
577
		return PASSWORD_NOT_FOUND;
578 579 580
	}

	closeCursor(stmt);
581

582 583
	timings.tGotResult = steady_clock::now();
	foundPassword.assign((char *)password);
584
	string key(createPasswordKey(id, auth));
585
	cachePassword(key, domain, foundPassword, -1);
586
	LOGD("Password found %s for %s", foundPassword.c_str(), id.c_str());
587
	return PASSWORD_FOUND;
588
}
589

590
void OdbcAuthDb::getUserWithPhoneFromBackend(const std::string & phone, const std::string & domain, AuthDbListener *listener) {
591 592 593
		LOGE("%s not supported with ODBC", __FUNCTION__);
		if (listener) listener->onResult(AuthDbResult::PASSWORD_NOT_FOUND, "");
}