/**
 * Copyright 2010 JBoss Inc
 *
 * 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.eclipse.webdav.internal.authentication;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import org.eclipse.webdav.IContext;
import org.eclipse.webdav.IResponse;
import org.eclipse.webdav.http.client.IAuthenticator;
import org.eclipse.webdav.http.client.Request;
import org.eclipse.webdav.internal.kernel.utils.Assert;

/**
 * The <code>AuthorizationAuthority</code> authorizes client
 * <code>Request</codes>s for communication with HTTP servers. Subclasses
 * provide the necessary behavior for different authentication schemes.
 */
public class AuthorizationAuthority {

	public static String[] authenticationSchemes = {"Digest", "Basic"}; //$NON-NLS-1$ //$NON-NLS-2$
	protected IAuthenticator authenticatorStore = null;

	/**
	 * Creates a new authenticator that stores its authentication information
	 * in the given authenticator store.
	 *
	 * @param authenticatorStore a store that holds authentication
	 * information
	 */
	public AuthorizationAuthority(IAuthenticator authenticatorStore) {
		Assert.isNotNull(authenticatorStore);
		this.authenticatorStore = authenticatorStore;
	}

	/**
	 * Authorizes the given request by setting its authorization credentials
	 * in the given context. If the given response is not <code>null</code>,
	 * it is assumed to contain an authenticate challenge that is used to
	 * derive the authorization credentials. Returns true if the authorization
	 * succeeds, and false otherwise.
	 *
	 * @param request the request to authorize
	 * @param response the response containing the authenticate challenge
	 * @param context the context where the authorization credentials are set
	 * @param proxyServerUrl the URL of the proxy server, or <code>null</code>
	 * if there is no proxy server
	 * @param isProxyAuthorization a boolean indicating whether the
	 * authorization credentials should be computed for the proxy server or
	 * the origin server
	 * @return a boolean indicating whether the request was successfully
	 * authorized
	 */
	public boolean authorize(Request request, IResponse response, IContext context, URL proxyServerUrl, boolean isProxyAuthorization) {
		Assert.isNotNull(request);
		Assert.isNotNull(context);

		URL serverUrl = null;
		URL protectionSpaceUrl = null;

		if (isProxyAuthorization) {
			if (proxyServerUrl == null) {
				return false;
			}
			serverUrl = proxyServerUrl;
			protectionSpaceUrl = proxyServerUrl;
		} else {
			URL resourceUrl = request.getResourceUrl();
			try {
				serverUrl = new URL(resourceUrl.getProtocol(), resourceUrl.getHost(), resourceUrl.getPort(), "/"); //$NON-NLS-1$
			} catch (MalformedURLException e) {
				return false;
			}
			protectionSpaceUrl = resourceUrl;
		}

		if (response != null) {
			String challengeString = null;
			if (isProxyAuthorization) {
				challengeString = response.getContext().getProxyAuthenticate();
			} else {
				challengeString = response.getContext().getWWWAuthenticate();
			}

			if (challengeString == null) {
				return false;
			}

			AuthenticateChallenge challenge = null;
			try {
				challenge = new AuthenticateChallenge(challengeString);
			} catch (ParserException e) {
				return false;
			}

			String authScheme = challenge.getAuthScheme();
			String realm = challenge.getRealm();

			AuthorizationAuthority authority = getAuthorizationAuthority(authScheme);
			if (authority == null) {
				return false;
			}

			Map oldInfo = authenticatorStore.getAuthenticationInfo(serverUrl, realm, authScheme);
			Map info = authority.getAuthenticationInfo(challenge, oldInfo, serverUrl, protectionSpaceUrl);
			if (info == null) {
				return false;
			}

			authenticatorStore.addAuthenticationInfo(serverUrl, realm, authScheme, info);
			authenticatorStore.addProtectionSpace(protectionSpaceUrl, realm);
		}

		String realm = authenticatorStore.getProtectionSpace(protectionSpaceUrl);
		if (realm == null) {
			return false;
		}

		Map info = null;
		String authScheme = null;

		for (int i = 0; i < authenticationSchemes.length; ++i) {
			authScheme = authenticationSchemes[i];
			info = authenticatorStore.getAuthenticationInfo(serverUrl, realm, authScheme);
			if (info != null) {
				break;
			}
		}

		if (info == null) {
			return false;
		}

		AuthorizationAuthority authority = getAuthorizationAuthority(authScheme);
		if (authority == null) {
			return false;
		}

		String authorization = authority.getAuthorization(request, info, serverUrl, protectionSpaceUrl, proxyServerUrl);
		if (authorization == null) {
			return false;
		}

		if (isProxyAuthorization) {
			if (authorization.equals(context.getProxyAuthorization()))
				return false; // we already had that auth so it must've failed
			context.setProxyAuthorization(authorization);
		} else {
			if (authorization.equals(context.getAuthorization()))
				return false; // we already had that auth so it must've failed
			context.setAuthorization(authorization);
		}

		return true;
	}

	/**
	 * Confirms whether the given response is valid by proving the server
	 * knows the client's authentication secret (password). Moreover, the
	 * server may wish to communicate some authentication information in the
	 * response for the purposes of authorizing future request.
	 *
	 * @param request the request that has already been sent
	 * @param response the response back from the server to be verified
	 * @param proxyServerUrl the URL of the proxy server, or <code>null</code>
	 * if there is none
	 * @returns a boolean indicating whether the given response is valid
	 */
	public boolean confirm(Request request, IResponse response, URL proxyServerUrl) {
		Assert.isNotNull(request);
		Assert.isNotNull(response);

		URL resourceUrl = request.getResourceUrl();

		URL serverUrl = null;
		try {
			serverUrl = new URL(resourceUrl.getProtocol(), resourceUrl.getHost(), resourceUrl.getPort(), "/"); //$NON-NLS-1$
		} catch (MalformedURLException e) {
			return false;
		}

		String realm = authenticatorStore.getProtectionSpace(resourceUrl);
		if (realm == null) {
			return false;
		}

		Map info = null;
		String authScheme = null;

		for (int i = 0; i < authenticationSchemes.length; ++i) {
			authScheme = authenticationSchemes[i];
			info = authenticatorStore.getAuthenticationInfo(serverUrl, realm, authScheme);
			if (info != null) {
				break;
			}
		}

		if (info == null) {
			return false;
		}

		AuthorizationAuthority authority = getAuthorizationAuthority(authScheme);
		if (authority == null) {
			return false;
		}

		return authority.confirmResponse(request, response, proxyServerUrl);
	}

	/**
	 * Confirms whether the given response is valid by proving the server
	 * knows the client's authentication secret (password). Moreover, the
	 * server may wish to communicate some authentication information in the
	 * response for the purposes of authorizing future request.
	 * <p>This method should be overridden by schema specific authenticators.
	 *
	 * @param request the request that has already been sent
	 * @param response the response back from the server to be verified
	 * @param proxyServerUrl the URL of the proxy server, or <code>null</code>
	 * if there is none
	 * @returns a boolean indicating whether the given response is valid
	 */
	protected boolean confirmResponse(Request request, IResponse response, URL proxyServerUrl) {
		Assert.isNotNull(request);
		Assert.isNotNull(response);
		return false;
	}

	/**
	 * Returns the new authentication information gleaned from the given
	 * authenticate challenge and the given old authentication information.
	 * The old authentication information may be <code>null</code>.
	 * The authentication information usually contains directives such as
	 * usernames and passwords.
	 * <p>This method should be overridden by schema specific authenticators.
	 *
	 * @param challenge the authenticate challenge from the server
	 * @param oldInfo the old authentication information
	 * @param serverUrl the URL of the server
	 * @param protectionSpaceUrl the URL of the protected resource
	 * @return new authentication information
	 */
	protected Map getAuthenticationInfo(AuthenticateChallenge challenge, Map oldInfo, URL serverUrl, URL protectionSpaceUrl) {
		Assert.isNotNull(challenge);
		Assert.isNotNull(serverUrl);
		Assert.isNotNull(protectionSpaceUrl);
		return null;
	}

	/**
	 * Returns the authorization credentials for the given request. The
	 * authorization credentials are derived from the given authentication
	 * info. The authentication info may contain directives such as usernames
	 * and passwords.
	 * <p>This method should be overridden by schema specific authenticators.
	 *
	 * @param request the request being authorized
	 * @param info the authentication information used to derive the
	 * authorization credentials
	 * @param serverUrl the URL of the server
	 * @param protectionSpaceUrl the URL of the protected resource
	 * @param proxyServerUrl the URL of the proxy server, or <code>null</code>
	 * if there is none
	 * @return the authorization credentials for the given request
	 */
	protected String getAuthorization(Request request, Map info, URL serverUrl, URL protectionSpaceUrl, URL proxyServerUrl) {
		Assert.isNotNull(request);
		Assert.isNotNull(info);
		Assert.isNotNull(serverUrl);
		Assert.isNotNull(protectionSpaceUrl);
		return null;
	}

	/**
	 * Returns an authorization authority for the given authentication
	 * scheme, or <code>null</code> if there is no such authority.
	 *
	 * @param scheme an authentication scheme, for example: "Basic"
	 * @return an authorization authority for the given authentication scheme
	 */
	private AuthorizationAuthority getAuthorizationAuthority(String scheme) {
		try {
			scheme = Character.toUpperCase(scheme.charAt(0)) + scheme.substring(1).toLowerCase();
			String packageName = "org.eclipse.webdav.internal.authentication"; //$NON-NLS-1$
			String className = scheme + "Authority"; //$NON-NLS-1$
			Class clazz = Class.forName(packageName + "." + className); //$NON-NLS-1$
			Constructor constructor = clazz.getConstructor(new Class[] {IAuthenticator.class});
			return (AuthorizationAuthority) constructor.newInstance(new Object[] {authenticatorStore});
		} catch (ClassCastException e) {
			// ignore or log?
		} catch (Exception e) {
			// ignore or log?
		}
		return null;
	}

	/**
	 * Computes the MD5 hash value of the given <code>String</code> and
	 * returns the result as a HEX <code>String</code>.
	 *
	 * @param s
	 * @return a HEX <code>String</code> containing the MD5 hash value of the
	 * given <code>String</code>
	 * @exception NoSuchAlgorithmException
	 * @exception UnsupportedEncodingException
	 */
	protected String md5(String s) throws NoSuchAlgorithmException, UnsupportedEncodingException {
		MessageDigest md5 = MessageDigest.getInstance("MD5"); //$NON-NLS-1$
		byte[] hash = md5.digest(s.getBytes("UTF8")); //$NON-NLS-1$
		return HexConverter.toHex(hash);
	}

	/**
	 * Computes the MD5 hash value of the body of the given request and
	 * returns the result as a HEX <code>String</code>.
	 *
	 * @param request
	 * @return a HEX <code>String</code> containing the MD5 hash value of the
	 * body of the given request
	 * @exception NoSuchAlgorithmException
	 * @exception IOException
	 */
	protected String md5(Request request) throws NoSuchAlgorithmException, IOException {
		DigestOutputStream dos = new DigestOutputStream("MD5"); //$NON-NLS-1$
		request.write(dos);
		String result = HexConverter.toHex(dos.digest());
		dos.close();
		return result;
	}

	/**
	 * Returns the given <code>String</code> with its quotes removed.
	 *
	 * @param s a <code>String</code>
	 * @return the given <code>String</code> with its quotes removed
	 */
	protected String unquote(String s) {
		if (s.charAt(0) == '"' && s.charAt(s.length() - 1) == '"') //$NON-NLS-1$ //$NON-NLS-2$
			return s.substring(1, s.length() - 1);
		return s;
	}
}
