SocketException: Connection failed (OS Error: Too many open files, errno =24)
insinfo opened this issue · 9 comments
I've been getting this error almost every week so I have to restart my application, this is strange because I've closed the connection whenever someone authenticates to the Microsft Active Directory, does the "dartdap" lib have a bug and it's not closing the connection?
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:dartdap/dartdap.dart';
import 'package:jubarte2server/services/exceptions/password_cannot_be_empty_excepion.dart';
import 'package:jubarte2server/services/exceptions/password_expired_exception.dart';
import 'package:jubarte2server/services/exceptions/password_must_be_changed_exception.dart';
import 'package:jubarte2server/services/exceptions/user_deactivated_exception.dart';
import 'package:jubarte2server/services/exceptions/user_not_found_exception.dart';
import 'package:jubarte2server/services/exceptions/username_cannot_be_empty_excepion.dart';
import 'package:galileo_utf/galileo_utf.dart' as myutf;
class ActiveDirectoryService {
String host = '192.168.133.10';
String domainAdminbindDN =
'CN=Usuario Desenvolvimento Sistemas,CN=Users,DC=dcro,DC=gov';
String domainAdminPassword = 'pass';
String baseDN = 'DC=dcro,DC=gov';
bool ssl = true; // true = use LDAPS (i.e. LDAP over SSL/TLS)
/// 389 Standard port for LDAP (i.e. without TLS/SSL)
/// 636 Standard port for LDAP over TLS/SSL
int port = 636; // null = use standard LDAP/LDAPS port
LdapConnection connectionAdm;
Future<void> connectAndBindAdm() async {
connectionAdm = await getNewConnection();
LdapResult r = await connectionAdm.bind(
DN: domainAdminbindDN, password: domainAdminPassword);
//await Future.delayed(Duration(milliseconds: 30));
print('conected to ActiveDirectory $host:$port $r ');
}
Future<void> disconnectAdm() async {
print('disconnectAdm ');
await connectionAdm.close();
}
Future<bool> isConected() async {
try {
await connectionAdm.query(
baseDN,
'(&(objectClass=user)(objectCategory=person)(sAMAccountName=desenvolvimento))',
['sAMAccountType']);
} catch (e) {
print('ActiveDirectoryService@isConected connection.query $e ');
/*if ('$e'.contains('not connected')) {
return false;
}*/
return false;
}
return true;
}
Future<dynamic> changePassword(String login, String newPassword) async {
var _isConected = await isConected();
print('ActiveDirectoryService@changePassword isConected $_isConected');
if (!_isConected) {
await connectAndBindAdm();
}
try {
var r = await connectionAdm.bind(
DN: domainAdminbindDN, password: domainAdminPassword);
print('ActiveDirectoryService@changePassword bind $r');
final attrs = [
"userAccountControl",
"distinguishedName",
"dn",
"objectClass",
"cn",
"sAMAccountName",
"pwdLastSet",
"cpfNumber"
];
final String query =
"(&(objectClass=user)(objectCategory=person)(sAMAccountName=$login))";
final searchResult = await connectionAdm.query(baseDN, query, attrs);
if (searchResult == null) {
throw UserNotFoundException();
}
//distinguished name of user who is logging in
var dnOfUserLoggingIn = '';
bool userNotFound = true;
bool userDisable = false;
await for (var entry in searchResult.stream) {
if (entry != null) {
if (entry.attributes.values != null &&
entry.attributes.values.isNotEmpty) {
final dn = entry.attributes['distinguishedName'];
final userAccountControl = entry.attributes['userAccountControl'];
if (userAccountControl != null) {
//se userAccountControl for igual a 514 é porque o usuario esta desativado
if (int.tryParse(userAccountControl.values.first.toString()) ==
514) {
userDisable = true;
}
}
if (dn != null) {
dnOfUserLoggingIn = dn.values.first.toString();
userNotFound = false;
}
}
}
}
if (userNotFound) {
throw UserNotFoundException();
}
if (userDisable) {
throw UserDeactivatedException();
}
//iconv('UTF-8', 'UTF-16LE', '"'.$password.'"')
var nPass = myutf.encodeUtf16le('"$newPassword"');
//define('LDAP_MODIFY_BATCH_REPLACE', 3);
var mod1 = Modification.replace('unicodepwd', [nPass]);
await connectionAdm.modify(dnOfUserLoggingIn, [mod1]);
} catch (e) {
rethrow;
} finally {
await disconnectAdm();
}
}
Future<void> authenticate(String user, String password) async {
var _isConected = await isConected();
print('ActiveDirectoryService@authenticate isConected $_isConected');
if (!_isConected) {
await connectAndBindAdm();
}
//distinguished name of user who is logging in
var dnOfUserLoggingIn = '';
try {
if (user?.isEmpty ?? true) {
throw UsernameCannotBeEmptyException();
}
if (password?.isEmpty ?? true) {
throw PasswordCannotBeEmptyException();
}
//checar se a espaço em branco e remover
final username = user.replaceAll(RegExp(r"\s+\b|\b\s"), "");
final attrs = [
"userAccountControl",
"distinguishedName",
"dn",
"objectClass",
"cn",
"sAMAccountName",
"pwdLastSet",
"cpfNumber"
];
print('ActiveDirectoryService@authenticate executando query');
final query =
'(&(objectClass=user)(objectCategory=person)(sAMAccountName=$username))';
var searchResult = await connectionAdm.query(baseDN, query, attrs);
// var filter = Filter.equals("sAMAccountName", username);
//var searchResult = await connection.search(baseDN, filter, attrs);
if (searchResult == null) {
print('ActiveDirectoryService@authenticate UserNotFoundException');
throw UserNotFoundException();
}
bool userNotFound = true;
bool userDisable = false;
await for (var entry in searchResult.stream) {
if (entry != null) {
if (entry.attributes.values != null &&
entry.attributes.values.isNotEmpty) {
final dn = entry.attributes['distinguishedName'];
final userAccountControl = entry.attributes['userAccountControl'];
final pwdLastSet = entry.attributes['pwdLastSet'];
var senhaEspirou = false;
if (pwdLastSet != null) {
//se pwdLastSet for 0 o usuario tem que trocar a senha
final lastPassSet =
int.tryParse(pwdLastSet.values.first.toString());
if (lastPassSet != 0) {
//A aritmética é: pegue o valor lastPassSet e divida por 10000 para se converter em milissegundos.
// Subtraia 11644473600000 (número de milissegundos entre 1/1/1601 e 1/1/1970)
//para fornecer o UNIX Posix stamp.
final date = DateTime.fromMillisecondsSinceEpoch(
((lastPassSet / 10000) - 11644473600000).round());
final dif = DateTime.now().difference(date).inDays;
if (dif >= 365) {
senhaEspirou = true;
}
} else {
//Sua senha deve ser alterada, pois foi criada com o atributo alterar senha no primeiro login
print(
'ActiveDirectoryService@authenticate Sua senha deve ser alterada');
throw PasswordMustBeChangedException();
}
}
if (userAccountControl != null) {
if (senhaEspirou == true) {
//se userAccountControl for igual a 66048 é porque a senha nunca espira
if (userAccountControl.values.first.toString() == '66048') {
//
} else {
//Sua senha expirou. Use um computador Windows conectado à rede da Prefeitura para atualizar sua senha
print(
'ActiveDirectoryService@authenticate Sua senha expirou');
throw PasswordExpiredException();
}
}
//se userAccountControl for igual a 514 é porque o usuario esta desativado
if (int.tryParse(userAccountControl.values.first.toString()) ==
514) {
userDisable = true;
}
}
if (dn != null) {
dnOfUserLoggingIn = dn.values.first.toString();
userNotFound = false;
}
}
}
}
if (userNotFound) {
print('ActiveDirectoryService@authenticate UserNotFoundException');
throw UserNotFoundException();
}
if (userDisable) {
print('ActiveDirectoryService@authenticate UserDeactivatedException');
throw UserDeactivatedException();
}
} catch (e) {
rethrow;
} finally {
await disconnectAdm();
}
// ---------------------------------------------------------- //
LdapConnection connectionForAuth;
//run Authentication
connectionForAuth = LdapConnection(
host: host,
ssl: true,
port: port,
bindDN: domainAdminbindDN,
badCertificateHandler: (X509Certificate cert) {
return true;
},
);
await connectionForAuth.open();
try {
print('ActiveDirectoryService@authenticate open connectionForAuth');
await connectionForAuth.bind(DN: dnOfUserLoggingIn, password: password);
print('ActiveDirectoryService@authenticate usuario autenticado');
} catch (e) {
rethrow;
} finally {
await connectionForAuth.close();
print('ActiveDirectoryService@authenticate connectionForAuth.close');
}
}
Future<LdapConnection> getNewConnection() async {
var connectionForAuth = LdapConnection(
host: host,
ssl: ssl,
port: port,
bindDN: domainAdminbindDN,
password: domainAdminPassword,
badCertificateHandler: (X509Certificate cert) {
return true;
},
);
await connectionForAuth.open();
return connectionForAuth;
}
}
If dartdap that is leaking sockets (quite likely - but on linux you could lsof
to find out) - I'm guessing it's related to the connection for the connectionForAuth
not being closed. After a bunch of authenticate() calls, it eventually leaks the sockets.
This could be a a dartdap bug - I'll have a look this week. It's also possible it might be an unhandled exception in your code that gets an error, and does not catch and close the socket.
Just a shot in the dark: Is it possible that the admin connection connectionAdm
can time out (say due to lack of activity), then AD closes the connection, but your code never cleans up the connection, and assigns a new LdapConnection() to connectionAdm. This would leak the old connection, and possibly the socket.
dartdap
could probably be more defensive, and force the socket closed if the connection goes to an error state.
The quick work around would be something like:
Future<void> connectAndBindAdm() async {
if (connectionAdm != null ) {
try { connectionAdm.close(); } catch(e) { ...}
}
connectionAdm = await getNewConnection();
LdapResult r = await connectionAdm.bind(
DN: domainAdminbindDN, password: domainAdminPassword);
//await Future.delayed(Duration(milliseconds: 30));
print('conected to ActiveDirectory $host:$port $r ');
}
Another thought:
- Consider making use of
final
and null safety. It might help to diagnose the problem
Couple of other things:
- You are not checking the results of the bind(). It returns an LdapResult - you should check for OK.
- There are a few places where calls to dartdap are not guarded with try/catch. For example, open().
- If you can provide logs leading up to this, that would be helpful.
I suspect an LdapConnection() is being opened, fails for some reason (AD times out), but close() is never called. A new LdapConnection is being created, without destroying the old one.
I refactored the code to ensure I catch any exceptions and close the connection on finaly
I still don't know if it solved the problem or I won't wait to see if the problem will happen again
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:dartdap/dartdap.dart';
import 'package:jubarte2server/services/exceptions/password_cannot_be_empty_excepion.dart';
import 'package:jubarte2server/services/exceptions/password_expired_exception.dart';
import 'package:jubarte2server/services/exceptions/password_must_be_changed_exception.dart';
import 'package:jubarte2server/services/exceptions/user_deactivated_exception.dart';
import 'package:jubarte2server/services/exceptions/user_not_found_exception.dart';
import 'package:jubarte2server/services/exceptions/username_cannot_be_empty_excepion.dart';
import 'package:galileo_utf/galileo_utf.dart' as myutf;
class ActiveDirectoryService {
String host = '192.168.133.10'; //'192.168.28.6'
String domainAdminbindDN =
'CN=Usuario Desenvolvimento Sistemas,CN=Users,DC=dcro,DC=gov';
String domainAdminPassword = 'pass';
String baseDN = 'DC=dcro,DC=gov';
bool ssl = true; // true = use LDAPS (i.e. LDAP over SSL/TLS)
/// 389 Standard port for LDAP (i.e. without TLS/SSL)
/// 636 Standard port for LDAP over TLS/SSL
int port = 636; // null = use standard LDAP/LDAPS port
LdapConnection connectionAdm;
ActiveDirectoryService();
Future<LdapResult> _connectAndBindAdm() async {
connectionAdm = await _getNewConnection();
LdapResult r = await connectionAdm.bind(
DN: domainAdminbindDN, password: domainAdminPassword);
print('ActiveDirectoryService@_connectAndBindAdm $host:$port $r ');
return r;
}
Future<void> _disconnectAdm() async {
await connectionAdm.close();
print('ActiveDirectoryService@_disconnectAdm');
}
Future<LdapResult> changePassword(String login, String newPassword) async {
LdapResult r;
try {
r = await _connectAndBindAdm();
print('ActiveDirectoryService@changePassword bind $r');
final attrs = [
"userAccountControl",
"distinguishedName",
"dn",
"objectClass",
"cn",
"sAMAccountName",
"pwdLastSet",
"cpfNumber"
];
final String query =
"(&(objectClass=user)(objectCategory=person)(sAMAccountName=$login))";
final searchResult = await connectionAdm.query(baseDN, query, attrs);
if (searchResult == null) {
throw UserNotFoundException();
}
//distinguished name of user who is logging in
var dnOfUserLoggingIn = '';
bool userNotFound = true;
bool userDisable = false;
await for (var entry in searchResult.stream) {
if (entry != null) {
if (entry.attributes.values != null &&
entry.attributes.values.isNotEmpty) {
final dn = entry.attributes['distinguishedName'];
final userAccountControl = entry.attributes['userAccountControl'];
if (userAccountControl != null) {
//se userAccountControl for igual a 514 é porque o usuario esta desativado
if (int.tryParse(userAccountControl.values.first.toString()) ==
514) {
userDisable = true;
}
}
if (dn != null) {
dnOfUserLoggingIn = dn.values.first.toString();
userNotFound = false;
}
}
}
}
if (userNotFound) {
throw UserNotFoundException();
}
if (userDisable) {
throw UserDeactivatedException();
}
//iconv('UTF-8', 'UTF-16LE', '"'.$password.'"')
var nPass = myutf.encodeUtf16le('"$newPassword"');
//define('LDAP_MODIFY_BATCH_REPLACE', 3);
var mod1 = Modification.replace('unicodepwd', [nPass]);
await connectionAdm.modify(dnOfUserLoggingIn, [mod1]);
} catch (e) {
rethrow;
} finally {
try {
await _disconnectAdm();
} catch (e) {
print('error on _disconnectAdm');
}
}
return r;
}
Future<void> authenticate(String user, String password) async {
LdapResult r;
//distinguished name of user who is logging in
String dnOfUserLoggingIn = '';
try {
r = await _connectAndBindAdm();
print('ActiveDirectoryService@authenticate bind $r');
if (user?.isEmpty ?? true) {
throw UsernameCannotBeEmptyException();
}
if (password?.isEmpty ?? true) {
throw PasswordCannotBeEmptyException();
}
//checar se a espaço em branco e remover
final username = user.replaceAll(RegExp(r"\s+\b|\b\s"), "");
final attrs = [
"userAccountControl",
"distinguishedName",
"dn",
"objectClass",
"cn",
"sAMAccountName",
"pwdLastSet",
"cpfNumber"
];
print('ActiveDirectoryService@authenticate execute search query');
final query =
'(&(objectClass=user)(objectCategory=person)(sAMAccountName=$username))';
var searchResult = await connectionAdm.query(baseDN, query, attrs);
// var filter = Filter.equals("sAMAccountName", username);
//var searchResult = await connection.search(baseDN, filter, attrs);
if (searchResult == null) {
print('ActiveDirectoryService@authenticate UserNotFoundException');
throw UserNotFoundException();
}
bool userNotFound = true;
bool userDisable = false;
await for (var entry in searchResult.stream) {
if (entry != null) {
if (entry.attributes.values != null &&
entry.attributes.values.isNotEmpty) {
final dn = entry.attributes['distinguishedName'];
final userAccountControl = entry.attributes['userAccountControl'];
final pwdLastSet = entry.attributes['pwdLastSet'];
var senhaEspirou = false;
if (pwdLastSet != null) {
//se pwdLastSet for 0 o usuario tem que trocar a senha
final lastPassSet =
int.tryParse(pwdLastSet.values.first.toString());
if (lastPassSet != 0) {
//A aritmética é: pegue o valor lastPassSet e divida por 10000 para se converter em milissegundos.
// Subtraia 11644473600000 (número de milissegundos entre 1/1/1601 e 1/1/1970)
//para fornecer o UNIX Posix stamp.
final date = DateTime.fromMillisecondsSinceEpoch(
((lastPassSet / 10000) - 11644473600000).round());
final dif = DateTime.now().difference(date).inDays;
if (dif >= 365) {
senhaEspirou = true;
}
} else {
//Sua senha deve ser alterada, pois foi criada com o atributo alterar senha no primeiro login
print(
'ActiveDirectoryService@authenticate Your password must be changed');
throw PasswordMustBeChangedException();
}
}
if (userAccountControl != null) {
if (senhaEspirou == true) {
//se userAccountControl for igual a 66048 é porque a senha nunca espira
if (userAccountControl.values.first.toString() == '66048') {
//
} else {
//Sua senha expirou. Use um computador Windows conectado à rede da Prefeitura para atualizar sua senha
print(
'ActiveDirectoryService@authenticate Your password has expired');
throw PasswordExpiredException();
}
}
//se userAccountControl for igual a 514 é porque o usuario esta desativado
if (int.tryParse(userAccountControl.values.first.toString()) ==
514) {
userDisable = true;
}
}
if (dn != null) {
dnOfUserLoggingIn = dn.values.first.toString();
userNotFound = false;
}
}
}
}
if (userNotFound) {
print('ActiveDirectoryService@authenticate UserNotFoundException');
throw UserNotFoundException();
}
if (userDisable) {
print('ActiveDirectoryService@authenticate UserDeactivatedException');
throw UserDeactivatedException();
}
} catch (e) {
rethrow;
} finally {
try {
await _disconnectAdm();
} catch (e) {
print('error on _disconnectAdm');
}
}
// ---------------------------------------------------------- //
//run Authentication
LdapConnection connectionForAuth = LdapConnection(
host: host,
ssl: true,
port: port,
bindDN: domainAdminbindDN,
badCertificateHandler: (X509Certificate cert) {
return true;
},
);
try {
await connectionForAuth.open();
print('ActiveDirectoryService@authenticate open connectionForAuth');
r = await connectionForAuth.bind(
DN: dnOfUserLoggingIn, password: password);
print('ActiveDirectoryService@authenticate authenticated user $r');
} catch (e) {
rethrow;
} finally {
try {
await connectionForAuth.close();
print('ActiveDirectoryService@authenticate connectionForAuth closed');
} catch (e) {
print('error on _disconnectAdm');
}
}
return r;
}
Future<LdapConnection> _getNewConnection() async {
var connectionForAuth = LdapConnection(
host: host,
ssl: ssl,
port: port,
bindDN: domainAdminbindDN,
password: domainAdminPassword,
badCertificateHandler: (X509Certificate cert) {
return true;
},
);
await connectionForAuth.open();
return connectionForAuth;
}
}
While you are waiting (🤞 ) you could periodically use lsof
or other tools to see if the number of file descriptors is growing.
Any more data to share?
sorry for the delay in responding but I've been very busy, finally after days of racking my brains I discovered where the leak of open file descriptors (Sockets) was happening, you can close this problem, because the failure was in another part of my code, basically was leaking open connections on an IMAP connection that I opened but not closed.
Thanks for the update