private LdapContext bind(
        String principalName, String password, SocketInfo server, Hashtable<String, String> props)
        throws NamingException {
      String ldapUrl = (FORCE_LDAPS ? "ldaps://" : "ldap://") + server + '/';
      String oldName = Thread.currentThread().getName();
      Thread.currentThread().setName("Connecting to " + ldapUrl + " : " + oldName);
      LOGGER.fine("Connecting to " + ldapUrl);
      try {
        props.put(Context.PROVIDER_URL, ldapUrl);
        props.put("java.naming.ldap.version", "3");

        customizeLdapProperties(props);

        LdapContext context = (LdapContext) LdapCtxFactory.getLdapCtxInstance(ldapUrl, props);

        if (!FORCE_LDAPS) {
          // try to upgrade to TLS if we can, but failing to do so isn't fatal
          // see http://download.oracle.com/javase/jndi/tutorial/ldap/ext/starttls.html
          try {
            StartTlsResponse rsp =
                (StartTlsResponse) context.extendedOperation(new StartTlsRequest());
            rsp.negotiate((SSLSocketFactory) TrustAllSocketFactory.getDefault());
            LOGGER.fine("Connection upgraded to TLS");
          } catch (NamingException e) {
            LOGGER.log(
                Level.FINE,
                "Failed to start TLS. Authentication will be done via plain-text LDAP",
                e);
            context.removeFromEnvironment("java.naming.ldap.factory.socket");
          } catch (IOException e) {
            LOGGER.log(
                Level.FINE,
                "Failed to start TLS. Authentication will be done via plain-text LDAP",
                e);
            context.removeFromEnvironment("java.naming.ldap.factory.socket");
          }
        }

        if (principalName == null || password == null || password.equals("")) {
          // anonymous bind. LDAP uses empty password as a signal to anonymous bind (RFC 2829 5.1),
          // which means it can never be the actual user password.
          context.addToEnvironment(Context.SECURITY_AUTHENTICATION, "none");
          LOGGER.fine("Binding anonymously to " + ldapUrl);
        } else {
          // authenticate after upgrading to TLS, so that the credential won't go in clear text
          context.addToEnvironment(Context.SECURITY_PRINCIPAL, principalName);
          context.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
          LOGGER.fine("Binding as " + principalName + " to " + ldapUrl);
        }

        // this is supposed to cause the LDAP bind operation with the server,
        // but I notice that AD may still accept this and yet fail to search later,
        // when I tried anonymous bind.
        // if I do specify a wrong credential, this seems to fail.
        context.reconnect(null);

        return context; // worked
      } finally {
        Thread.currentThread().setName(oldName);
      }
    }