/*
 * Copyright (c) 2010. The Codehaus. All Rights Reserved.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */

package org.codehaus.httpcache4j.auth.digest;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.codehaus.httpcache4j.*;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.text.NumberFormat;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author <a href="mailto:hamnis@codehaus.org">Erlend Hamnaberg</a>
 * @version $Revision: $
 */
//TODO: handle auth-int
public class RequestDigest {

    private static final String CNONCE_COUNT = "00000001";
    private final Map<String, Directive> directives = new LinkedHashMap<String, Directive>();
    private final UsernamePasswordChallenge challenge;
    private final HTTPMethod method;
    private final URI requestURI;
    private final Digest serverDigest;
    private final Algorithm algorithm;

    RequestDigest(UsernamePasswordChallenge challenge, HTTPMethod method, URI requestURI, Digest serverDigest) {
        this.challenge = challenge;
        this.method = method;
        this.requestURI = requestURI;
        this.serverDigest = serverDigest;

        addDirective("username", challenge.getIdentifier(), true);
        addDirective("realm", serverDigest.getScheme().getRealm(), true);
        addDirective("nonce", serverDigest.getNonce(), true);
        addDirective("uri", requestURI.toString(), true);
        if (serverDigest.getAlgorithm() != null) {
            addDirective("algorithm", serverDigest.getAlgorithm().getValue(), true);
            algorithm = serverDigest.getAlgorithm();
        }
        else {
            algorithm = Algorithm.MD5;
        }
        if (StringUtils.isNotBlank(serverDigest.getQop())) {
            addDirective("qop", serverDigest.getQop(), false);
            addDirective("nc", CNONCE_COUNT, false);
            addDirective("cnonce", calculateCNonce(), true);
        }
        addDirective("response", calculateResponse(), true);
        if (StringUtils.isNotBlank(serverDigest.getOpaque())) {
            addDirective("opaque", serverDigest.getOpaque(), true);
        }
    }

    String calculateResponse() {
        String ha1 = calculateHashA1();
        Directive qopDirective = directives.get("qop");
        Directive nonce = directives.get("nonce");

        String response;
        String ha2 = calculateHashA2();

        if (qopDirective != null) {
            if (isAuthIntQualityOfProtection(qopDirective)) {
                throw new IllegalArgumentException("Auth-int not supported yet");
            }
            else if (isAuthQualityOfProtection(qopDirective)) {
                Directive clientNonce = directives.get("cnonce");
                StringBuilder builder = new StringBuilder();
                builder.append(ha1);
                builder.append(':');
                builder.append(nonce.getValue());
                builder.append(':');
                builder.append(CNONCE_COUNT);
                builder.append(':');
                builder.append(clientNonce.getValue());
                builder.append(':');
                builder.append(qopDirective.getValue());
                builder.append(':');
                builder.append(ha2);
                response = hash(algorithm, builder.toString(), "ISO-8859-1");
            }
            else {
                throw new IllegalArgumentException("Unknown QOP: " + qopDirective.getValue());
            }
        }
        else {
            response = hash(algorithm, String.format("%s:%s:%s", ha1, nonce.getValue(), ha2), "US-ASCII");
        }
        return response;
    }

    String calculateHashA2() {
        return hash(algorithm, String.format("%s:%s", method, requestURI), "ISO-8859-1");
    }

    String calculateHashA1() {
        StringBuilder a1 = new StringBuilder();
        a1.append(challenge.getIdentifier());
        a1.append(':');
        a1.append(serverDigest.getScheme().getRealm());
        a1.append(':');
        a1.append(challenge.getPassword());
        return hash(algorithm, a1.toString(), "US-ASCII");
    }

    private boolean isAuthQualityOfProtection(Directive directive) {
        return "auth".equals(directive.getValue());
    }

    private boolean isAuthIntQualityOfProtection(Directive directive) {
        return "auth-int".equals(directive.getValue());
    }

    private String hash(Algorithm algorithm, String input, final String charset) {
        switch (algorithm) {
            case MD5:
            case MD5_SESSION:
                try {
                    return DigestUtils.md5Hex(input.getBytes(charset));
                } catch (UnsupportedEncodingException e) {
                    throw new Error(e);
                }
            case TOKEN:
            default:
                throw new IllegalArgumentException("No such algorithm");
        }
    }

    String calculateCNonce() {
        return hash(Algorithm.MD5, Long.toString(System.currentTimeMillis()), "US-ASCII");
    }

    public String toHeaderValue() {
        StringBuilder builder = new StringBuilder();
        builder.append("Digest ");
        for (Directive directive : directives.values()) {
            if (builder.length() > "Digest ".length()) {
                builder.append(", ");
            }
            builder.append(directive);
        }
        return builder.toString();
    }

    public static RequestDigest newInstance(UsernamePasswordChallenge challenge, HTTPRequest request, Digest digest) {
        return new RequestDigest(challenge, request.getMethod(), URI.create(request.getRequestURI().getPath()), digest);
    }

    private void addDirective(String name, String value, boolean quoted) {
        if (quoted) {
            directives.put(name, new QuotedDirective(name, value));
        }
        else {
            directives.put(name, new Directive(name, value));
        }
    }
}

@@ -6232,16 +6232,19 @@
 RI().get
+Raw
 Path()),

Use rawPath instead of path since path is decoding anf will break if the path has characters not valid in the path.