/**
   * Generates the WWW-Authenticate header.
   *
   * <p>The header MUST follow this template :
   *
   * <pre>
   *      WWW-Authenticate    = "WWW-Authenticate" ":" "Digest"
   *                            digest-challenge
   *
   *      digest-challenge    = 1#( realm | [ domain ] | nOnce |
   *                  [ digest-opaque ] |[ stale ] | [ algorithm ] )
   *
   *      realm               = "realm" "=" realm-value
   *      realm-value         = quoted-string
   *      domain              = "domain" "=" <"> 1#URI <">
   *      nonce               = "nonce" "=" nonce-value
   *      nonce-value         = quoted-string
   *      opaque              = "opaque" "=" quoted-string
   *      stale               = "stale" "=" ( "true" | "false" )
   *      algorithm           = "algorithm" "=" ( "MD5" | token )
   * </pre>
   *
   * @param request HTTP Servlet request
   * @param response HTTP Servlet response
   * @param config Login configuration describing how authentication should be performed
   * @param nOnce nonce token
   */
  protected void setAuthenticateHeader(
      SipServletRequestImpl request,
      SipServletResponseImpl response,
      SipLoginConfig config,
      String nOnce) {

    // Get the realm name
    String realmName = config.getRealmName();
    if (realmName == null) realmName = request.getServerName() + ":" + request.getServerPort();

    byte[] buffer = null;
    synchronized (md5Helper) {
      buffer = md5Helper.digest(nOnce.getBytes());
    }

    String authenticateHeader =
        "Digest realm=\""
            + realmName
            + "\", "
            + "qop=\"auth\", nonce=\""
            + nOnce
            + "\", "
            + "opaque=\""
            + md5Encoder.encode(buffer)
            + "\"";

    // There are different headers for different types of auth
    if (response.getStatus() == SipServletResponseImpl.SC_PROXY_AUTHENTICATION_REQUIRED) {
      response.setHeader("Proxy-Authenticate", authenticateHeader);
    } else {
      response.setHeader("WWW-Authenticate", authenticateHeader);
    }
  }
  /**
   * Return the Principal associated with the specified username, which matches the digest
   * calculated using the given parameters using the method described in RFC 2069; otherwise return
   * <code>null</code>.
   *
   * @param username Username of the Principal to look up
   * @param clientDigest Digest which has been submitted by the client
   * @param nOnce Unique (or supposedly unique) token which has been used for this request
   * @param realm Realm name
   * @param md5a2 Second MD5 digest used to calculate the digest : MD5(Method + ":" + uri)
   */
  public Principal authenticate(
      String username,
      String clientDigest,
      String nOnce,
      String nc,
      String cnonce,
      String qop,
      String realm,
      String md5a2) {

    /*
      System.out.println("Digest : " + clientDigest);

      System.out.println("************ Digest info");
      System.out.println("Username:"******"ClientSigest:" + clientDigest);
      System.out.println("nOnce:" + nOnce);
      System.out.println("nc:" + nc);
      System.out.println("cnonce:" + cnonce);
      System.out.println("qop:" + qop);
      System.out.println("realm:" + realm);
      System.out.println("md5a2:" + md5a2);
    */

    String md5a1 = getDigest(username, realm);
    if (md5a1 == null) return null;
    String serverDigestValue =
        md5a1 + ":" + nOnce + ":" + nc + ":" + cnonce + ":" + qop + ":" + md5a2;
    String serverDigest = md5Encoder.encode(md5Helper.digest(serverDigestValue.getBytes()));
    // System.out.println("Server digest : " + serverDigest);

    if (serverDigest.equals(clientDigest)) return getPrincipal(username);
    else return null;
  }
  /** Return the digest associated with given principal's user name. */
  protected String getDigest(String username, String realmName) {
    if (md5Helper == null) {
      try {
        md5Helper = MessageDigest.getInstance("MD5");
      } catch (NoSuchAlgorithmException e) {
        log.error("Couldn't get MD5 digest: ", e);
        throw new IllegalStateException(e.getMessage());
      }
    }

    if (hasMessageDigest()) {
      // Use pre-generated digest
      return getPassword(username);
    }

    String digestValue = username + ":" + realmName + ":" + getPassword(username);

    byte[] valueBytes = null;
    try {
      valueBytes = digestValue.getBytes(getDigestCharset());
    } catch (UnsupportedEncodingException uee) {
      log.error("Illegal digestEncoding: " + getDigestEncoding(), uee);
      throw new IllegalArgumentException(uee.getMessage());
    }

    byte[] digest = null;
    // Bugzilla 32137
    synchronized (md5Helper) {
      digest = md5Helper.digest(valueBytes);
    }

    return md5Encoder.encode(digest);
  }
  /**
   * Generate a unique token. The token is generated according to the following pattern. NOnceToken
   * = Base64 ( MD5 ( client-IP ":" time-stamp ":" private-key ) ).
   *
   * @param request HTTP Servlet request
   */
  protected String generateNonce(Request request) {

    long currentTime = System.currentTimeMillis();

    synchronized (lastTimestampLock) {
      if (currentTime > lastTimestamp) {
        lastTimestamp = currentTime;
      } else {
        currentTime = ++lastTimestamp;
      }
    }

    String ipTimeKey = request.getRemoteAddr() + ":" + currentTime + ":" + getKey();

    byte[] buffer =
        ConcurrentMessageDigest.digestMD5(ipTimeKey.getBytes(StandardCharsets.ISO_8859_1));
    String nonce = currentTime + ":" + MD5Encoder.encode(buffer);

    NonceInfo info = new NonceInfo(currentTime, getNonceCountWindowSize());
    synchronized (nonces) {
      nonces.put(nonce, info);
    }

    return nonce;
  }
    public Principal authenticate(Realm realm) {
      // Second MD5 digest used to calculate the digest :
      // MD5(Method + ":" + uri)
      String a2 = method + ":" + uri;

      byte[] buffer = ConcurrentMessageDigest.digestMD5(a2.getBytes(StandardCharsets.ISO_8859_1));
      String md5a2 = MD5Encoder.encode(buffer);

      return realm.authenticate(userName, response, nonce, nc, cnonce, qop, realmName, md5a2);
    }
 /** Return the digest associated with given principal's user name. */
 protected String getDigest(String username, String realmName) {
   if (md5Helper == null) {
     try {
       md5Helper = MessageDigest.getInstance("MD5");
     } catch (NoSuchAlgorithmException e) {
       e.printStackTrace();
       throw new IllegalStateException();
     }
   }
   String digestValue = username + ":" + realmName + ":" + getPassword(username);
   byte[] digest = md5Helper.digest(digestValue.getBytes());
   return md5Encoder.encode(digest);
 }
  /**
   * Generate a unique token. The token is generated according to the following pattern. NOnceToken
   * = Base64 ( MD5 ( client-IP ":" time-stamp ":" private-key ) ).
   *
   * @param request HTTP Servlet request
   */
  protected String generateNOnce(SipServletRequestImpl request) {

    long currentTime = System.currentTimeMillis();

    String nOnceValue = request.getRemoteAddr() + ":" + currentTime + ":" + key;

    byte[] buffer = null;
    synchronized (md5Helper) {
      buffer = md5Helper.digest(nOnceValue.getBytes());
    }
    nOnceValue = md5Encoder.encode(buffer);

    return nOnceValue;
  }
  /**
   * Parse the specified authorization credentials, and return the associated Principal that these
   * credentials authenticate (if any) from the specified Realm. If there is no such Principal,
   * return <code>null</code>.
   *
   * @param request HTTP servlet request
   * @param authorization Authorization credentials from this request
   * @param realm Realm used to authenticate Principals
   */
  protected static Principal findPrincipal(
      SipServletRequestImpl request, String authorization, Realm realm) {

    // System.out.println("Authorization token : " + authorization);
    // Validate the authorization credentials format
    if (authorization == null) return (null);
    if (!authorization.startsWith("Digest ")) return (null);
    authorization = authorization.substring(7).trim();

    // Bugzilla 37132: http://issues.apache.org/bugzilla/show_bug.cgi?id=37132
    // The solution of 37132 doesn't work with :
    // response="2d05f1206becab904c1f311f205b405b",cnonce="5644k1k670",username="******",nc=00000001,qop=auth,nonce="b6c73ab509830b8c0897984f6b0526e8",realm="sip-servlets-realm",opaque="9ed6d115d11f505f9ee20f6a68654cc2",uri="sip:192.168.1.142",algorithm=MD5
    // That's why I am going back to simple comma (Vladimir). TODO: Review this.
    String[] tokens = authorization.split(","); // (?=(?:[^\"]*\"[^\"]*\")+$)");

    String userName = null;
    String realmName = null;
    String nOnce = null;
    String nc = null;
    String cnonce = null;
    String qop = null;
    String uri = null;
    String response = null;
    String method = request.getMethod();

    for (int i = 0; i < tokens.length; i++) {
      String currentToken = tokens[i];
      if (currentToken.length() == 0) continue;

      int equalSign = currentToken.indexOf('=');
      if (equalSign < 0) return null;
      String currentTokenName = currentToken.substring(0, equalSign).trim();
      String currentTokenValue = currentToken.substring(equalSign + 1).trim();
      if ("username".equals(currentTokenName)) userName = removeQuotes(currentTokenValue);
      if ("realm".equals(currentTokenName)) realmName = removeQuotes(currentTokenValue, true);
      if ("nonce".equals(currentTokenName)) nOnce = removeQuotes(currentTokenValue);
      if ("nc".equals(currentTokenName)) nc = removeQuotes(currentTokenValue);
      if ("cnonce".equals(currentTokenName)) cnonce = removeQuotes(currentTokenValue);
      if ("qop".equals(currentTokenName)) qop = removeQuotes(currentTokenValue);
      if ("uri".equals(currentTokenName)) uri = removeQuotes(currentTokenValue);
      if ("response".equals(currentTokenName)) response = removeQuotes(currentTokenValue);
    }

    if ((userName == null)
        || (realmName == null)
        || (nOnce == null)
        || (uri == null)
        || (response == null)) return null;

    // Second MD5 digest used to calculate the digest :
    // MD5(Method + ":" + uri)
    String a2 = method + ":" + uri;
    // System.out.println("A2:" + a2);

    byte[] buffer = null;
    synchronized (md5Helper) {
      buffer = md5Helper.digest(a2.getBytes());
    }
    String md5a2 = md5Encoder.encode(buffer);

    return (realm.authenticate(userName, response, nOnce, nc, cnonce, qop, realmName, md5a2));
  }
  /**
   * Return the Principal associated with the specified username, which matches the digest
   * calculated using the given parameters using the method described in RFC 2069; otherwise return
   * <code>null</code>.
   *
   * @param username Username of the Principal to look up
   * @param clientDigest Digest which has been submitted by the client
   * @param nonce Unique (or supposedly unique) token which has been used for this request
   * @param realm Realm name
   * @param md5a2 Second MD5 digest used to calculate the digest : MD5(Method + ":" + uri)
   */
  @Override
  public Principal authenticate(
      String username,
      String clientDigest,
      String nonce,
      String nc,
      String cnonce,
      String qop,
      String realm,
      String md5a2) {

    // In digest auth, digests are always lower case
    String md5a1 = getDigest(username, realm).toLowerCase(Locale.ENGLISH);
    if (md5a1 == null) return null;
    String serverDigestValue;
    if (qop == null) {
      serverDigestValue = md5a1 + ":" + nonce + ":" + md5a2;
    } else {
      serverDigestValue = md5a1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + md5a2;
    }

    byte[] valueBytes = null;
    try {
      valueBytes = serverDigestValue.getBytes(getDigestCharset());
    } catch (UnsupportedEncodingException uee) {
      log.error("Illegal digestEncoding: " + getDigestEncoding(), uee);
      throw new IllegalArgumentException(uee.getMessage());
    }

    String serverDigest = null;
    // Bugzilla 32137
    synchronized (md5Helper) {
      serverDigest = md5Encoder.encode(md5Helper.digest(valueBytes));
    }

    if (log.isDebugEnabled()) {
      log.debug(
          "Digest : "
              + clientDigest
              + " Username:"******" ClientSigest:"
              + clientDigest
              + " nonce:"
              + nonce
              + " nc:"
              + nc
              + " cnonce:"
              + cnonce
              + " qop:"
              + qop
              + " realm:"
              + realm
              + "md5a2:"
              + md5a2
              + " Server digest:"
              + serverDigest);
    }

    if (serverDigest.equals(clientDigest)) {
      return getPrincipal(username);
    }

    return null;
  }
    public boolean validate(Request request) {
      if ((userName == null)
          || (realmName == null)
          || (nonce == null)
          || (uri == null)
          || (response == null)) {
        return false;
      }

      // Validate the URI - should match the request line sent by client
      if (validateUri) {
        String uriQuery;
        String query = request.getQueryString();
        if (query == null) {
          uriQuery = request.getRequestURI();
        } else {
          uriQuery = request.getRequestURI() + "?" + query;
        }
        if (!uri.equals(uriQuery)) {
          // Some clients (older Android) use an absolute URI for
          // DIGEST but a relative URI in the request line.
          // request. 2.3.5 < fixed Android version <= 4.0.3
          String host = request.getHeader("host");
          String scheme = request.getScheme();
          if (host != null && !uriQuery.startsWith(scheme)) {
            StringBuilder absolute = new StringBuilder();
            absolute.append(scheme);
            absolute.append("://");
            absolute.append(host);
            absolute.append(uriQuery);
            if (!uri.equals(absolute.toString())) {
              return false;
            }
          } else {
            return false;
          }
        }
      }

      // Validate the Realm name
      String lcRealm = getRealmName(request.getContext());
      if (!lcRealm.equals(realmName)) {
        return false;
      }

      // Validate the opaque string
      if (!opaque.equals(opaqueReceived)) {
        return false;
      }

      // Validate nonce
      int i = nonce.indexOf(":");
      if (i < 0 || (i + 1) == nonce.length()) {
        return false;
      }
      long nonceTime;
      try {
        nonceTime = Long.parseLong(nonce.substring(0, i));
      } catch (NumberFormatException nfe) {
        return false;
      }
      String md5clientIpTimeKey = nonce.substring(i + 1);
      long currentTime = System.currentTimeMillis();
      if ((currentTime - nonceTime) > nonceValidity) {
        nonceStale = true;
        synchronized (nonces) {
          nonces.remove(nonce);
        }
      }
      String serverIpTimeKey = request.getRemoteAddr() + ":" + nonceTime + ":" + key;
      byte[] buffer =
          ConcurrentMessageDigest.digestMD5(serverIpTimeKey.getBytes(StandardCharsets.ISO_8859_1));
      String md5ServerIpTimeKey = MD5Encoder.encode(buffer);
      if (!md5ServerIpTimeKey.equals(md5clientIpTimeKey)) {
        return false;
      }

      // Validate qop
      if (qop != null && !QOP.equals(qop)) {
        return false;
      }

      // Validate cnonce and nc
      // Check if presence of nc and Cnonce is consistent with presence of qop
      if (qop == null) {
        if (cnonce != null || nc != null) {
          return false;
        }
      } else {
        if (cnonce == null || nc == null) {
          return false;
        }
        // RFC 2617 says nc must be 8 digits long. Older Android clients
        // use 6. 2.3.5 < fixed Android version <= 4.0.3
        if (nc.length() < 6 || nc.length() > 8) {
          return false;
        }
        long count;
        try {
          count = Long.parseLong(nc, 16);
        } catch (NumberFormatException nfe) {
          return false;
        }
        NonceInfo info;
        synchronized (nonces) {
          info = nonces.get(nonce);
        }
        if (info == null) {
          // Nonce is valid but not in cache. It must have dropped out
          // of the cache - force a re-authentication
          nonceStale = true;
        } else {
          if (!info.nonceCountValid(count)) {
            return false;
          }
        }
      }
      return true;
    }