

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import java.net.URL;
import java.net.URLConnection;

import java.security.MessageDigest;

import java.util.StringTokenizer;

import javax.xml.parsers.*;

public class Session {
	protected static final String SESSION_COOKIE = "PHPSESSID";
	private String sessionId = null;
	private String masterHost = null;

	public String getHost() {
		return this.masterHost;
	}

	public void open(String masterHost, String username, String password)
			throws Exception {
		DocumentBuilderFactory builderFactory = DocumentBuilderFactory
				.newInstance();
		DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();

		//
		// Phase 1. Getting login parameters and calculating password hash
		//
		URL loginInfoURL = new URL("http://" + masterHost
				+ "/usr/api/login.php?return=logininfo");
		URLConnection urlConnection = loginInfoURL.openConnection();
		urlConnection.setUseCaches(false);
		urlConnection.connect();

		// parse login info document
		Document doc = documentBuilder.parse(urlConnection.getInputStream());
		Element docElement = doc.getDocumentElement();
		checkRequestStatus(docElement);

		String encryptionKey = getLoginParameter(docElement, "ENCRYPTIONKEY");

		if (encryptionKey == null) {
			throw new Exception(
					"Failed to parse login info documents: missing required parameter \"ENCRYPTIONKEY\"");
		}

		String tempSessionId = getCookie(urlConnection, SESSION_COOKIE);

		if (tempSessionId == null) {
			throw new Exception("Failed to retrieve temporary session Id");
		}

		// calculate password hash
		String passwordHash = getSHA1Digest(password, encryptionKey);

		//
		// Phase 2. Submit password hash
		//
		URL loginURL = new URL("http://" + masterHost
				+ "/usr/api/login.php?return=login" + "&username=" + username
				+ "&credentials=" + passwordHash);
		urlConnection = loginURL.openConnection();
		setCookie(urlConnection, SESSION_COOKIE, tempSessionId);
		urlConnection.connect();

		// parse login document
		doc = documentBuilder.parse(urlConnection.getInputStream());
		checkRequestStatus(doc.getDocumentElement());

		sessionId = getCookie(urlConnection, SESSION_COOKIE);

		if (sessionId == null) {
			throw new Exception("Failed to retrieve session Id");
		}

		this.masterHost = masterHost;
	}

	public void close() {
		if (sessionId != null) {
			try {
				URL logoutURL = new URL("http://" + masterHost
						+ "/usr/logout.php");
				URLConnection urlConnection = logoutURL.openConnection();
				setCookie(urlConnection, SESSION_COOKIE, sessionId);
				urlConnection.connect();
			} catch (Exception e) {
			}

			sessionId = null;
		}
	}

	public String getSessionId() {
		return sessionId;
	}

	/**
	 * addSessionCookie
	 * 
	 * @param connection
	 */
	public void addSessionCookie(URLConnection connection) {
		if (sessionId != null) {
			setCookie(connection, SESSION_COOKIE, sessionId);
		}
	}

	/**
	 * Retrieve named cookie
	 * 
	 * @param connection
	 * @param name
	 * @return
	 */
	protected String getCookie(URLConnection connection, String name) {
		String cookieValue = null;
		String headerName = null;

		for (int i = 1; (headerName = connection.getHeaderFieldKey(i)) != null; i++) {
			if (headerName.equalsIgnoreCase("Set-Cookie")) {
				StringTokenizer st = new StringTokenizer(connection
						.getHeaderField(i), "; ");

				while (st.hasMoreTokens()) {
					String token = st.nextToken();

					if (name.equalsIgnoreCase(token.substring(0, token
							.indexOf("=")))) {
						cookieValue = token.substring(token.indexOf("=") + 1,
								token.length());

						break;
					}
				}

				break;
			}
		}

		return cookieValue;
	}

	/**
	 * Set cookie
	 * 
	 * @param connection
	 * @param name
	 * @param value
	 */
	protected void setCookie(URLConnection connection, String name, String value) {
		connection.setRequestProperty("Cookie", name + "=" + value);
	}

	/**
	 * Check if request status is OK
	 * 
	 * @param docElement
	 * @throws Exception
	 */
	protected void checkRequestStatus(Element docElement) throws Exception {
		NodeList nl = docElement.getElementsByTagName("STATUS");

		if ((nl == null) || (nl.getLength() == 0)) {
			throw new Exception(
					"Failed to parse login info documents: missing STATUS element");
		}

		Element statusElement = (Element) nl.item(0);
		String status = statusElement.getAttribute("VALUE");

		if (status == null) {
			throw new Exception(
					"Failed to parse login info documents: missing attribute STATUS.VALUE");
		} else if (!status.equalsIgnoreCase("OK")) {
			String msg = statusElement.getAttribute("MESSAGE");
			throw new Exception("Login failed: "
					+ ((msg == null) ? "unknown error" : msg));
		}
	}

	/**
	 * 
	 * @param docElement
	 * @param name
	 * @return
	 * @throws Exception
	 */
	protected String getLoginParameter(Element docElement, String name)
			throws Exception {
		NodeList nl = docElement.getElementsByTagName("LOGIN");

		if ((nl == null) || (nl.getLength() == 0)) {
			throw new Exception(
					"Failed to parse login info documents: missing LOGIN element");
		}

		Element loginElement = (Element) nl.item(0);

		String value = null;
		nl = loginElement.getElementsByTagName("PARAM");

		if ((nl != null) && (nl.getLength() > 0)) {
			for (int i = 0; i < nl.getLength(); i++) {
				Element param = (Element) nl.item(i);
				String attrName = param.getAttribute("NAME");

				if (name.equals(attrName)) {
					value = param.getAttribute("VALUE");

					break;
				}
			}
		}

		return value;
	}

	/**
	 * @param value
	 * @param digestKey
	 * @return returns SHA1 digest of value and digestKey
	 */
	protected String getSHA1Digest(String value, String digestKey)
			throws Exception {
		String digest = null;

		try {
			MessageDigest sha1 = MessageDigest.getInstance("SHA");

			// get digest of value first
			byte[] valueBytes = value.getBytes("US-ASCII");
			byte[] valueDigest = sha1.digest(valueBytes);
			digest = toHex(valueDigest);

			if ((digestKey != null) && !"".equals(digestKey)) {
				String tmp = digestKey + digest.toUpperCase() + digestKey;

				byte[] combinedBytes = tmp.getBytes("US-ASCII");
				byte[] combinedDigest = sha1.digest(combinedBytes);
				digest = toHex(combinedDigest);
			}
		} catch (Exception e) {
			throw new Exception(
					"Error happened while calculating password digest: "
							+ e.getMessage(), e);
		}

		return digest;
	}

	/**
	 * Converts the array of bytes to a hex String
	 */
	protected static String toHex(byte[] buf) {
		char[] cbf = new char[buf.length * 2];

		for (int jj = 0, kk = 0; jj < buf.length; jj++) {
			cbf[kk++] = "0123456789ABCDEF".charAt((buf[jj] >> 4) & 0x0F);
			cbf[kk++] = "0123456789ABCDEF".charAt(buf[jj] & 0x0F);
		}

		return new String(cbf);
	}
}
