/**
 * Copyright 2005-2013 Restlet S.A.S.
 * 
 * The contents of this file are subject to the terms of one of the following
 * open source licenses: Apache 2.0 or LGPL 3.0 or LGPL 2.1 or CDDL 1.0 or EPL
 * 1.0 (the "Licenses"). You can select the license that you prefer but you may
 * not use this file except in compliance with one of these Licenses.
 * 
 * You can obtain a copy of the Apache 2.0 license at
 * http://www.opensource.org/licenses/apache-2.0
 * 
 * You can obtain a copy of the LGPL 3.0 license at
 * http://www.opensource.org/licenses/lgpl-3.0
 * 
 * You can obtain a copy of the LGPL 2.1 license at
 * http://www.opensource.org/licenses/lgpl-2.1
 * 
 * You can obtain a copy of the CDDL 1.0 license at
 * http://www.opensource.org/licenses/cddl1
 * 
 * You can obtain a copy of the EPL 1.0 license at
 * http://www.opensource.org/licenses/eclipse-1.0
 * 
 * See the Licenses for the specific language governing permissions and
 * limitations under the Licenses.
 * 
 * Alternatively, you can obtain a royalty free commercial license with less
 * limitations, transferable or non-transferable, directly at
 * http://www.restlet.com/products/restlet-framework
 * 
 * Restlet is a registered trademark of Restlet S.A.S.
 */

package org.restlet.ext.crypto.internal;

import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.ChallengeResponse;
import org.restlet.data.Digest;
import org.restlet.data.Reference;
import org.restlet.engine.security.AuthenticatorUtils;
import org.restlet.ext.crypto.DigestAuthenticator;
import org.restlet.ext.crypto.DigestUtils;
import org.restlet.security.LocalVerifier;
import org.restlet.security.User;

/**
 * Verifier for the HTTP DIGEST authentication scheme. Note that the "A1" hash
 * specified in RFC 2617 is available via the
 * {@link #getWrappedSecretDigest(String)} method.
 * 
 * @author Jerome Louvel
 */
public class HttpDigestVerifier extends
        org.restlet.ext.crypto.DigestVerifier<LocalVerifier> {

    /** The associated digest authenticator. */
    private DigestAuthenticator digestAuthenticator;

    /**
     * Constructor.
     * 
     * @param digestAuthenticator
     *            The associated digest authenticator.
     * @param wrappedAlgorithm
     *            The digest algorithm of secrets provided by the wrapped
     *            verifier.
     * @param wrappedVerifier
     *            The wrapped secret verifier.
     */
    public HttpDigestVerifier(DigestAuthenticator digestAuthenticator,
            LocalVerifier wrappedVerifier, String wrappedAlgorithm) {
        super(Digest.ALGORITHM_HTTP_DIGEST, wrappedVerifier, wrappedAlgorithm);
        this.digestAuthenticator = digestAuthenticator;
    }

    /**
     * If the algorithm is {@link Digest#ALGORITHM_HTTP_DIGEST}, then is
     * retrieves the realm for {@link #getDigestAuthenticator()} to compute the
     * digest, otherwise, it keeps the default behavior.
     */
    @Override
    protected char[] digest(String identifier, char[] secret, String algorithm) {
        if (Digest.ALGORITHM_HTTP_DIGEST.equals(algorithm)) {
            String result = DigestUtils.toHttpDigest(identifier, secret,
                    getDigestAuthenticator().getRealm());
            if (result != null) {
                return result.toCharArray();
            }

            return null;
        }

        return super.digest(identifier, secret, algorithm);
    }

    /**
     * Returns the associated digest authenticator.
     * 
     * @return The associated digest authenticator.
     */
    public DigestAuthenticator getDigestAuthenticator() {
        return digestAuthenticator;
    }

    /**
     * Sets the associated digest authenticator.
     * 
     * @param digestAuthenticator
     *            The associated digest authenticator.
     */
    public void setDigestAuthenticator(DigestAuthenticator digestAuthenticator) {
        this.digestAuthenticator = digestAuthenticator;
    }

    @Override
    public int verify(Request request, Response response) {
        int result = RESULT_VALID;
        ChallengeResponse cr = request.getChallengeResponse();

        if (cr == null) {
            result = RESULT_MISSING;
        } else {
            String nonce = cr.getServerNonce();
            String uri = (cr.getDigestRef() == null) ? null : cr.getDigestRef()
                    .toString();
            String qop = cr.getQuality();
            int nc = cr.getServerNounceCount();
            String cnonce = cr.getClientNonce();
            String username = getIdentifier(request, response);
            String cresponse = null;
            char[] secret = getSecret(request, response);

            if (secret != null) {
                cresponse = new String(secret);
            } else {
                result = RESULT_INVALID;
            }

            try {
                if (!HttpDigestHelper.isNonceValid(nonce, getDigestAuthenticator()
                        .getServerKey(), getDigestAuthenticator()
                        .getMaxServerNonceAge())) {
                    // Nonce expired, send challenge request with stale=true
                    result = RESULT_STALE;
                }
            } catch (Exception ce) {
                // Invalid nonce, probably doesn't match serverKey
                result = RESULT_INVALID;
            }

            if (result == RESULT_VALID) {
                if (AuthenticatorUtils.anyNull(nonce, uri)) {
                    result = RESULT_MISSING;
                } else {
                    Reference resourceRef = request.getResourceRef();
                    String requestUri = resourceRef.getPath();

                    if ((resourceRef.getQuery() != null)
                            && (uri.indexOf('?') > -1)) {
                        // IE neglects to include the query string, so
                        // the workaround is to leave it off
                        // unless both the calculated URI and the
                        // specified URI contain a query string
                        requestUri += "?" + resourceRef.getQuery();
                    }

                    if (uri.equals(requestUri)) {
                        char[] a1 = getWrappedSecretDigest(username);
                        if (a1 != null) {
                            String a2 = DigestUtils.toMd5(request.getMethod()
                                    .toString() + ":" + requestUri);
                            StringBuilder expectedResponse = new StringBuilder()
                                    .append(a1).append(':').append(nonce);
                            if (!AuthenticatorUtils.anyNull(qop, cnonce, nc)) {
                                expectedResponse
                                        .append(':')
                                        .append(AuthenticatorUtils
                                                .formatNonceCount(nc))
                                        .append(':').append(cnonce).append(':')
                                        .append(qop);
                            }
                            expectedResponse.append(':').append(a2);

                            if (!DigestUtils.toMd5(expectedResponse.toString())
                                    .equals(cresponse)) {
                                result = RESULT_INVALID;
                            }
                        } else {
                            // The HA1 is null
                            result = RESULT_INVALID;
                        }
                    } else {
                        // The request URI doesn't match
                        result = RESULT_INVALID;
                    }
                }
            }

            if (result == RESULT_VALID) {
                request.getClientInfo().setUser(new User(username));
            }
        }

        return result;
    }

}
