package hudson.security;

import hudson.model.Hudson;
import hudson.util.Scrambler;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.context.SecurityContextHolder;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;

/**
 * Implements the dual authentcation mechanism.
 *
 * <p>
 * Hudson supports both the HTTP basic authentication and the form-based authentication.
 * The former is for scripted clients, and the latter is for humans. Unfortunately,
 * becase the servlet spec does not allow us to programatically authenticate users,
 * we need to rely on some hack to make it work, and this is the class that implements
 * that hack.
 *
 * <p>
 * When an HTTP request arrives with an HTTP basic auth header, this filter detects
 * that and emulate an invocation of <tt>/j_security_check</tt>
 * (see <a href="http://mail-archives.apache.org/mod_mbox/tomcat-users/200105.mbox/%3C9005C0C9C85BD31181B20060085DAC8B10C8EF@tuvi.andmevara.ee%3E">this page</a> for the original technique.)
 *
 * <p>
 * This causes the container to perform authentication, but there's no way
 * to find out whether the user has been successfully authenticated or not.
 * So to find this out, we then redirect the user to
 * {@link Hudson#doSecured(StaplerRequest, StaplerResponse) <tt>/secured/...</tt> page}.
 *
 * <p>
 * The handler of the above URL checks if the user is authenticated,
 * and if not report an HTTP error code. Otherwise the user is
 * redirected back to the original URL, where the request is served.
 *
 * <p>
 * So all in all, the redirection works like <tt>/abc/def</tt> -> <tt>/secured/abc/def</tt>
 * -> <tt>/abc/def</tt>.
 *
 * <h2>Notes</h2>
 * <ul>
 * <li>
 * The technique of getting a request dispatcher for <tt>/j_security_check</tt> may not
 * work for all containers, but so far that seems like the only way to make this work.
 * <li>
 * This A->B->A redirect is a cyclic redirection, so we need to watch out for clients
 * that detect this as an error.
 * </ul> 
 *
 * @author Kohsuke Kawaguchi
 */
public class BasicAuthenticationFilter implements Filter {
    private ServletContext servletContext;

    public void init(FilterConfig filterConfig) throws ServletException {
        servletContext = filterConfig.getServletContext();
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse rsp = (HttpServletResponse) response;
        String authorization = req.getHeader("Authorization");

        String path = req.getServletPath();
        if(authorization==null || req.getUserPrincipal() !=null || path.startsWith("/secured/")
        || !Hudson.getInstance().isUseSecurity()) {
            // normal requests, or security not enabled
            if(req.getUserPrincipal()!=null) {
                // before we route this request, integrate the container authentication
                // to Acegi. For anonymous users that doesn't have user principal,
                // AnonymousProcessingFilter that follows this should create
                // an Authentication object.
                SecurityContextHolder.getContext().setAuthentication(new ContainerAuthentication(req));
            }
            try {
                chain.doFilter(request,response);
            } finally {
                SecurityContextHolder.clearContext();
            }
            return;
        }

        // authenticate the user
        String username = null;
        String password = null;
        String uidpassword = Scrambler.descramble(authorization.substring(6));
        int idx = uidpassword.indexOf(':');
        if (idx >= 0) {
            username = uidpassword.substring(0, idx);
            password = uidpassword.substring(idx+1);
        }

        if(username==null) {
            rsp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            rsp.setHeader("WWW-Authenticate","Basic realm=\"Hudson administrator\"");
            return;
        }

        path = req.getContextPath()+"/secured"+path;
        String q = req.getQueryString();
        if(q!=null)
            path += '?'+q;

        // prepare a redirect
        rsp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
        rsp.setHeader("Location",path);

        // ... but first let the container authenticate this request
        RequestDispatcher d = servletContext.getRequestDispatcher("/j_security_check?j_username="+
            URLEncoder.encode(username,"UTF-8")+"&j_password="+URLEncoder.encode(password,"UTF-8"));
        d.include(req,rsp);
    }

    //public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    //    HttpServletRequest req = (HttpServletRequest) request;
    //    String authorization = req.getHeader("Authorization");
    //
    //    String path = req.getServletPath();
    //    if(authorization==null || req.getUserPrincipal()!=null || path.startsWith("/secured/")) {
    //        chain.doFilter(request,response);
    //    } else {
    //        if(req.getQueryString()!=null)
    //            path += req.getQueryString();
    //        ((HttpServletResponse)response).sendRedirect(req.getContextPath()+"/secured"+path);
    //    }
    //}

    public void destroy() {
    }

    private static final GrantedAuthorityImpl[] EMPTY_AUTHORITIES = {new GrantedAuthorityImpl("")};
}
