com.adito.vfs.webdav.DAVTransaction.java Source code

Java tutorial

Introduction

Here is the source code for com.adito.vfs.webdav.DAVTransaction.java

Source

/* ========================================================================== *
 * Copyright (C) 2004-2005 Pier Fumagalli <http://www.betaversion.org/~pier/> *
 *                            All rights reserved.                            *
 * ========================================================================== *
 *                                                                            *
 * 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 com.adito.vfs.webdav;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.maverick.crypto.encoders.Base64;
import com.adito.boot.HttpConstants;
import com.adito.boot.SystemProperties;
import com.adito.boot.Util;
import com.adito.core.ServletRequestAdapter;
import com.adito.core.ServletResponseAdapter;
import com.adito.core.UserDatabaseManager;
import com.adito.policyframework.LaunchSession;
import com.adito.policyframework.LaunchSessionFactory;
import com.adito.properties.Property;
import com.adito.properties.impl.systemconfig.SystemConfigKey;
import com.adito.security.AccountLockedException;
import com.adito.security.AuthenticationModuleManager;
import com.adito.security.AuthenticationScheme;
import com.adito.security.Constants;
import com.adito.security.DefaultAuthenticationScheme;
import com.adito.security.InvalidLoginCredentialsException;
import com.adito.security.LogonController;
import com.adito.security.LogonControllerFactory;
import com.adito.security.PasswordCredentials;
import com.adito.security.SessionInfo;
import com.adito.security.SystemDatabaseFactory;
import com.adito.security.User;
import com.adito.security.UserNotFoundException;
import com.adito.security.WebDAVAuthenticationModule;
import com.adito.security.actions.LogonAction;
import com.adito.vfs.VFSResource;

/**
 * <p>
 * A simple wrapper isolating the Java Servlet API from this <a
 * href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> implementation.
 * </p>
 * 
 * @author <a href="http://www.betaversion.org/~pier/">Pier Fumagalli</a>
 */
public class DAVTransaction {

    private static Log log = LogFactory.getLog(DAVTransaction.class);

    public final static String ATTR_EXPECTING_REALM_AUTHENTICATION = "expectingRealmAuth";
    public final static String ATTR_DEREGISTER_SUB_AUTHS = "deregisterSubAuths";
    public final static String ATTR_AUTH_ATTEMPTS = "authAttempts";

    /**
     * <p>
     * The identifyication of the <code>infinity</code> value in the
     * <code>Depth</code> header.
     * </p>
     */
    public static final int INFINITY = Integer.MAX_VALUE;

    /**
     * <p>
     * The nested {@link HttpServletRequest}.
     * </p>
     */
    private HttpServletRequest req = null;
    /**
     * <p>
     * The nested {@link HttpServletResponse}.
     * </p>
     */
    private HttpServletResponse res = null;
    /**
     * <p>
     * The {@link URI} associated with the base of the repository.
     * </p>
     */
    private URI base = null;
    /**
     * <p>
     * The path for this transaction. contains user etc.
     * </p>
     */
    private String path;
    /**
     * <p>
     * A cache of resources for the life of this transaction
     */
    private Map resourceCache;

    /**
     * The session
     */
    private SessionInfo sessionInfo;

    /**
     * <p>
     * The current credentials object being used for authentication to the
     * resources
     */
    // private DAVCredentials currentCredentials;
    /* ====================================================================== */
    /* Constructors */
    /* ====================================================================== */

    /**
     * <p>
     * Create a new {@link DAVTransaction} instance.
     * </p>
     * 
     * @throws URISyntaxException
     */
    public DAVTransaction(ServletRequest request, ServletResponse response)
            // throws ServletException, DAVAuthenticationRequiredException {
            throws ServletException, URISyntaxException {
        if (request == null)
            throw new NullPointerException("Null request");
        if (response == null)
            throw new NullPointerException("Null response");
        this.req = (HttpServletRequest) request;
        this.res = (HttpServletResponse) response;
        this.resourceCache = new HashMap();

        /*
         * First see if the launch ID has been provided as a parameter. If it
         * has we can just get the resource session directly. This should happen
         * for web folders that are first launched from an active user session
         * or from a file download from the network place HTML file browser.
         */

        String launchId = request.getParameter(LaunchSession.LAUNCH_ID);

        if (launchId != null) {
            LaunchSession launchSession = LaunchSessionFactory.getInstance().getLaunchSession(launchId);
            if (launchSession != null) {
                sessionInfo = launchSession.getSession();
                LogonControllerFactory.getInstance().addCookies(
                        new ServletRequestAdapter((HttpServletRequest) request),
                        new ServletResponseAdapter((HttpServletResponse) response),
                        launchSession.getSession().getLogonTicket(), launchSession.getSession());
                sessionInfo.access();
            } else if (log.isDebugEnabled())
                log.debug("Could not locate session using ticket");
        }
        sessionInfo = LogonControllerFactory.getInstance().getSessionInfo(req);
        configureFromRequest();
    }

    public void putCachedResource(VFSResource resource) {
        String key = DAVUtilities.concatenatePaths(resource.getMount().getMountString(),
                resource.getRelativePath());
        resourceCache.put(key, resource);
    }

    public VFSResource getCachedResource(String fullPath) {
        return (VFSResource) resourceCache.get(fullPath);
    }

    public boolean attemptToAuthorize() throws IOException, UserNotFoundException, Exception {
        if (!verifyIp()) {
            return false;
        }

        String expectingRealm = (String) req.getSession().getAttribute(ATTR_EXPECTING_REALM_AUTHENTICATION);

        /* Attempt authentication if cookieless clients are allowed to connect or
         * if we are definitely expecting some realm to be authenticated 
         */
        if (Property.getPropertyBoolean(new SystemConfigKey("security.allowUntrackedWebDAVSessions"))
                || expectingRealm != null) {
            for (Enumeration e = req.getHeaders(HttpConstants.HDR_AUTHORIZATION); e.hasMoreElements();) {
                String val = (String) e.nextElement();
                authorize(expectingRealm, val);
            }
        }

        return true;

    }

    boolean verifyIp() {
        try {
            if (SystemDatabaseFactory.getInstance().verifyIPAddress(req.getRemoteAddr())) {
                return true;
            }
        } catch (Exception e) {
            log.error("Failed to verify IP address. Considering unauthorized.", e);
        }
        if (log.isDebugEnabled())
            log.debug(req.getRemoteHost() + " is not authorized");
        res.setStatus(HttpServletResponse.SC_FORBIDDEN);
        return false;
    }

    /**
     * Authorise the provided realm using the data the
     * {@link HttpConstants#HDR_AUTHORIZATION} header.
     * 
     * @param expectingRealm realm authenticating against
     * @param authorization authorisation data from
     *        {@link HttpConstants#HDR_AUTHORIZATION} header.
     * 
     * @throws IOException on any serious error
     * @throws UserNotFoundException if user cannot be found
     * @throws DAVAuthenticationRequiredException if authorisation data is wrong
     */
    public void authorize(String expectingRealm, String authorization)
            throws IOException, UserNotFoundException, DAVAuthenticationRequiredException {
        int idx = authorization.indexOf(' ');
        if (idx == -1 || idx == authorization.length() - 1) {
            throw new DAVAuthenticationRequiredException(expectingRealm);
        }
        // Authenticate the user
        String method = authorization.substring(0, idx);
        if (!method.equalsIgnoreCase("basic")) {
            throw new DAVAuthenticationRequiredException(expectingRealm);
        }

        // Extract the credentials - should be ticket:tunnel
        String encoded = authorization.substring(idx + 1);
        String credentials = new String(Base64.decode(encoded));

        idx = credentials.indexOf(':');
        if (idx == 0 || idx == -1) {
            throw new DAVAuthenticationRequiredException(expectingRealm);
        }

        // Get the user credentials
        String username = credentials.substring(0, idx);

        if (expectingRealm == null) {

            /*
             * If we wern't expecting authentication, but we got it anyway, the
             * client probably doesn't support cookies.
             */
            AuthenticationScheme authScheme = (DefaultAuthenticationScheme) req.getSession()
                    .getAttribute(Constants.AUTH_SESSION);
            if (authScheme != null) {
                throw new IOException(
                        "Not expecting a realm, yet an authentication session is available. This is unexpected!");
            }

            doAuth(expectingRealm, username, DAVServlet.configureAuthenticationScheme(req, res));

            /*
             * We now can get the sessionInfo object for this session and make
             * it temporary this will ensure it is destroyed once the request is
             * complete.
             */
            sessionInfo = LogonControllerFactory.getInstance().getSessionInfo(req);
            sessionInfo.setTemporary(true);

        } else if (expectingRealm.equals(WebDAVAuthenticationModule.DEFAULT_REALM)) {
            AuthenticationScheme authScheme = (DefaultAuthenticationScheme) req.getSession()
                    .getAttribute(Constants.AUTH_SESSION);
            if (authScheme == null) {
                throw new IOException("No authentication scheme initialised.");
            }

            doAuth(expectingRealm, username, authScheme);

            /*
             * We now can get the sessionInfo object for this session and make
             * it temporary this will ensure it is destroyed once the request is
             * complete.
             */
            if (sessionInfo == null) {
                sessionInfo = LogonControllerFactory.getInstance().getSessionInfo(req);
            }
        } else {

            if (log.isDebugEnabled())
                log.debug("Logging " + username + " [" + req.getRemoteHost() + "] onto realm " + expectingRealm
                        + " using Basic authentication for session " + req.getSession().getId());
            // subAuths.put(expectingRealm, new AuthPair(username,
            // password.toCharArray()));
        }
        req.getSession().removeAttribute(ATTR_EXPECTING_REALM_AUTHENTICATION);
        req.getSession().removeAttribute(Constants.AUTH_SENT);

        /* Logging method */
        if (log.isDebugEnabled())
            log.debug(req.getMethod() + ' ' + req.getRequestURI() + ' ' + req.getProtocol());
    }

    private void doAuth(String expectingRealm, String username, AuthenticationScheme authScheme)
            throws DAVAuthenticationRequiredException, IOException {
        if (authScheme == null) {
            throw new DAVAuthenticationRequiredException("No valid authentication scheme.");
        }
        // Find user
        try {
            User user = UserDatabaseManager.getInstance().getDefaultUserDatabase().getAccount(username);
            authScheme.setUser(user);
            LogonAction.authenticate(authScheme, req);
            LogonAction.finishAuthentication(authScheme, req, res);
        } catch (InvalidLoginCredentialsException ilce) {
            // Incorrect details, try again
            throw new DAVAuthenticationRequiredException(expectingRealm);
        } catch (Exception e) {
            IOException ioe = new IOException("Failed to authenticate using scheme.");
            ioe.initCause(e);
            throw ioe;
        }
    }

    public HttpServletResponse getResponse() {
        return (HttpServletResponse) res;
    }

    /* ====================================================================== */
    /* Request methods */
    /* ====================================================================== */

    /**
     * <p>
     * Get the request object.
     * </p>
     */
    public HttpServletRequest getRequest() {
        return req;
    }

    /**
     * <p>
     * Return the path originally requested by the client.
     * </p>
     */
    public String getMethod() {
        return this.req.getMethod();
    }

    /**
     * <p>
     * Return the path for this transaction. This will be the path as the client
     * sees it less the first element
     * 
     * @return path
     */
    public String getPath() {
        return path;
        // String path = this.req.getPathInfo();
        // if (path == null) return "";
        // if ((path.length() > 0) && (path.charAt(0) == '/')) {
        // return path.substring(1);
        // } else {
        // return path;
        // }
    }

    public boolean isRequiredRootRedirect() {
        return false;
    }

    /**
     * <p>
     * Return the path originally requested by the client encoded.
     * </p>
     */
    public String getPathEncoded() {
        return DAVUtilities.encodePath(getPath());
    }

    /**
     * <p>
     * Return the depth requested by the client for this transaction.
     * </p>
     */
    public int getDepth() {
        String depth = req.getHeader("Depth");
        if (depth == null)
            return INFINITY;
        if ("infinity".equals(depth))
            return INFINITY;
        try {
            return Integer.parseInt(depth);
        } catch (NumberFormatException exception) {
            throw new DAVException(412, "Unable to parse depth", exception);
        }
    }

    /**
     * <p>
     * Return a {@link URI}
     */
    public URI getDestination() {
        String destination = this.req.getHeader("Destination");
        if (destination != null)
            try {
                return this.base.relativize(new URI(destination.replaceAll(" ", "%20")));
            } catch (URISyntaxException exception) {
                throw new DAVException(412, "Can't parse destination", exception);
            }
        return null;
    }

    /**
     * <p>
     * Return the overwrite flag requested by the client for this transaction.
     * </p>
     */
    public boolean getOverwrite() {
        String overwrite = req.getHeader("Overwrite");
        if (overwrite == null)
            return true;
        if ("T".equals(overwrite))
            return true;
        if ("F".equals(overwrite))
            return false;
        throw new DAVException(412, "Unable to parse overwrite flag");
    }

    /**
     * <p>
     * Check if the client requested a date-based conditional operation.
     * </p>
     */
    public Date getIfModifiedSince() {
        String name = "If-Modified-Since";
        if (this.req.getHeader(name) == null)
            return null;
        return new Date(this.req.getDateHeader(name));
    }

    /* ====================================================================== */
    /* Response methods */
    /* ====================================================================== */

    /**
     * <p>
     * Set the HTTP status code of the response.
     * </p>
     */
    public void setStatus(int status) {
        this.res.setStatus(status);
    }

    /**
     * <p>
     * Set the HTTP <code>Content-Type</code> header.
     * </p>
     */
    public void setContentType(String type) {
        this.res.setContentType(type);
    }

    /**
     * <p>
     * Set an HTTP header in the response.
     * </p>
     */
    public void setHeader(String name, String value) {
        this.res.setHeader(name, value);
    }

    /**
     * <p>
     * Set an HTTP header in the response.
     * </p>
     * 
     * @param name name
     * @param value value
     */
    public void setDateHeader(String name, int value) {
        this.res.setDateHeader(name, value);
    }

    /* ====================================================================== */
    /* I/O methods */
    /* ====================================================================== */

    /**
     * <p>
     * Read from the body of the original request.
     * </p>
     */
    public InputStream getInputStream() throws IOException {
        /* We don't support ranges */
        if (req.getHeader("Content-Range") != null)
            throw new DAVException(501, "Content-Range not supported");

        if (this.req.getContentLength() >= 0)
            this.req.getInputStream();
        String len = this.req.getHeader("Content-Length");
        if (len != null)
            try {
                if (Long.parseLong(len) > 0)
                    return this.req.getInputStream();
            } catch (NumberFormatException exception) {
                // Unparseable content length header...
            }

        // Do not throw an exception, this could be null without an error
        // condition
        return null;
    }

    /**
     * <p>
     * Write the body of the response.
     * </p>
     */
    public OutputStream getOutputStream() throws IOException {
        if (SystemProperties.get("adito.webdav.debug", "false").equals("true"))
            return new TempOutputStream(this.res.getOutputStream());
        else
            return this.res.getOutputStream();
    }

    class TempOutputStream extends OutputStream {

        OutputStream out;
        StringBuffer wbBuf;

        TempOutputStream(OutputStream out) {
            this.out = out;
            wbBuf = new StringBuffer();
        }

        public void write(byte[] buf, int off, int len) throws IOException {
            wbBuf.append(new String(buf, off, len));
            out.write(buf, off, len);
        }

        public void write(int b) throws IOException {
            wbBuf.append((byte) b);
            out.write((byte) b);
        }

        public void flush() throws IOException {
            log.info(wbBuf.toString());
            wbBuf.setLength(0);
            out.flush();
        }

    }

    /**
     * <p>
     * Write the body of the response.
     * </p>
     */
    public PrintWriter write(String encoding) throws IOException {
        return new PrintWriter(new OutputStreamWriter(this.getOutputStream(), encoding));
    }

    /* ====================================================================== */
    /* Lookup methods */
    /* ====================================================================== */

    /**
     * <p>
     * Look up the final URI of a {@link VFSResource} as visible from the HTTP
     * client requesting this transaction.
     * </p>
     */
    public URI lookup(VFSResource resource) {
        URI uri = resource.getRelativeURI();
        URI resolved = null;
        if (uri == null || uri.toString().equals("")) {
            resolved = this.base;
        } else {
            resolved = this.base.resolve(uri).normalize();
            ;
        }
        return resolved;
    }

    public PasswordCredentials getCredentials() {
        String authorization = req.getHeader("Authorization");

        if (authorization == null) {
            return null;
        }

        int idx = authorization.indexOf(' ');

        if (idx == -1 || idx == authorization.length() - 1) {
            return null;
        }

        // Authenticate the user
        String method = authorization.substring(0, idx);

        if (!method.equalsIgnoreCase("basic")) {
            return null;
        }

        // Extract the credentials - should be ticket:tunnel
        String encoded = authorization.substring(idx + 1);

        String credentials = new String(Base64.decode(encoded));
        idx = credentials.indexOf(':');

        if (idx == 0 || idx == -1) {
            return null;
        }

        // Get the user credentials
        String username = credentials.substring(0, idx);
        String password = credentials.substring(idx + 1);

        return new PasswordCredentials(username, password.toCharArray());
    }

    /**
     * Get the session info for this transaction. The user and other session
     * related objects may be found here.
     * 
     * @return session info
     */
    public SessionInfo getSessionInfo() {
        return sessionInfo;
    }

    /**
     * Check if the supplied resource path is valid for this transaction path.
     * This will be used to force a redirect to the required path if not.
     * 
     * @param fullResourcePath
     * @return is resource path
     */
    public boolean isResourcePath(String fullResourcePath) {
        String fullUri = DAVUtilities.stripTrailingSlash(DAVUtilities.stripLeadingSlash(fullResourcePath));
        return fullUri.equals(getPath());
    }

    void configureFromRequest() throws URISyntaxException {
        String scheme = this.req.getScheme();
        String host = this.req.getServerName();
        String basePath = DAVUtilities.concatenatePaths(this.req.getServletPath(), this.req.getPathInfo());
        int port = this.req.getServerPort();
        this.base = new URI(scheme, null, host, port, basePath, null, null);
        this.base = this.base.normalize();
        path = DAVUtilities
                .stripTrailingSlash(DAVUtilities.stripLeadingSlash(DAVUtilities.stripFirstPath(base.getPath())));

    }
}