/**********************************************************************************
 * $URL: https://source.sakaiproject.org/svn/user/tags/sakai_2-3-1/user-util/util/src/java/org/sakaiproject/util/BasicAuth.java $
 * $Id: BasicAuth.java 14694 2006-09-15 12:41:13Z ian@caret.cam.ac.uk $
 ***********************************************************************************
 *
 * Copyright (c) 2006 The Sakai Foundation.
 * 
 * Licensed under the Educational Community License, Version 1.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.opensource.org/licenses/ecl1.php
 * 
 * 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.sakaiproject.util;

import java.io.IOException;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.sakaiproject.user.api.Authentication;
import org.sakaiproject.user.api.AuthenticationException;
import org.sakaiproject.user.api.Evidence;
import org.sakaiproject.user.cover.AuthenticationManager;
import org.sakaiproject.component.cover.ServerConfigurationService;
import org.sakaiproject.event.cover.UsageSessionService;

import sun.misc.BASE64Decoder;

/**
 * This is implemented in a filter, since most httpclients (ie non browser
 * clients) dont know what to do with a redirect.
 * 
 * There are 2 mechanisms for selecting basic authentication. 1. The client is
 * not a browser as reported by the BasicAuthFilter.isBrowser method. 2. The
 * user requested basic auth in the URL and the
 * BasicAuthFilter.requestedBasicAuth confirms this.
 * 
 * in sakai.properties if allowbasicauth.login = true, then this feature is
 * enabled in BasicAuthFilter, the determination of non browser clients is
 * driven by matching user agent headers against a sequence of regex patterns.
 * These are defined in BasicAuthFilter with the form if the pattern matches a
 * browser 1pattern or if it does not match 0pattern
 * 
 * Addtional patterns may be added to sakai.properties as a multiple string
 * property against login.browser.user.agent
 * 
 * The list is matched in order, the first match found being definitive. If no
 * match is found, then the client is assumed to be a browser.
 * 
 * e.g. if itunes was not listed as a client, either:
 * 
 * Add
 * 
 *    login.browser.user.agent.count=1
 *    login.browser.user.agent.1=0itunes.*
 * 
 * to sakai.properties, or
 * 
 * Add __auth=basic to the end of the url, e.g.
 * 
 *    http://localhost:8080/access/wiki/123-1231-32123-132123/-.20.rss?someparam=someval&__auth=basic
 * 
 * This string is available in BasicAuthFilter.BASIC_AUTH_LOGIN_REQUEST
 * 
 */

public class BasicAuth {

	/**
	 * The query parameter and value that indicates the request will want basic
	 * auth if required
	 */
	public static final String BASIC_AUTH_LOGIN_REQUEST = "__auth=basic";

	public static Pattern[] patterns = null;

	private static String[] match;

	/**
	 * The default set of UserAgent patterns to force basic auth with
	 */
	private static String[] matchPatterns = { 
			"0.*Thunderbird.*", "1Mozilla.*", 
			"0i[tT]unes.*", "0Jakarta Commons-HttpClient.*", 
			"0.*Googlebot/2.1.*", "0[gG]oogle[bB]ot.*", "0curl.*"
	};

	/**
	 * Initialise the patterns, since some of the spring stuf may not be up when
	 * the bean is created, this is here to make certain that init is performed
	 * when spring is ready
	 * 
	 */
	public void init() {
		ArrayList pat = new ArrayList();
		ArrayList mat = new ArrayList();
		String[] morepatterns = null;
		try {
			morepatterns = ServerConfigurationService.getStrings("login.browser.user.agent");
		} catch (Exception ex) {

		}
		if (morepatterns != null) {
			for (int i = 0; i < morepatterns.length; i++) {
				String line = morepatterns[i];
				String check = line.substring(0, 1);
				mat.add(check);
				line = line.substring(1);
				pat.add(Pattern.compile(line));
			}
		}
		for (int i = 0; i < matchPatterns.length; i++) {
			String line = matchPatterns[i];
			String check = line.substring(0, 1);
			mat.add(check);
			line = line.substring(1);
			pat.add(Pattern.compile(line));
		}

		patterns = new Pattern[pat.size()];
		patterns = (Pattern[]) pat.toArray(patterns);
		match = new String[mat.size()];
		match = (String[]) mat.toArray(match);
	}

	/**
	 * If this method returns true, the user agent is a browser
	 * 
	 * @param header
	 * @return
	 */
	protected boolean isBrowser(String userAgentHeader) {
		if (patterns != null) {
			for (int i = 0; i < patterns.length; i++) {
				Matcher m = patterns[i].matcher(userAgentHeader);
				if (m.matches()) {
					return "1".equals(match[i]);
				}
			}
			return true;
		}
		return true;
	}

	/**
	 * This method looks at the returnUrl and if there is a request parameter in
	 * the URL requesting basic authentication, this method returns true
	 * 
	 * @param returnUrl
	 * @return
	 */
	protected boolean requestedBasicAuth(HttpServletRequest request) {
		String queryString = request.getQueryString();
		if (queryString == null) {
			return false;
		} else {
			boolean ret = (queryString.indexOf(BASIC_AUTH_LOGIN_REQUEST) != -1);
			return ret;
		}
	}

	/**
	 * Should a basic auth be used
	 * @param req
	 * @return
	 */
	protected boolean doBasicAuth(HttpServletRequest req) {
		boolean allowBasicAuth = ServerConfigurationService.getBoolean(
				"allow.basic.auth.login", false);

		if (allowBasicAuth) {
			if (requestedBasicAuth(req)
					|| !isBrowser(req.getHeader("User-Agent"))) {
				allowBasicAuth = true;
			} else {
				allowBasicAuth = false;
			}
		}

		return allowBasicAuth;
	}

	/**
	 * Perform a login based on the headers, if they are not present or it fails do nothing
	 * @param req
	 * @return
	 * @throws IOException
	 */
	public boolean doLogin(HttpServletRequest req) throws IOException {

		if (doBasicAuth(req)) {
		
			String auth;
			auth = req.getHeader("Authorization");
			
			Evidence e = null;
			try {
				if (auth != null) {
					auth = auth.trim();
					if (auth.startsWith("Basic ")) {
						auth = auth.substring(6).trim();
						BASE64Decoder denc = new BASE64Decoder();
						auth = new String(denc.decodeBuffer(auth));
						int colon = auth.indexOf(":");
						if (colon != -1) {
							String eid = auth.substring(0, colon);
							String pw = auth.substring(colon + 1);
							if (eid.length() > 0 && pw.length() > 0) {
								e = new IdPwEvidence(eid, pw);
							}
						}
					}
				}
			} catch (Throwable ex) {
				ex.printStackTrace();
			}

			// authenticate
			try {
				if (e == null) {
					throw new AuthenticationException("missing required fields");
				}

				Authentication a = AuthenticationManager.authenticate(e);

				// login the user
				if (UsageSessionService.login(a, req)) {
					return true;
				} else {
					return false;
				}
			} catch (AuthenticationException ex) {
				// ex.printStackTrace();
				return false;
			}
		}
		return true;
	}

	/**
	 * Emit the basic auth headers and a 401
	 * @param req
	 * @param res
	 * @return
	 * @throws IOException
	 */
	public boolean doAuth(HttpServletRequest req, HttpServletResponse res)
			throws IOException {
		if (doBasicAuth(req)) {
			String uiService = ServerConfigurationService.getString("ui.service");
			res.addHeader("WWW-Authenticate", "Basic realm=\"" + uiService + "\"");
			res.sendError(HttpServletResponse.SC_UNAUTHORIZED,
					"Authorization Required");
			return true;
		}
		return false;

	}

}
