wstrange/dartdap

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?

WhatsApp Image 2022-08-01 at 11 54 54

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;
  }


}

image

image

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?

@wstrange

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