/*
 * ====================================================================
 * Copyright (c) 2004-2012 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core.internal.io.dav.http;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.StringTokenizer;

import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
class HTTPDigestAuthentication extends HTTPAuthentication {

    private static final char[] HEXADECIMAL = {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
        'e', 'f'
    };

    private String myCnonce;
    private String myQop;
    private String myLastNonce;
    private int myNC;

	private String myCharset;

    protected HTTPDigestAuthentication (String charset) {
        myNC = 0;
        myCharset = charset;
    }

    public void init() throws SVNException {
        String qop = getChallengeParameter("qop");
        String selectedQop = null;

        if (qop != null) {
            for(StringTokenizer tok = new StringTokenizer(qop,","); tok.hasMoreTokens();) {
                selectedQop = tok.nextToken().trim();
                if ("auth".equals(selectedQop)) {
                    break;
                }
            }
        }
        if (selectedQop != null && !"auth".equals(selectedQop)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_DAV_REQUEST_FAILED, "Digest HTTP auth: ''(0}'' is not supported", selectedQop);
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        }
        myQop = selectedQop;
        myCnonce = createCnonce();
    }
    
    public String authenticate() throws SVNException {
        if (getUserName() == null || getPassword() == null) {
            return null;
        }
        
        String uname = getUserName();
        String nonce = getParameter("nonce");
        if (nonce == null || !nonce.equals(myLastNonce)) {
            myNC = 0;
        } 
        myNC++;
        myLastNonce = nonce;
        String digest = createDigest(uname, getPassword(), myCharset);

        String uri = getParameter("uri");
        String realm = getParameter("realm");
        String opaque = getParameter("opaque");
        String algorithm = getParameter("algorithm", "MD5");

        StringBuffer sb = new StringBuffer();

        sb.append("Digest ");

        sb.append("username=\"" + uname + "\"")
          .append(", realm=\"" + realm + "\"")
          .append(", nonce=\"" + nonce + "\"").append(", uri=\"" + uri + "\"")
          .append(", response=\"" + digest + "\"");
        
        String nc = formatNC(myNC);        
        if (myQop != null) {
            sb.append(", qop=\"" + myQop + "\"")
              .append(", nc="+ nc)
              .append(", cnonce=\"" + myCnonce + "\"");
        }
        if (algorithm != null) {
            sb.append(", algorithm=\"" + algorithm + "\"");
        }
        if (opaque != null) {
            sb.append(", opaque=\"" + opaque + "\"");
        }
        return sb.toString();
    }

    public String getAuthenticationScheme(){
        return "Digest";
    }

    private String createDigest(String uname, char[] pwd, String charset) throws SVNException {
        final String digAlg = "MD5";

        String uri = getParameter("uri");
        String realm = getParameter("realm");
        String nonce = getParameter("nonce");
        String method = getParameter("method");
        String algorithm = getParameter("algorithm", "MD5");

        MessageDigest md5Helper;
        try {
            md5Helper = MessageDigest.getInstance(digAlg);
        } catch (Exception e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_DAV_REQUEST_FAILED, "Unsupported algorithm in HTTP Digest authentication: ''{0}''", digAlg);
            SVNErrorManager.error(err, SVNLogType.NETWORK);
            return null;
        }

        final char[] tmp = new char[uname.length() + realm.length() + pwd.length + 2];
        System.arraycopy(uname.toCharArray(), 0, tmp, 0, uname.length());
        tmp[uname.length()] = ':';
        System.arraycopy(realm.toCharArray(), 0, tmp, uname.length() + 1, realm.length());
        tmp[uname.length() + realm.length() + 1] = ':';
        System.arraycopy(pwd, 0, tmp, uname.length() + realm.length() + 2, pwd.length);

        char[] a1 = tmp;
        if ("MD5-sess".equals(algorithm)) {
            String tmp2=encode(md5Helper.digest(HTTPAuthentication.getBytes(tmp, charset)));
            StringBuffer tmp3 = new StringBuffer(tmp2.length() + nonce.length() + myCnonce.length() + 2);
            tmp3.append(tmp2);
            tmp3.append(':');
            tmp3.append(nonce);
            tmp3.append(':');
            tmp3.append(myCnonce);
            a1 = tmp3.toString().toCharArray();
        }        

        String md5a1 = encode(md5Helper.digest(HTTPAuthentication.getBytes(a1, charset)));
        String a2 = method + ":" + uri;
        String md5a2 = encode(md5Helper.digest(HTTPAuthentication.getBytes(a2, charset)));
        HTTPAuthentication.clear(tmp);

        StringBuffer tmp2;
        if (myQop == null) {
            tmp2 = new StringBuffer(md5a1.length() + nonce.length() + md5a2.length());
            tmp2.append(md5a1);
            tmp2.append(':');
            tmp2.append(nonce);
            tmp2.append(':');
            tmp2.append(md5a2);
        } else {
            String qopOption = "auth";
            String nc = formatNC(myNC);
            tmp2 = new StringBuffer(md5a1.length() + nonce.length()
                    + nc.length() + myCnonce.length() + qopOption.length() + md5a2.length() + 5);
            tmp2.append(md5a1);
            tmp2.append(':');
            tmp2.append(nonce);
            tmp2.append(':');
            tmp2.append(nc);
            tmp2.append(':');
            tmp2.append(myCnonce);
            tmp2.append(':');
            tmp2.append(qopOption);
            tmp2.append(':');
            tmp2.append(md5a2);
        }

        return encode(md5Helper.digest(HTTPAuthentication.getBytes(tmp2.toString(), charset)));
    }

    private String getParameter(String name) {
        return getParameter(name, null);
    }

    private String getParameter(String name, String defaultValue) {
        String value = getChallengeParameter(name);
        if (value == null) {
            value = defaultValue;
        }
        return value;
    }

    private String createCnonce() {
        String cnonce;
        final String digAlg = "MD5";
        MessageDigest md5Helper;

        try {
            md5Helper = MessageDigest.getInstance(digAlg);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
        cnonce = Long.toString(System.currentTimeMillis());
        cnonce = encode(md5Helper.digest(HTTPAuthentication.getBytes(cnonce, myCharset)));
        return cnonce;
    }

    private static String encode(byte[] binaryData) {
        if (binaryData.length != 16) {
            return null;
        }

        char[] buffer = new char[32];
        for (int i = 0; i < 16; i++) {
            int low = binaryData[i] & 0x0f;
            int high = (binaryData[i] & 0xf0) >> 4;
            buffer[i * 2] = HEXADECIMAL[high];
            buffer[(i * 2) + 1] = HEXADECIMAL[low];
        }

        return new String(buffer);
    }
    
    private static String formatNC(int nc) {
        String value = Integer.toHexString(nc);
        int count = 8 - value.length();
        for(int i = 0; i < count; i++) {
            value = '0' + value;
        }
        return value;
    }

}
