/*
  Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.

  The MySQL Connector/J is licensed under the terms of the GPLv2
  <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most MySQL Connectors.
  There are special exceptions to the terms and conditions of the GPLv2 as it is applied to
  this software, see the FLOSS License Exception
  <http://www.mysql.com/about/legal/licensing/foss-exception.html>.

  This program is free software; you can redistribute it and/or modify it under the terms
  of the GNU General Public License as published by the Free Software Foundation; version 2
  of the License.

  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  See the GNU General Public License for more details.

  You should have received a copy of the GNU General Public License along with this
  program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth
  Floor, Boston, MA 02110-1301  USA

 */

package com.mysql.fabric.proto.xmlrpc;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 * HTTP/1.1 Digest Authentication - RFC 2617
 */
public class DigestAuthentication {

	/**
	 * Get the digest challenge header by connecting to the resource
	 * with no credentials.
	 */
	public static String getChallengeHeader(String url) throws IOException {
		HttpURLConnection conn = (HttpURLConnection)new URL(url).openConnection();
		conn.setDoOutput(true);
		conn.getOutputStream().close();
		try {
			conn.getInputStream().close();
		} catch (IOException ex) {
			// we expect a 401-unauthorized response with the
			// WWW-Authenticate header to create the request with the
			// necessary auth data
			if (401 != conn.getResponseCode()) {
				throw ex;
			}
			String hdr = conn.getHeaderField("WWW-Authenticate");
			if (hdr != null && !"".equals(hdr)) {
				return hdr;
			}
		}
		return null;
	}

	/**
	 * Calculate the request digest for algorithm=MD5.
	 */
	public static String calculateMD5RequestDigest(String uri, String username, String password, String realm,
													String nonce, String nc, String cnonce, String qop) {
		String reqA1 = username + ":" + realm + ":" + password;
		// valid only for qop="auth"
		String reqA2 = "POST:" + uri;

		String hashA1 = checksumMD5(reqA1);
		String hashA2 = checksumMD5(reqA2);
		String requestDigest = digestMD5(hashA1,
										 nonce + ":" +
										 nc + ":" +
										 cnonce + ":" +
										 qop + ":" +
										 hashA2);

		return requestDigest;
	}

	/**
	 * MD5 version of the "H()" function from rfc2617.
	 */
	private static String checksumMD5(String data) {
		MessageDigest md5 = null;
		try {
			md5 = MessageDigest.getInstance("MD5");
		} catch(NoSuchAlgorithmException ex) {
			throw new RuntimeException("Unable to create MD5 instance", ex);
		}
		// TODO encoding
		return hexEncode(md5.digest(data.getBytes()));
	}

	/**
	 * MD5 version of the "KD()" function from rfc2617.
	 */
	private static String digestMD5(String secret, String data) {
		return checksumMD5(secret + ":" + data);
	}

	/**
	 * hex-encode a byte array
	 */
	private static String hexEncode(byte data[]) {
		StringBuilder sb = new StringBuilder();
		for(int i = 0; i < data.length; ++i) {
			sb.append(String.format("%02x", data[i]));
		}
		return sb.toString();
	}

	/**
	 * Serialize a parameter map into a digest response. This is used
	 * as the "Authorization" header in the request. All parameters in
	 * the supplied map will be added to the header.
	 */
	public static String serializeDigestResponse(Map<String, String> paramMap) {
		StringBuffer sb = new StringBuffer("Digest ");

		boolean prefixComma = false;
		for (Map.Entry<String, String> entry : paramMap.entrySet()) {
			if (!prefixComma) {
				prefixComma = true;
			} else {
				sb.append(", ");
			}
			sb.append(entry.getKey());
			sb.append("=");
			sb.append(entry.getValue());
		}

		return sb.toString();
	}

	/**
	 * Parse a digest challenge from the WWW-Authenticate header
	 * return as the initial response during the authentication
	 * exchange.
	 */
	public static Map<String, String> parseDigestChallenge(String headerValue) {
		if (!headerValue.startsWith("Digest "))
			throw new IllegalArgumentException("Header is not a digest challenge");

		String params = headerValue.substring(7);
		Map<String, String> paramMap = new HashMap<String, String>();
		for (String param : params.split(",\\s*")) {
			String pieces[] = param.split("=");
			paramMap.put(pieces[0], pieces[1].replaceAll("^\"(.*)\"$", "$1"));
		}
		return paramMap;
	}

	/**
	 * Generate the cnonce value. This allows the client provide a
	 * value used in the digest calculation.  Same as Python. (no
	 * motivation given for this algorithm)
	 */
	public static String generateCnonce(String nonce, String nc) {
		// Random string, keep it in basic printable ASCII range
		byte buf[] = new byte[8];
		new Random().nextBytes(buf);
		for (int i = 0; i < 8; ++i)
			buf[i] = (byte) (0x20 + (buf[i] % 95));

		String combo = String.format("%s:%s:%s:%s", nonce, nc,
									 new Date().toGMTString(),
									 new String(buf));
		MessageDigest sha1 = null;
		try {
			sha1 = MessageDigest.getInstance("SHA-1");
		} catch(NoSuchAlgorithmException ex) {
			throw new RuntimeException("Unable to create SHA-1 instance", ex);
		}

		return hexEncode(sha1.digest(combo.getBytes()));
	}

	/**
	 * Quote a parameter to be included in the header. Parameters with
	 * embedded quotes will be rejected.
	 */
	private static String quoteParam(String param) {
		if (param.contains("\"") || param.contains("'")) {
			throw new IllegalArgumentException("Invalid character in parameter");
		}
		return "\"" + param + "\"";
	}

	/**
	 * Generate the Authorization header to make the authenticated
	 * request.
	 */
	public static String generateAuthorizationHeader(Map<String, String> digestChallenge,
													 String username, String password) {
		String nonce = digestChallenge.get("nonce");
		String nc = "00000001";
		String cnonce = generateCnonce(nonce, nc);
		String qop = "auth";
		String uri = "/RPC2";
		String realm = digestChallenge.get("realm");
		String opaque = digestChallenge.get("opaque");

		String requestDigest = calculateMD5RequestDigest(uri, username, password,
														 realm, nonce, nc, cnonce, qop);
		Map<String, String> digestResponseMap = new HashMap<String, String>();
		digestResponseMap.put("algorithm", "MD5");
		digestResponseMap.put("username", quoteParam(username));
		digestResponseMap.put("realm", quoteParam(realm));
		digestResponseMap.put("nonce", quoteParam(nonce));
		digestResponseMap.put("uri", quoteParam(uri));
		digestResponseMap.put("qop", qop);
		digestResponseMap.put("nc", nc);
		digestResponseMap.put("cnonce", quoteParam(cnonce));
		digestResponseMap.put("response", quoteParam(requestDigest));
		digestResponseMap.put("opaque", quoteParam(opaque));

		return serializeDigestResponse(digestResponseMap);
	}
}
