/**
   * Process a type 3 NTLM message
   *
   * @param type3Msg Type3NTLMMessage
   * @param req HttpServletRequest
   * @param res HttpServletResponse
   * @exception IOException
   * @exception ServletException
   */
  protected boolean processType3(
      Type3NTLMMessage type3Msg,
      ServletContext context,
      HttpServletRequest req,
      HttpServletResponse res)
      throws IOException, ServletException {
    Log logger = getLogger();

    if (logger.isDebugEnabled()) logger.debug("Received type3 " + type3Msg);

    // Get the existing NTLM details
    NTLMLogonDetails ntlmDetails = null;
    SessionUser user = null;

    user = getSessionUser(context, req, res, true);
    HttpSession session = req.getSession();
    ntlmDetails = (NTLMLogonDetails) session.getAttribute(NTLM_AUTH_DETAILS);

    // Get the NTLM logon details
    String userName = type3Msg.getUserName();
    String workstation = type3Msg.getWorkstation();
    String domain = type3Msg.getDomain();

    // ALF-10997 fix, normalize the userName
    // the system runAs is acceptable because we are resolving a username i.e. it's a system-wide
    // operation that does not need permission checks

    final String userName_f = userName;
    String normalized =
        transactionService
            .getRetryingTransactionHelper()
            .doInTransaction(
                new RetryingTransactionHelper.RetryingTransactionCallback<String>() {
                  public String execute() throws Throwable {
                    return AuthenticationUtil.runAs(
                        new AuthenticationUtil.RunAsWork<String>() {
                          public String doWork() throws Exception {
                            String normalized = personService.getUserIdentifier(userName_f);
                            return normalized;
                          }
                        },
                        AuthenticationUtil.SYSTEM_USER_NAME);
                  }
                },
                true);

    if (normalized != null) {
      userName = normalized;
    }

    boolean authenticated = false;

    // Check if we are using cached details for the authentication
    if (user != null && ntlmDetails != null && ntlmDetails.hasNTLMHashedPassword()) {
      // Check if the received NTLM hashed password matches the cached password
      byte[] ntlmPwd = type3Msg.getNTLMHash();
      byte[] cachedPwd = ntlmDetails.getNTLMHashedPassword();

      if (ntlmPwd != null) {
        authenticated = Arrays.equals(cachedPwd, ntlmPwd);
      }

      if (logger.isDebugEnabled())
        logger.debug("Using cached NTLM hash, authenticated = " + authenticated);

      onValidate(context, req, res, new NTLMCredentials(userName, ntlmPwd));

      // Allow the user to access the requested page
      return true;
    } else {
      WebCredentials credentials;
      // Check if we are using local MD4 password hashes or passthru authentication
      if (nltmAuthenticator.getNTLMMode() == NTLMMode.MD4_PROVIDER) {
        // Check if guest logons are allowed and this is a guest logon
        if (m_allowGuest && userName.equalsIgnoreCase(authenticationComponent.getGuestUserName())) {
          credentials = new GuestCredentials();
          // Indicate that the user has been authenticated
          authenticated = true;

          if (getLogger().isDebugEnabled()) getLogger().debug("Guest logon");
        } else {
          // Get the stored MD4 hashed password for the user, or null if the user does not exist
          String md4hash = getMD4Hash(userName);

          if (md4hash != null) {
            authenticated =
                validateLocalHashedPassword(type3Msg, ntlmDetails, authenticated, md4hash);
            credentials =
                new NTLMCredentials(ntlmDetails.getUserName(), ntlmDetails.getNTLMHashedPassword());
          } else {
            // Check if unknown users should be logged on as guest
            if (m_mapUnknownUserToGuest) {
              // Reset the user name to be the guest user
              userName = authenticationComponent.getGuestUserName();
              authenticated = true;
              credentials = new GuestCredentials();

              if (logger.isDebugEnabled())
                logger.debug("User " + userName + " logged on as guest, no Alfresco account");
            } else {
              if (logger.isDebugEnabled())
                logger.debug("User " + userName + " does not have Alfresco account");

              // Bypass NTLM authentication and display the logon screen,
              // as user account does not exist in Alfresco
              credentials = new UnknownCredentials();
              authenticated = false;
            }
          }
        }
      } else {
        credentials = new NTLMCredentials(type3Msg.getUserName(), type3Msg.getNTLMHash());
        //  Determine if the client sent us NTLMv1 or NTLMv2
        if (type3Msg.hasFlag(NTLM.Flag128Bit) && type3Msg.hasFlag(NTLM.FlagNTLM2Key)
            || (type3Msg.getNTLMHash() != null && type3Msg.getNTLMHash().length > 24)) {
          // Cannot accept NTLMv2 if we are using passthru auth
          if (logger.isErrorEnabled())
            logger.error(
                "Client "
                    + workstation
                    + " using NTLMv2 logon, not valid with passthru authentication");
        } else {
          if (ntlmDetails == null) {
            if (logger.isWarnEnabled())
              logger.warn(
                  "Authentication failed: NTLM details can not be retrieved from session. Client must support cookies.");
            restartLoginChallenge(context, req, res);
            return false;
          }
          // Passthru mode, send the hashed password details to the passthru authentication server
          NTLMPassthruToken authToken = (NTLMPassthruToken) ntlmDetails.getAuthenticationToken();
          authToken.setUserAndPassword(
              type3Msg.getUserName(), type3Msg.getNTLMHash(), PasswordEncryptor.NTLM1);
          try {
            // Run the second stage of the passthru authentication
            nltmAuthenticator.authenticate(authToken);
            authenticated = true;

            // Check if the user has been logged on as guest
            if (authToken.isGuestLogon()) {
              userName = authenticationComponent.getGuestUserName();
            }

            // Set the authentication context
            authenticationComponent.setCurrentUser(userName);
          } catch (BadCredentialsException ex) {
            if (logger.isDebugEnabled()) logger.debug("Authentication failed, " + ex.getMessage());
          } catch (AuthenticationException ex) {
            if (logger.isDebugEnabled()) logger.debug("Authentication failed, " + ex.getMessage());
          } finally {
            // Clear the authentication token from the NTLM details
            ntlmDetails.setAuthenticationToken(null);
          }
        }
      }

      // Check if the user has been authenticated, if so then setup the user environment
      if (authenticated == true) {
        boolean userInit = false;
        if (user == null) {
          try {
            user = createUserEnvironment(session, userName);
            userInit = true;
          } catch (AuthenticationException ex) {
            if (logger.isDebugEnabled()) logger.debug("Failed to validate user " + userName, ex);

            onValidateFailed(context, req, res, session, credentials);
            return false;
          }
        }

        onValidate(context, req, res, credentials);

        // Update the NTLM logon details in the session
        String srvName = getServerName();
        if (ntlmDetails == null) {
          // No cached NTLM details
          ntlmDetails = new NTLMLogonDetails(userName, workstation, domain, false, srvName);
          ntlmDetails.setNTLMHashedPassword(type3Msg.getNTLMHash());
          session.setAttribute(NTLM_AUTH_DETAILS, ntlmDetails);

          if (logger.isDebugEnabled()) logger.debug("No cached NTLM details, created");
        } else {
          // Update the cached NTLM details
          ntlmDetails.setDetails(userName, workstation, domain, false, srvName);
          ntlmDetails.setNTLMHashedPassword(type3Msg.getNTLMHash());

          if (logger.isDebugEnabled()) logger.debug("Updated cached NTLM details");
        }

        if (logger.isDebugEnabled()) logger.debug("User logged on via NTLM, " + ntlmDetails);

        if (onLoginComplete(context, req, res, userInit)) {
          // Allow the user to access the requested page
          return true;
        }
      } else {
        restartLoginChallenge(context, req, res);
      }
    }
    return false;
  }
  /**
   * Validate the MD4 hash against local password
   *
   * @param type3Msg Type3NTLMMessage
   * @param ntlmDetails NTLMLogonDetails
   * @param authenticated boolean
   * @param md4hash String
   * @return true if password hash is valid, false otherwise
   */
  protected boolean validateLocalHashedPassword(
      Type3NTLMMessage type3Msg,
      NTLMLogonDetails ntlmDetails,
      boolean authenticated,
      String md4hash) {
    // Make sure we have hte cached NTLM details, including the type2 message with the server
    // challenge

    if (ntlmDetails == null || ntlmDetails.getType2Message() == null) {
      // DEBUG

      if (getLogger().isDebugEnabled())
        getLogger().debug("No cached Type2, ntlmDetails=" + ntlmDetails);

      // Not authenticated

      return false;
    }

    //  Determine if the client sent us NTLMv1 or NTLMv2
    if (type3Msg.hasFlag(NTLM.FlagNTLM2Key)) {
      //  Determine if the client sent us an NTLMv2 blob or an NTLMv2 session key
      if (type3Msg.getNTLMHashLength() > 24) {
        //  Looks like an NTLMv2 blob
        authenticated = checkNTLMv2(md4hash, ntlmDetails.getChallengeKey(), type3Msg);

        if (getLogger().isDebugEnabled())
          getLogger()
              .debug((authenticated ? "Logged on" : "Logon failed") + " using NTLMSSP/NTLMv2");

        // If the NTlmv2 autentication failed then check if the client has sent an NTLMv1 hash

        if (authenticated == false
            && type3Msg.hasFlag(NTLM.Flag56Bit)
            && type3Msg.getLMHashLength() == 24) {
          // Check the LM hash field

          authenticated = checkNTLMv1(md4hash, ntlmDetails.getChallengeKey(), type3Msg, true);

          // DEBUG

          if (getLogger().isDebugEnabled())
            getLogger()
                .debug(
                    (authenticated ? "Logged on" : "Logon failed")
                        + " using NTLMSSP/NTLMv1 (via fallback)");
        }
      } else {
        //  Looks like an NTLMv2 session key
        authenticated = checkNTLMv2SessionKey(md4hash, ntlmDetails.getChallengeKey(), type3Msg);

        if (getLogger().isDebugEnabled())
          getLogger()
              .debug(
                  (authenticated ? "Logged on" : "Logon failed") + " using NTLMSSP/NTLMv2SessKey");
      }
    } else {
      //  Looks like an NTLMv1 blob
      authenticated = checkNTLMv1(md4hash, ntlmDetails.getChallengeKey(), type3Msg, false);

      if (getLogger().isDebugEnabled())
        getLogger().debug((authenticated ? "Logged on" : "Logon failed") + " using NTLMSSP/NTLMv1");
    }
    return authenticated;
  }
  /**
   * Process a type 1 NTLM message
   *
   * @param type1Msg Type1NTLMMessage
   * @param req HttpServletRequest
   * @param res HttpServletResponse
   * @exception IOException
   */
  protected void processType1(
      Type1NTLMMessage type1Msg, HttpServletRequest req, HttpServletResponse res)
      throws IOException {
    if (getLogger().isDebugEnabled()) getLogger().debug("Received type1 " + type1Msg);

    // Get the existing NTLM details
    NTLMLogonDetails ntlmDetails = null;
    HttpSession session = req.getSession();
    ntlmDetails = (NTLMLogonDetails) session.getAttribute(NTLM_AUTH_DETAILS);

    // Check if cached logon details are available
    if (ntlmDetails != null
        && ntlmDetails.hasType2Message()
        && ntlmDetails.hasNTLMHashedPassword()
        && ntlmDetails.hasAuthenticationToken()) {
      // Get the authentication server type2 response
      Type2NTLMMessage cachedType2 = ntlmDetails.getType2Message();

      byte[] type2Bytes = cachedType2.getBytes();
      String ntlmBlob = "NTLM " + new String(Base64.encodeBase64(type2Bytes));

      if (getLogger().isDebugEnabled())
        getLogger().debug("Sending cached NTLM type2 to client - " + cachedType2);

      // Send back a request for NTLM authentication
      res.setHeader(WWW_AUTHENTICATE, ntlmBlob);
      res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
      res.flushBuffer();
    } else {
      // Clear any cached logon details
      session.removeAttribute(NTLM_AUTH_DETAILS);

      // Set the 8 byte challenge for the new logon request
      byte[] challenge = null;
      NTLMPassthruToken authToken = null;

      if (nltmAuthenticator.getNTLMMode() == NTLMMode.MD4_PROVIDER) {
        // Generate a random 8 byte challenge

        challenge = new byte[8];
        DataPacker.putIntelLong(m_random.nextLong(), challenge, 0);
      } else {
        // Get the client domain
        String domain = type1Msg.getDomain();
        if (domain == null || domain.length() == 0) {
          domain = mapClientAddressToDomain(req.getRemoteAddr());
        }

        if (getLogger().isDebugEnabled()) getLogger().debug("Client domain " + domain);

        // Create an authentication token for the new logon
        authToken = new NTLMPassthruToken(domain);

        // Run the first stage of the passthru authentication to get the challenge
        nltmAuthenticator.authenticate(authToken);

        // Get the challenge from the token
        if (authToken.getChallenge() != null) {
          challenge = authToken.getChallenge().getBytes();
        }
      }

      // Get the flags from the client request and mask out unsupported features
      int ntlmFlags = type1Msg.getFlags() & m_ntlmFlags;

      // Build a type2 message to send back to the client, containing the challenge
      List<TargetInfo> tList = new ArrayList<TargetInfo>();
      String srvName = getServerName();
      tList.add(new TargetInfo(NTLM.TargetServer, srvName));

      Type2NTLMMessage type2Msg = new Type2NTLMMessage();
      type2Msg.buildType2(ntlmFlags, srvName, challenge, null, tList);

      // Store the NTLM logon details, cache the type2 message, and token if using passthru
      ntlmDetails = new NTLMLogonDetails();
      ntlmDetails.setType2Message(type2Msg);
      ntlmDetails.setAuthenticationToken(authToken);

      session.setAttribute(NTLM_AUTH_DETAILS, ntlmDetails);

      if (getLogger().isDebugEnabled())
        getLogger().debug("Sending NTLM type2 to client - " + type2Msg);

      // Send back a request for NTLM authentication
      byte[] type2Bytes = type2Msg.getBytes();
      String ntlmBlob = "NTLM " + new String(Base64.encodeBase64(type2Bytes));

      res.setHeader(WWW_AUTHENTICATE, ntlmBlob);
      res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
      res.flushBuffer();
    }
  }